forked from cybrespace/pinafore
parent
631603b0b7
commit
1940260631
|
@ -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])
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
})
|
||||||
|
|
Loading…
Reference in New Issue