From 1940260631fd9a0b13600924c9829abef43f5157 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Wed, 12 Dec 2018 23:45:52 -0800 Subject: [PATCH] fix: fix delete and redraft on replies (#789) fixes #786 --- .../_actions/addStatusOrNotification.js | 11 ++-- src/routes/_actions/delete.js | 2 + src/routes/_actions/deleteStatuses.js | 3 +- .../_components/compose/ComposeBox.html | 6 +- .../components/StatusOptionsDialog.html | 8 ++- src/routes/_utils/statusHtmlToPlainText.js | 24 ++++++++ tests/spec/121-delete-and-redraft.js | 56 ++++++++++++++++++- 7 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 src/routes/_utils/statusHtmlToPlainText.js diff --git a/src/routes/_actions/addStatusOrNotification.js b/src/routes/_actions/addStatusOrNotification.js index 3cbe7977..9138854c 100644 --- a/src/routes/_actions/addStatusOrNotification.js +++ b/src/routes/_actions/addStatusOrNotification.js @@ -1,14 +1,11 @@ -import throttle from 'lodash-es/throttle' import { mark, stop } from '../_utils/marks' import { store } from '../_store/store' import uniqBy from 'lodash-es/uniqBy' import uniq from 'lodash-es/uniq' import isEqual from 'lodash-es/isEqual' import { database } from '../_database/database' -import { runMediumPriorityTask } from '../_utils/runMediumPriorityTask' import { concat } from '../_utils/arrays' - -const STREAMING_THROTTLE_DELAY = 3000 +import { scheduleIdleTask } from '../_utils/scheduleIdleTask' function getExistingItemIdsSet (instanceName, timelineName) { let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') || [] @@ -95,11 +92,11 @@ async function processFreshUpdates (instanceName, timelineName) { stop('processFreshUpdates') } -const lazilyProcessFreshUpdates = throttle((instanceName, timelineName) => { - runMediumPriorityTask(() => { +function lazilyProcessFreshUpdates (instanceName, timelineName) { + scheduleIdleTask(() => { /* no await */ processFreshUpdates(instanceName, timelineName) }) -}, STREAMING_THROTTLE_DELAY) +} export function addStatusOrNotification (instanceName, timelineName, newStatusOrNotification) { addStatusesOrNotifications(instanceName, timelineName, [newStatusOrNotification]) diff --git a/src/routes/_actions/delete.js b/src/routes/_actions/delete.js index edc6a823..c668f07f 100644 --- a/src/routes/_actions/delete.js +++ b/src/routes/_actions/delete.js @@ -1,11 +1,13 @@ import { store } from '../_store/store' import { deleteStatus } from '../_api/delete' import { toast } from '../_utils/toast' +import { deleteStatus as deleteStatusLocally } from './deleteStatuses' export async function doDeleteStatus (statusId) { let { currentInstance, accessToken } = store.get() try { await deleteStatus(currentInstance, accessToken, statusId) + deleteStatusLocally(currentInstance, statusId) toast.say('Status deleted.') } catch (e) { console.error(e) diff --git a/src/routes/_actions/deleteStatuses.js b/src/routes/_actions/deleteStatuses.js index 0ce6eed6..2fdcd9ba 100644 --- a/src/routes/_actions/deleteStatuses.js +++ b/src/routes/_actions/deleteStatuses.js @@ -1,8 +1,8 @@ import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses' import { store } from '../_store/store' -import { scheduleIdleTask } from '../_utils/scheduleIdleTask' import isEqual from 'lodash-es/isEqual' import { database } from '../_database/database' +import { scheduleIdleTask } from '../_utils/scheduleIdleTask' function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) { let keys = ['timelineItemIds', 'itemIdsToAdd'] @@ -16,6 +16,7 @@ function filterItemIdsFromTimelines (instanceName, timelineFilter, idFilter) { } let filteredIds = ids.filter(idFilter) if (!isEqual(ids, filteredIds)) { + console.log('deleting an item from timelineName', timelineName, 'for key', key) store.setForTimeline(instanceName, timelineName, { [key]: filteredIds }) diff --git a/src/routes/_components/compose/ComposeBox.html b/src/routes/_components/compose/ComposeBox.html index d9d6728d..94d861ac 100644 --- a/src/routes/_components/compose/ComposeBox.html +++ b/src/routes/_components/compose/ComposeBox.html @@ -143,6 +143,7 @@ composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {}, text: ({ composeData }) => composeData.text || '', media: ({ composeData }) => composeData.media || [], + inReplyToId: ({ composeData }) => composeData.inReplyToId, postPrivacy: ({ postPrivacyKey }) => POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey), defaultPostPrivacyKey: ({ $currentVerifyCredentials }) => $currentVerifyCredentials.source.privacy, postPrivacyKey: ({ composeData, defaultPostPrivacyKey }) => composeData.postPrivacy || defaultPostPrivacyKey, @@ -167,12 +168,13 @@ contentWarning, realm, overLimit, - inReplyToUuid + inReplyToUuid, // typical replies, using Pinafore-specific uuid + inReplyToId // delete-and-redraft replies, using standard id } = this.get() let sensitive = media.length && !!contentWarning let mediaIds = media.map(_ => _.data.id) 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)) { return // do nothing if invalid diff --git a/src/routes/_components/dialog/components/StatusOptionsDialog.html b/src/routes/_components/dialog/components/StatusOptionsDialog.html index e5bb927b..fd21c0e4 100644 --- a/src/routes/_components/dialog/components/StatusOptionsDialog.html +++ b/src/routes/_components/dialog/components/StatusOptionsDialog.html @@ -20,7 +20,7 @@ import { setAccountMuted } from '../../../_actions/mute' import { setStatusPinnedOrUnpinned } from '../../../_actions/pin' import { setConversationMuted } from '../../../_actions/muteConversation' import { copyText } from '../../../_actions/copyText' -import { htmlToPlainText } from '../../../_utils/htmlToPlainText' +import { statusHtmlToPlainText } from '../../../_utils/statusHtmlToPlainText' import { importShowComposeDialog } from '../asyncDialogs' export default { @@ -192,15 +192,17 @@ export default { let deleteStatusPromise = doDeleteStatus(status.id) let dialogPromise = importShowComposeDialog() await deleteStatusPromise + this.store.setComposeData('dialog', { - text: (status.content && htmlToPlainText(status.content)) || '', + text: statusHtmlToPlainText(status.content, status.mentions), contentWarningShown: !!status.spoiler_text, contentWarning: status.spoiler_text || '', postPrivacy: status.visibility, media: status.media_attachments && status.media_attachments.map(_ => ({ description: _.description || '', data: _ - })) + })), + inReplyToId: status.in_reply_to_id }) this.close() let showComposeDialog = await dialogPromise diff --git a/src/routes/_utils/statusHtmlToPlainText.js b/src/routes/_utils/statusHtmlToPlainText.js new file mode 100644 index 00000000..40f912ac --- /dev/null +++ b/src/routes/_utils/statusHtmlToPlainText.js @@ -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 +} diff --git a/tests/spec/121-delete-and-redraft.js b/tests/spec/121-delete-and-redraft.js index 2e423a7d..bbd7a375 100644 --- a/tests/spec/121-delete-and-redraft.js +++ b/tests/spec/121-delete-and-redraft.js @@ -9,7 +9,11 @@ import { getNthStatusMediaImg, composeModalPostPrivacyButton, getComposeModalNthMediaImg, - getComposeModalNthMediaAltInput, getNthStatusSpoiler, composeModalContentWarningInput, dialogOptionsOption + getComposeModalNthMediaAltInput, + getNthStatusSpoiler, + composeModalContentWarningInput, + dialogOptionsOption, + getNthReplyButton, getNthComposeReplyInput, getNthComposeReplyButton, getUrl } from '../utils' import { postAs, postEmptyStatusWithMediaAs, postWithSpoilerAndPrivacyAs } from '../serverActions' @@ -91,3 +95,53 @@ test('privacy and spoiler delete and redraft', async t => { .expect(modalDialog.exists).notOk() .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() +})