1
0
mirror of https://github.com/tooot-app/app synced 2025-02-18 12:50:46 +01:00

Basic editing works

This commit is contained in:
Zhiyuan Zheng 2022-04-30 17:44:39 +02:00
parent 95ec76f411
commit d4f91a5756
17 changed files with 220 additions and 71 deletions

View File

@ -302,7 +302,7 @@ const ParseHTML = React.memo(
/> />
) )
}, },
() => true (prev, next) => prev.content === next.content
) )
export default ParseHTML export default ParseHTML

View File

@ -72,7 +72,7 @@ const TimelineContent = React.memo(
</> </>
) )
}, },
() => true (prev, next) => prev.status.content === next.status.content
) )
export default TimelineContent export default TimelineContent

View File

@ -167,11 +167,23 @@
}, },
"status": { "status": {
"heading": "About toot", "heading": "About toot",
"edit": {
"function": "Edit toot",
"button": "Edit this toot"
},
"delete": { "delete": {
"function": "Delete toot", "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", "function": "Delete toot",
"button": "Delete and re-draft", "button": "Delete and re-draft",
"alert": { "alert": {

View File

@ -16,6 +16,7 @@
"default": "Toot", "default": "Toot",
"conversation": "Toot DM", "conversation": "Toot DM",
"reply": "Toot reply", "reply": "Toot reply",
"deleteEdit": "Toot",
"edit": "Toot" "edit": "Toot"
}, },
"alert": { "alert": {

View File

@ -160,7 +160,7 @@
"function": "툿 삭제", "function": "툿 삭제",
"button": "이 툿 삭제" "button": "이 툿 삭제"
}, },
"edit": { "deleteEdit": {
"function": "툿 삭제", "function": "툿 삭제",
"button": "삭제하고 다시 쓰기", "button": "삭제하고 다시 쓰기",
"alert": { "alert": {

View File

@ -162,7 +162,7 @@
"function": "Xóa tút", "function": "Xóa tút",
"button": "Xóa tút này" "button": "Xóa tút này"
}, },
"edit": { "deleteEdit": {
"function": "Xóa tút", "function": "Xóa tút",
"button": "Xóa và viết lại", "button": "Xóa và viết lại",
"alert": { "alert": {

View File

@ -162,7 +162,7 @@
"function": "删除", "function": "删除",
"button": "删除此条嘟文" "button": "删除此条嘟文"
}, },
"edit": { "deleteEdit": {
"function": "删除", "function": "删除",
"button": "删除并重新编辑此条嘟文", "button": "删除并重新编辑此条嘟文",
"alert": { "alert": {

View File

@ -14,6 +14,8 @@ import { useTheme } from '@utils/styles/ThemeManager'
import apiInstance from '@api/instance' import apiInstance from '@api/instance'
import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { RootStackParamList } from '@utils/navigation/navigators' import { RootStackParamList } from '@utils/navigation/navigators'
import { useSelector } from 'react-redux'
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
export interface Props { export interface Props {
navigation: NativeStackNavigationProp<RootStackParamList, 'Screen-Actions'> navigation: NativeStackNavigationProp<RootStackParamList, 'Screen-Actions'>
@ -59,22 +61,89 @@ const ActionsStatus: React.FC<Props> = ({
} }
}) })
const canEditPost = useSelector(checkInstanceFeature('edit_post'))
return ( return (
<MenuContainer> <MenuContainer>
<MenuHeader heading={t('shared.header.actions.status.heading')} /> <MenuHeader heading={t('shared.header.actions.status.heading')} />
{canEditPost ? (
<MenuRow
onPress={async () => {
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<Mastodon.Status>({
method: 'get',
url: `statuses/${status.in_reply_to_id}`
}).then(res => res.body)
}
apiInstance<{
id: Mastodon.Status['id']
text: NonNullable<Mastodon.Status['text']>
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}
<MenuRow <MenuRow
onPress={() => { onPress={() => {
analytics('timeline_shared_headeractions_status_delete_press', { analytics('timeline_shared_headeractions_status_delete_press', {
page: queryKey && queryKey[1].page page: queryKey && queryKey[1].page
}) })
dismiss() Alert.alert(
mutation.mutate({ t('shared.header.actions.status.delete.alert.title'),
type: 'deleteItem', t('shared.header.actions.status.delete.alert.message'),
source: 'statuses', [
queryKey, {
rootQueryKey, text: t(
id: status.id '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' iconFront='Trash'
title={t('shared.header.actions.status.delete.button')} title={t('shared.header.actions.status.delete.button')}
@ -85,18 +154,18 @@ const ActionsStatus: React.FC<Props> = ({
page: queryKey && queryKey[1].page page: queryKey && queryKey[1].page
}) })
Alert.alert( Alert.alert(
t('shared.header.actions.status.edit.alert.title'), t('shared.header.actions.status.deleteEdit.alert.title'),
t('shared.header.actions.status.edit.alert.message'), t('shared.header.actions.status.deleteEdit.alert.message'),
[ [
{ {
text: t( text: t(
'shared.header.actions.status.edit.alert.buttons.cancel' 'shared.header.actions.status.deleteEdit.alert.buttons.cancel'
), ),
style: 'cancel' style: 'cancel'
}, },
{ {
text: t( text: t(
'shared.header.actions.status.edit.alert.buttons.confirm' 'shared.header.actions.status.deleteEdit.alert.buttons.confirm'
), ),
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
@ -106,7 +175,7 @@ const ActionsStatus: React.FC<Props> = ({
page: queryKey && queryKey[1].page page: queryKey && queryKey[1].page
} }
) )
let replyToStatus: Mastodon.Status let replyToStatus: Mastodon.Status | undefined = undefined
if (status.in_reply_to_id) { if (status.in_reply_to_id) {
replyToStatus = await apiInstance<Mastodon.Status>({ replyToStatus = await apiInstance<Mastodon.Status>({
method: 'get', method: 'get',
@ -122,10 +191,9 @@ const ActionsStatus: React.FC<Props> = ({
}) })
.then(res => { .then(res => {
dismiss() dismiss()
// @ts-ignore
navigation.navigate('Screen-Compose', { navigation.navigate('Screen-Compose', {
type: 'edit', type: 'deleteEdit',
incomingStatus: res.body, incomingStatus: res.body as Mastodon.Status,
...(replyToStatus && { replyToStatus }), ...(replyToStatus && { replyToStatus }),
queryKey queryKey
}) })
@ -136,7 +204,7 @@ const ActionsStatus: React.FC<Props> = ({
) )
}} }}
iconFront='Edit' iconFront='Edit'
title={t('shared.header.actions.status.edit.button')} title={t('shared.header.actions.status.deleteEdit.button')}
/> />
<MenuRow <MenuRow
onPress={() => { onPress={() => {

View File

@ -1,11 +1,14 @@
import analytics from '@components/analytics' 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 { createNativeStackNavigator } from '@react-navigation/native-stack'
import haptics from '@root/components/haptics' import haptics from '@root/components/haptics'
import formatText from '@screens/Compose/formatText' import formatText from '@screens/Compose/formatText'
import ComposeRoot from '@screens/Compose/Root' import ComposeRoot from '@screens/Compose/Root'
import { RootStackScreenProps } from '@utils/navigation/navigators' 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 { updateStoreReview } from '@utils/slices/contextsSlice'
import { import {
getInstanceAccount, getInstanceAccount,
@ -133,6 +136,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
useEffect(() => { useEffect(() => {
switch (params?.type) { switch (params?.type) {
case 'edit': case 'edit':
case 'deleteEdit':
if (params.incomingStatus.spoiler_text) { if (params.incomingStatus.spoiler_text) {
formatText({ formatText({
textInput: 'spoiler', textInput: 'spoiler',
@ -268,6 +272,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
} }
return false return false
}, [totalTextCount, composeState.attachments.uploads, composeState.text.raw]) }, [totalTextCount, composeState.attachments.uploads, composeState.text.raw])
const mutateTimeline = useTimelineMutation({ onMutate: true })
const headerRight = useCallback( const headerRight = useCallback(
() => ( () => (
<HeaderRight <HeaderRight
@ -282,7 +287,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
composeDispatch({ type: 'posting', payload: true }) composeDispatch({ type: 'posting', payload: true })
composePost(params, composeState) composePost(params, composeState)
.then(() => { .then(res => {
haptics('Success') haptics('Success')
if ( if (
Platform.OS === 'ios' && Platform.OS === 'ios' &&
@ -300,6 +305,15 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
switch (params?.type) { switch (params?.type) {
case 'edit': case 'edit':
console.log('firing mutation')
mutateTimeline.mutate({
type: 'editItem',
queryKey: params.queryKey,
rootQueryKey: params.rootQueryKey,
status: res.body
})
break
case 'deleteEdit':
case 'reply': case 'reply':
if (params?.queryKey && params.queryKey[1].page === 'Toot') { if (params?.queryKey && params.queryKey[1].page === 'Toot') {
queryClient.invalidateQueries(params.queryKey) queryClient.invalidateQueries(params.queryKey)

View File

@ -39,6 +39,7 @@ const composeParseState = (
): ComposeState => { ): ComposeState => {
switch (params.type) { switch (params.type) {
case 'edit': case 'edit':
case 'deleteEdit':
return { return {
...composeInitialState, ...composeInitialState,
dirty: true, dirty: true,

View File

@ -51,8 +51,11 @@ const composePost = async (
formData.append('visibility', composeState.visibility) formData.append('visibility', composeState.visibility)
return apiInstance<Mastodon.Status>({ return apiInstance<Mastodon.Status>({
method: 'post', method: params?.type === 'edit' ? 'put' : 'post',
url: 'statuses', url:
params?.type === 'edit'
? `statuses/${params.incomingStatus.id}`
: 'statuses',
headers: { headers: {
'Idempotency-Key': await Crypto.digestStringAsync( 'Idempotency-Key': await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256, Crypto.CryptoDigestAlgorithm.SHA256,
@ -67,7 +70,9 @@ const composePost = async (
composeState.attachments.sensitive + composeState.attachments.sensitive +
composeState.attachments.uploads.map(upload => upload.remote?.id) + composeState.attachments.uploads.map(upload => upload.remote?.id) +
composeState.visibility + composeState.visibility +
(params?.type === 'edit' ? Math.random() : '') (params?.type === 'edit' || params?.type === 'deleteEdit'
? Math.random()
: '')
) )
}, },
body: formData body: formData

View File

@ -26,31 +26,20 @@ export type RootStackParamList = {
type: 'edit' type: 'edit'
incomingStatus: Mastodon.Status incomingStatus: Mastodon.Status
replyToStatus?: Mastodon.Status replyToStatus?: Mastodon.Status
queryKey?: [ queryKey?: QueryKeyTimeline
'Timeline', rootQueryKey?: QueryKeyTimeline
{ }
page: App.Pages | {
hashtag?: Mastodon.Tag['name'] type: 'deleteEdit'
list?: Mastodon.List['id'] incomingStatus: Mastodon.Status
toot?: Mastodon.Status['id'] replyToStatus?: Mastodon.Status
account?: Mastodon.Account['id'] queryKey?: QueryKeyTimeline
}
]
} }
| { | {
type: 'reply' type: 'reply'
incomingStatus: Mastodon.Status incomingStatus: Mastodon.Status
accts: Mastodon.Account['acct'][] accts: Mastodon.Account['acct'][]
queryKey?: [ queryKey?: QueryKeyTimeline
'Timeline',
{
page: App.Pages
hashtag?: Mastodon.Tag['name']
list?: Mastodon.List['id']
toot?: Mastodon.Status['id']
account?: Mastodon.Account['id']
}
]
} }
| { | {
type: 'conversation' type: 'conversation'

View File

@ -13,6 +13,7 @@ import {
useMutation useMutation
} from 'react-query' } from 'react-query'
import deleteItem from './timeline/deleteItem' import deleteItem from './timeline/deleteItem'
import editItem from './timeline/editItem'
import updateStatusProperty from './timeline/updateStatusProperty' import updateStatusProperty from './timeline/updateStatusProperty'
export type QueryKeyTimeline = [ 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 = { export type MutationVarsTimelineDeleteItem = {
// This is for deleting status and conversation // This is for deleting status and conversation
type: 'deleteItem' type: 'deleteItem'
source: 'statuses' | 'conversations' source: 'statuses' | 'conversations'
queryKey?: QueryKeyTimeline queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline rootQueryKey?: QueryKeyTimeline
id: Mastodon.Conversation['id'] id: Mastodon.Status['id']
} }
export type MutationVarsTimelineDomainBlock = { export type MutationVarsTimelineDomainBlock = {
@ -322,6 +331,7 @@ export type MutationVarsTimelineDomainBlock = {
export type MutationVarsTimeline = export type MutationVarsTimeline =
| MutationVarsTimelineUpdateStatusProperty | MutationVarsTimelineUpdateStatusProperty
| MutationVarsTimelineUpdateAccountProperty | MutationVarsTimelineUpdateAccountProperty
| MutationVarsTimelineEditItem
| MutationVarsTimelineDeleteItem | MutationVarsTimelineDeleteItem
| MutationVarsTimelineDomainBlock | MutationVarsTimelineDomainBlock
@ -371,6 +381,8 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
} }
}) })
} }
case 'editItem':
return { body: params.status }
case 'deleteItem': case 'deleteItem':
return apiInstance<Mastodon.Conversation>({ return apiInstance<Mastodon.Conversation>({
method: 'delete', method: 'delete',
@ -423,6 +435,9 @@ const useTimelineMutation = ({
case 'updateStatusProperty': case 'updateStatusProperty':
updateStatusProperty(params) updateStatusProperty(params)
break break
case 'editItem':
editItem(params)
break
case 'deleteItem': case 'deleteItem':
deleteItem(params) deleteItem(params)
break break

View File

@ -6,11 +6,7 @@ const deleteItem = ({
queryKey, queryKey,
rootQueryKey, rootQueryKey,
id id
}: { }: MutationVarsTimelineDeleteItem) => {
queryKey?: MutationVarsTimelineDeleteItem['queryKey']
rootQueryKey?: MutationVarsTimelineDeleteItem['rootQueryKey']
id: MutationVarsTimelineDeleteItem['id']
}) => {
queryKey && queryKey &&
queryClient.setQueryData<InfiniteData<any> | undefined>(queryKey, old => { queryClient.setQueryData<InfiniteData<any> | undefined>(queryKey, old => {
if (old) { if (old) {

View File

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

View File

@ -14,13 +14,7 @@ const updateStatusProperty = ({
id, id,
reblog, reblog,
payload payload
}: { }: MutationVarsTimelineUpdateStatusProperty) => {
queryKey: MutationVarsTimelineUpdateStatusProperty['queryKey']
rootQueryKey?: MutationVarsTimelineUpdateStatusProperty['rootQueryKey']
id: MutationVarsTimelineUpdateStatusProperty['id']
reblog?: MutationVarsTimelineUpdateStatusProperty['reblog']
payload: MutationVarsTimelineUpdateStatusProperty['payload']
}) => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>( queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey, queryKey,
old => { old => {

View File

@ -344,14 +344,16 @@ export const getInstanceVersion = ({ instances: { instances } }: RootState) =>
instances[findInstanceActive(instances)]?.version instances[findInstanceActive(instances)]?.version
export const checkInstanceFeature = export const checkInstanceFeature =
(feature: string) => (feature: string) =>
({ instances: { instances } }: RootState) => { ({ instances: { instances } }: RootState): Boolean => {
return features return (
.filter(f => f.feature === feature) features
.filter( .filter(f => f.feature === feature)
f => .filter(
parseFloat(instances[findInstanceActive(instances)]?.version) >= f =>
f.version parseFloat(instances[findInstanceActive(instances)]?.version) >=
) f.version
).length > 0
)
} }
/* Get Instance Configuration */ /* Get Instance Configuration */