forked from cybrespace/pinafore
		
	
							parent
							
								
									631603b0b7
								
							
						
					
					
						commit
						1940260631
					
				
					 7 changed files with 96 additions and 14 deletions
				
			
		| 
						 | 
				
			
			@ -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])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
        })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										24
									
								
								src/routes/_utils/statusHtmlToPlainText.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/routes/_utils/statusHtmlToPlainText.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue