fix: fix delete and redraft on replies (#789)

fixes #786
This commit is contained in:
Nolan Lawson 2018-12-12 23:45:52 -08:00 committed by GitHub
parent 631603b0b7
commit 1940260631
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 96 additions and 14 deletions

View File

@ -1,14 +1,11 @@
import throttle from 'lodash-es/throttle'
import { mark, stop } from '../_utils/marks' import { mark, stop } from '../_utils/marks'
import { store } from '../_store/store' import { store } from '../_store/store'
import uniqBy from 'lodash-es/uniqBy' import uniqBy from 'lodash-es/uniqBy'
import uniq from 'lodash-es/uniq' import uniq from 'lodash-es/uniq'
import isEqual from 'lodash-es/isEqual' import isEqual from 'lodash-es/isEqual'
import { database } from '../_database/database' import { database } from '../_database/database'
import { runMediumPriorityTask } from '../_utils/runMediumPriorityTask'
import { concat } from '../_utils/arrays' import { concat } from '../_utils/arrays'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
const STREAMING_THROTTLE_DELAY = 3000
function getExistingItemIdsSet (instanceName, timelineName) { function getExistingItemIdsSet (instanceName, timelineName) {
let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') || [] let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') || []
@ -95,11 +92,11 @@ async function processFreshUpdates (instanceName, timelineName) {
stop('processFreshUpdates') stop('processFreshUpdates')
} }
const lazilyProcessFreshUpdates = throttle((instanceName, timelineName) => { function lazilyProcessFreshUpdates (instanceName, timelineName) {
runMediumPriorityTask(() => { scheduleIdleTask(() => {
/* no await */ processFreshUpdates(instanceName, timelineName) /* no await */ processFreshUpdates(instanceName, timelineName)
}) })
}, STREAMING_THROTTLE_DELAY) }
export function addStatusOrNotification (instanceName, timelineName, newStatusOrNotification) { export function addStatusOrNotification (instanceName, timelineName, newStatusOrNotification) {
addStatusesOrNotifications(instanceName, timelineName, [newStatusOrNotification]) addStatusesOrNotifications(instanceName, timelineName, [newStatusOrNotification])

View File

@ -1,11 +1,13 @@
import { store } from '../_store/store' import { store } from '../_store/store'
import { deleteStatus } from '../_api/delete' import { deleteStatus } from '../_api/delete'
import { toast } from '../_utils/toast' import { toast } from '../_utils/toast'
import { deleteStatus as deleteStatusLocally } from './deleteStatuses'
export async function doDeleteStatus (statusId) { export async function doDeleteStatus (statusId) {
let { currentInstance, accessToken } = store.get() let { currentInstance, accessToken } = store.get()
try { try {
await deleteStatus(currentInstance, accessToken, statusId) await deleteStatus(currentInstance, accessToken, statusId)
deleteStatusLocally(currentInstance, statusId)
toast.say('Status deleted.') toast.say('Status deleted.')
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View File

@ -1,8 +1,8 @@
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses' import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses'
import { store } from '../_store/store' import { store } from '../_store/store'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
import isEqual from 'lodash-es/isEqual' import isEqual from 'lodash-es/isEqual'
import { database } from '../_database/database' import { database } from '../_database/database'
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) { function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
let keys = ['timelineItemIds', 'itemIdsToAdd'] let keys = ['timelineItemIds', 'itemIdsToAdd']
@ -16,6 +16,7 @@ function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) {
} }
let filteredIds = ids.filter(idFilter) let filteredIds = ids.filter(idFilter)
if (!isEqual(ids, filteredIds)) { if (!isEqual(ids, filteredIds)) {
console.log('deleting an item from timelineName', timelineName, 'for key', key)
store.setForTimeline(instanceName, timelineName, { store.setForTimeline(instanceName, timelineName, {
[key]: filteredIds [key]: filteredIds
}) })

View File

@ -143,6 +143,7 @@
composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {}, composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {},
text: ({ composeData }) => composeData.text || '', text: ({ composeData }) => composeData.text || '',
media: ({ composeData }) => composeData.media || [], media: ({ composeData }) => composeData.media || [],
inReplyToId: ({ composeData }) => composeData.inReplyToId,
postPrivacy: ({ postPrivacyKey }) => POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey), postPrivacy: ({ postPrivacyKey }) => POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey),
defaultPostPrivacyKey: ({ $currentVerifyCredentials }) => $currentVerifyCredentials.source.privacy, defaultPostPrivacyKey: ({ $currentVerifyCredentials }) => $currentVerifyCredentials.source.privacy,
postPrivacyKey: ({ composeData, defaultPostPrivacyKey }) => composeData.postPrivacy || defaultPostPrivacyKey, postPrivacyKey: ({ composeData, defaultPostPrivacyKey }) => composeData.postPrivacy || defaultPostPrivacyKey,
@ -167,12 +168,13 @@
contentWarning, contentWarning,
realm, realm,
overLimit, overLimit,
inReplyToUuid inReplyToUuid, // typical replies, using Pinafore-specific uuid
inReplyToId // delete-and-redraft replies, using standard id
} = this.get() } = this.get()
let sensitive = media.length && !!contentWarning let sensitive = media.length && !!contentWarning
let mediaIds = media.map(_ => _.data.id) let mediaIds = media.map(_ => _.data.id)
let mediaDescriptions = media.map(_ => _.description) let mediaDescriptions = media.map(_ => _.description)
let inReplyTo = (realm === 'home' || realm === 'dialog') ? null : realm let inReplyTo = inReplyToId || ((realm === 'home' || realm === 'dialog') ? null : realm)
if (overLimit || (!text && !media.length)) { if (overLimit || (!text && !media.length)) {
return // do nothing if invalid return // do nothing if invalid

View File

@ -20,7 +20,7 @@ import { setAccountMuted } from '../../../_actions/mute'
import { setStatusPinnedOrUnpinned } from '../../../_actions/pin' import { setStatusPinnedOrUnpinned } from '../../../_actions/pin'
import { setConversationMuted } from '../../../_actions/muteConversation' import { setConversationMuted } from '../../../_actions/muteConversation'
import { copyText } from '../../../_actions/copyText' import { copyText } from '../../../_actions/copyText'
import { htmlToPlainText } from '../../../_utils/htmlToPlainText' import { statusHtmlToPlainText } from '../../../_utils/statusHtmlToPlainText'
import { importShowComposeDialog } from '../asyncDialogs' import { importShowComposeDialog } from '../asyncDialogs'
export default { export default {
@ -192,15 +192,17 @@ export default {
let deleteStatusPromise = doDeleteStatus(status.id) let deleteStatusPromise = doDeleteStatus(status.id)
let dialogPromise = importShowComposeDialog() let dialogPromise = importShowComposeDialog()
await deleteStatusPromise await deleteStatusPromise
this.store.setComposeData('dialog', { this.store.setComposeData('dialog', {
text: (status.content && htmlToPlainText(status.content)) || '', text: statusHtmlToPlainText(status.content, status.mentions),
contentWarningShown: !!status.spoiler_text, contentWarningShown: !!status.spoiler_text,
contentWarning: status.spoiler_text || '', contentWarning: status.spoiler_text || '',
postPrivacy: status.visibility, postPrivacy: status.visibility,
media: status.media_attachments && status.media_attachments.map(_ => ({ media: status.media_attachments && status.media_attachments.map(_ => ({
description: _.description || '', description: _.description || '',
data: _ data: _
})) })),
inReplyToId: status.in_reply_to_id
}) })
this.close() this.close()
let showComposeDialog = await dialogPromise let showComposeDialog = await dialogPromise

View File

@ -0,0 +1,24 @@
import { mark, stop } from './marks'
let domParser = process.browser && new DOMParser()
export function statusHtmlToPlainText (html, mentions) {
if (!html) {
return ''
}
mark('statusHtmlToPlainText')
let doc = domParser.parseFromString(html, 'text/html')
// mentions like "@foo" have to be expanded to "@foo@example.com"
let anchors = doc.querySelectorAll('a.mention')
for (let i = 0; i < anchors.length; i++) {
let anchor = anchors[i]
let href = anchor.getAttribute('href')
let mention = mentions.find(mention => mention.url === href)
if (mention) {
anchor.innerText = `@${mention.acct}`
}
}
let res = doc.documentElement.textContent
stop('statusHtmlToPlainText')
return res
}

View File

@ -9,7 +9,11 @@ import {
getNthStatusMediaImg, getNthStatusMediaImg,
composeModalPostPrivacyButton, composeModalPostPrivacyButton,
getComposeModalNthMediaImg, getComposeModalNthMediaImg,
getComposeModalNthMediaAltInput, getNthStatusSpoiler, composeModalContentWarningInput, dialogOptionsOption getComposeModalNthMediaAltInput,
getNthStatusSpoiler,
composeModalContentWarningInput,
dialogOptionsOption,
getNthReplyButton, getNthComposeReplyInput, getNthComposeReplyButton, getUrl
} from '../utils' } from '../utils'
import { postAs, postEmptyStatusWithMediaAs, postWithSpoilerAndPrivacyAs } from '../serverActions' import { postAs, postEmptyStatusWithMediaAs, postWithSpoilerAndPrivacyAs } from '../serverActions'
@ -91,3 +95,53 @@ test('privacy and spoiler delete and redraft', async t => {
.expect(modalDialog.exists).notOk() .expect(modalDialog.exists).notOk()
.expect(getNthStatusSpoiler(0).innerText).contains('no really, you should click this!') .expect(getNthStatusSpoiler(0).innerText).contains('no really, you should click this!')
}) })
test('delete and redraft reply', async t => {
await postAs('admin', 'hey hello')
await loginAsFoobar(t)
await t
.hover(getNthStatus(0))
.expect(getNthStatusContent(0).innerText).contains('hey hello')
.click(getNthReplyButton(0))
.typeText(getNthComposeReplyInput(0), 'hello there admin', { paste: true })
.click(getNthComposeReplyButton(0))
.expect(getNthStatus(0).innerText).contains('@admin hello there admin')
.click(getNthStatusOptionsButton(0))
.click(dialogOptionsOption.withText('Delete and redraft'))
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
.typeText(composeModalInput, ' oops forgot to say thank you')
.click(composeModalComposeButton)
.expect(modalDialog.exists).notOk()
.expect(getNthStatusContent(0).innerText).match(/@admin hello there admin\s+oops forgot to say thank you/, {
timeout: 30000
})
.click(getNthStatus(0))
.expect(getUrl()).match(/statuses/)
.expect(getNthStatusContent(0).innerText).contains('hey hello')
.expect(getNthStatusContent(1).innerText).match(/@admin hello there admin\s+oops forgot to say thank you/)
})
test('delete and redraft reply within thread', async t => {
await postAs('admin', 'this is a thread')
await loginAsFoobar(t)
await t
.hover(getNthStatus(0))
.expect(getNthStatusContent(0).innerText).contains('this is a thread')
.click(getNthStatus(0))
.expect(getUrl()).match(/statuses/)
.expect(getNthStatusContent(0).innerText).contains('this is a thread')
.click(getNthReplyButton(0))
.typeText(getNthComposeReplyInput(0), 'heyo', { paste: true })
.click(getNthComposeReplyButton(0))
.expect(getNthStatus(1).innerText).contains('@admin heyo')
.click(getNthStatusOptionsButton(1))
.click(dialogOptionsOption.withText('Delete and redraft'))
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
.typeText(composeModalInput, ' update!', { paste: true })
.click(composeModalComposeButton)
.expect(modalDialog.exists).notOk()
.expect(getNthStatusContent(1).innerText).match(/@admin heyo\s+update!/, {
timeout: 30000
})
.expect(getNthStatus(2).exists).notOk()
})