From bb3ddd27792478736062fa027e750dabc06406e8 Mon Sep 17 00:00:00 2001 From: xmflsct Date: Wed, 30 Nov 2022 22:42:42 +0100 Subject: [PATCH] Partial fix #495 Add list Edit list info --- src/@types/mastodon.d.ts | 1 + src/components/ContextMenu/status.ts | 3 +- src/components/Instance.tsx | 4 +- src/components/Message.tsx | 2 +- src/components/Selections.tsx | 71 ++++++++++ src/i18n/en/common.json | 10 +- src/i18n/en/components/instance.json | 6 +- src/i18n/en/screens/compose.json | 10 +- src/i18n/en/screens/imageViewer.json | 3 +- src/i18n/en/screens/tabs.json | 45 ++++--- src/screens/Actions/Domain.tsx | 8 +- src/screens/Actions/Status.tsx | 31 ++--- src/screens/Compose.tsx | 2 +- src/screens/Compose/Root/Actions.tsx | 2 +- src/screens/Compose/Root/Footer/Poll.tsx | 4 +- src/screens/ImagesViewer.tsx | 2 +- src/screens/Tabs/Me.tsx | 81 +++++------ src/screens/Tabs/Me/List.tsx | 40 ++++++ src/screens/Tabs/Me/ListEdit.tsx | 163 +++++++++++++++++++++++ src/screens/Tabs/Me/Lists.tsx | 28 ++-- src/screens/Tabs/Me/ListsList.tsx | 26 ---- src/screens/Tabs/Me/Profile/Fields.tsx | 15 ++- src/screens/Tabs/Me/Profile/Name.tsx | 15 ++- src/screens/Tabs/Me/Profile/Note.tsx | 15 ++- src/screens/Tabs/Me/Profile/Root.tsx | 2 +- src/screens/Tabs/Me/Root/Logout.tsx | 34 +++-- src/screens/Tabs/Me/Settings/App.tsx | 6 +- src/utils/navigation/navigators.ts | 47 ++++--- src/utils/queryHooks/lists.ts | 54 ++++++-- 29 files changed, 506 insertions(+), 224 deletions(-) create mode 100644 src/components/Selections.tsx create mode 100644 src/screens/Tabs/Me/List.tsx create mode 100644 src/screens/Tabs/Me/ListEdit.tsx delete mode 100644 src/screens/Tabs/Me/ListsList.tsx diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index 0e452f3a..54bc1571 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -274,6 +274,7 @@ declare namespace Mastodon { type List = { id: string title: string + replies_policy: 'none' | 'list' | 'followed' } type Instance = { diff --git a/src/components/ContextMenu/status.ts b/src/components/ContextMenu/status.ts index b5cc3402..6f23326f 100644 --- a/src/components/ContextMenu/status.ts +++ b/src/components/ContextMenu/status.ts @@ -121,7 +121,8 @@ const contextMenuStatus = ({ actions, status, queryKey, rootQueryKey }: Props) = } }, { - text: t('common:buttons.cancel') + text: t('common:buttons.cancel'), + style: 'default' } ]) } diff --git a/src/components/Instance.tsx b/src/components/Instance.tsx index 46be34e8..958c4338 100644 --- a/src/components/Instance.tsx +++ b/src/components/Instance.tsx @@ -60,11 +60,11 @@ const ComponentInstance: React.FC = ({ if (instances && instances.filter(instance => instance.url === domain).length) { Alert.alert(t('update.alert.title'), t('update.alert.message'), [ { - text: t('update.alert.buttons.cancel'), + text: t('common:buttons.cancel'), style: 'cancel' }, { - text: t('update.alert.buttons.continue'), + text: t('common:buttons.continue'), onPress: () => { appsQuery.refetch() } diff --git a/src/components/Message.tsx b/src/components/Message.tsx index ddcdc5fa..85e6e62c 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -125,7 +125,7 @@ const Message = React.forwardRef((_, ref) => { shadowOpacity: theme === 'light' ? 0.16 : 0.24, shadowRadius: 4, paddingRight: StyleConstants.Spacing.M * 2, - marginTop: insets.top + marginTop: ref ? undefined : insets.top }} titleStyle={{ color: colors.primaryDefault, diff --git a/src/components/Selections.tsx b/src/components/Selections.tsx new file mode 100644 index 00000000..3e39cdfd --- /dev/null +++ b/src/components/Selections.tsx @@ -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> +} + +const Selections: React.FC = ({ 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) => ( + { + 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 } + } + }) + ) + } + } + }} + > + + + + + + + + ))} + + ) +} + +export default Selections diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 4b3fa57e..6a81edbb 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -2,7 +2,9 @@ "buttons": { "OK": "OK", "apply": "Apply", - "cancel": "Cancel" + "cancel": "Cancel", + "discard": "Discard", + "continue": "Continue" }, "customEmoji": { "accessibilityLabel": "Custom emoji {{emoji}}" @@ -18,5 +20,9 @@ "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?" + } } \ No newline at end of file diff --git a/src/i18n/en/components/instance.json b/src/i18n/en/components/instance.json index b66fda37..f59b012e 100644 --- a/src/i18n/en/components/instance.json +++ b/src/i18n/en/components/instance.json @@ -20,11 +20,7 @@ "update": { "alert": { "title": "Logged in to this instance", - "message": "You can login to another account, keeping existing logged in account", - "buttons": { - "cancel": "$t(common:buttons.cancel)", - "continue": "Continue" - } + "message": "You can login to another account, keeping existing logged in account" } } } \ No newline at end of file diff --git a/src/i18n/en/screens/compose.json b/src/i18n/en/screens/compose.json index 6290ee6a..c4ea0f80 100644 --- a/src/i18n/en/screens/compose.json +++ b/src/i18n/en/screens/compose.json @@ -28,7 +28,6 @@ "removeReply": { "title": "Replied toot could not be found", "description": "Replied toot could have been deleted. Do you want to remove it from your reference?", - "cancel": "$t(common:buttons.cancel)", "confirm": "Remove reference" } } @@ -89,8 +88,7 @@ "heading": "Choice type", "options": { "single": "Single choice", - "multiple": "Multiple choice", - "cancel": "$t(common:buttons.cancel)" + "multiple": "Multiple choice" } }, "expiration": { @@ -102,8 +100,7 @@ "21600": "6 hours", "86400": "1 day", "259200": "3 days", - "604800": "7 days", - "cancel": "$t(common:buttons.cancel)" + "604800": "7 days" } } } @@ -130,8 +127,7 @@ "public": "Public", "unlisted": "Unlisted", "private": "Followers only", - "direct": "Direct message", - "cancel": "$t(common:buttons.cancel)" + "direct": "Direct message" } }, "spoiler": { diff --git a/src/i18n/en/screens/imageViewer.json b/src/i18n/en/screens/imageViewer.json index 7b41e857..7638236a 100644 --- a/src/i18n/en/screens/imageViewer.json +++ b/src/i18n/en/screens/imageViewer.json @@ -6,8 +6,7 @@ }, "options": { "save": "Save image", - "share": "Share image", - "cancel": "$t(common:buttons.cancel)" + "share": "Share image" }, "save": { "succeed": "Image saved", diff --git a/src/i18n/en/screens/tabs.json b/src/i18n/en/screens/tabs.json index 53d0f315..22303258 100644 --- a/src/i18n/en/screens/tabs.json +++ b/src/i18n/en/screens/tabs.json @@ -49,6 +49,12 @@ "lists": { "name": "Lists" }, + "listAdd": { + "name": "Add a List" + }, + "listEdit": { + "name": "Edit List" + }, "list": { "name": "List: {{list}}" }, @@ -87,15 +93,18 @@ "XXL": "XXL" } }, - "profile": { - "cancellation": { - "title": "Change Not Saved", - "message": "Your change has not been saved. Would you discard saving the changes?", - "buttons": { - "cancel": "$t(common:buttons.cancel)", - "discard": "Discard" + "listEdit": { + "title": "Title", + "repliesPolicy": { + "heading": "Show replies to:", + "options": { + "none": "No one", + "list": "Members of the list", + "followed": "Any followed user" } - }, + } + }, + "profile": { "feedback": { "succeed": "{{type}} updated", "failed": "{{type}} update failed, please try again" @@ -125,8 +134,7 @@ "options": { "public": "Public", "unlisted": "Unlisted", - "private": "Followers only", - "cancel": "$t(common:buttons.cancel)" + "private": "Followers only" } }, "sensitive": { @@ -211,8 +219,7 @@ "title": "Logging out?", "message": "After logging out, you need to log in again", "buttons": { - "logout": "Logout", - "cancel": "$t(common:buttons.cancel)" + "logout": "Logout" } } } @@ -229,34 +236,28 @@ } }, "language": { - "heading": "$t(me.stacks.language.name)", - "options": { - "cancel": "$t(common:buttons.cancel)" - } + "heading": "$t(me.stacks.language.name)" }, "theme": { "heading": "Appearance", "options": { "auto": "As system", "light": "Light mode", - "dark": "Dark mode", - "cancel": "$t(common:buttons.cancel)" + "dark": "Dark mode" } }, "darkTheme": { "heading": "Dark theme", "options": { "lighter": "Lighter", - "darker": "Darker", - "cancel": "$t(common:buttons.cancel)" + "darker": "Darker" } }, "browser": { "heading": "Opening Link", "options": { "internal": "Inside app", - "external": "Use system browser", - "cancel": "$t(common:buttons.cancel)" + "external": "Use system browser" } }, "staticEmoji": { diff --git a/src/screens/Actions/Domain.tsx b/src/screens/Actions/Domain.tsx index 4354ac91..009987e0 100644 --- a/src/screens/Actions/Domain.tsx +++ b/src/screens/Actions/Domain.tsx @@ -43,10 +43,6 @@ const ActionsDomain: React.FC = ({ queryKey, rootQueryKey, domain, dismis t('shared.header.actions.domain.alert.title', { domain }), 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'), style: 'destructive', @@ -58,6 +54,10 @@ const ActionsDomain: React.FC = ({ queryKey, rootQueryKey, domain, dismis domain: domain }) } + }, + { + text: t('shared.header.actions.domain.alert.buttons.cancel'), + style: 'default' } ] ) diff --git a/src/screens/Actions/Status.tsx b/src/screens/Actions/Status.tsx index 9dfce233..751a9c6e 100644 --- a/src/screens/Actions/Status.tsx +++ b/src/screens/Actions/Status.tsx @@ -38,8 +38,7 @@ const ActionsStatus: React.FC = ({ const mutation = useTimelineMutation({ onMutate: true, onError: (err: any, params, oldData) => { - const theFunction = (params as MutationVarsTimelineUpdateStatusProperty) - .payload + const theFunction = (params as MutationVarsTimelineUpdateStatusProperty).payload ? (params as MutationVarsTimelineUpdateStatusProperty).payload.property : 'delete' displayMessage({ @@ -108,15 +107,7 @@ const ActionsStatus: React.FC = ({ t('shared.header.actions.status.delete.alert.message'), [ { - text: t( - 'shared.header.actions.status.delete.alert.buttons.cancel' - ), - style: 'cancel' - }, - { - text: t( - 'shared.header.actions.status.delete.alert.buttons.confirm' - ), + text: t('shared.header.actions.status.delete.alert.buttons.confirm'), style: 'destructive', onPress: async () => { dismiss() @@ -128,6 +119,10 @@ const ActionsStatus: React.FC = ({ id: status.id }) } + }, + { + text: t('shared.header.actions.status.delete.alert.buttons.cancel'), + style: 'default' } ] ) @@ -142,15 +137,7 @@ const ActionsStatus: React.FC = ({ t('shared.header.actions.status.deleteEdit.alert.message'), [ { - text: t( - 'shared.header.actions.status.deleteEdit.alert.buttons.cancel' - ), - style: 'cancel' - }, - { - text: t( - 'shared.header.actions.status.deleteEdit.alert.buttons.confirm' - ), + text: t('shared.header.actions.status.deleteEdit.alert.buttons.confirm'), style: 'destructive', onPress: async () => { let replyToStatus: Mastodon.Status | undefined = undefined @@ -177,6 +164,10 @@ const ActionsStatus: React.FC = ({ }) }) } + }, + { + text: t('shared.header.actions.status.deleteEdit.alert.buttons.cancel'), + style: 'default' } ] ) diff --git a/src/screens/Compose.tsx b/src/screens/Compose.tsx index 7d17bc82..8d8ee53c 100644 --- a/src/screens/Compose.tsx +++ b/src/screens/Compose.tsx @@ -300,7 +300,7 @@ const ScreenCompose: React.FC> = ({ t('heading.right.alert.removeReply.description'), [ { - text: t('heading.right.alert.removeReply.cancel'), + text: t('common:buttons.cancel'), onPress: () => { composeDispatch({ type: 'posting', payload: false }) }, diff --git a/src/screens/Compose/Root/Actions.tsx b/src/screens/Compose/Root/Actions.tsx index 5ab0945f..ed32fa5e 100644 --- a/src/screens/Compose/Root/Actions.tsx +++ b/src/screens/Compose/Root/Actions.tsx @@ -86,7 +86,7 @@ const ComposeActions: React.FC = () => { t('content.root.actions.visibility.options.unlisted'), t('content.root.actions.visibility.options.private'), t('content.root.actions.visibility.options.direct'), - t('content.root.actions.visibility.options.cancel') + t('common:buttons.cancel') ], cancelButtonIndex: 4, userInterfaceStyle: mode diff --git a/src/screens/Compose/Root/Footer/Poll.tsx b/src/screens/Compose/Root/Footer/Poll.tsx index ec72d5e4..b2fbcca7 100644 --- a/src/screens/Compose/Root/Footer/Poll.tsx +++ b/src/screens/Compose/Root/Footer/Poll.tsx @@ -195,7 +195,7 @@ const ComposePoll: React.FC = () => { options: [ t('content.root.footer.poll.multiple.options.single'), t('content.root.footer.poll.multiple.options.multiple'), - t('content.root.footer.poll.multiple.options.cancel') + t('common:buttons.cancel') ], cancelButtonIndex: 2, userInterfaceStyle: mode @@ -235,7 +235,7 @@ const ComposePoll: React.FC = () => { ...expirations.map(e => t(`content.root.footer.poll.expiration.options.${e}`) ), - t('content.root.footer.poll.expiration.options.cancel') + t('common:buttons.cancel') ], cancelButtonIndex: expirations.length, userInterfaceStyle: mode diff --git a/src/screens/ImagesViewer.tsx b/src/screens/ImagesViewer.tsx index 72c29015..dff8754f 100644 --- a/src/screens/ImagesViewer.tsx +++ b/src/screens/ImagesViewer.tsx @@ -53,7 +53,7 @@ const ScreenImagesViewer = ({ options: [ t('content.options.save'), t('content.options.share'), - t('content.options.cancel') + t('common:buttons.cancel') ], cancelButtonIndex: 2, userInterfaceStyle: mode diff --git a/src/screens/Tabs/Me.tsx b/src/screens/Tabs/Me.tsx index d7d2ac86..7c244ac5 100644 --- a/src/screens/Tabs/Me.tsx +++ b/src/screens/Tabs/Me.tsx @@ -7,8 +7,9 @@ import { Platform } from 'react-native' import TabMeBookmarks from './Me/Bookmarks' import TabMeConversations from './Me/Cconversations' import TabMeFavourites from './Me/Favourites' +import TabMeList from './Me/List' +import TabMeListEdit from './Me/ListEdit' import TabMeLists from './Me/Lists' -import TabMeListsList from './Me/ListsList' import TabMeProfile from './Me/Profile' import TabMePush from './Me/Push' import TabMeRoot from './Me/Root' @@ -41,9 +42,7 @@ const TabMe = React.memo( options={({ navigation }: any) => ({ title: t('me.stacks.bookmarks.name'), ...(Platform.OS === 'android' && { - headerCenter: () => ( - - ) + headerCenter: () => }), headerLeft: () => navigation.pop(1)} /> })} @@ -54,9 +53,7 @@ const TabMe = React.memo( options={({ navigation }: any) => ({ title: t('me.stacks.conversations.name'), ...(Platform.OS === 'android' && { - headerCenter: () => ( - - ) + headerCenter: () => }), headerLeft: () => navigation.pop(1)} /> })} @@ -67,29 +64,14 @@ const TabMe = React.memo( options={({ navigation }: any) => ({ title: t('me.stacks.favourites.name'), ...(Platform.OS === 'android' && { - headerCenter: () => ( - - ) + headerCenter: () => }), headerLeft: () => navigation.pop(1)} /> })} /> ({ - title: t('me.stacks.lists.name'), - ...(Platform.OS === 'android' && { - headerCenter: () => ( - - ) - }), - headerLeft: () => navigation.pop(1)} /> - })} - /> - ({ title: t('me.stacks.list.name', { list: route.params.title }), ...(Platform.OS === 'android' && { @@ -104,6 +86,29 @@ const TabMe = React.memo( headerLeft: () => navigation.pop(1)} /> })} /> + + }) + }} + /> + ({ + title: t('me.stacks.lists.name'), + ...(Platform.OS === 'android' && { + headerCenter: () => + }), + headerLeft: () => navigation.pop(1)} /> + })} + /> ( - - ) + headerCenter: () => }), headerLeft: () => ( - navigation.goBack()} - /> + navigation.goBack()} /> ) })} /> @@ -146,9 +146,7 @@ const TabMe = React.memo( options={({ navigation }: any) => ({ title: t('me.stacks.fontSize.name'), ...(Platform.OS === 'android' && { - headerCenter: () => ( - - ) + headerCenter: () => }), headerLeft: () => navigation.pop(1)} /> })} @@ -159,9 +157,7 @@ const TabMe = React.memo( options={({ navigation }: any) => ({ title: t('me.stacks.language.name'), ...(Platform.OS === 'android' && { - headerCenter: () => ( - - ) + headerCenter: () => }), headerLeft: () => navigation.pop(1)} /> })} @@ -174,15 +170,10 @@ const TabMe = React.memo( headerShown: true, title: t('me.stacks.switch.name'), ...(Platform.OS === 'android' && { - headerCenter: () => ( - - ) + headerCenter: () => }), headerLeft: () => ( - navigation.goBack()} - /> + navigation.goBack()} /> ) })} /> diff --git a/src/screens/Tabs/Me/List.tsx b/src/screens/Tabs/Me/List.tsx new file mode 100644 index 00000000..1a776be3 --- /dev/null +++ b/src/screens/Tabs/Me/List.tsx @@ -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> = ({ + navigation, + route: { key, params } +}) => { + const { t } = useTranslation('screenTabs') + const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', list: params.id }] + + useEffect(() => { + navigation.setOptions({ + headerRight: () => ( + + navigation.navigate('Tab-Me-List-Edit', { type: 'edit', payload: params, key }) + } + /> + ) + }) + }, [params]) + + return ( + + }} + /> + ) +} + +export default TabMeList diff --git a/src/screens/Tabs/Me/ListEdit.tsx b/src/screens/Tabs/Me/ListEdit.tsx new file mode 100644 index 00000000..a4eab6ee --- /dev/null +++ b/src/screens/Tabs/Me/ListEdit.tsx @@ -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> = ({ + 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(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(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: () => ( + { + 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: () => ( + { + 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 ( + + + + + {t('me.listEdit.repliesPolicy.heading')} + + + + + + ) +} + +export default TabMeListEdit diff --git a/src/screens/Tabs/Me/Lists.tsx b/src/screens/Tabs/Me/Lists.tsx index 1a03abe5..76efe7c0 100644 --- a/src/screens/Tabs/Me/Lists.tsx +++ b/src/screens/Tabs/Me/Lists.tsx @@ -1,12 +1,25 @@ +import { HeaderRight } from '@components/Header' import { MenuContainer, MenuRow } from '@components/Menu' import { TabMeStackScreenProps } from '@utils/navigation/navigators' import { useListsQuery } from '@utils/queryHooks/lists' -import React from 'react' +import React, { useEffect } from 'react' +import { useTranslation } from 'react-i18next' -const TabMeLists: React.FC> = ({ - navigation -}) => { +const TabMeLists: React.FC> = ({ navigation }) => { const { data } = useListsQuery({}) + const { t } = useTranslation('screenTabs') + + useEffect(() => { + navigation.setOptions({ + headerRight: () => ( + navigation.navigate('Tab-Me-List-Edit', { type: 'add' })} + /> + ) + }) + }, []) return ( @@ -16,12 +29,7 @@ const TabMeLists: React.FC> = ({ iconFront='List' iconBack='ChevronRight' title={d.title} - onPress={() => - navigation.navigate('Tab-Me-Lists-List', { - list: d.id, - title: d.title - }) - } + onPress={() => navigation.navigate('Tab-Me-List', d)} /> ))} diff --git a/src/screens/Tabs/Me/ListsList.tsx b/src/screens/Tabs/Me/ListsList.tsx deleted file mode 100644 index 3c76cf85..00000000 --- a/src/screens/Tabs/Me/ListsList.tsx +++ /dev/null @@ -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> = ({ - route: { - params: { list } - } -}) => { - const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', list }] - - return ( - ( - - ) - }} - /> - ) -} - -export default TabMeListsList diff --git a/src/screens/Tabs/Me/Profile/Fields.tsx b/src/screens/Tabs/Me/Profile/Fields.tsx index a39b62d6..52cc1b4a 100644 --- a/src/screens/Tabs/Me/Profile/Fields.tsx +++ b/src/screens/Tabs/Me/Profile/Fields.tsx @@ -90,20 +90,21 @@ const TabMeProfileFields: React.FC< navigation.setOptions({ headerLeft: () => ( { if (dirty) { Alert.alert( - t('me.profile.cancellation.title'), - t('me.profile.cancellation.message'), + t('common:discard.title'), + t('common:discard.message'), [ { - text: t('me.profile.cancellation.buttons.cancel'), - style: 'default' - }, - { - text: t('me.profile.cancellation.buttons.discard'), + text: t('common:buttons.discard'), style: 'destructive', onPress: () => navigation.navigate('Tab-Me-Profile-Root') + }, + { + text: t('common:buttons.cancel'), + style: 'default' } ] ) diff --git a/src/screens/Tabs/Me/Profile/Name.tsx b/src/screens/Tabs/Me/Profile/Name.tsx index 0760ef7a..e2add288 100644 --- a/src/screens/Tabs/Me/Profile/Name.tsx +++ b/src/screens/Tabs/Me/Profile/Name.tsx @@ -44,20 +44,21 @@ const TabMeProfileName: React.FC< navigation.setOptions({ headerLeft: () => ( { if (dirty) { Alert.alert( - t('me.profile.cancellation.title'), - t('me.profile.cancellation.message'), + t('common:discard.title'), + t('common:discard.message'), [ { - text: t('me.profile.cancellation.buttons.cancel'), - style: 'default' - }, - { - text: t('me.profile.cancellation.buttons.discard'), + text: t('common:buttons.discard'), style: 'destructive', onPress: () => navigation.navigate('Tab-Me-Profile-Root') + }, + { + text: t('common:buttons.cancel'), + style: 'default' } ] ) diff --git a/src/screens/Tabs/Me/Profile/Note.tsx b/src/screens/Tabs/Me/Profile/Note.tsx index cda09ebf..f3cc5407 100644 --- a/src/screens/Tabs/Me/Profile/Note.tsx +++ b/src/screens/Tabs/Me/Profile/Note.tsx @@ -44,20 +44,21 @@ const TabMeProfileNote: React.FC< navigation.setOptions({ headerLeft: () => ( { if (dirty) { Alert.alert( - t('me.profile.cancellation.title'), - t('me.profile.cancellation.message'), + t('common:discard.title'), + t('common:discard.message'), [ { - text: t('me.profile.cancellation.buttons.cancel'), - style: 'default' - }, - { - text: t('me.profile.cancellation.buttons.discard'), + text: t('common:buttons.discard'), style: 'destructive', onPress: () => navigation.navigate('Tab-Me-Profile-Root') + }, + { + text: t('common:buttons.cancel'), + style: 'default' } ] ) diff --git a/src/screens/Tabs/Me/Profile/Root.tsx b/src/screens/Tabs/Me/Profile/Root.tsx index 10d43dd8..d66aa704 100644 --- a/src/screens/Tabs/Me/Profile/Root.tsx +++ b/src/screens/Tabs/Me/Profile/Root.tsx @@ -33,7 +33,7 @@ const TabMeProfileRoot: React.FC< t('me.profile.root.visibility.options.public'), t('me.profile.root.visibility.options.unlisted'), t('me.profile.root.visibility.options.private'), - t('me.profile.root.visibility.options.cancel') + t('common:buttons.cancel') ], cancelButtonIndex: 3, userInterfaceStyle: mode diff --git a/src/screens/Tabs/Me/Root/Logout.tsx b/src/screens/Tabs/Me/Root/Logout.tsx index 5a07c7f0..a25f7bb3 100644 --- a/src/screens/Tabs/Me/Root/Logout.tsx +++ b/src/screens/Tabs/Me/Root/Logout.tsx @@ -27,27 +27,23 @@ const Logout: React.FC = () => { }} destructive onPress={() => - Alert.alert( - t('me.root.logout.alert.title'), - t('me.root.logout.alert.message'), - [ - { - text: t('me.root.logout.alert.buttons.logout'), - style: 'destructive' as const, - onPress: () => { - if (instance) { - haptics('Success') - queryClient.clear() - dispatch(removeInstance(instance)) - } + Alert.alert(t('me.root.logout.alert.title'), t('me.root.logout.alert.message'), [ + { + text: t('me.root.logout.alert.buttons.logout'), + style: 'destructive', + onPress: () => { + if (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' + } + ]) } /> ) diff --git a/src/screens/Tabs/Me/Settings/App.tsx b/src/screens/Tabs/Me/Settings/App.tsx index 7621a635..e927104c 100644 --- a/src/screens/Tabs/Me/Settings/App.tsx +++ b/src/screens/Tabs/Me/Settings/App.tsx @@ -69,7 +69,7 @@ const SettingsApp: React.FC = () => { t('me.settings.theme.options.auto'), t('me.settings.theme.options.light'), t('me.settings.theme.options.dark'), - t('me.settings.theme.options.cancel') + t('common:buttons.cancel') ], cancelButtonIndex: 3 }, @@ -103,7 +103,7 @@ const SettingsApp: React.FC = () => { options: [ t('me.settings.darkTheme.options.lighter'), t('me.settings.darkTheme.options.darker'), - t('me.settings.darkTheme.options.cancel') + t('common:buttons.cancel') ], cancelButtonIndex: 2 }, @@ -133,7 +133,7 @@ const SettingsApp: React.FC = () => { options: [ t('me.settings.browser.options.internal'), t('me.settings.browser.options.external'), - t('me.settings.browser.options.cancel') + t('common:buttons.cancel') ], cancelButtonIndex: 2 }, diff --git a/src/utils/navigation/navigators.ts b/src/utils/navigation/navigators.ts index f75eaba8..f9659575 100644 --- a/src/utils/navigation/navigators.ts +++ b/src/utils/navigation/navigators.ts @@ -63,17 +63,18 @@ export type RootStackParamList = { share?: { text?: string; media?: { uri: string; mime: string }[] } } } -export type RootStackScreenProps = - NativeStackScreenProps +export type RootStackScreenProps = NativeStackScreenProps< + RootStackParamList, + T +> export type ScreenComposeStackParamList = { 'Screen-Compose-Root': undefined 'Screen-Compose-EditAttachment': { index: number } 'Screen-Compose-DraftsList': { timestamp: number } } -export type ScreenComposeStackScreenProps< - T extends keyof ScreenComposeStackParamList -> = NativeStackScreenProps +export type ScreenComposeStackScreenProps = + NativeStackScreenProps export type ScreenTabsStackParamList = { 'Tab-Local': NavigatorScreenParams @@ -82,8 +83,10 @@ export type ScreenTabsStackParamList = { 'Tab-Notifications': NavigatorScreenParams 'Tab-Me': NavigatorScreenParams } -export type ScreenTabsScreenProps = - BottomTabScreenProps +export type ScreenTabsScreenProps = BottomTabScreenProps< + ScreenTabsStackParamList, + T +> export type TabSharedStackParamList = { 'Tab-Shared-Account': { @@ -135,11 +138,17 @@ export type TabMeStackParamList = { 'Tab-Me-Bookmarks': undefined 'Tab-Me-Conversations': 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-List': { - list: Mastodon.List['id'] - title: Mastodon.List['title'] - } 'Tab-Me-Profile': undefined 'Tab-Me-Push': undefined 'Tab-Me-Settings': undefined @@ -147,11 +156,12 @@ export type TabMeStackParamList = { 'Tab-Me-Settings-Language': undefined 'Tab-Me-Switch': undefined } & TabSharedStackParamList -export type TabMeStackScreenProps = - NativeStackScreenProps -export type TabMeStackNavigationProp< - RouteName extends keyof TabMeStackParamList -> = StackNavigationProp +export type TabMeStackScreenProps = NativeStackScreenProps< + TabMeStackParamList, + T +> +export type TabMeStackNavigationProp = + StackNavigationProp export type TabMeProfileStackParamList = { 'Tab-Me-Profile-Root': undefined @@ -165,6 +175,5 @@ export type TabMeProfileStackParamList = { fields?: Mastodon.Source['fields'] } } -export type TabMeProfileStackScreenProps< - T extends keyof TabMeProfileStackParamList -> = NativeStackScreenProps +export type TabMeProfileStackScreenProps = + NativeStackScreenProps diff --git a/src/utils/queryHooks/lists.ts b/src/utils/queryHooks/lists.ts index c1d381de..ac09607f 100644 --- a/src/utils/queryHooks/lists.ts +++ b/src/utils/queryHooks/lists.ts @@ -1,8 +1,8 @@ import apiInstance from '@api/instance' 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 res = await apiInstance({ @@ -12,13 +12,49 @@ const queryFunction = async () => { return res.body } -const useListsQuery = ({ - options -}: { - options?: UseQueryOptions -}) => { - const queryKey: QueryKey = ['Lists'] +const useListsQuery = ({ options }: { options?: UseQueryOptions }) => { + const queryKey: QueryKeyLists = ['Lists'] return useQuery(queryKey, queryFunction, options) } -export { useListsQuery } +type MutationVarsLists = + | { + type: 'add' + payload: Omit + } + | { + 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({ + 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({ + method: 'put', + url: `lists/${params.payload.id}`, + body + }).then(res => res.body) + } +} + +const useListsMutation = ( + options: UseMutationOptions +) => { + return useMutation(mutationFunction, options) +} + +export { useListsQuery, useListsMutation }