diff --git a/routes/_actions/compose.js b/routes/_actions/compose.js index d0c0a1b1..f221178e 100644 --- a/routes/_actions/compose.js +++ b/routes/_actions/compose.js @@ -4,6 +4,7 @@ import { postStatus as postStatusToServer } from '../_api/statuses' import { addStatusOrNotification } from './addStatusOrNotification' import { database } from '../_database/database' import { emit } from '../_utils/eventBus' +import { putMediaDescription } from '../_api/media' export async function insertHandleForReply (statusId) { let instanceName = store.get('currentInstance') @@ -20,7 +21,8 @@ export async function insertHandleForReply (statusId) { } export async function postStatus (realm, text, inReplyToId, mediaIds, - sensitive, spoilerText, visibility) { + sensitive, spoilerText, visibility, + mediaDescriptions = []) { let instanceName = store.get('currentInstance') let accessToken = store.get('accessToken') let online = store.get('online') @@ -34,6 +36,9 @@ export async function postStatus (realm, text, inReplyToId, mediaIds, postingStatus: true }) try { + await Promise.all(mediaDescriptions.map(async (description, i) => { + return description && putMediaDescription(instanceName, accessToken, mediaIds[i], description) + })) let status = await postStatusToServer(instanceName, accessToken, text, inReplyToId, mediaIds, sensitive, spoilerText, visibility) addStatusOrNotification(instanceName, 'home', status) diff --git a/routes/_actions/media.js b/routes/_actions/media.js index 14bc322f..131f3938 100644 --- a/routes/_actions/media.js +++ b/routes/_actions/media.js @@ -36,9 +36,15 @@ export function deleteMedia (realm, i) { let composeText = store.getComposeData(realm, 'text') || '' composeText = composeText.replace(' ' + deletedMedia.data.text_url, '') + let mediaDescriptions = store.getComposeData(realm, 'mediaDescriptions') || [] + if (mediaDescriptions[i]) { + mediaDescriptions[i] = null + } + store.setComposeData(realm, { media: composeMedia, - text: composeText + text: composeText, + mediaDescriptions: mediaDescriptions }) scheduleIdleTask(() => store.save()) } diff --git a/routes/_api/media.js b/routes/_api/media.js index 13110615..aefc22f8 100644 --- a/routes/_api/media.js +++ b/routes/_api/media.js @@ -1,10 +1,17 @@ import { auth, basename } from './utils' -import { postWithTimeout } from '../_utils/ajax' +import { postWithTimeout, putWithTimeout } from '../_utils/ajax' export async function uploadMedia (instanceName, accessToken, file, description) { let formData = new FormData() formData.append('file', file) - formData.append('description', description) + if (description) { + formData.append('description', description) + } let url = `${basename(instanceName)}/api/v1/media` return postWithTimeout(url, formData, auth(accessToken)) } + +export async function putMediaDescription (instanceName, accessToken, mediaId, description) { + let url = `${basename(instanceName)}/api/v1/media/${mediaId}` + return putWithTimeout(url, {description}, auth(accessToken)) +} diff --git a/routes/_components/compose/ComposeBox.html b/routes/_components/compose/ComposeBox.html index 19a41132..b03e5a13 100644 --- a/routes/_components/compose/ComposeBox.html +++ b/routes/_components/compose/ComposeBox.html @@ -10,7 +10,7 @@ - +
@@ -170,7 +170,8 @@ overLimit: (length) => length > CHAR_LIMIT, contentWarningShown: (composeData) => composeData.contentWarningShown, contentWarning: (composeData) => composeData.contentWarning || '', - timelineInitialized: ($timelineInitialized) => $timelineInitialized + timelineInitialized: ($timelineInitialized) => $timelineInitialized, + mediaDescriptions: (composeData) => composeData.mediaDescriptions || [] }, transitions: { slide @@ -193,6 +194,7 @@ let mediaIds = media.map(_ => _.data.id) let inReplyTo = (realm === 'home' || realm === 'dialog') ? null : realm let overLimit = this.get('overLimit') + let mediaDescriptions = this.get('mediaDescriptions') if (!text || overLimit) { return // do nothing if invalid @@ -200,7 +202,7 @@ /* no await */ postStatus(realm, text, inReplyTo, mediaIds, - sensitive, contentWarning, postPrivacyKey) + sensitive, contentWarning, postPrivacyKey, mediaDescriptions) } }, setupStickyObserver() { diff --git a/routes/_components/compose/ComposeInput.html b/routes/_components/compose/ComposeInput.html index 5a17b1f5..f00043a1 100644 --- a/routes/_components/compose/ComposeInput.html +++ b/routes/_components/compose/ComposeInput.html @@ -67,13 +67,13 @@ }) }, setupSyncToStore() { - const saveText = debounce(() => scheduleIdleTask(() => this.store.save()), 1000) + const saveStore = debounce(() => scheduleIdleTask(() => this.store.save()), 1000) this.observe('rawText', rawText => { mark('observe rawText') let realm = this.get('realm') this.store.setComposeData(realm, {text: rawText}) - saveText() + saveStore() stop('observe rawText') }, {init: false}) }, diff --git a/routes/_components/compose/ComposeMedia.html b/routes/_components/compose/ComposeMedia.html index 2a5d93cf..03371cab 100644 --- a/routes/_components/compose/ComposeMedia.html +++ b/routes/_components/compose/ComposeMedia.html @@ -1,23 +1,7 @@ {{#if media.length}}
- {{#each media as mediaItem, i}} -
- {{mediaItem.file.name}} -
- -
-
- -
-
+ {{#each media as mediaItem, index}} + {{/each}}
{{/if}} @@ -33,72 +17,15 @@ padding: 5px; border-radius: 4px; } - .compose-media { - height: 200px; - overflow: hidden; - flex-direction: column; - position: relative; - display: flex; - background: var(--main-bg); - } - .compose-media img { - object-fit: contain; - object-position: center center; - flex: 1; - height: 100%; - width: 100%; - } - .compose-media-alt { - z-index: 10; - position: absolute; - bottom: 0; - left: 0; - right: 0; - display: flex; - justify-content: center; - } - .compose-media-alt input { - width: 100%; - font-size: 1.2em; - background: var(--alt-input-bg); - } - .compose-media-alt input:focus { - background: var(--main-bg); - } - .compose-media-delete { - position: absolute; - z-index: 10; - top: 0; - right: 0; - left: 0; - display: flex; - justify-content: flex-end; - margin: 2px; - } - .compose-media-delete-button { - padding: 10px; - background: none; - border: none; - } - .compose-media-delete-button:hover { - background: var(--toast-border); - } - .compose-media-delete-button-svg { - fill: var(--button-text); - width: 18px; - height: 18px; - } \ No newline at end of file diff --git a/routes/_components/compose/ComposeMediaItem.html b/routes/_components/compose/ComposeMediaItem.html new file mode 100644 index 00000000..c56caead --- /dev/null +++ b/routes/_components/compose/ComposeMediaItem.html @@ -0,0 +1,126 @@ +
+ {{mediaItem.file.name}} +
+ +
+
+ +
+
+ + \ No newline at end of file diff --git a/routes/_components/dialog/helpers/createDialogId.js b/routes/_components/dialog/helpers/createDialogId.js index 12f63daf..3dfd0cce 100644 --- a/routes/_components/dialog/helpers/createDialogId.js +++ b/routes/_components/dialog/helpers/createDialogId.js @@ -2,4 +2,4 @@ let count = -1 export function createDialogId () { return ++count -} \ No newline at end of file +} diff --git a/routes/_utils/ajax.js b/routes/_utils/ajax.js index 85896aaf..00b12f64 100644 --- a/routes/_utils/ajax.js +++ b/routes/_utils/ajax.js @@ -41,6 +41,26 @@ async function _post (url, body, headers, timeout) { return throwErrorIfInvalidResponse(response) } +async function _put (url, body, headers, timeout) { + let fetchFunc = timeout ? fetchWithTimeout : fetch + let opts = { + method: 'PUT' + } + if (body) { + opts.headers = Object.assign(headers, { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }) + opts.body = JSON.stringify(body) + } else { + opts.headers = Object.assign(headers, { + 'Accept': 'application/json' + }) + } + let response = await fetchFunc(url, opts) + return throwErrorIfInvalidResponse(response) +} + async function _get (url, headers, timeout) { let fetchFunc = timeout ? fetchWithTimeout : fetch let response = await fetchFunc(url, { @@ -63,6 +83,14 @@ async function _delete (url, headers, timeout) { return throwErrorIfInvalidResponse(response) } +export async function put (url, body, headers = {}) { + return _put(url, body, headers, false) +} + +export async function putWithTimeout (url, body, headers = {}) { + return _put(url, body, headers, true) +} + export async function post (url, body, headers = {}) { return _post(url, body, headers, false) } diff --git a/tests/spec/013-compose-media.js b/tests/spec/013-compose-media.js index 361d7e05..8d5d282e 100644 --- a/tests/spec/013-compose-media.js +++ b/tests/spec/013-compose-media.js @@ -1,4 +1,7 @@ -import { composeInput, getNthDeleteMediaButton, getNthMedia, mediaButton, uploadKittenImage } from '../utils' +import { + composeInput, getNthDeleteMediaButton, getNthMedia, mediaButton, + uploadKittenImage +} from '../utils' import { foobarRole } from '../roles' fixture`013-compose-media.js` diff --git a/tests/spec/108-compose-dialog.js b/tests/spec/108-compose-dialog.js index 0218a885..951d078f 100644 --- a/tests/spec/108-compose-dialog.js +++ b/tests/spec/108-compose-dialog.js @@ -48,4 +48,4 @@ test('can use emoji dialog within compose dialog', async t => { .expect(showMoreButton.innerText).contains('Show 1 more') .click(showMoreButton) await t.expect(getNthStatus(0).find('img[alt=":blobpats:"]').exists).ok({timeout: 20000}) -}) \ No newline at end of file +}) diff --git a/tests/spec/109-compose-media.js b/tests/spec/109-compose-media.js new file mode 100644 index 00000000..fbc8f164 --- /dev/null +++ b/tests/spec/109-compose-media.js @@ -0,0 +1,73 @@ +import { + composeButton, getNthDeleteMediaButton, getNthMedia, getNthMediaAltInput, getNthStatusAndImage, getUrl, + homeNavButton, + mediaButton, notificationsNavButton, + uploadKittenImage +} from '../utils' +import { foobarRole } from '../roles' + +fixture`109-compose-media.js` + .page`http://localhost:4002` + +async function uploadTwoKittens (t) { + await (uploadKittenImage(1)()) + await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg') + await (uploadKittenImage(2)()) + await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg') + .expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg') +} + +test('uploads alts for media', async t => { + await t.useRole(foobarRole) + .expect(mediaButton.hasAttribute('disabled')).notOk() + await uploadTwoKittens(t) + await t.typeText(getNthMediaAltInput(2), 'kitten 2') + .typeText(getNthMediaAltInput(1), 'kitten 1') + .click(composeButton) + .expect(getNthStatusAndImage(0, 0).getAttribute('alt')).eql('kitten 1') + .expect(getNthStatusAndImage(0, 1).getAttribute('alt')).eql('kitten 2') +}) + +test('uploads alts when deleting and re-uploading media', async t => { + await t.useRole(foobarRole) + .expect(mediaButton.hasAttribute('disabled')).notOk() + await (uploadKittenImage(1)()) + await t.typeText(getNthMediaAltInput(1), 'this will be deleted') + .click(getNthDeleteMediaButton(1)) + .expect(getNthMedia(1).exists).notOk() + await (uploadKittenImage(2)()) + await t.expect(getNthMediaAltInput(1).value).eql('') + .expect(getNthMedia(1).getAttribute('alt')).eql('kitten2.jpg') + .typeText(getNthMediaAltInput(1), 'this will not be deleted') + .click(composeButton) + .expect(getNthStatusAndImage(0, 0).getAttribute('alt')).eql('this will not be deleted') +}) + +test('uploads alts mixed with no-alts', async t => { + await t.useRole(foobarRole) + .expect(mediaButton.hasAttribute('disabled')).notOk() + await uploadTwoKittens(t) + await t.typeText(getNthMediaAltInput(2), 'kitten numero dos') + .click(composeButton) + .expect(getNthStatusAndImage(0, 0).getAttribute('alt')).eql('') + .expect(getNthStatusAndImage(0, 1).getAttribute('alt')).eql('kitten numero dos') +}) + +test('saves alts to local storage', async t => { + await t.useRole(foobarRole) + .expect(mediaButton.hasAttribute('disabled')).notOk() + await uploadTwoKittens(t) + await t.typeText(getNthMediaAltInput(1), 'kitten numero uno') + .typeText(getNthMediaAltInput(2), 'kitten numero dos') + .click(notificationsNavButton) + .expect(getUrl()).contains('/notifications') + .click(homeNavButton) + .expect(getUrl()).eql('http://localhost:4002/') + .expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg') + .expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg') + .expect(getNthMediaAltInput(1).value).eql('kitten numero uno') + .expect(getNthMediaAltInput(2).value).eql('kitten numero dos') + .click(composeButton) + .expect(getNthStatusAndImage(0, 0).getAttribute('alt')).eql('kitten numero uno') + .expect(getNthStatusAndImage(0, 1).getAttribute('alt')).eql('kitten numero dos') +}) diff --git a/tests/utils.js b/tests/utils.js index 25f3fb64..d628bf01 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -96,6 +96,10 @@ export const uploadKittenImage = i => (exec(() => { } })) +export function getNthMediaAltInput (n) { + return $(`.compose-box .compose-media:nth-child(${n}) .compose-media-alt input`) +} + export function getNthComposeReplyInput (n) { return getNthStatus(n).find('.compose-box-input') } @@ -128,6 +132,10 @@ export function getNthStatus (n) { return $(`div[aria-hidden="false"] > article[aria-posinset="${n}"]`) } +export function getNthStatusAndImage (nStatus, nImage) { + return getNthStatus(nStatus).find(`.status-media .show-image-button:nth-child(${nImage + 1}) img`) +} + export function getLastVisibleStatus () { return $(`div[aria-hidden="false"] > article[aria-posinset]`).nth(-1) }