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}}
{{/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 @@
+
+
+
\ 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)
}