summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authornullobsi <me@nullob.si>2021-05-27 17:38:09 -0700
committerGitHub <noreply@github.com>2021-05-28 09:38:09 +0900
commitffb9646ce9c3d2326a3e922e58702674eb65646c (patch)
treebb656e32c4b61313faad218578018a7bcc989e05 /src
parentimprove types (diff)
downloadmisskey-ffb9646ce9c3d2326a3e922e58702674eb65646c.tar.gz
misskey-ffb9646ce9c3d2326a3e922e58702674eb65646c.tar.bz2
misskey-ffb9646ce9c3d2326a3e922e58702674eb65646c.zip
Add image description support (#7518)
* recieve image descriptions under the name property * fix other components * use comment for alt and title * allow editing of file comment * allow editing of file comment in note dialog * federate note comments * use file instead of this * backend should accept comment on update * update now actually accepts comment * allow multiline descriptions * image should also have description attached * Update locales/ja-JP.yml Co-authored-by: rinsuki <428rinsuki+git@gmail.com> * Use custom component with side-by-side image * improve usability on mobile devices * revert changes * Update post-form-attaches.vue * Update drive.file.vue * Update media-caption.vue Co-authored-by: rinsuki <428rinsuki+git@gmail.com> Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Diffstat (limited to 'src')
-rw-r--r--src/client/components/drive.file.vue24
-rw-r--r--src/client/components/image-viewer.vue2
-rw-r--r--src/client/components/media-caption.vue238
-rw-r--r--src/client/components/media-image.vue4
-rw-r--r--src/client/components/post-form-attaches.vue25
-rw-r--r--src/remote/activitypub/models/image.ts2
-rw-r--r--src/remote/activitypub/renderer/document.ts3
-rw-r--r--src/remote/activitypub/renderer/image.ts3
-rw-r--r--src/server/api/endpoints/drive/files/update.ts11
-rw-r--r--src/services/drive/upload-from-url.ts6
10 files changed, 312 insertions, 6 deletions
diff --git a/src/client/components/drive.file.vue b/src/client/components/drive.file.vue
index 37b1afc1b3..3d20de23e9 100644
--- a/src/client/components/drive.file.vue
+++ b/src/client/components/drive.file.vue
@@ -87,6 +87,10 @@ export default defineComponent({
text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
action: this.toggleSensitive
+ }, {
+ text: this.$ts.describeFile,
+ icon: 'fas fa-i-cursor',
+ action: this.describe
}, null, {
text: this.$ts.copyUrl,
icon: 'fas fa-link',
@@ -150,6 +154,26 @@ export default defineComponent({
});
},
+ describe() {
+ os.popup(import('@client/components/media-caption.vue'), {
+ title: this.$ts.describeFile,
+ input: {
+ placeholder: this.$ts.inputNewDescription,
+ default: this.file.comment !== null ? this.file.comment : '',
+ },
+ image: this.file
+ }, {
+ done: result => {
+ if (!result || result.canceled) return;
+ let comment = result.result;
+ os.api('drive/files/update', {
+ fileId: this.file.id,
+ comment: comment.length == 0 ? null : comment
+ });
+ }
+ }, 'closed');
+ },
+
toggleSensitive() {
os.api('drive/files/update', {
fileId: this.file.id,
diff --git a/src/client/components/image-viewer.vue b/src/client/components/image-viewer.vue
index ec22bd98ec..7701ae926f 100644
--- a/src/client/components/image-viewer.vue
+++ b/src/client/components/image-viewer.vue
@@ -2,7 +2,7 @@
<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
<div class="xubzgfga">
<header>{{ image.name }}</header>
- <img :src="image.url" :alt="image.name" :title="image.name" @click="$refs.modal.close()"/>
+ <img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
<footer>
<span>{{ image.type }}</span>
<span>{{ bytes(image.size) }}</span>
diff --git a/src/client/components/media-caption.vue b/src/client/components/media-caption.vue
new file mode 100644
index 0000000000..690927d4c5
--- /dev/null
+++ b/src/client/components/media-caption.vue
@@ -0,0 +1,238 @@
+<template>
+ <MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
+ <div class="container">
+ <div class="fullwidth top-caption">
+ <div class="mk-dialog">
+ <header v-if="title"><Mfm :text="title"/></header>
+ <textarea autofocus v-model="inputValue" :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea>
+ <div class="buttons" v-if="(showOkButton || showCancelButton)">
+ <MkButton inline @click="ok" primary>{{ $ts.ok }}</MkButton>
+ <MkButton inline @click="cancel" >{{ $ts.cancel }}</MkButton>
+ </div>
+ </div>
+ </div>
+ <div class="hdrwpsaf fullwidth">
+ <header>{{ image.name }}</header>
+ <img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
+ <footer>
+ <span>{{ image.type }}</span>
+ <span>{{ bytes(image.size) }}</span>
+ <span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
+ </footer>
+ </div>
+ </div>
+ </MkModal>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import MkModal from '@client/components/ui/modal.vue';
+import MkButton from '@client/components/ui/button.vue';
+import bytes from '@client/filters/bytes';
+import number from '@client/filters/number';
+
+export default defineComponent({
+ components: {
+ MkModal,
+ MkButton,
+ },
+
+ props: {
+ image: {
+ type: Object,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: false
+ },
+ input: {
+ required: true
+ },
+ showOkButton: {
+ type: Boolean,
+ default: true
+ },
+ showCancelButton: {
+ type: Boolean,
+ default: true
+ },
+ cancelableByBgClick: {
+ type: Boolean,
+ default: true
+ },
+ },
+
+ emits: ['done', 'closed'],
+
+ data() {
+ return {
+ inputValue: this.input.default ? this.input.default : null
+ };
+ },
+
+ mounted() {
+ document.addEventListener('keydown', this.onKeydown);
+ },
+
+ beforeUnmount() {
+ document.removeEventListener('keydown', this.onKeydown);
+ },
+
+ methods: {
+ bytes,
+ number,
+
+ done(canceled, result?) {
+ this.$emit('done', { canceled, result });
+ this.$refs.modal.close();
+ },
+
+ async ok() {
+ if (!this.showOkButton) return;
+
+ const result = this.inputValue;
+ this.done(false, result);
+ },
+
+ cancel() {
+ this.done(true);
+ },
+
+ onBgClick() {
+ if (this.cancelableByBgClick) {
+ this.cancel();
+ }
+ },
+
+ onKeydown(e) {
+ if (e.which === 27) { // ESC
+ this.cancel();
+ }
+ },
+
+ onInputKeydown(e) {
+ if (e.which === 13) { // Enter
+ if (e.ctrlKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.ok();
+ }
+ }
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.container {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ flex-direction: row;
+}
+@media (max-width: 850px) {
+ .container {
+ flex-direction: column;
+ }
+ .top-caption {
+ padding-bottom: 8px;
+ }
+}
+.fullwidth {
+ width: 100%;
+ margin: auto;
+}
+.mk-dialog {
+ position: relative;
+ padding: 32px;
+ min-width: 320px;
+ max-width: 480px;
+ box-sizing: border-box;
+ text-align: center;
+ background: var(--panel);
+ border-radius: var(--radius);
+ margin: auto;
+
+ > header {
+ margin: 0 0 8px 0;
+ font-weight: bold;
+ font-size: 20px;
+ }
+
+ > .buttons {
+ margin-top: 16px;
+
+ > * {
+ margin: 0 8px;
+ }
+ }
+
+ > textarea {
+ display: block;
+ box-sizing: border-box;
+ padding: 0 24px;
+ margin: 0;
+ width: 100%;
+ font-size: 16px;
+ border: none;
+ border-radius: 0;
+ background: transparent;
+ color: var(--fg);
+ font-family: inherit;
+ max-width: 100%;
+ min-width: 100%;
+ min-height: 90px;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ }
+ }
+}
+.hdrwpsaf {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ > header,
+ > footer {
+ align-self: center;
+ display: inline-block;
+ padding: 6px 9px;
+ font-size: 90%;
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: 6px;
+ color: #fff;
+ }
+
+ > header {
+ margin-bottom: 8px;
+ opacity: 0.9;
+ }
+
+ > img {
+ display: block;
+ flex: 1;
+ min-height: 0;
+ object-fit: contain;
+ width: 100%;
+ cursor: zoom-out;
+ image-orientation: from-image;
+ }
+
+ > footer {
+ margin-top: 8px;
+ opacity: 0.8;
+
+ > span + span {
+ margin-left: 0.5em;
+ padding-left: 0.5em;
+ border-left: solid 1px rgba(255, 255, 255, 0.5);
+ }
+ }
+}
+</style>
diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue
index 267e4debd2..863eb10272 100644
--- a/src/client/components/media-image.vue
+++ b/src/client/components/media-image.vue
@@ -1,6 +1,6 @@
<template>
<div class="qjewsnkg" v-if="hide" @click="hide = false">
- <ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.name"/>
+ <ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
<div class="text">
<div>
<b><i class="fas fa-exclamation-triangle"></i> {{ $ts.sensitive }}</b>
@@ -14,7 +14,7 @@
:title="image.name"
@click.prevent="onClick"
>
- <ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name" :cover="false"/>
+ <ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment" :title="image.comment" :cover="false"/>
<div class="gif" v-if="image.type === 'image/gif'">GIF</div>
</a>
<i class="fas fa-eye-slash" @click="hide = true"></i>
diff --git a/src/client/components/post-form-attaches.vue b/src/client/components/post-form-attaches.vue
index f832ea87b5..27e20fdfa8 100644
--- a/src/client/components/post-form-attaches.vue
+++ b/src/client/components/post-form-attaches.vue
@@ -89,6 +89,27 @@ export default defineComponent({
file.name = result;
});
},
+
+ async describe(file) {
+ os.popup(import("@client/components/media-caption.vue"), {
+ title: this.$ts.describeFile,
+ input: {
+ placeholder: this.$ts.inputNewDescription,
+ default: file.comment !== null ? file.comment : "",
+ },
+ image: file
+ }, {
+ done: result => {
+ if (!result || result.canceled) return;
+ let comment = result.result;
+ os.api('drive/files/update', {
+ fileId: file.id,
+ comment: comment.length == 0 ? null : comment
+ });
+ }
+ }, 'closed');
+ },
+
showFileMenu(file, ev: MouseEvent) {
if (this.menu) return;
this.menu = os.modalMenu([{
@@ -100,6 +121,10 @@ export default defineComponent({
icon: file.isSensitive ? 'fas fa-eye-slash' : 'fas fa-eye',
action: () => { this.toggleSensitive(file) }
}, {
+ text: this.$ts.describeFile,
+ icon: 'fas fa-i-cursor',
+ action: () => { this.describe(file) }
+ }, {
text: this.$ts.attachCancel,
icon: 'fas fa-times-circle',
action: () => { this.detachMedia(file.id) }
diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts
index 79fc2bf4a6..7bec1d6030 100644
--- a/src/remote/activitypub/models/image.ts
+++ b/src/remote/activitypub/models/image.ts
@@ -28,7 +28,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
const instance = await fetchMeta();
const cache = instance.cacheRemoteFiles;
- let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache);
+ let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, image.name);
if (file.isLink) {
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
diff --git a/src/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts
index 4f6ea8c4ee..f6e9dca45d 100644
--- a/src/remote/activitypub/renderer/document.ts
+++ b/src/remote/activitypub/renderer/document.ts
@@ -4,5 +4,6 @@ import { DriveFiles } from '../../../models';
export default (file: DriveFile) => ({
type: 'Document',
mediaType: file.type,
- url: DriveFiles.getPublicUrl(file)
+ url: DriveFiles.getPublicUrl(file),
+ name: file.comment,
});
diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts
index ce98f98c62..cbd4fbbe68 100644
--- a/src/remote/activitypub/renderer/image.ts
+++ b/src/remote/activitypub/renderer/image.ts
@@ -4,5 +4,6 @@ import { DriveFiles } from '../../../models';
export default (file: DriveFile) => ({
type: 'Image',
url: DriveFiles.getPublicUrl(file),
- sensitive: file.isSensitive
+ sensitive: file.isSensitive,
+ name: file.comment
});
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index 6eda83967b..f740fea67e 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -49,6 +49,14 @@ export const meta = {
'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか',
'en-US': 'Whether this media is NSFW'
}
+ },
+
+ comment: {
+ validator: $.optional.nullable.str,
+ default: undefined as any,
+ desc: {
+ 'ja-JP': 'コメント'
+ }
}
},
@@ -92,6 +100,8 @@ export default define(meta, async (ps, user) => {
if (ps.name) file.name = ps.name;
+ if (ps.comment !== undefined) file.comment = ps.comment;
+
if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
if (ps.folderId !== undefined) {
@@ -113,6 +123,7 @@ export default define(meta, async (ps, user) => {
await DriveFiles.update(file.id, {
name: file.name,
+ comment: file.comment,
folderId: file.folderId,
isSensitive: file.isSensitive
});
diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
index 2f4c5aeeaf..2f660d9035 100644
--- a/src/services/drive/upload-from-url.ts
+++ b/src/services/drive/upload-from-url.ts
@@ -25,6 +25,12 @@ export default async (
name = null;
}
+ // If the comment is same as the name, skip comment
+ // (image.name is passed in when receiving attachment)
+ if (comment !== null && name == comment) {
+ comment = null;
+ }
+
// Create temp file
const [path, cleanup] = await createTemp();