diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx index 518305dd..cb9cb39d 100644 --- a/src/components/Parse/HTML.tsx +++ b/src/components/Parse/HTML.tsx @@ -302,7 +302,7 @@ const ParseHTML = React.memo( /> ) }, - () => true + (prev, next) => prev.content === next.content ) export default ParseHTML diff --git a/src/components/Timeline/Shared/Content.tsx b/src/components/Timeline/Shared/Content.tsx index 576f3247..31c1ccbb 100644 --- a/src/components/Timeline/Shared/Content.tsx +++ b/src/components/Timeline/Shared/Content.tsx @@ -72,7 +72,7 @@ const TimelineContent = React.memo( ) }, - () => true + (prev, next) => prev.status.content === next.status.content ) export default TimelineContent diff --git a/src/i18n/en/components/timeline.json b/src/i18n/en/components/timeline.json index cf4b5596..8ee94ff5 100644 --- a/src/i18n/en/components/timeline.json +++ b/src/i18n/en/components/timeline.json @@ -167,11 +167,23 @@ }, "status": { "heading": "About toot", + "edit": { + "function": "Edit toot", + "button": "Edit this toot" + }, "delete": { "function": "Delete toot", - "button": "Delete this toot" + "button": "Delete this toot", + "alert": { + "title": "Confirm deleting toot?", + "message": "Are you sure to delete this toot? All boosts and favourites will be cleared, including all replies.", + "buttons": { + "confirm": "Confirm deleting", + "cancel": "$t(common:buttons.cancel)" + } + } }, - "edit": { + "deleteEdit": { "function": "Delete toot", "button": "Delete and re-draft", "alert": { diff --git a/src/i18n/en/screens/compose.json b/src/i18n/en/screens/compose.json index a0e41ae1..15eb4537 100644 --- a/src/i18n/en/screens/compose.json +++ b/src/i18n/en/screens/compose.json @@ -16,6 +16,7 @@ "default": "Toot", "conversation": "Toot DM", "reply": "Toot reply", + "deleteEdit": "Toot", "edit": "Toot" }, "alert": { diff --git a/src/i18n/ko/components/timeline.json b/src/i18n/ko/components/timeline.json index f78c80a4..4a0d1729 100644 --- a/src/i18n/ko/components/timeline.json +++ b/src/i18n/ko/components/timeline.json @@ -160,7 +160,7 @@ "function": "툿 삭제", "button": "이 툿 삭제" }, - "edit": { + "deleteEdit": { "function": "툿 삭제", "button": "삭제하고 다시 쓰기", "alert": { diff --git a/src/i18n/vi/components/timeline.json b/src/i18n/vi/components/timeline.json index a1eb9053..d3e84f49 100644 --- a/src/i18n/vi/components/timeline.json +++ b/src/i18n/vi/components/timeline.json @@ -162,7 +162,7 @@ "function": "Xóa tút", "button": "Xóa tút này" }, - "edit": { + "deleteEdit": { "function": "Xóa tút", "button": "Xóa và viết lại", "alert": { diff --git a/src/i18n/zh-Hans/components/timeline.json b/src/i18n/zh-Hans/components/timeline.json index dd2756d6..d39a21c3 100644 --- a/src/i18n/zh-Hans/components/timeline.json +++ b/src/i18n/zh-Hans/components/timeline.json @@ -162,7 +162,7 @@ "function": "删除", "button": "删除此条嘟文" }, - "edit": { + "deleteEdit": { "function": "删除", "button": "删除并重新编辑此条嘟文", "alert": { diff --git a/src/screens/Actions/Status.tsx b/src/screens/Actions/Status.tsx index d509c549..da423c41 100644 --- a/src/screens/Actions/Status.tsx +++ b/src/screens/Actions/Status.tsx @@ -14,6 +14,8 @@ import { useTheme } from '@utils/styles/ThemeManager' import apiInstance from '@api/instance' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { RootStackParamList } from '@utils/navigation/navigators' +import { useSelector } from 'react-redux' +import { checkInstanceFeature } from '@utils/slices/instancesSlice' export interface Props { navigation: NativeStackNavigationProp @@ -59,22 +61,89 @@ const ActionsStatus: React.FC = ({ } }) + const canEditPost = useSelector(checkInstanceFeature('edit_post')) + return ( + {canEditPost ? ( + { + analytics('timeline_shared_headeractions_status_edit_press', { + page: queryKey && queryKey[1].page + }) + let replyToStatus: Mastodon.Status | undefined = undefined + if (status.in_reply_to_id) { + replyToStatus = await apiInstance({ + method: 'get', + url: `statuses/${status.in_reply_to_id}` + }).then(res => res.body) + } + apiInstance<{ + id: Mastodon.Status['id'] + text: NonNullable + spoiler_text: Mastodon.Status['spoiler_text'] + }>({ + method: 'get', + url: `statuses/${status.id}/source` + }).then(res => { + dismiss() + navigation.navigate('Screen-Compose', { + type: 'edit', + incomingStatus: { + ...status, + text: res.body.text, + spoiler_text: res.body.spoiler_text + }, + ...(replyToStatus && { replyToStatus }), + queryKey, + rootQueryKey + }) + }) + }} + iconFront='Edit3' + title={t('shared.header.actions.status.edit.button')} + /> + ) : null} { analytics('timeline_shared_headeractions_status_delete_press', { page: queryKey && queryKey[1].page }) - dismiss() - mutation.mutate({ - type: 'deleteItem', - source: 'statuses', - queryKey, - rootQueryKey, - id: status.id - }) + Alert.alert( + t('shared.header.actions.status.delete.alert.title'), + 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' + ), + style: 'destructive', + onPress: async () => { + analytics( + 'timeline_shared_headeractions_status_delete_confirm', + { + page: queryKey && queryKey[1].page + } + ) + dismiss() + mutation.mutate({ + type: 'deleteItem', + source: 'statuses', + queryKey, + rootQueryKey, + id: status.id + }) + } + } + ] + ) }} iconFront='Trash' title={t('shared.header.actions.status.delete.button')} @@ -85,18 +154,18 @@ const ActionsStatus: React.FC = ({ page: queryKey && queryKey[1].page }) Alert.alert( - t('shared.header.actions.status.edit.alert.title'), - t('shared.header.actions.status.edit.alert.message'), + t('shared.header.actions.status.deleteEdit.alert.title'), + t('shared.header.actions.status.deleteEdit.alert.message'), [ { text: t( - 'shared.header.actions.status.edit.alert.buttons.cancel' + 'shared.header.actions.status.deleteEdit.alert.buttons.cancel' ), style: 'cancel' }, { text: t( - 'shared.header.actions.status.edit.alert.buttons.confirm' + 'shared.header.actions.status.deleteEdit.alert.buttons.confirm' ), style: 'destructive', onPress: async () => { @@ -106,7 +175,7 @@ const ActionsStatus: React.FC = ({ page: queryKey && queryKey[1].page } ) - let replyToStatus: Mastodon.Status + let replyToStatus: Mastodon.Status | undefined = undefined if (status.in_reply_to_id) { replyToStatus = await apiInstance({ method: 'get', @@ -122,10 +191,9 @@ const ActionsStatus: React.FC = ({ }) .then(res => { dismiss() - // @ts-ignore navigation.navigate('Screen-Compose', { - type: 'edit', - incomingStatus: res.body, + type: 'deleteEdit', + incomingStatus: res.body as Mastodon.Status, ...(replyToStatus && { replyToStatus }), queryKey }) @@ -136,7 +204,7 @@ const ActionsStatus: React.FC = ({ ) }} iconFront='Edit' - title={t('shared.header.actions.status.edit.button')} + title={t('shared.header.actions.status.deleteEdit.button')} /> { diff --git a/src/screens/Compose.tsx b/src/screens/Compose.tsx index 92b23b8c..dab9093d 100644 --- a/src/screens/Compose.tsx +++ b/src/screens/Compose.tsx @@ -1,11 +1,14 @@ import analytics from '@components/analytics' -import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header' +import { HeaderLeft, HeaderRight } from '@components/Header' import { createNativeStackNavigator } from '@react-navigation/native-stack' import haptics from '@root/components/haptics' import formatText from '@screens/Compose/formatText' import ComposeRoot from '@screens/Compose/Root' import { RootStackScreenProps } from '@utils/navigation/navigators' -import { QueryKeyTimeline } from '@utils/queryHooks/timeline' +import { + QueryKeyTimeline, + useTimelineMutation +} from '@utils/queryHooks/timeline' import { updateStoreReview } from '@utils/slices/contextsSlice' import { getInstanceAccount, @@ -133,6 +136,7 @@ const ScreenCompose: React.FC> = ({ useEffect(() => { switch (params?.type) { case 'edit': + case 'deleteEdit': if (params.incomingStatus.spoiler_text) { formatText({ textInput: 'spoiler', @@ -268,6 +272,7 @@ const ScreenCompose: React.FC> = ({ } return false }, [totalTextCount, composeState.attachments.uploads, composeState.text.raw]) + const mutateTimeline = useTimelineMutation({ onMutate: true }) const headerRight = useCallback( () => ( > = ({ composeDispatch({ type: 'posting', payload: true }) composePost(params, composeState) - .then(() => { + .then(res => { haptics('Success') if ( Platform.OS === 'ios' && @@ -300,6 +305,15 @@ const ScreenCompose: React.FC> = ({ switch (params?.type) { case 'edit': + console.log('firing mutation') + mutateTimeline.mutate({ + type: 'editItem', + queryKey: params.queryKey, + rootQueryKey: params.rootQueryKey, + status: res.body + }) + break + case 'deleteEdit': case 'reply': if (params?.queryKey && params.queryKey[1].page === 'Toot') { queryClient.invalidateQueries(params.queryKey) diff --git a/src/screens/Compose/utils/parseState.ts b/src/screens/Compose/utils/parseState.ts index ae874d18..285206de 100644 --- a/src/screens/Compose/utils/parseState.ts +++ b/src/screens/Compose/utils/parseState.ts @@ -39,6 +39,7 @@ const composeParseState = ( ): ComposeState => { switch (params.type) { case 'edit': + case 'deleteEdit': return { ...composeInitialState, dirty: true, diff --git a/src/screens/Compose/utils/post.ts b/src/screens/Compose/utils/post.ts index 9be1334c..d1ee13c4 100644 --- a/src/screens/Compose/utils/post.ts +++ b/src/screens/Compose/utils/post.ts @@ -51,8 +51,11 @@ const composePost = async ( formData.append('visibility', composeState.visibility) return apiInstance({ - method: 'post', - url: 'statuses', + method: params?.type === 'edit' ? 'put' : 'post', + url: + params?.type === 'edit' + ? `statuses/${params.incomingStatus.id}` + : 'statuses', headers: { 'Idempotency-Key': await Crypto.digestStringAsync( Crypto.CryptoDigestAlgorithm.SHA256, @@ -67,7 +70,9 @@ const composePost = async ( composeState.attachments.sensitive + composeState.attachments.uploads.map(upload => upload.remote?.id) + composeState.visibility + - (params?.type === 'edit' ? Math.random() : '') + (params?.type === 'edit' || params?.type === 'deleteEdit' + ? Math.random() + : '') ) }, body: formData diff --git a/src/utils/navigation/navigators.ts b/src/utils/navigation/navigators.ts index b4956ac7..ef8e7a5b 100644 --- a/src/utils/navigation/navigators.ts +++ b/src/utils/navigation/navigators.ts @@ -26,31 +26,20 @@ export type RootStackParamList = { type: 'edit' incomingStatus: Mastodon.Status replyToStatus?: Mastodon.Status - queryKey?: [ - 'Timeline', - { - page: App.Pages - hashtag?: Mastodon.Tag['name'] - list?: Mastodon.List['id'] - toot?: Mastodon.Status['id'] - account?: Mastodon.Account['id'] - } - ] + queryKey?: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline + } + | { + type: 'deleteEdit' + incomingStatus: Mastodon.Status + replyToStatus?: Mastodon.Status + queryKey?: QueryKeyTimeline } | { type: 'reply' incomingStatus: Mastodon.Status accts: Mastodon.Account['acct'][] - queryKey?: [ - 'Timeline', - { - page: App.Pages - hashtag?: Mastodon.Tag['name'] - list?: Mastodon.List['id'] - toot?: Mastodon.Status['id'] - account?: Mastodon.Account['id'] - } - ] + queryKey?: QueryKeyTimeline } | { type: 'conversation' diff --git a/src/utils/queryHooks/timeline.ts b/src/utils/queryHooks/timeline.ts index 384db03e..b2956faa 100644 --- a/src/utils/queryHooks/timeline.ts +++ b/src/utils/queryHooks/timeline.ts @@ -13,6 +13,7 @@ import { useMutation } from 'react-query' import deleteItem from './timeline/deleteItem' +import editItem from './timeline/editItem' import updateStatusProperty from './timeline/updateStatusProperty' export type QueryKeyTimeline = [ @@ -303,13 +304,21 @@ export type MutationVarsTimelineUpdateAccountProperty = { } } +export type MutationVarsTimelineEditItem = { + // This is for editing status + type: 'editItem' + queryKey?: QueryKeyTimeline + rootQueryKey?: QueryKeyTimeline + status: Mastodon.Status +} + export type MutationVarsTimelineDeleteItem = { // This is for deleting status and conversation type: 'deleteItem' source: 'statuses' | 'conversations' queryKey?: QueryKeyTimeline rootQueryKey?: QueryKeyTimeline - id: Mastodon.Conversation['id'] + id: Mastodon.Status['id'] } export type MutationVarsTimelineDomainBlock = { @@ -322,6 +331,7 @@ export type MutationVarsTimelineDomainBlock = { export type MutationVarsTimeline = | MutationVarsTimelineUpdateStatusProperty | MutationVarsTimelineUpdateAccountProperty + | MutationVarsTimelineEditItem | MutationVarsTimelineDeleteItem | MutationVarsTimelineDomainBlock @@ -371,6 +381,8 @@ const mutationFunction = async (params: MutationVarsTimeline) => { } }) } + case 'editItem': + return { body: params.status } case 'deleteItem': return apiInstance({ method: 'delete', @@ -423,6 +435,9 @@ const useTimelineMutation = ({ case 'updateStatusProperty': updateStatusProperty(params) break + case 'editItem': + editItem(params) + break case 'deleteItem': deleteItem(params) break diff --git a/src/utils/queryHooks/timeline/deleteItem.ts b/src/utils/queryHooks/timeline/deleteItem.ts index fa2ddaa3..d2911bf8 100644 --- a/src/utils/queryHooks/timeline/deleteItem.ts +++ b/src/utils/queryHooks/timeline/deleteItem.ts @@ -6,11 +6,7 @@ const deleteItem = ({ queryKey, rootQueryKey, id -}: { - queryKey?: MutationVarsTimelineDeleteItem['queryKey'] - rootQueryKey?: MutationVarsTimelineDeleteItem['rootQueryKey'] - id: MutationVarsTimelineDeleteItem['id'] -}) => { +}: MutationVarsTimelineDeleteItem) => { queryKey && queryClient.setQueryData | undefined>(queryKey, old => { if (old) { diff --git a/src/utils/queryHooks/timeline/editItem.ts b/src/utils/queryHooks/timeline/editItem.ts new file mode 100644 index 00000000..0ea5a818 --- /dev/null +++ b/src/utils/queryHooks/timeline/editItem.ts @@ -0,0 +1,52 @@ +import queryClient from '@helpers/queryClient' +import { InfiniteData } from 'react-query' +import { MutationVarsTimelineEditItem } from '../timeline' + +const editItem = ({ + queryKey, + rootQueryKey, + status +}: MutationVarsTimelineEditItem) => { + console.log('START') + queryKey && + queryClient.setQueryData | undefined>(queryKey, old => { + if (old) { + old.pages = old.pages.map(page => { + page.body = page.body.map((item: Mastodon.Status) => { + if (item.id === status.id) { + console.log('found queryKey', queryKey) + console.log('new content', status.content) + item = status + } + return item + }) + return page + }) + return old + } + }) + + rootQueryKey && + queryClient.setQueryData | undefined>( + rootQueryKey, + old => { + if (old) { + old.pages = old.pages.map(page => { + page.body = page.body.map((item: Mastodon.Status) => { + if (item.id === status.id) { + console.log('found rootQueryKey', queryKey) + console.log('new content', status.content) + item = status + } + return item + }) + return page + }) + return old + } + } + ) + console.log('EDN') +} + +export default editItem diff --git a/src/utils/queryHooks/timeline/updateStatusProperty.ts b/src/utils/queryHooks/timeline/updateStatusProperty.ts index 893aa52d..2a697404 100644 --- a/src/utils/queryHooks/timeline/updateStatusProperty.ts +++ b/src/utils/queryHooks/timeline/updateStatusProperty.ts @@ -14,13 +14,7 @@ const updateStatusProperty = ({ id, reblog, payload -}: { - queryKey: MutationVarsTimelineUpdateStatusProperty['queryKey'] - rootQueryKey?: MutationVarsTimelineUpdateStatusProperty['rootQueryKey'] - id: MutationVarsTimelineUpdateStatusProperty['id'] - reblog?: MutationVarsTimelineUpdateStatusProperty['reblog'] - payload: MutationVarsTimelineUpdateStatusProperty['payload'] -}) => { +}: MutationVarsTimelineUpdateStatusProperty) => { queryClient.setQueryData | undefined>( queryKey, old => { diff --git a/src/utils/slices/instancesSlice.ts b/src/utils/slices/instancesSlice.ts index 5d62ee10..605c036a 100644 --- a/src/utils/slices/instancesSlice.ts +++ b/src/utils/slices/instancesSlice.ts @@ -344,14 +344,16 @@ export const getInstanceVersion = ({ instances: { instances } }: RootState) => instances[findInstanceActive(instances)]?.version export const checkInstanceFeature = (feature: string) => - ({ instances: { instances } }: RootState) => { - return features - .filter(f => f.feature === feature) - .filter( - f => - parseFloat(instances[findInstanceActive(instances)]?.version) >= - f.version - ) + ({ instances: { instances } }: RootState): Boolean => { + return ( + features + .filter(f => f.feature === feature) + .filter( + f => + parseFloat(instances[findInstanceActive(instances)]?.version) >= + f.version + ).length > 0 + ) } /* Get Instance Configuration */