forked from cybrespace/pinafore
		
	fix: preserve newlines correctly in delete-and-redraft (#845)
fixes #830
This commit is contained in:
		
							parent
							
								
									d5eac4e119
								
							
						
					
					
						commit
						27da387a01
					
				
					 4 changed files with 68 additions and 27 deletions
				
			
		
							
								
								
									
										24
									
								
								src/routes/_actions/deleteAndRedraft.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/routes/_actions/deleteAndRedraft.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText'
 | 
			
		||||
import { importShowComposeDialog } from '../_components/dialog/asyncDialogs'
 | 
			
		||||
import { doDeleteStatus } from './delete'
 | 
			
		||||
import { store } from '../_store/store'
 | 
			
		||||
 | 
			
		||||
export async function deleteAndRedraft (status) {
 | 
			
		||||
  let deleteStatusPromise = doDeleteStatus(status.id)
 | 
			
		||||
  let dialogPromise = importShowComposeDialog()
 | 
			
		||||
  await deleteStatusPromise
 | 
			
		||||
 | 
			
		||||
  store.setComposeData('dialog', {
 | 
			
		||||
    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
 | 
			
		||||
  })
 | 
			
		||||
  let showComposeDialog = await dialogPromise
 | 
			
		||||
  showComposeDialog()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -20,8 +20,7 @@ import { setAccountMuted } from '../../../_actions/mute'
 | 
			
		|||
import { setStatusPinnedOrUnpinned } from '../../../_actions/pin'
 | 
			
		||||
import { setConversationMuted } from '../../../_actions/muteConversation'
 | 
			
		||||
import { copyText } from '../../../_actions/copyText'
 | 
			
		||||
import { statusHtmlToPlainText } from '../../../_utils/statusHtmlToPlainText'
 | 
			
		||||
import { importShowComposeDialog } from '../asyncDialogs'
 | 
			
		||||
import { deleteAndRedraft } from '../../../_actions/deleteAndRedraft'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  oncreate,
 | 
			
		||||
| 
						 | 
				
			
			@ -189,24 +188,8 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    async onRedraft () {
 | 
			
		||||
      let { status } = this.get()
 | 
			
		||||
      let deleteStatusPromise = doDeleteStatus(status.id)
 | 
			
		||||
      let dialogPromise = importShowComposeDialog()
 | 
			
		||||
      await deleteStatusPromise
 | 
			
		||||
 | 
			
		||||
      this.store.setComposeData('dialog', {
 | 
			
		||||
        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
 | 
			
		||||
      })
 | 
			
		||||
      await deleteAndRedraft(status)
 | 
			
		||||
      this.close()
 | 
			
		||||
      let showComposeDialog = await dialogPromise
 | 
			
		||||
      showComposeDialog()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,13 +2,8 @@ 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"
 | 
			
		||||
// mentions like "@foo" have to be expanded to "@foo@example.com"
 | 
			
		||||
function massageMentions (doc, mentions) {
 | 
			
		||||
  let anchors = doc.querySelectorAll('a.mention')
 | 
			
		||||
  for (let i = 0; i < anchors.length; i++) {
 | 
			
		||||
    let anchor = anchors[i]
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +13,29 @@ export function statusHtmlToPlainText (html, mentions) {
 | 
			
		|||
      anchor.innerText = `@${mention.acct}`
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  let res = doc.documentElement.textContent
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// paragraphs should be separated by double newlines
 | 
			
		||||
// single <br/>s should become single newlines
 | 
			
		||||
function innerTextRetainingNewlines (doc) {
 | 
			
		||||
  let paragraphs = doc.querySelectorAll('p')
 | 
			
		||||
  return Array.from(paragraphs).map(paragraph => {
 | 
			
		||||
    let brs = paragraph.querySelectorAll('br')
 | 
			
		||||
    Array.from(brs).forEach(br => {
 | 
			
		||||
      br.parentNode.replaceChild(doc.createTextNode('\n'), br)
 | 
			
		||||
    })
 | 
			
		||||
    return paragraph.textContent
 | 
			
		||||
  }).join('\n\n')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function statusHtmlToPlainText (html, mentions) {
 | 
			
		||||
  if (!html) {
 | 
			
		||||
    return ''
 | 
			
		||||
  }
 | 
			
		||||
  mark('statusHtmlToPlainText')
 | 
			
		||||
  let doc = domParser.parseFromString(html, 'text/html')
 | 
			
		||||
  massageMentions(doc, mentions)
 | 
			
		||||
  let res = innerTextRetainingNewlines(doc)
 | 
			
		||||
  stop('statusHtmlToPlainText')
 | 
			
		||||
  return res
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -145,3 +145,20 @@ test('delete and redraft reply within thread', async t => {
 | 
			
		|||
    })
 | 
			
		||||
    .expect(getNthStatus(2).exists).notOk()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('multiple paragraphs', async t => {
 | 
			
		||||
  let text = 'hey ho\n\ndouble newline!\njust one newline\njust another newline\n\nanother double newline!'
 | 
			
		||||
  await postAs('foobar', text)
 | 
			
		||||
  await loginAsFoobar(t)
 | 
			
		||||
  await t
 | 
			
		||||
    .hover(getNthStatus(0))
 | 
			
		||||
    .expect(getNthStatusContent(0).innerText).contains(text)
 | 
			
		||||
    .click(getNthStatusOptionsButton(0))
 | 
			
		||||
    .click(dialogOptionsOption.withText('Delete and redraft'))
 | 
			
		||||
    .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
 | 
			
		||||
    .expect(composeModalInput.value).eql(text)
 | 
			
		||||
    .typeText(composeModalInput, '\n\nwoot', { paste: true })
 | 
			
		||||
    .click(composeModalComposeButton)
 | 
			
		||||
    .expect(modalDialog.exists).notOk()
 | 
			
		||||
    .expect(getNthStatusContent(0).innerText).contains(text + '\n\nwoot')
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue