1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Partial fix #495

Add list
Edit list info
This commit is contained in:
xmflsct
2022-11-30 22:42:42 +01:00
parent de7498b218
commit bb3ddd2779
29 changed files with 506 additions and 224 deletions

View File

@ -274,6 +274,7 @@ declare namespace Mastodon {
type List = { type List = {
id: string id: string
title: string title: string
replies_policy: 'none' | 'list' | 'followed'
} }
type Instance = { type Instance = {

View File

@ -121,7 +121,8 @@ const contextMenuStatus = ({ actions, status, queryKey, rootQueryKey }: Props) =
} }
}, },
{ {
text: t('common:buttons.cancel') text: t('common:buttons.cancel'),
style: 'default'
} }
]) ])
} }

View File

@ -60,11 +60,11 @@ const ComponentInstance: React.FC<Props> = ({
if (instances && instances.filter(instance => instance.url === domain).length) { if (instances && instances.filter(instance => instance.url === domain).length) {
Alert.alert(t('update.alert.title'), t('update.alert.message'), [ Alert.alert(t('update.alert.title'), t('update.alert.message'), [
{ {
text: t('update.alert.buttons.cancel'), text: t('common:buttons.cancel'),
style: 'cancel' style: 'cancel'
}, },
{ {
text: t('update.alert.buttons.continue'), text: t('common:buttons.continue'),
onPress: () => { onPress: () => {
appsQuery.refetch() appsQuery.refetch()
} }

View File

@ -125,7 +125,7 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
shadowOpacity: theme === 'light' ? 0.16 : 0.24, shadowOpacity: theme === 'light' ? 0.16 : 0.24,
shadowRadius: 4, shadowRadius: 4,
paddingRight: StyleConstants.Spacing.M * 2, paddingRight: StyleConstants.Spacing.M * 2,
marginTop: insets.top marginTop: ref ? undefined : insets.top
}} }}
titleStyle={{ titleStyle={{
color: colors.primaryDefault, color: colors.primaryDefault,

View File

@ -0,0 +1,71 @@
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { Pressable, View } from 'react-native'
import haptics from './haptics'
import Icon from './Icon'
import { ParseEmojis } from './Parse'
import CustomText from './Text'
export interface Props {
multiple?: boolean
options: { selected: boolean; content: string }[]
setOptions: React.Dispatch<React.SetStateAction<{ selected: boolean; content: string }[]>>
}
const Selections: React.FC<Props> = ({ multiple = false, options, setOptions }) => {
const { colors } = useTheme()
const isSelected = (index: number): string =>
options[index].selected
? `Check${multiple ? 'Square' : 'Circle'}`
: `${multiple ? 'Square' : 'Circle'}`
return (
<>
{options.map((option, index) => (
<Pressable
key={index}
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) {
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={colors.primaryDefault}
/>
<CustomText style={{ flex: 1 }}>
<ParseEmojis content={option.content} />
</CustomText>
</View>
</Pressable>
))}
</>
)
}
export default Selections

View File

@ -2,7 +2,9 @@
"buttons": { "buttons": {
"OK": "OK", "OK": "OK",
"apply": "Apply", "apply": "Apply",
"cancel": "Cancel" "cancel": "Cancel",
"discard": "Discard",
"continue": "Continue"
}, },
"customEmoji": { "customEmoji": {
"accessibilityLabel": "Custom emoji {{emoji}}" "accessibilityLabel": "Custom emoji {{emoji}}"
@ -18,5 +20,9 @@
"message": "{{function}} failed, please retry" "message": "{{function}} failed, please retry"
} }
}, },
"separator": ", " "separator": ", ",
"discard": {
"title": "Change Not Saved",
"message": "Your change has not been saved. Would you discard saving the changes?"
}
} }

View File

@ -20,11 +20,7 @@
"update": { "update": {
"alert": { "alert": {
"title": "Logged in to this instance", "title": "Logged in to this instance",
"message": "You can login to another account, keeping existing logged in account", "message": "You can login to another account, keeping existing logged in account"
"buttons": {
"cancel": "$t(common:buttons.cancel)",
"continue": "Continue"
}
} }
} }
} }

View File

@ -28,7 +28,6 @@
"removeReply": { "removeReply": {
"title": "Replied toot could not be found", "title": "Replied toot could not be found",
"description": "Replied toot could have been deleted. Do you want to remove it from your reference?", "description": "Replied toot could have been deleted. Do you want to remove it from your reference?",
"cancel": "$t(common:buttons.cancel)",
"confirm": "Remove reference" "confirm": "Remove reference"
} }
} }
@ -89,8 +88,7 @@
"heading": "Choice type", "heading": "Choice type",
"options": { "options": {
"single": "Single choice", "single": "Single choice",
"multiple": "Multiple choice", "multiple": "Multiple choice"
"cancel": "$t(common:buttons.cancel)"
} }
}, },
"expiration": { "expiration": {
@ -102,8 +100,7 @@
"21600": "6 hours", "21600": "6 hours",
"86400": "1 day", "86400": "1 day",
"259200": "3 days", "259200": "3 days",
"604800": "7 days", "604800": "7 days"
"cancel": "$t(common:buttons.cancel)"
} }
} }
} }
@ -130,8 +127,7 @@
"public": "Public", "public": "Public",
"unlisted": "Unlisted", "unlisted": "Unlisted",
"private": "Followers only", "private": "Followers only",
"direct": "Direct message", "direct": "Direct message"
"cancel": "$t(common:buttons.cancel)"
} }
}, },
"spoiler": { "spoiler": {

View File

@ -6,8 +6,7 @@
}, },
"options": { "options": {
"save": "Save image", "save": "Save image",
"share": "Share image", "share": "Share image"
"cancel": "$t(common:buttons.cancel)"
}, },
"save": { "save": {
"succeed": "Image saved", "succeed": "Image saved",

View File

@ -49,6 +49,12 @@
"lists": { "lists": {
"name": "Lists" "name": "Lists"
}, },
"listAdd": {
"name": "Add a List"
},
"listEdit": {
"name": "Edit List"
},
"list": { "list": {
"name": "List: {{list}}" "name": "List: {{list}}"
}, },
@ -87,15 +93,18 @@
"XXL": "XXL" "XXL": "XXL"
} }
}, },
"profile": { "listEdit": {
"cancellation": { "title": "Title",
"title": "Change Not Saved", "repliesPolicy": {
"message": "Your change has not been saved. Would you discard saving the changes?", "heading": "Show replies to:",
"buttons": { "options": {
"cancel": "$t(common:buttons.cancel)", "none": "No one",
"discard": "Discard" "list": "Members of the list",
"followed": "Any followed user"
} }
}, }
},
"profile": {
"feedback": { "feedback": {
"succeed": "{{type}} updated", "succeed": "{{type}} updated",
"failed": "{{type}} update failed, please try again" "failed": "{{type}} update failed, please try again"
@ -125,8 +134,7 @@
"options": { "options": {
"public": "Public", "public": "Public",
"unlisted": "Unlisted", "unlisted": "Unlisted",
"private": "Followers only", "private": "Followers only"
"cancel": "$t(common:buttons.cancel)"
} }
}, },
"sensitive": { "sensitive": {
@ -211,8 +219,7 @@
"title": "Logging out?", "title": "Logging out?",
"message": "After logging out, you need to log in again", "message": "After logging out, you need to log in again",
"buttons": { "buttons": {
"logout": "Logout", "logout": "Logout"
"cancel": "$t(common:buttons.cancel)"
} }
} }
} }
@ -229,34 +236,28 @@
} }
}, },
"language": { "language": {
"heading": "$t(me.stacks.language.name)", "heading": "$t(me.stacks.language.name)"
"options": {
"cancel": "$t(common:buttons.cancel)"
}
}, },
"theme": { "theme": {
"heading": "Appearance", "heading": "Appearance",
"options": { "options": {
"auto": "As system", "auto": "As system",
"light": "Light mode", "light": "Light mode",
"dark": "Dark mode", "dark": "Dark mode"
"cancel": "$t(common:buttons.cancel)"
} }
}, },
"darkTheme": { "darkTheme": {
"heading": "Dark theme", "heading": "Dark theme",
"options": { "options": {
"lighter": "Lighter", "lighter": "Lighter",
"darker": "Darker", "darker": "Darker"
"cancel": "$t(common:buttons.cancel)"
} }
}, },
"browser": { "browser": {
"heading": "Opening Link", "heading": "Opening Link",
"options": { "options": {
"internal": "Inside app", "internal": "Inside app",
"external": "Use system browser", "external": "Use system browser"
"cancel": "$t(common:buttons.cancel)"
} }
}, },
"staticEmoji": { "staticEmoji": {

View File

@ -43,10 +43,6 @@ const ActionsDomain: React.FC<Props> = ({ queryKey, rootQueryKey, domain, dismis
t('shared.header.actions.domain.alert.title', { domain }), t('shared.header.actions.domain.alert.title', { domain }),
t('shared.header.actions.domain.alert.message'), t('shared.header.actions.domain.alert.message'),
[ [
{
text: t('shared.header.actions.domain.alert.buttons.cancel'),
style: 'cancel'
},
{ {
text: t('shared.header.actions.domain.alert.buttons.confirm'), text: t('shared.header.actions.domain.alert.buttons.confirm'),
style: 'destructive', style: 'destructive',
@ -58,6 +54,10 @@ const ActionsDomain: React.FC<Props> = ({ queryKey, rootQueryKey, domain, dismis
domain: domain domain: domain
}) })
} }
},
{
text: t('shared.header.actions.domain.alert.buttons.cancel'),
style: 'default'
} }
] ]
) )

View File

@ -38,8 +38,7 @@ const ActionsStatus: React.FC<Props> = ({
const mutation = useTimelineMutation({ const mutation = useTimelineMutation({
onMutate: true, onMutate: true,
onError: (err: any, params, oldData) => { onError: (err: any, params, oldData) => {
const theFunction = (params as MutationVarsTimelineUpdateStatusProperty) const theFunction = (params as MutationVarsTimelineUpdateStatusProperty).payload
.payload
? (params as MutationVarsTimelineUpdateStatusProperty).payload.property ? (params as MutationVarsTimelineUpdateStatusProperty).payload.property
: 'delete' : 'delete'
displayMessage({ displayMessage({
@ -108,15 +107,7 @@ const ActionsStatus: React.FC<Props> = ({
t('shared.header.actions.status.delete.alert.message'), t('shared.header.actions.status.delete.alert.message'),
[ [
{ {
text: t( text: t('shared.header.actions.status.delete.alert.buttons.confirm'),
'shared.header.actions.status.delete.alert.buttons.cancel'
),
style: 'cancel'
},
{
text: t(
'shared.header.actions.status.delete.alert.buttons.confirm'
),
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
dismiss() dismiss()
@ -128,6 +119,10 @@ const ActionsStatus: React.FC<Props> = ({
id: status.id id: status.id
}) })
} }
},
{
text: t('shared.header.actions.status.delete.alert.buttons.cancel'),
style: 'default'
} }
] ]
) )
@ -142,15 +137,7 @@ const ActionsStatus: React.FC<Props> = ({
t('shared.header.actions.status.deleteEdit.alert.message'), t('shared.header.actions.status.deleteEdit.alert.message'),
[ [
{ {
text: t( text: t('shared.header.actions.status.deleteEdit.alert.buttons.confirm'),
'shared.header.actions.status.deleteEdit.alert.buttons.cancel'
),
style: 'cancel'
},
{
text: t(
'shared.header.actions.status.deleteEdit.alert.buttons.confirm'
),
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
let replyToStatus: Mastodon.Status | undefined = undefined let replyToStatus: Mastodon.Status | undefined = undefined
@ -177,6 +164,10 @@ const ActionsStatus: React.FC<Props> = ({
}) })
}) })
} }
},
{
text: t('shared.header.actions.status.deleteEdit.alert.buttons.cancel'),
style: 'default'
} }
] ]
) )

View File

@ -300,7 +300,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
t('heading.right.alert.removeReply.description'), t('heading.right.alert.removeReply.description'),
[ [
{ {
text: t('heading.right.alert.removeReply.cancel'), text: t('common:buttons.cancel'),
onPress: () => { onPress: () => {
composeDispatch({ type: 'posting', payload: false }) composeDispatch({ type: 'posting', payload: false })
}, },

View File

@ -86,7 +86,7 @@ const ComposeActions: React.FC = () => {
t('content.root.actions.visibility.options.unlisted'), t('content.root.actions.visibility.options.unlisted'),
t('content.root.actions.visibility.options.private'), t('content.root.actions.visibility.options.private'),
t('content.root.actions.visibility.options.direct'), t('content.root.actions.visibility.options.direct'),
t('content.root.actions.visibility.options.cancel') t('common:buttons.cancel')
], ],
cancelButtonIndex: 4, cancelButtonIndex: 4,
userInterfaceStyle: mode userInterfaceStyle: mode

View File

@ -195,7 +195,7 @@ const ComposePoll: React.FC = () => {
options: [ options: [
t('content.root.footer.poll.multiple.options.single'), t('content.root.footer.poll.multiple.options.single'),
t('content.root.footer.poll.multiple.options.multiple'), t('content.root.footer.poll.multiple.options.multiple'),
t('content.root.footer.poll.multiple.options.cancel') t('common:buttons.cancel')
], ],
cancelButtonIndex: 2, cancelButtonIndex: 2,
userInterfaceStyle: mode userInterfaceStyle: mode
@ -235,7 +235,7 @@ const ComposePoll: React.FC = () => {
...expirations.map(e => ...expirations.map(e =>
t(`content.root.footer.poll.expiration.options.${e}`) t(`content.root.footer.poll.expiration.options.${e}`)
), ),
t('content.root.footer.poll.expiration.options.cancel') t('common:buttons.cancel')
], ],
cancelButtonIndex: expirations.length, cancelButtonIndex: expirations.length,
userInterfaceStyle: mode userInterfaceStyle: mode

View File

@ -53,7 +53,7 @@ const ScreenImagesViewer = ({
options: [ options: [
t('content.options.save'), t('content.options.save'),
t('content.options.share'), t('content.options.share'),
t('content.options.cancel') t('common:buttons.cancel')
], ],
cancelButtonIndex: 2, cancelButtonIndex: 2,
userInterfaceStyle: mode userInterfaceStyle: mode

View File

@ -7,8 +7,9 @@ import { Platform } from 'react-native'
import TabMeBookmarks from './Me/Bookmarks' import TabMeBookmarks from './Me/Bookmarks'
import TabMeConversations from './Me/Cconversations' import TabMeConversations from './Me/Cconversations'
import TabMeFavourites from './Me/Favourites' import TabMeFavourites from './Me/Favourites'
import TabMeList from './Me/List'
import TabMeListEdit from './Me/ListEdit'
import TabMeLists from './Me/Lists' import TabMeLists from './Me/Lists'
import TabMeListsList from './Me/ListsList'
import TabMeProfile from './Me/Profile' import TabMeProfile from './Me/Profile'
import TabMePush from './Me/Push' import TabMePush from './Me/Push'
import TabMeRoot from './Me/Root' import TabMeRoot from './Me/Root'
@ -41,9 +42,7 @@ const TabMe = React.memo(
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
title: t('me.stacks.bookmarks.name'), title: t('me.stacks.bookmarks.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
headerCenter: () => ( headerCenter: () => <HeaderCenter content={t('me.stacks.bookmarks.name')} />
<HeaderCenter content={t('me.stacks.bookmarks.name')} />
)
}), }),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})} })}
@ -54,9 +53,7 @@ const TabMe = React.memo(
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
title: t('me.stacks.conversations.name'), title: t('me.stacks.conversations.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
headerCenter: () => ( headerCenter: () => <HeaderCenter content={t('me.stacks.conversations.name')} />
<HeaderCenter content={t('me.stacks.conversations.name')} />
)
}), }),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})} })}
@ -67,29 +64,14 @@ const TabMe = React.memo(
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
title: t('me.stacks.favourites.name'), title: t('me.stacks.favourites.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
headerCenter: () => ( headerCenter: () => <HeaderCenter content={t('me.stacks.favourites.name')} />
<HeaderCenter content={t('me.stacks.favourites.name')} />
)
}), }),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})} })}
/> />
<Stack.Screen <Stack.Screen
name='Tab-Me-Lists' name='Tab-Me-List'
component={TabMeLists} component={TabMeList}
options={({ navigation }: any) => ({
title: t('me.stacks.lists.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter content={t('me.stacks.lists.name')} />
)
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
<Stack.Screen
name='Tab-Me-Lists-List'
component={TabMeListsList}
options={({ route, navigation }: any) => ({ options={({ route, navigation }: any) => ({
title: t('me.stacks.list.name', { list: route.params.title }), title: t('me.stacks.list.name', { list: route.params.title }),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
@ -104,6 +86,29 @@ const TabMe = React.memo(
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})} })}
/> />
<Stack.Screen
name='Tab-Me-List-Edit'
component={TabMeListEdit}
options={{
gestureEnabled: false,
presentation: 'modal',
title: t('me.stacks.listsAdd.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.listsAdd.name')} />
})
}}
/>
<Stack.Screen
name='Tab-Me-Lists'
component={TabMeLists}
options={({ navigation }: any) => ({
title: t('me.stacks.lists.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.lists.name')} />
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
<Stack.Screen <Stack.Screen
name='Tab-Me-Profile' name='Tab-Me-Profile'
component={TabMeProfile} component={TabMeProfile}
@ -120,15 +125,10 @@ const TabMe = React.memo(
headerShown: true, headerShown: true,
title: t('me.stacks.push.name'), title: t('me.stacks.push.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
headerCenter: () => ( headerCenter: () => <HeaderCenter content={t('me.stacks.push.name')} />
<HeaderCenter content={t('me.stacks.push.name')} />
)
}), }),
headerLeft: () => ( headerLeft: () => (
<HeaderLeft <HeaderLeft content='ChevronDown' onPress={() => navigation.goBack()} />
content='ChevronDown'
onPress={() => navigation.goBack()}
/>
) )
})} })}
/> />
@ -146,9 +146,7 @@ const TabMe = React.memo(
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
title: t('me.stacks.fontSize.name'), title: t('me.stacks.fontSize.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
headerCenter: () => ( headerCenter: () => <HeaderCenter content={t('me.stacks.fontSize.name')} />
<HeaderCenter content={t('me.stacks.fontSize.name')} />
)
}), }),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})} })}
@ -159,9 +157,7 @@ const TabMe = React.memo(
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
title: t('me.stacks.language.name'), title: t('me.stacks.language.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
headerCenter: () => ( headerCenter: () => <HeaderCenter content={t('me.stacks.language.name')} />
<HeaderCenter content={t('me.stacks.language.name')} />
)
}), }),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})} })}
@ -174,15 +170,10 @@ const TabMe = React.memo(
headerShown: true, headerShown: true,
title: t('me.stacks.switch.name'), title: t('me.stacks.switch.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {
headerCenter: () => ( headerCenter: () => <HeaderCenter content={t('me.stacks.switch.name')} />
<HeaderCenter content={t('me.stacks.switch.name')} />
)
}), }),
headerLeft: () => ( headerLeft: () => (
<HeaderLeft <HeaderLeft content='ChevronDown' onPress={() => navigation.goBack()} />
content='ChevronDown'
onPress={() => navigation.goBack()}
/>
) )
})} })}
/> />

View File

@ -0,0 +1,40 @@
import { HeaderRight } from '@components/Header'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
const TabMeList: React.FC<TabMeStackScreenProps<'Tab-Me-List'>> = ({
navigation,
route: { key, params }
}) => {
const { t } = useTranslation('screenTabs')
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', list: params.id }]
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<HeaderRight
accessibilityLabel={t('me.stacks.listEdit.name')}
content='Edit'
onPress={() =>
navigation.navigate('Tab-Me-List-Edit', { type: 'edit', payload: params, key })
}
/>
)
})
}, [params])
return (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />
}}
/>
)
}
export default TabMeList

View File

@ -0,0 +1,163 @@
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header'
import ComponentInput from '@components/Input'
import { displayMessage, Message } from '@components/Message'
import Selections from '@components/Selections'
import CustomText from '@components/Text'
import { CommonActions } from '@react-navigation/native'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyLists, useListsMutation } from '@utils/queryHooks/lists'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, ScrollView, TextInput } from 'react-native'
import { useQueryClient } from 'react-query'
const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
navigation,
route: { params }
}) => {
const { colors, theme } = useTheme()
const { t } = useTranslation('screenTabs')
const messageRef = useRef(null)
const queryKeyLists: QueryKeyLists = ['Lists']
const queryClient = useQueryClient()
const mutation = useListsMutation({
onSuccess: res => {
haptics('Success')
queryClient.refetchQueries(queryKeyLists)
navigation.pop(1)
if (params.type === 'edit') {
navigation.dispatch({
...CommonActions.setParams(res),
source: params.key
})
}
},
onError: () => {
displayMessage({
ref: messageRef,
theme,
type: 'error',
message: t('common:message.error.message', {
function:
params.type === 'add' ? t('me.stacks.listAdd.name') : t('me.stacks.listEdit.name')
})
})
}
})
const ref = useRef<TextInput>(null)
const [title, setTitle] = useState(params.type === 'edit' ? params.payload.title : '')
const inputProps: EmojisState['inputProps'][0] = {
ref,
value: [title, setTitle],
selection: useState({ start: title.length }),
isFocused: useRef<boolean>(false),
maxLength: 50
}
const [options, setOptions] = useState<
{ id: 'none' | 'list' | 'followed'; content: string; selected: boolean }[]
>([
{
id: 'none',
content: t('me.listEdit.repliesPolicy.options.none'),
selected: params.type === 'edit' ? params.payload.replies_policy === 'none' : false
},
{
id: 'list',
content: t('me.listEdit.repliesPolicy.options.list'),
selected: params.type === 'edit' ? params.payload.replies_policy === 'list' : true
},
{
id: 'followed',
content: t('me.listEdit.repliesPolicy.options.followed'),
selected: params.type === 'edit' ? params.payload.replies_policy === 'followed' : false
}
])
useEffect(() => {
navigation.setOptions({
headerLeft: () => (
<HeaderLeft
content='X'
onPress={() => {
if (params.type === 'edit' ? params.payload.title !== title : title.length) {
Alert.alert(t('common:discard.title'), t('common:discard.message'), [
{
text: t('common:buttons.discard'),
style: 'destructive',
onPress: () => navigation.pop(1)
},
{
text: t('common:buttons.cancel'),
style: 'default'
}
])
} else {
navigation.pop(1)
}
}}
/>
),
headerRight: () => (
<HeaderRight
content='Save'
disabled={!title.length}
loading={mutation.isLoading}
onPress={() => {
switch (params.type) {
case 'add':
mutation.mutate({
type: 'add',
payload: {
title,
replies_policy: options.find(option => option.selected)?.id || 'list'
}
})
break
case 'edit':
mutation.mutate({
type: 'edit',
payload: {
id: params.payload.id,
title,
replies_policy: options.find(option => option.selected)?.id || 'list'
}
})
break
}
}}
/>
)
})
}, [title.length, options])
return (
<ScrollView style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}>
<ComponentInput {...inputProps} autoFocus title={t('me.listEdit.title')} />
<CustomText
fontStyle='M'
fontWeight='Bold'
style={{
color: colors.primaryDefault,
marginBottom: StyleConstants.Spacing.XS,
marginTop: StyleConstants.Spacing.M
}}
>
{t('me.listEdit.repliesPolicy.heading')}
</CustomText>
<Selections options={options} setOptions={setOptions} />
<Message ref={messageRef} />
</ScrollView>
)
}
export default TabMeListEdit

View File

@ -1,12 +1,25 @@
import { HeaderRight } from '@components/Header'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import { TabMeStackScreenProps } from '@utils/navigation/navigators' import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { useListsQuery } from '@utils/queryHooks/lists' import { useListsQuery } from '@utils/queryHooks/lists'
import React from 'react' import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
const TabMeLists: React.FC<TabMeStackScreenProps<'Tab-Me-Lists'>> = ({ const TabMeLists: React.FC<TabMeStackScreenProps<'Tab-Me-Lists'>> = ({ navigation }) => {
navigation
}) => {
const { data } = useListsQuery({}) const { data } = useListsQuery({})
const { t } = useTranslation('screenTabs')
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<HeaderRight
accessibilityLabel={t('me.stacks.listAdd.name')}
content='Plus'
onPress={() => navigation.navigate('Tab-Me-List-Edit', { type: 'add' })}
/>
)
})
}, [])
return ( return (
<MenuContainer> <MenuContainer>
@ -16,12 +29,7 @@ const TabMeLists: React.FC<TabMeStackScreenProps<'Tab-Me-Lists'>> = ({
iconFront='List' iconFront='List'
iconBack='ChevronRight' iconBack='ChevronRight'
title={d.title} title={d.title}
onPress={() => onPress={() => navigation.navigate('Tab-Me-List', d)}
navigation.navigate('Tab-Me-Lists-List', {
list: d.id,
title: d.title
})
}
/> />
))} ))}
</MenuContainer> </MenuContainer>

View File

@ -1,26 +0,0 @@
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React from 'react'
const TabMeListsList: React.FC<TabMeStackScreenProps<'Tab-Me-Lists-List'>> = ({
route: {
params: { list }
}
}) => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', list }]
return (
<Timeline
queryKey={queryKey}
customProps={{
renderItem: ({ item }) => (
<TimelineDefault item={item} queryKey={queryKey} />
)
}}
/>
)
}
export default TabMeListsList

View File

@ -90,20 +90,21 @@ const TabMeProfileFields: React.FC<
navigation.setOptions({ navigation.setOptions({
headerLeft: () => ( headerLeft: () => (
<HeaderLeft <HeaderLeft
content='X'
onPress={() => { onPress={() => {
if (dirty) { if (dirty) {
Alert.alert( Alert.alert(
t('me.profile.cancellation.title'), t('common:discard.title'),
t('me.profile.cancellation.message'), t('common:discard.message'),
[ [
{ {
text: t('me.profile.cancellation.buttons.cancel'), text: t('common:buttons.discard'),
style: 'default'
},
{
text: t('me.profile.cancellation.buttons.discard'),
style: 'destructive', style: 'destructive',
onPress: () => navigation.navigate('Tab-Me-Profile-Root') onPress: () => navigation.navigate('Tab-Me-Profile-Root')
},
{
text: t('common:buttons.cancel'),
style: 'default'
} }
] ]
) )

View File

@ -44,20 +44,21 @@ const TabMeProfileName: React.FC<
navigation.setOptions({ navigation.setOptions({
headerLeft: () => ( headerLeft: () => (
<HeaderLeft <HeaderLeft
content='X'
onPress={() => { onPress={() => {
if (dirty) { if (dirty) {
Alert.alert( Alert.alert(
t('me.profile.cancellation.title'), t('common:discard.title'),
t('me.profile.cancellation.message'), t('common:discard.message'),
[ [
{ {
text: t('me.profile.cancellation.buttons.cancel'), text: t('common:buttons.discard'),
style: 'default'
},
{
text: t('me.profile.cancellation.buttons.discard'),
style: 'destructive', style: 'destructive',
onPress: () => navigation.navigate('Tab-Me-Profile-Root') onPress: () => navigation.navigate('Tab-Me-Profile-Root')
},
{
text: t('common:buttons.cancel'),
style: 'default'
} }
] ]
) )

View File

@ -44,20 +44,21 @@ const TabMeProfileNote: React.FC<
navigation.setOptions({ navigation.setOptions({
headerLeft: () => ( headerLeft: () => (
<HeaderLeft <HeaderLeft
content='X'
onPress={() => { onPress={() => {
if (dirty) { if (dirty) {
Alert.alert( Alert.alert(
t('me.profile.cancellation.title'), t('common:discard.title'),
t('me.profile.cancellation.message'), t('common:discard.message'),
[ [
{ {
text: t('me.profile.cancellation.buttons.cancel'), text: t('common:buttons.discard'),
style: 'default'
},
{
text: t('me.profile.cancellation.buttons.discard'),
style: 'destructive', style: 'destructive',
onPress: () => navigation.navigate('Tab-Me-Profile-Root') onPress: () => navigation.navigate('Tab-Me-Profile-Root')
},
{
text: t('common:buttons.cancel'),
style: 'default'
} }
] ]
) )

View File

@ -33,7 +33,7 @@ const TabMeProfileRoot: React.FC<
t('me.profile.root.visibility.options.public'), t('me.profile.root.visibility.options.public'),
t('me.profile.root.visibility.options.unlisted'), t('me.profile.root.visibility.options.unlisted'),
t('me.profile.root.visibility.options.private'), t('me.profile.root.visibility.options.private'),
t('me.profile.root.visibility.options.cancel') t('common:buttons.cancel')
], ],
cancelButtonIndex: 3, cancelButtonIndex: 3,
userInterfaceStyle: mode userInterfaceStyle: mode

View File

@ -27,27 +27,23 @@ const Logout: React.FC = () => {
}} }}
destructive destructive
onPress={() => onPress={() =>
Alert.alert( Alert.alert(t('me.root.logout.alert.title'), t('me.root.logout.alert.message'), [
t('me.root.logout.alert.title'), {
t('me.root.logout.alert.message'), text: t('me.root.logout.alert.buttons.logout'),
[ style: 'destructive',
{ onPress: () => {
text: t('me.root.logout.alert.buttons.logout'), if (instance) {
style: 'destructive' as const, haptics('Success')
onPress: () => { queryClient.clear()
if (instance) { dispatch(removeInstance(instance))
haptics('Success')
queryClient.clear()
dispatch(removeInstance(instance))
}
} }
},
{
text: t('me.root.logout.alert.buttons.cancel'),
style: 'cancel' as const
} }
] },
) {
text: t('common:buttons.cancel'),
style: 'default'
}
])
} }
/> />
) )

View File

@ -69,7 +69,7 @@ const SettingsApp: React.FC = () => {
t('me.settings.theme.options.auto'), t('me.settings.theme.options.auto'),
t('me.settings.theme.options.light'), t('me.settings.theme.options.light'),
t('me.settings.theme.options.dark'), t('me.settings.theme.options.dark'),
t('me.settings.theme.options.cancel') t('common:buttons.cancel')
], ],
cancelButtonIndex: 3 cancelButtonIndex: 3
}, },
@ -103,7 +103,7 @@ const SettingsApp: React.FC = () => {
options: [ options: [
t('me.settings.darkTheme.options.lighter'), t('me.settings.darkTheme.options.lighter'),
t('me.settings.darkTheme.options.darker'), t('me.settings.darkTheme.options.darker'),
t('me.settings.darkTheme.options.cancel') t('common:buttons.cancel')
], ],
cancelButtonIndex: 2 cancelButtonIndex: 2
}, },
@ -133,7 +133,7 @@ const SettingsApp: React.FC = () => {
options: [ options: [
t('me.settings.browser.options.internal'), t('me.settings.browser.options.internal'),
t('me.settings.browser.options.external'), t('me.settings.browser.options.external'),
t('me.settings.browser.options.cancel') t('common:buttons.cancel')
], ],
cancelButtonIndex: 2 cancelButtonIndex: 2
}, },

View File

@ -63,17 +63,18 @@ export type RootStackParamList = {
share?: { text?: string; media?: { uri: string; mime: string }[] } share?: { text?: string; media?: { uri: string; mime: string }[] }
} }
} }
export type RootStackScreenProps<T extends keyof RootStackParamList> = export type RootStackScreenProps<T extends keyof RootStackParamList> = NativeStackScreenProps<
NativeStackScreenProps<RootStackParamList, T> RootStackParamList,
T
>
export type ScreenComposeStackParamList = { export type ScreenComposeStackParamList = {
'Screen-Compose-Root': undefined 'Screen-Compose-Root': undefined
'Screen-Compose-EditAttachment': { index: number } 'Screen-Compose-EditAttachment': { index: number }
'Screen-Compose-DraftsList': { timestamp: number } 'Screen-Compose-DraftsList': { timestamp: number }
} }
export type ScreenComposeStackScreenProps< export type ScreenComposeStackScreenProps<T extends keyof ScreenComposeStackParamList> =
T extends keyof ScreenComposeStackParamList NativeStackScreenProps<ScreenComposeStackParamList, T>
> = NativeStackScreenProps<ScreenComposeStackParamList, T>
export type ScreenTabsStackParamList = { export type ScreenTabsStackParamList = {
'Tab-Local': NavigatorScreenParams<TabLocalStackParamList> 'Tab-Local': NavigatorScreenParams<TabLocalStackParamList>
@ -82,8 +83,10 @@ export type ScreenTabsStackParamList = {
'Tab-Notifications': NavigatorScreenParams<TabNotificationsStackParamList> 'Tab-Notifications': NavigatorScreenParams<TabNotificationsStackParamList>
'Tab-Me': NavigatorScreenParams<TabMeStackParamList> 'Tab-Me': NavigatorScreenParams<TabMeStackParamList>
} }
export type ScreenTabsScreenProps<T extends keyof ScreenTabsStackParamList> = export type ScreenTabsScreenProps<T extends keyof ScreenTabsStackParamList> = BottomTabScreenProps<
BottomTabScreenProps<ScreenTabsStackParamList, T> ScreenTabsStackParamList,
T
>
export type TabSharedStackParamList = { export type TabSharedStackParamList = {
'Tab-Shared-Account': { 'Tab-Shared-Account': {
@ -135,11 +138,17 @@ export type TabMeStackParamList = {
'Tab-Me-Bookmarks': undefined 'Tab-Me-Bookmarks': undefined
'Tab-Me-Conversations': undefined 'Tab-Me-Conversations': undefined
'Tab-Me-Favourites': undefined 'Tab-Me-Favourites': undefined
'Tab-Me-List': Mastodon.List
'Tab-Me-List-Edit':
| {
type: 'add'
}
| {
type: 'edit'
payload: Mastodon.List
key: string // To update title after successful mutation
}
'Tab-Me-Lists': undefined 'Tab-Me-Lists': undefined
'Tab-Me-Lists-List': {
list: Mastodon.List['id']
title: Mastodon.List['title']
}
'Tab-Me-Profile': undefined 'Tab-Me-Profile': undefined
'Tab-Me-Push': undefined 'Tab-Me-Push': undefined
'Tab-Me-Settings': undefined 'Tab-Me-Settings': undefined
@ -147,11 +156,12 @@ export type TabMeStackParamList = {
'Tab-Me-Settings-Language': undefined 'Tab-Me-Settings-Language': undefined
'Tab-Me-Switch': undefined 'Tab-Me-Switch': undefined
} & TabSharedStackParamList } & TabSharedStackParamList
export type TabMeStackScreenProps<T extends keyof TabMeStackParamList> = export type TabMeStackScreenProps<T extends keyof TabMeStackParamList> = NativeStackScreenProps<
NativeStackScreenProps<TabMeStackParamList, T> TabMeStackParamList,
export type TabMeStackNavigationProp< T
RouteName extends keyof TabMeStackParamList >
> = StackNavigationProp<TabMeStackParamList, RouteName> export type TabMeStackNavigationProp<RouteName extends keyof TabMeStackParamList> =
StackNavigationProp<TabMeStackParamList, RouteName>
export type TabMeProfileStackParamList = { export type TabMeProfileStackParamList = {
'Tab-Me-Profile-Root': undefined 'Tab-Me-Profile-Root': undefined
@ -165,6 +175,5 @@ export type TabMeProfileStackParamList = {
fields?: Mastodon.Source['fields'] fields?: Mastodon.Source['fields']
} }
} }
export type TabMeProfileStackScreenProps< export type TabMeProfileStackScreenProps<T extends keyof TabMeProfileStackParamList> =
T extends keyof TabMeProfileStackParamList NativeStackScreenProps<TabMeProfileStackParamList, T>
> = NativeStackScreenProps<TabMeProfileStackParamList, T>

View File

@ -1,8 +1,8 @@
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query' import { useMutation, UseMutationOptions, useQuery, UseQueryOptions } from 'react-query'
export type QueryKey = ['Lists'] export type QueryKeyLists = ['Lists']
const queryFunction = async () => { const queryFunction = async () => {
const res = await apiInstance<Mastodon.List[]>({ const res = await apiInstance<Mastodon.List[]>({
@ -12,13 +12,49 @@ const queryFunction = async () => {
return res.body return res.body
} }
const useListsQuery = ({ const useListsQuery = ({ options }: { options?: UseQueryOptions<Mastodon.List[], AxiosError> }) => {
options const queryKey: QueryKeyLists = ['Lists']
}: {
options?: UseQueryOptions<Mastodon.List[], AxiosError>
}) => {
const queryKey: QueryKey = ['Lists']
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
export { useListsQuery } type MutationVarsLists =
| {
type: 'add'
payload: Omit<Mastodon.List, 'id'>
}
| {
type: 'edit'
payload: Mastodon.List
}
const mutationFunction = async (params: MutationVarsLists) => {
const body = new FormData()
switch (params.type) {
case 'add':
body.append('title', params.payload.title)
body.append('replies_policy', params.payload.replies_policy)
return apiInstance<Mastodon.List>({
method: 'post',
url: 'lists',
body
}).then(res => res.body)
case 'edit':
body.append('title', params.payload.title)
body.append('replies_policy', params.payload.replies_policy)
return apiInstance<Mastodon.List>({
method: 'put',
url: `lists/${params.payload.id}`,
body
}).then(res => res.body)
}
}
const useListsMutation = (
options: UseMutationOptions<Mastodon.List, AxiosError, MutationVarsLists>
) => {
return useMutation(mutationFunction, options)
}
export { useListsQuery, useListsMutation }