feat(statuses): implement "Delete and redraft" (#719)

Fixes #469
This commit is contained in:
Nolan Lawson 2018-12-03 23:23:29 -08:00 committed by GitHub
parent 92edb3d835
commit 60751b3339
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 182 additions and 15 deletions

View file

@ -1,8 +1,8 @@
<div class="compose-media"> <div class="compose-media">
<img src={mediaItem.data.preview_url} alt={mediaItem.file.name}/> <img src={mediaItem.data.preview_url} {alt} />
<div class="compose-media-delete"> <div class="compose-media-delete">
<button class="compose-media-delete-button" <button class="compose-media-delete-button"
aria-label="Delete {mediaItem.file.name}" aria-label="Delete {shortName}"
on:click="onDeleteMedia()" > on:click="onDeleteMedia()" >
<svg class="compose-media-delete-button-svg"> <svg class="compose-media-delete-button-svg">
<use xlink:href="#fa-times" /> <use xlink:href="#fa-times" />
@ -10,12 +10,15 @@
</button> </button>
</div> </div>
<div class="compose-media-alt"> <div class="compose-media-alt">
<input type="text" <input id="compose-media-input-{uuid}"
type="text"
class="compose-media-alt-input" class="compose-media-alt-input"
placeholder="Description" placeholder="Description"
aria-label="Describe {mediaItem.file.name} for the visually impaired"
bind:value=rawText bind:value=rawText
> >
<label for="compose-media-input-{uuid}" class="sr-only">
Describe {shortName} for the visually impaired
</label>
</div> </div>
</div> </div>
<style> <style>
@ -91,6 +94,16 @@
data: () => ({ data: () => ({
rawText: '' rawText: ''
}), }),
computed: {
filename: ({ mediaItem }) => mediaItem.file && mediaItem.file.name,
alt: ({ filename, mediaItem }) => (
// sometimes we no longer have the file, e.g. in a delete and redraft situation,
// so fall back to the description if it was provided
filename || mediaItem.description || ''
),
shortName: ({ filename }) => filename || 'media',
uuid: ({ realm, mediaItem }) => `${realm}-${mediaItem.data.id}`
},
store: () => store, store: () => store,
methods: { methods: {
observe, observe,

View file

@ -20,6 +20,8 @@ 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 { importShowComposeDialog } from '../asyncDialogs'
export default { export default {
oncreate, oncreate,
@ -109,6 +111,11 @@ export default {
label: muteConversationLabel, label: muteConversationLabel,
icon: muteConversationIcon icon: muteConversationIcon
}, },
isUser && {
key: 'redraft',
label: 'Delete and redraft',
icon: '#fa-pencil'
},
{ {
key: 'copy', key: 'copy',
label: 'Copy link to toot', label: 'Copy link to toot',
@ -140,6 +147,8 @@ export default {
return this.onCopyClicked() return this.onCopyClicked()
case 'muteConversation': case 'muteConversation':
return this.onMuteConversationClicked() return this.onMuteConversationClicked()
case 'redraft':
return this.onRedraft()
} }
}, },
async onDeleteClicked () { async onDeleteClicked () {
@ -177,6 +186,25 @@ export default {
let { url } = status let { url } = status
await copyText(url) await copyText(url)
this.close() this.close()
},
async onRedraft () {
let { status } = this.get()
let deleteStatusPromise = doDeleteStatus(status.id)
let dialogPromise = importShowComposeDialog()
await deleteStatusPromise
this.store.setComposeData('dialog', {
text: (status.content && htmlToPlainText(status.content)) || '',
contentWarningShown: !!status.spoiler_text,
contentWarning: status.spoiler_text || '',
postPrivacy: status.visibility,
media: status.media_attachments && status.media_attachments.map(_ => ({
description: _.description || '',
data: _
}))
})
this.close()
let showComposeDialog = await dialogPromise
showComposeDialog()
} }
} }
} }

View file

@ -29,6 +29,11 @@ export async function postAs (username, text) {
null, null, false, null, 'public') null, null, false, null, 'public')
} }
export async function postWithSpoilerAndPrivacyAs (username, text, spoiler, privacy) {
return postStatus(instanceName, users[username].accessToken, text,
null, null, true, spoiler, privacy)
}
export async function postEmptyStatusWithMediaAs (username, filename, alt) { export async function postEmptyStatusWithMediaAs (username, filename, alt) {
let mediaResponse = await submitMedia(users[username].accessToken, filename, alt) let mediaResponse = await submitMedia(users[username].accessToken, filename, alt)
return postStatus(instanceName, users[username].accessToken, '', return postStatus(instanceName, users[username].accessToken, '',

View file

@ -1,5 +1,5 @@
import { import {
accountProfileMoreOptionsButton, closeDialogButton, accountProfileMoreOptionsButton, closeDialogButton, composeModalInput,
getNthDialogOptionsOption, modalDialog getNthDialogOptionsOption, modalDialog
} from '../utils' } from '../utils'
import { loginAsFoobar } from '../roles' import { loginAsFoobar } from '../roles'
@ -14,7 +14,7 @@ test('can mention from account profile', async t => {
.click(accountProfileMoreOptionsButton) .click(accountProfileMoreOptionsButton)
.expect(getNthDialogOptionsOption(1).innerText).contains('Mention @baz') .expect(getNthDialogOptionsOption(1).innerText).contains('Mention @baz')
.click(getNthDialogOptionsOption(1)) .click(getNthDialogOptionsOption(1))
.expect(modalDialog.find('.compose-box-input').value).eql('@baz ') .expect(composeModalInput.value).eql('@baz ')
.click(closeDialogButton) .click(closeDialogButton)
.expect(modalDialog.exists).notOk() .expect(modalDialog.exists).notOk()
}) })

View file

@ -1,6 +1,15 @@
import { import {
composeButton, getNthStatus, scrollToStatus, modalDialog, sleep, composeButton,
notificationsNavButton, getUrl, getNthStatusSelector getNthStatus,
scrollToStatus,
modalDialog,
sleep,
notificationsNavButton,
getUrl,
getNthStatusSelector,
composeModalEmojiButton,
composeModalInput,
composeModalComposeButton
} from '../utils' } from '../utils'
import { loginAsFoobar } from '../roles' import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe' import { Selector as $ } from 'testcafe'
@ -16,8 +25,8 @@ test('can compose using a dialog', async t => {
await sleep(2000) await sleep(2000)
await t.click(composeButton) await t.click(composeButton)
.expect(modalDialog.hasAttribute('aria-hidden')).notOk() .expect(modalDialog.hasAttribute('aria-hidden')).notOk()
.typeText(modalDialog.find('.compose-box-input'), 'hello from the modal') .typeText(composeModalInput, 'hello from the modal')
.click(modalDialog.find('.compose-box-button-compose')) .click(composeModalComposeButton)
.expect(modalDialog.exists).notOk() .expect(modalDialog.exists).notOk()
.click(notificationsNavButton) .click(notificationsNavButton)
.expect(getUrl()).contains('/notifications') .expect(getUrl()).contains('/notifications')
@ -32,10 +41,10 @@ test('can use emoji dialog within compose dialog', async t => {
await t.expect(composeButton.getAttribute('aria-label')).eql('Compose') await t.expect(composeButton.getAttribute('aria-label')).eql('Compose')
await sleep(2000) await sleep(2000)
await t.click(composeButton) await t.click(composeButton)
.click(modalDialog.find('.compose-box-toolbar button:nth-child(1)')) .click(composeModalEmojiButton)
.click($('button img[title=":blobpats:"]')) .click($('button img[title=":blobpats:"]'))
.expect(modalDialog.find('.compose-box-input').value).eql(':blobpats: ') .expect(composeModalInput.value).eql(':blobpats: ')
.click(modalDialog.find('.compose-box-button-compose')) .click(composeModalComposeButton)
.expect(modalDialog.exists).notOk() .expect(modalDialog.exists).notOk()
.click(notificationsNavButton) .click(notificationsNavButton)
.expect(getUrl()).contains('/notifications') .expect(getUrl()).contains('/notifications')

View file

@ -0,0 +1,93 @@
import { loginAsFoobar } from '../roles'
import {
composeModalComposeButton,
getNthStatus,
getNthStatusContent,
getNthStatusOptionsButton,
modalDialog,
composeModalInput,
getNthStatusMediaImg,
composeModalPostPrivacyButton,
getComposeModalNthMediaImg,
getComposeModalNthMediaAltInput, getNthStatusSpoiler, composeModalContentWarningInput, dialogOptionsOption
} from '../utils'
import { postAs, postEmptyStatusWithMediaAs, postWithSpoilerAndPrivacyAs } from '../serverActions'
fixture`121-delete-and-redraft.js`
.page`http://localhost:4002`
test('basic delete and redraft', async t => {
await postAs('foobar', 'hey ho this is grate')
await loginAsFoobar(t)
await t
.hover(getNthStatus(0))
.expect(getNthStatusContent(0).innerText).contains('hey ho this is grate')
.click(getNthStatusOptionsButton(0))
.click(dialogOptionsOption.withText('Delete and redraft'))
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
.expect(composeModalInput.value).contains('hey ho this is grate')
.expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
.typeText(composeModalInput, 'hey ho this is great', { replace: true, paste: true })
.click(composeModalComposeButton)
.expect(modalDialog.exists).notOk()
.expect(getNthStatusContent(0).innerText).contains('hey ho this is great')
})
test('image with empty text delete and redraft', async t => {
await postEmptyStatusWithMediaAs('foobar', 'kitten2.jpg', 'what a kitteh')
await loginAsFoobar(t)
await t
.hover(getNthStatus(0))
.expect(getNthStatusMediaImg(0).getAttribute('alt')).eql('what a kitteh')
.click(getNthStatusOptionsButton(0))
.click(dialogOptionsOption.withText('Delete and redraft'))
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
.expect(composeModalInput.value).eql('')
.expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
.expect(getComposeModalNthMediaImg(1).getAttribute('alt')).eql('what a kitteh')
.expect(getComposeModalNthMediaAltInput(1).value).eql('what a kitteh')
.typeText(composeModalInput, 'I love this kitteh', { replace: true, paste: true })
.click(composeModalComposeButton)
.expect(modalDialog.exists).notOk()
.expect(getNthStatusContent(0).innerText).contains('I love this kitteh')
.expect(getNthStatusMediaImg(0).getAttribute('alt')).eql('what a kitteh')
})
test('image with no alt delete and redraft', async t => {
await postEmptyStatusWithMediaAs('foobar', 'kitten3.jpg', '')
await loginAsFoobar(t)
await t
.hover(getNthStatus(0))
.expect(getNthStatusMediaImg(0).getAttribute('alt')).eql('')
.click(getNthStatusOptionsButton(0))
.click(dialogOptionsOption.withText('Delete and redraft'))
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
.expect(composeModalInput.value).eql('')
.expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
.expect(getComposeModalNthMediaImg(1).getAttribute('alt')).eql('')
.expect(getComposeModalNthMediaAltInput(1).value).eql('')
.typeText(composeModalInput, 'oops forgot an alt', { replace: true, paste: true })
.typeText(getComposeModalNthMediaAltInput(1), 'lovely kitteh', { replace: true, paste: true })
.click(composeModalComposeButton)
.expect(modalDialog.exists).notOk()
.expect(getNthStatusContent(0).innerText).contains('oops forgot an alt')
.expect(getNthStatusMediaImg(0).getAttribute('alt')).eql('lovely kitteh')
})
test('privacy and spoiler delete and redraft', async t => {
await postWithSpoilerAndPrivacyAs('foobar', 'this is hidden', 'click to see!', 'private')
await loginAsFoobar(t)
await t
.hover(getNthStatus(0))
.expect(getNthStatusSpoiler(0).innerText).contains('click to see!')
.click(getNthStatusOptionsButton(0))
.click(dialogOptionsOption.withText('Delete and redraft'))
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
.expect(composeModalInput.value).eql('this is hidden')
.expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Followers-only)')
.expect(composeModalContentWarningInput.value).eql('click to see!')
.typeText(composeModalContentWarningInput, 'no really, you should click this!', { replace: true, paste: true })
.click(composeModalComposeButton)
.expect(modalDialog.exists).notOk()
.expect(getNthStatusSpoiler(0).innerText).contains('no really, you should click this!')
})

View file

@ -45,6 +45,21 @@ export const generalSettingsButton = $('a[href="/settings/general"]')
export const markMediaSensitiveInput = $('#choice-mark-media-sensitive') export const markMediaSensitiveInput = $('#choice-mark-media-sensitive')
export const neverMarkMediaSensitiveInput = $('#choice-never-mark-media-sensitive') export const neverMarkMediaSensitiveInput = $('#choice-never-mark-media-sensitive')
export const removeEmojiFromDisplayNamesInput = $('#choice-omit-emoji-in-display-names') export const removeEmojiFromDisplayNamesInput = $('#choice-omit-emoji-in-display-names')
export const dialogOptionsOption = $(`.modal-dialog button`)
export const composeModalInput = $('.modal-dialog .compose-box-input')
export const composeModalComposeButton = $('.modal-dialog .compose-box-button')
export const composeModalContentWarningInput = $('.modal-dialog .content-warning-input')
export const composeModalEmojiButton = $('.modal-dialog .compose-box-toolbar button:nth-child(1)')
export const composeModalPostPrivacyButton = $('.modal-dialog .compose-box-toolbar button:nth-child(3)')
export function getComposeModalNthMediaAltInput (n) {
return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt input`)
}
export function getComposeModalNthMediaImg (n) {
return $(`.modal-dialog .compose-media:nth-child(${n}) img`)
}
export const favoritesCountElement = $('.status-favs').addCustomDOMProperties({ export const favoritesCountElement = $('.status-favs').addCustomDOMProperties({
innerCount: el => parseInt(el.innerText, 10) innerCount: el => parseInt(el.innerText, 10)
@ -190,6 +205,10 @@ export function getNthStatusMedia (n) {
return $(`${getNthStatusSelector(n)} .status-media`) return $(`${getNthStatusSelector(n)} .status-media`)
} }
export function getNthStatusMediaImg (n) {
return $(`${getNthStatusSelector(n)} .status-media img`)
}
export function getNthStatusHeader (n) { export function getNthStatusHeader (n) {
return $(`${getNthStatusSelector(n)} .status-header`) return $(`${getNthStatusSelector(n)} .status-header`)
} }