summaryrefslogtreecommitdiff
path: root/packages/client/src/components
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2022-01-21 21:32:50 +0900
committertamaina <tamaina@hotmail.co.jp>2022-01-21 21:32:50 +0900
commitb5a20494f6032f840475bb69c79ccc2ffac11b7c (patch)
tree9da47076c540446cf818f456eca697d423fd5ef4 /packages/client/src/components
parentMerge branch 'develop' into pizzax-indexeddb (diff)
parentchore(client): add #misskey button (diff)
downloadmisskey-b5a20494f6032f840475bb69c79ccc2ffac11b7c.tar.gz
misskey-b5a20494f6032f840475bb69c79ccc2ffac11b7c.tar.bz2
misskey-b5a20494f6032f840475bb69c79ccc2ffac11b7c.zip
Merge branch 'develop' into pizzax-indexeddb
Diffstat (limited to 'packages/client/src/components')
-rw-r--r--packages/client/src/components/abuse-report.vue102
-rw-r--r--packages/client/src/components/captcha.vue10
-rw-r--r--packages/client/src/components/dialog.vue7
-rw-r--r--packages/client/src/components/drive-file-thumbnail.vue95
-rw-r--r--packages/client/src/components/drive-select-dialog.vue71
-rw-r--r--packages/client/src/components/drive-window.vue39
-rw-r--r--packages/client/src/components/drive.file.vue288
-rw-r--r--packages/client/src/components/drive.folder.vue394
-rw-r--r--packages/client/src/components/drive.nav-folder.vue169
-rw-r--r--packages/client/src/components/drive.vue1026
-rw-r--r--packages/client/src/components/emoji-picker-dialog.vue95
-rw-r--r--packages/client/src/components/emoji-picker-window.vue51
-rw-r--r--packages/client/src/components/emoji-picker.section.vue38
-rw-r--r--packages/client/src/components/emoji-picker.vue471
-rw-r--r--packages/client/src/components/featured-photos.vue22
-rw-r--r--packages/client/src/components/follow-button.vue171
-rw-r--r--packages/client/src/components/forgot-password.vue76
-rw-r--r--packages/client/src/components/global/avatar.vue4
-rw-r--r--packages/client/src/components/modal-page-window.vue4
-rw-r--r--packages/client/src/components/note-detailed.vue9
-rw-r--r--packages/client/src/components/note.vue8
-rw-r--r--packages/client/src/components/notes.vue4
-rw-r--r--packages/client/src/components/post-form-attaches.vue9
-rw-r--r--packages/client/src/components/post-form.vue83
-rw-r--r--packages/client/src/components/timeline.vue12
-rw-r--r--packages/client/src/components/ui/menu.vue2
-rw-r--r--packages/client/src/components/ui/pagination.vue47
-rw-r--r--packages/client/src/components/ui/window.vue4
28 files changed, 1650 insertions, 1661 deletions
diff --git a/packages/client/src/components/abuse-report.vue b/packages/client/src/components/abuse-report.vue
new file mode 100644
index 0000000000..b67cef209b
--- /dev/null
+++ b/packages/client/src/components/abuse-report.vue
@@ -0,0 +1,102 @@
+<template>
+<div class="bcekxzvu _card _gap">
+ <div class="_content target">
+ <MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
+ <MkA class="info" :to="userPage(report.targetUser)" v-user-preview="report.targetUserId">
+ <MkUserName class="name" :user="report.targetUser"/>
+ <MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
+ </MkA>
+ </div>
+ <div class="_content">
+ <div>
+ <Mfm :text="report.comment"/>
+ </div>
+ <hr/>
+ <div>{{ $ts.reporter }}: <MkAcct :user="report.reporter"/></div>
+ <div v-if="report.assignee">
+ {{ $ts.moderator }}:
+ <MkAcct :user="report.assignee"/>
+ </div>
+ <div><MkTime :time="report.createdAt"/></div>
+ </div>
+ <div class="_footer">
+ <MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
+ {{ $ts.forwardReport }}
+ <template #caption>{{ $ts.forwardReportIsAnonymous }}</template>
+ </MkSwitch>
+ <MkButton v-if="!report.resolved" primary @click="resolve">{{ $ts.abuseMarkAsResolved }}</MkButton>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+import MkButton from '@/components/ui/button.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import { acct, userPage } from '@/filters/user';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ MkButton,
+ MkSwitch,
+ },
+
+ emits: ['resolved'],
+
+ props: {
+ report: {
+ type: Object,
+ required: true,
+ }
+ }
+
+ data() {
+ return {
+ forward: this.report.forwarded,
+ };
+ }
+
+ methods: {
+ acct,
+ userPage,
+
+ resolve() {
+ os.apiWithDialog('admin/resolve-abuse-user-report', {
+ forward: this.forward,
+ reportId: this.report.id,
+ }).then(() => {
+ this.$emit('resolved', this.report.id);
+ });
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.bcekxzvu {
+ > .target {
+ display: flex;
+ width: 100%;
+ box-sizing: border-box;
+ text-align: left;
+ align-items: center;
+
+ > .avatar {
+ width: 42px;
+ height: 42px;
+ }
+
+ > .info {
+ margin-left: 0.3em;
+ padding: 0 8px;
+ flex: 1;
+
+ > .name {
+ font-weight: bold;
+ }
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/components/captcha.vue b/packages/client/src/components/captcha.vue
index 7fe499dc86..770804cf44 100644
--- a/packages/client/src/components/captcha.vue
+++ b/packages/client/src/components/captcha.vue
@@ -55,12 +55,10 @@ const variable = computed(() => {
const loaded = computed(() => !!window[variable.value]);
const src = computed(() => {
- const endpoint = ({
- hcaptcha: 'https://hcaptcha.com/1',
- recaptcha: 'https://www.recaptcha.net/recaptcha',
- } as Record<CaptchaProvider, string>)[props.provider];
-
- return `${typeof endpoint === 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
+ switch (props.provider) {
+ case 'hcaptcha': return 'https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off';
+ case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
+ }
});
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
diff --git a/packages/client/src/components/dialog.vue b/packages/client/src/components/dialog.vue
index 9cd5234684..b6b649cde9 100644
--- a/packages/client/src/components/dialog.vue
+++ b/packages/client/src/components/dialog.vue
@@ -1,5 +1,5 @@
<template>
-<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="$emit('closed')">
+<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
<div class="mk-dialog">
<div v-if="icon" class="icon">
<i :class="icon"></i>
@@ -28,8 +28,8 @@
</template>
</MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
- <MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? $ts.ok : $ts.gotIt }}</MkButton>
- <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ $ts.cancel }}</MkButton>
+ <MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.locale.ok : i18n.locale.gotIt }}</MkButton>
+ <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</MkButton>
</div>
<div v-if="actions" class="buttons">
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
@@ -44,6 +44,7 @@ import MkModal from '@/components/ui/modal.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
+import { i18n } from '@/i18n';
type Input = {
type: HTMLInputElement['type'];
diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue
index e94b6b8bcb..81b80e7e8e 100644
--- a/packages/client/src/components/drive-file-thumbnail.vue
+++ b/packages/client/src/components/drive-file-thumbnail.vue
@@ -14,71 +14,42 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as Misskey from 'misskey-js';
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
-import { ColdDeviceStorage } from '@/store';
-export default defineComponent({
- components: {
- ImgWithBlurhash
- },
- props: {
- file: {
- type: Object,
- required: true
- },
- fit: {
- type: String,
- required: false,
- default: 'cover'
- },
- },
- data() {
- return {
- isContextmenuShowing: false,
- isDragging: false,
+const props = defineProps<{
+ file: Misskey.entities.DriveFile;
+ fit: string;
+}>();
- };
- },
- computed: {
- is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' {
- if (this.file.type.startsWith('image/')) return 'image';
- if (this.file.type.startsWith('video/')) return 'video';
- if (this.file.type === 'audio/midi') return 'midi';
- if (this.file.type.startsWith('audio/')) return 'audio';
- if (this.file.type.endsWith('/csv')) return 'csv';
- if (this.file.type.endsWith('/pdf')) return 'pdf';
- if (this.file.type.startsWith('text/')) return 'textfile';
- if ([
- "application/zip",
- "application/x-cpio",
- "application/x-bzip",
- "application/x-bzip2",
- "application/java-archive",
- "application/x-rar-compressed",
- "application/x-tar",
- "application/gzip",
- "application/x-7z-compressed"
- ].some(e => e === this.file.type)) return 'archive';
- return 'unknown';
- },
- isThumbnailAvailable(): boolean {
- return this.file.thumbnailUrl
- ? (this.is === 'image' || this.is === 'video')
- : false;
- },
- },
- mounted() {
- const audioTag = this.$refs.volumectrl as HTMLAudioElement;
- if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
- },
- methods: {
- volumechange() {
- const audioTag = this.$refs.volumectrl as HTMLAudioElement;
- ColdDeviceStorage.set('mediaVolume', audioTag.volume);
- }
- }
+const is = computed(() => {
+ if (props.file.type.startsWith('image/')) return 'image';
+ if (props.file.type.startsWith('video/')) return 'video';
+ if (props.file.type === 'audio/midi') return 'midi';
+ if (props.file.type.startsWith('audio/')) return 'audio';
+ if (props.file.type.endsWith('/csv')) return 'csv';
+ if (props.file.type.endsWith('/pdf')) return 'pdf';
+ if (props.file.type.startsWith('text/')) return 'textfile';
+ if ([
+ "application/zip",
+ "application/x-cpio",
+ "application/x-bzip",
+ "application/x-bzip2",
+ "application/java-archive",
+ "application/x-rar-compressed",
+ "application/x-tar",
+ "application/gzip",
+ "application/x-7z-compressed"
+ ].some(e => e === props.file.type)) return 'archive';
+ return 'unknown';
+});
+
+const isThumbnailAvailable = computed(() => {
+ return props.file.thumbnailUrl
+ ? (is.value === 'image' as const || is.value === 'video')
+ : false;
});
</script>
diff --git a/packages/client/src/components/drive-select-dialog.vue b/packages/client/src/components/drive-select-dialog.vue
index 75537dfe3e..6d84511277 100644
--- a/packages/client/src/components/drive-select-dialog.vue
+++ b/packages/client/src/components/drive-select-dialog.vue
@@ -7,64 +7,51 @@
@click="cancel()"
@close="cancel()"
@ok="ok()"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<template #header>
- {{ multiple ? ((type === 'file') ? $ts.selectFiles : $ts.selectFolders) : ((type === 'file') ? $ts.selectFile : $ts.selectFolder) }}
+ {{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.selectFolder) }}
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
</template>
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
</XModalWindow>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
import XDrive from './drive.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import number from '@/filters/number';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XDrive,
- XModalWindow,
- },
-
- props: {
- type: {
- type: String,
- required: false,
- default: 'file'
- },
- multiple: {
- type: Boolean,
- default: false
- }
- },
+withDefaults(defineProps<{
+ type?: 'file' | 'folder';
+ multiple: boolean;
+}>(), {
+ type: 'file',
+});
- emits: ['done', 'closed'],
+const emit = defineEmits<{
+ (e: 'done', r?: Misskey.entities.DriveFile[]): void;
+ (e: 'closed'): void;
+}>();
- data() {
- return {
- selected: []
- };
- },
+const dialog = ref<InstanceType<typeof XModalWindow>>();
- methods: {
- ok() {
- this.$emit('done', this.selected);
- this.$refs.dialog.close();
- },
+const selected = ref<Misskey.entities.DriveFile[]>([]);
- cancel() {
- this.$emit('done');
- this.$refs.dialog.close();
- },
+function ok() {
+ emit('done', selected.value);
+ dialog.value?.close();
+}
- onChangeSelection(xs) {
- this.selected = xs;
- },
+function cancel() {
+ emit('done');
+ dialog.value?.close();
+}
- number
- }
-});
+function onChangeSelection(files: Misskey.entities.DriveFile[]) {
+ selected.value = files;
+}
</script>
diff --git a/packages/client/src/components/drive-window.vue b/packages/client/src/components/drive-window.vue
index 43f07ebe76..8b60bf7794 100644
--- a/packages/client/src/components/drive-window.vue
+++ b/packages/client/src/components/drive-window.vue
@@ -3,42 +3,27 @@
:initial-width="800"
:initial-height="500"
:can-resize="true"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<template #header>
- {{ $ts.drive }}
+ {{ i18n.locale.drive }}
</template>
<XDrive :initial-folder="initialFolder"/>
</XWindow>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as Misskey from 'misskey-js';
import XDrive from './drive.vue';
import XWindow from '@/components/ui/window.vue';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XDrive,
- XWindow,
- },
+defineProps<{
+ initialFolder?: Misskey.entities.DriveFolder;
+}>();
- props: {
- initialFolder: {
- type: Object,
- required: false
- },
- },
-
- emits: ['closed'],
-
- data() {
- return {
- };
- },
-
- methods: {
-
- }
-});
+const emit = defineEmits<{
+ (e: 'closed'): void;
+}>();
</script>
diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue
index 511647229e..fd6a813838 100644
--- a/packages/client/src/components/drive.file.vue
+++ b/packages/client/src/components/drive.file.vue
@@ -8,17 +8,17 @@
@dragstart="onDragstart"
@dragend="onDragend"
>
- <div v-if="$i.avatarId == file.id" class="label">
+ <div v-if="$i?.avatarId == file.id" class="label">
<img src="/client-assets/label.svg"/>
- <p>{{ $ts.avatar }}</p>
+ <p>{{ i18n.locale.avatar }}</p>
</div>
- <div v-if="$i.bannerId == file.id" class="label">
+ <div v-if="$i?.bannerId == file.id" class="label">
<img src="/client-assets/label.svg"/>
- <p>{{ $ts.banner }}</p>
+ <p>{{ i18n.locale.banner }}</p>
</div>
<div v-if="file.isSensitive" class="label red">
<img src="/client-assets/label-red.svg"/>
- <p>{{ $ts.nsfw }}</p>
+ <p>{{ i18n.locale.nsfw }}</p>
</div>
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
@@ -30,179 +30,155 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+import * as Misskey from 'misskey-js';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
import bytes from '@/filters/bytes';
import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { $i } from '@/account';
-export default defineComponent({
- components: {
- MkDriveFileThumbnail
- },
+const props = withDefaults(defineProps<{
+ file: Misskey.entities.DriveFile;
+ isSelected?: boolean;
+ selectMode?: boolean;
+}>(), {
+ isSelected: false,
+ selectMode: false,
+});
- props: {
- file: {
- type: Object,
- required: true,
- },
- isSelected: {
- type: Boolean,
- required: false,
- default: false,
- },
- selectMode: {
- type: Boolean,
- required: false,
- default: false,
- }
- },
+const emit = defineEmits<{
+ (e: 'chosen', r: Misskey.entities.DriveFile): void;
+ (e: 'dragstart'): void;
+ (e: 'dragend'): void;
+}>();
- emits: ['chosen'],
+const isDragging = ref(false);
- data() {
- return {
- isDragging: false
- };
- },
+const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`);
- computed: {
- // TODO: parentへの参照を無くす
- browser(): any {
- return this.$parent;
- },
- title(): string {
- return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`;
- }
- },
-
- methods: {
- getMenu() {
- return [{
- text: this.$ts.rename,
- icon: 'fas fa-i-cursor',
- action: this.rename
- }, {
- 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',
- action: this.copyUrl
- }, {
- type: 'a',
- href: this.file.url,
- target: '_blank',
- text: this.$ts.download,
- icon: 'fas fa-download',
- download: this.file.name
- }, null, {
- text: this.$ts.delete,
- icon: 'fas fa-trash-alt',
- danger: true,
- action: this.deleteFile
- }];
- },
+function getMenu() {
+ return [{
+ text: i18n.locale.rename,
+ icon: 'fas fa-i-cursor',
+ action: rename
+ }, {
+ text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive,
+ icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
+ action: toggleSensitive
+ }, {
+ text: i18n.locale.describeFile,
+ icon: 'fas fa-i-cursor',
+ action: describe
+ }, null, {
+ text: i18n.locale.copyUrl,
+ icon: 'fas fa-link',
+ action: copyUrl
+ }, {
+ type: 'a',
+ href: props.file.url,
+ target: '_blank',
+ text: i18n.locale.download,
+ icon: 'fas fa-download',
+ download: props.file.name
+ }, null, {
+ text: i18n.locale.delete,
+ icon: 'fas fa-trash-alt',
+ danger: true,
+ action: deleteFile
+ }];
+}
- onClick(ev) {
- if (this.selectMode) {
- this.$emit('chosen', this.file);
- } else {
- os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
- }
- },
+function onClick(ev: MouseEvent) {
+ if (props.selectMode) {
+ emit('chosen', props.file);
+ } else {
+ os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
+ }
+}
- onContextmenu(e) {
- os.contextMenu(this.getMenu(), e);
- },
+function onContextmenu(e: MouseEvent) {
+ os.contextMenu(getMenu(), e);
+}
- onDragstart(e) {
- e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file));
- this.isDragging = true;
+function onDragstart(e: DragEvent) {
+ if (e.dataTransfer) {
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
+ }
+ isDragging.value = true;
- // 親ブラウザに対して、ドラッグが開始されたフラグを立てる
- // (=あなたの子供が、ドラッグを開始しましたよ)
- this.browser.isDragSource = true;
- },
+ emit('dragstart');
+}
- onDragend(e) {
- this.isDragging = false;
- this.browser.isDragSource = false;
- },
+function onDragend() {
+ isDragging.value = false;
+ emit('dragend');
+}
- rename() {
- os.inputText({
- title: this.$ts.renameFile,
- placeholder: this.$ts.inputNewFileName,
- default: this.file.name,
- allowEmpty: false
- }).then(({ canceled, result: name }) => {
- if (canceled) return;
- os.api('drive/files/update', {
- fileId: this.file.id,
- name: name
- });
- });
- },
+function rename() {
+ os.inputText({
+ title: i18n.locale.renameFile,
+ placeholder: i18n.locale.inputNewFileName,
+ default: props.file.name,
+ }).then(({ canceled, result: name }) => {
+ if (canceled) return;
+ os.api('drive/files/update', {
+ fileId: props.file.id,
+ name: name
+ });
+ });
+}
- describe() {
- os.popup(import('@/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');
+function describe() {
+ os.popup(import('@/components/media-caption.vue'), {
+ title: i18n.locale.describeFile,
+ input: {
+ placeholder: i18n.locale.inputNewDescription,
+ default: props.file.comment !== null ? props.file.comment : '',
},
-
- toggleSensitive() {
+ image: props.file
+ }, {
+ done: result => {
+ if (!result || result.canceled) return;
+ let comment = result.result;
os.api('drive/files/update', {
- fileId: this.file.id,
- isSensitive: !this.file.isSensitive
+ fileId: props.file.id,
+ comment: comment.length == 0 ? null : comment
});
- },
-
- copyUrl() {
- copyToClipboard(this.file.url);
- os.success();
- },
-
- addApp() {
- alert('not implemented yet');
- },
+ }
+ }, 'closed');
+}
- async deleteFile() {
- const { canceled } = await os.confirm({
- type: 'warning',
- text: this.$t('driveFileDeleteConfirm', { name: this.file.name }),
- });
- if (canceled) return;
+function toggleSensitive() {
+ os.api('drive/files/update', {
+ fileId: props.file.id,
+ isSensitive: !props.file.isSensitive
+ });
+}
- os.api('drive/files/delete', {
- fileId: this.file.id
- });
- },
+function copyUrl() {
+ copyToClipboard(props.file.url);
+ os.success();
+}
+/*
+function addApp() {
+ alert('not implemented yet');
+}
+*/
+async function deleteFile() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.t('driveFileDeleteConfirm', { name: props.file.name }),
+ });
- bytes
- }
-});
+ if (canceled) return;
+ os.api('drive/files/delete', {
+ fileId: props.file.id
+ });
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/drive.folder.vue b/packages/client/src/components/drive.folder.vue
index aaba736cf8..20a6343cfe 100644
--- a/packages/client/src/components/drive.folder.vue
+++ b/packages/client/src/components/drive.folder.vue
@@ -19,243 +19,233 @@
<template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template>
{{ folder.name }}
</p>
- <p v-if="$store.state.uploadFolder == folder.id" class="upload">
- {{ $ts.uploadFolder }}
+ <p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
+ {{ i18n.locale.uploadFolder }}
</p>
<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+import * as Misskey from 'misskey-js';
import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
-export default defineComponent({
- props: {
- folder: {
- type: Object,
- required: true,
- },
- isSelected: {
- type: Boolean,
- required: false,
- default: false,
- },
- selectMode: {
- type: Boolean,
- required: false,
- default: false,
- }
- },
+const props = withDefaults(defineProps<{
+ folder: Misskey.entities.DriveFolder;
+ isSelected?: boolean;
+ selectMode?: boolean;
+}>(), {
+ isSelected: false,
+ selectMode: false,
+});
- emits: ['chosen'],
+const emit = defineEmits<{
+ (ev: 'chosen', v: Misskey.entities.DriveFolder): void;
+ (ev: 'move', v: Misskey.entities.DriveFolder): void;
+ (ev: 'upload', file: File, folder: Misskey.entities.DriveFolder);
+ (ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
+ (ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
+ (ev: 'dragstart'): void;
+ (ev: 'dragend'): void;
+}>();
- data() {
- return {
- hover: false,
- draghover: false,
- isDragging: false,
- };
- },
+const hover = ref(false);
+const draghover = ref(false);
+const isDragging = ref(false);
- computed: {
- browser(): any {
- return this.$parent;
- },
- title(): string {
- return this.folder.name;
- }
- },
+const title = computed(() => props.folder.name);
- methods: {
- checkboxClicked(e) {
- this.$emit('chosen', this.folder);
- },
+function checkboxClicked() {
+ emit('chosen', props.folder);
+}
- onClick() {
- this.browser.move(this.folder);
- },
+function onClick() {
+ emit('move', props.folder);
+}
- onMouseover() {
- this.hover = true;
- },
+function onMouseover() {
+ hover.value = true;
+}
- onMouseout() {
- this.hover = false
- },
+function onMouseout() {
+ hover.value = false
+}
- onDragover(e) {
- // 自分自身がドラッグされている場合
- if (this.isDragging) {
- // 自分自身にはドロップさせない
- e.dataTransfer.dropEffect = 'none';
- return;
- }
+function onDragover(ev: DragEvent) {
+ if (!ev.dataTransfer) return;
- const isFile = e.dataTransfer.items[0].kind == 'file';
- const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
- const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+ // 自分自身がドラッグされている場合
+ if (isDragging.value) {
+ // 自分自身にはドロップさせない
+ ev.dataTransfer.dropEffect = 'none';
+ return;
+ }
- if (isFile || isDriveFile || isDriveFolder) {
- e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
- } else {
- e.dataTransfer.dropEffect = 'none';
- }
- },
+ const isFile = ev.dataTransfer.items[0].kind == 'file';
+ const isDriveFile = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+ const isDriveFolder = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
- onDragenter() {
- if (!this.isDragging) this.draghover = true;
- },
+ if (isFile || isDriveFile || isDriveFolder) {
+ ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+ } else {
+ ev.dataTransfer.dropEffect = 'none';
+ }
+}
- onDragleave() {
- this.draghover = false;
- },
+function onDragenter() {
+ if (!isDragging.value) draghover.value = true;
+}
- onDrop(e) {
- this.draghover = false;
+function onDragleave() {
+ draghover.value = false;
+}
- // ファイルだったら
- if (e.dataTransfer.files.length > 0) {
- for (const file of Array.from(e.dataTransfer.files)) {
- this.browser.upload(file, this.folder);
- }
- return;
- }
+function onDrop(ev: DragEvent) {
+ draghover.value = false;
- //#region ドライブのファイル
- const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
- if (driveFile != null && driveFile != '') {
- const file = JSON.parse(driveFile);
- this.browser.removeFile(file.id);
- os.api('drive/files/update', {
- fileId: file.id,
- folderId: this.folder.id
- });
- }
- //#endregion
+ if (!ev.dataTransfer) return;
+
+ // ファイルだったら
+ if (ev.dataTransfer.files.length > 0) {
+ for (const file of Array.from(ev.dataTransfer.files)) {
+ emit('upload', file, props.folder);
+ }
+ return;
+ }
- //#region ドライブのフォルダ
- const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
- if (driveFolder != null && driveFolder != '') {
- const folder = JSON.parse(driveFolder);
+ //#region ドライブのファイル
+ const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+ if (driveFile != null && driveFile != '') {
+ const file = JSON.parse(driveFile);
+ emit('removeFile', file.id);
+ os.api('drive/files/update', {
+ fileId: file.id,
+ folderId: props.folder.id
+ });
+ }
+ //#endregion
- // 移動先が自分自身ならreject
- if (folder.id == this.folder.id) return;
+ //#region ドライブのフォルダ
+ const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+ if (driveFolder != null && driveFolder != '') {
+ const folder = JSON.parse(driveFolder);
- this.browser.removeFolder(folder.id);
- os.api('drive/folders/update', {
- folderId: folder.id,
- parentId: this.folder.id
- }).then(() => {
- // noop
- }).catch(err => {
- switch (err) {
- case 'detected-circular-definition':
- os.alert({
- title: this.$ts.unableToProcess,
- text: this.$ts.circularReferenceFolder
- });
- break;
- default:
- os.alert({
- type: 'error',
- text: this.$ts.somethingHappened
- });
- }
- });
+ // 移動先が自分自身ならreject
+ if (folder.id == props.folder.id) return;
+
+ emit('removeFolder', folder.id);
+ os.api('drive/folders/update', {
+ folderId: folder.id,
+ parentId: props.folder.id
+ }).then(() => {
+ // noop
+ }).catch(err => {
+ switch (err) {
+ case 'detected-circular-definition':
+ os.alert({
+ title: i18n.locale.unableToProcess,
+ text: i18n.locale.circularReferenceFolder
+ });
+ break;
+ default:
+ os.alert({
+ type: 'error',
+ text: i18n.locale.somethingHappened
+ });
}
- //#endregion
- },
+ });
+ }
+ //#endregion
+}
- onDragstart(e) {
- e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder));
- this.isDragging = true;
+function onDragstart(ev: DragEvent) {
+ if (!ev.dataTransfer) return;
- // 親ブラウザに対して、ドラッグが開始されたフラグを立てる
- // (=あなたの子供が、ドラッグを開始しましたよ)
- this.browser.isDragSource = true;
- },
+ ev.dataTransfer.effectAllowed = 'move';
+ ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder));
+ isDragging.value = true;
- onDragend() {
- this.isDragging = false;
- this.browser.isDragSource = false;
- },
+ // 親ブラウザに対して、ドラッグが開始されたフラグを立てる
+ // (=あなたの子供が、ドラッグを開始しましたよ)
+ emit('dragstart');
+}
- go() {
- this.browser.move(this.folder.id);
- },
+function onDragend() {
+ isDragging.value = false;
+ emit('dragend');
+}
- newWindow() {
- this.browser.newWindow(this.folder);
- },
+function go() {
+ emit('move', props.folder.id);
+}
- rename() {
- os.inputText({
- title: this.$ts.renameFolder,
- placeholder: this.$ts.inputNewFolderName,
- default: this.folder.name
- }).then(({ canceled, result: name }) => {
- if (canceled) return;
- os.api('drive/folders/update', {
- folderId: this.folder.id,
- name: name
- });
- });
- },
+function rename() {
+ os.inputText({
+ title: i18n.locale.renameFolder,
+ placeholder: i18n.locale.inputNewFolderName,
+ default: props.folder.name
+ }).then(({ canceled, result: name }) => {
+ if (canceled) return;
+ os.api('drive/folders/update', {
+ folderId: props.folder.id,
+ name: name
+ });
+ });
+}
- deleteFolder() {
- os.api('drive/folders/delete', {
- folderId: this.folder.id
- }).then(() => {
- if (this.$store.state.uploadFolder === this.folder.id) {
- this.$store.set('uploadFolder', null);
- }
- }).catch(err => {
- switch(err.id) {
- case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
- os.alert({
- type: 'error',
- title: this.$ts.unableToDelete,
- text: this.$ts.hasChildFilesOrFolders
- });
- break;
- default:
- os.alert({
- type: 'error',
- text: this.$ts.unableToDelete
- });
- }
- });
- },
+function deleteFolder() {
+ os.api('drive/folders/delete', {
+ folderId: props.folder.id
+ }).then(() => {
+ if (defaultStore.state.uploadFolder === props.folder.id) {
+ defaultStore.set('uploadFolder', null);
+ }
+ }).catch(err => {
+ switch(err.id) {
+ case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
+ os.alert({
+ type: 'error',
+ title: i18n.locale.unableToDelete,
+ text: i18n.locale.hasChildFilesOrFolders
+ });
+ break;
+ default:
+ os.alert({
+ type: 'error',
+ text: i18n.locale.unableToDelete
+ });
+ }
+ });
+}
- setAsUploadFolder() {
- this.$store.set('uploadFolder', this.folder.id);
- },
+function setAsUploadFolder() {
+ defaultStore.set('uploadFolder', props.folder.id);
+}
- onContextmenu(e) {
- os.contextMenu([{
- text: this.$ts.openInWindow,
- icon: 'fas fa-window-restore',
- action: () => {
- os.popup(import('./drive-window.vue'), {
- initialFolder: this.folder
- }, {
- }, 'closed');
- }
- }, null, {
- text: this.$ts.rename,
- icon: 'fas fa-i-cursor',
- action: this.rename
- }, null, {
- text: this.$ts.delete,
- icon: 'fas fa-trash-alt',
- danger: true,
- action: this.deleteFolder
- }], e);
- },
- }
-});
+function onContextmenu(ev: MouseEvent) {
+ os.contextMenu([{
+ text: i18n.locale.openInWindow,
+ icon: 'fas fa-window-restore',
+ action: () => {
+ os.popup(import('./drive-window.vue'), {
+ initialFolder: props.folder
+ }, {
+ }, 'closed');
+ }
+ }, null, {
+ text: i18n.locale.rename,
+ icon: 'fas fa-i-cursor',
+ action: rename,
+ }, null, {
+ text: i18n.locale.delete,
+ icon: 'fas fa-trash-alt',
+ danger: true,
+ action: deleteFolder,
+ }], ev);
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/drive.nav-folder.vue b/packages/client/src/components/drive.nav-folder.vue
index 4f0e6ce0e9..7c35c5d3da 100644
--- a/packages/client/src/components/drive.nav-folder.vue
+++ b/packages/client/src/components/drive.nav-folder.vue
@@ -8,114 +8,111 @@
@drop.stop="onDrop"
>
<i v-if="folder == null" class="fas fa-cloud"></i>
- <span>{{ folder == null ? $ts.drive : folder.name }}</span>
+ <span>{{ folder == null ? i18n.locale.drive : folder.name }}</span>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
import * as os from '@/os';
+import { i18n } from '@/i18n';
-export default defineComponent({
- props: {
- folder: {
- type: Object,
- required: false,
- }
- },
+const props = defineProps<{
+ folder?: Misskey.entities.DriveFolder;
+ parentFolder: Misskey.entities.DriveFolder | null;
+}>();
- data() {
- return {
- hover: false,
- draghover: false,
- };
- },
+const emit = defineEmits<{
+ (e: 'move', v?: Misskey.entities.DriveFolder): void;
+ (e: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
+ (e: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
+ (e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
+}>();
- computed: {
- browser(): any {
- return this.$parent;
- }
- },
+const hover = ref(false);
+const draghover = ref(false);
- methods: {
- onClick() {
- this.browser.move(this.folder);
- },
+function onClick() {
+ emit('move', props.folder);
+}
- onMouseover() {
- this.hover = true;
- },
+function onMouseover() {
+ hover.value = true;
+}
- onMouseout() {
- this.hover = false;
- },
+function onMouseout() {
+ hover.value = false;
+}
- onDragover(e) {
- // このフォルダがルートかつカレントディレクトリならドロップ禁止
- if (this.folder == null && this.browser.folder == null) {
- e.dataTransfer.dropEffect = 'none';
- }
+function onDragover(e: DragEvent) {
+ if (!e.dataTransfer) return;
- const isFile = e.dataTransfer.items[0].kind == 'file';
- const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
- const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+ // このフォルダがルートかつカレントディレクトリならドロップ禁止
+ if (props.folder == null && props.parentFolder == null) {
+ e.dataTransfer.dropEffect = 'none';
+ }
- if (isFile || isDriveFile || isDriveFolder) {
- e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
- } else {
- e.dataTransfer.dropEffect = 'none';
- }
+ const isFile = e.dataTransfer.items[0].kind == 'file';
+ const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+ const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
- return false;
- },
+ if (isFile || isDriveFile || isDriveFolder) {
+ e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+ } else {
+ e.dataTransfer.dropEffect = 'none';
+ }
- onDragenter() {
- if (this.folder || this.browser.folder) this.draghover = true;
- },
+ return false;
+}
- onDragleave() {
- if (this.folder || this.browser.folder) this.draghover = false;
- },
+function onDragenter() {
+ if (props.folder || props.parentFolder) draghover.value = true;
+}
- onDrop(e) {
- this.draghover = false;
+function onDragleave() {
+ if (props.folder || props.parentFolder) draghover.value = false;
+}
- // ファイルだったら
- if (e.dataTransfer.files.length > 0) {
- for (const file of Array.from(e.dataTransfer.files)) {
- this.browser.upload(file, this.folder);
- }
- return;
- }
+function onDrop(e: DragEvent) {
+ draghover.value = false;
- //#region ドライブのファイル
- const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
- if (driveFile != null && driveFile != '') {
- const file = JSON.parse(driveFile);
- this.browser.removeFile(file.id);
- os.api('drive/files/update', {
- fileId: file.id,
- folderId: this.folder ? this.folder.id : null
- });
- }
- //#endregion
+ if (!e.dataTransfer) return;
- //#region ドライブのフォルダ
- const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
- if (driveFolder != null && driveFolder != '') {
- const folder = JSON.parse(driveFolder);
- // 移動先が自分自身ならreject
- if (this.folder && folder.id == this.folder.id) return;
- this.browser.removeFolder(folder.id);
- os.api('drive/folders/update', {
- folderId: folder.id,
- parentId: this.folder ? this.folder.id : null
- });
- }
- //#endregion
+ // ファイルだったら
+ if (e.dataTransfer.files.length > 0) {
+ for (const file of Array.from(e.dataTransfer.files)) {
+ emit('upload', file, props.folder);
}
+ return;
+ }
+
+ //#region ドライブのファイル
+ const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+ if (driveFile != null && driveFile != '') {
+ const file = JSON.parse(driveFile);
+ emit('removeFile', file.id);
+ os.api('drive/files/update', {
+ fileId: file.id,
+ folderId: props.folder ? props.folder.id : null
+ });
}
-});
+ //#endregion
+
+ //#region ドライブのフォルダ
+ const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+ if (driveFolder != null && driveFolder != '') {
+ const folder = JSON.parse(driveFolder);
+ // 移動先が自分自身ならreject
+ if (props.folder && folder.id == props.folder.id) return;
+ emit('removeFolder', folder.id);
+ os.api('drive/folders/update', {
+ folderId: folder.id,
+ parentId: props.folder ? props.folder.id : null
+ });
+ }
+ //#endregion
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue
index f8d3d810b7..e27b0a5fbb 100644
--- a/packages/client/src/components/drive.vue
+++ b/packages/client/src/components/drive.vue
@@ -2,10 +2,24 @@
<div class="yfudmmck">
<nav>
<div class="path" @contextmenu.prevent.stop="() => {}">
- <XNavFolder :class="{ current: folder == null }"/>
+ <XNavFolder
+ :class="{ current: folder == null }"
+ :parent-folder="folder"
+ @move="move"
+ @upload="upload"
+ @removeFile="removeFile"
+ @removeFolder="removeFolder"
+ />
<template v-for="f in hierarchyFolders">
<span class="separator"><i class="fas fa-angle-right"></i></span>
- <XNavFolder :folder="f"/>
+ <XNavFolder
+ :folder="f"
+ :parent-folder="folder"
+ @move="move"
+ @upload="upload"
+ @removeFile="removeFile"
+ @removeFolder="removeFolder"
+ />
</template>
<span v-if="folder != null" class="separator"><i class="fas fa-angle-right"></i></span>
<span v-if="folder != null" class="folder current">{{ folder.name }}</span>
@@ -22,617 +36,601 @@
>
<div ref="contents" class="contents">
<div v-show="folders.length > 0" ref="foldersContainer" class="folders">
- <XFolder v-for="(f, i) in folders" :key="f.id" v-anim="i" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/>
+ <XFolder
+ v-for="(f, i) in folders"
+ :key="f.id"
+ v-anim="i"
+ class="folder"
+ :folder="f"
+ :select-mode="select === 'folder'"
+ :is-selected="selectedFolders.some(x => x.id === f.id)"
+ @chosen="chooseFolder"
+ @move="move"
+ @upload="upload"
+ @removeFile="removeFile"
+ @removeFolder="removeFolder"
+ @dragstart="isDragSource = true"
+ @dragend="isDragSource = false"
+ />
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
- <MkButton v-if="moreFolders" ref="moreFolders">{{ $ts.loadMore }}</MkButton>
+ <MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton>
</div>
<div v-show="files.length > 0" ref="filesContainer" class="files">
- <XFile v-for="(file, i) in files" :key="file.id" v-anim="i" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/>
+ <XFile
+ v-for="(file, i) in files"
+ :key="file.id"
+ v-anim="i"
+ class="file"
+ :file="file"
+ :select-mode="select === 'file'"
+ :is-selected="selectedFiles.some(x => x.id === file.id)"
+ @chosen="chooseFile"
+ @dragstart="isDragSource = true"
+ @dragend="isDragSource = false"
+ />
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
- <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ $ts.loadMore }}</MkButton>
+ <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.locale.loadMore }}</MkButton>
</div>
<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
- <p v-if="draghover">{{ $t('empty-draghover') }}</p>
- <p v-if="!draghover && folder == null"><strong>{{ $ts.emptyDrive }}</strong><br/>{{ $t('empty-drive-description') }}</p>
- <p v-if="!draghover && folder != null">{{ $ts.emptyFolder }}</p>
+ <p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
+ <p v-if="!draghover && folder == null"><strong>{{ i18n.locale.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
+ <p v-if="!draghover && folder != null">{{ i18n.locale.emptyFolder }}</p>
</div>
</div>
<MkLoading v-if="fetching"/>
</div>
<div v-if="draghover" class="dropzone"></div>
- <input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
+ <input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
</div>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
import XNavFolder from './drive.nav-folder.vue';
import XFolder from './drive.folder.vue';
import XFile from './drive.file.vue';
import MkButton from './ui/button.vue';
import * as os from '@/os';
import { stream } from '@/stream';
+import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XNavFolder,
- XFolder,
- XFile,
- MkButton,
- },
-
- props: {
- initialFolder: {
- type: Object,
- required: false
- },
- type: {
- type: String,
- required: false,
- default: undefined
- },
- multiple: {
- type: Boolean,
- required: false,
- default: false
- },
- select: {
- type: String,
- required: false,
- default: null
- }
- },
-
- emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'],
-
- data() {
- return {
- /**
- * 現在の階層(フォルダ)
- * * null でルートを表す
- */
- folder: null,
+const props = withDefaults(defineProps<{
+ initialFolder?: Misskey.entities.DriveFolder;
+ type?: string;
+ multiple?: boolean;
+ select?: 'file' | 'folder' | null;
+}>(), {
+ multiple: false,
+ select: null,
+});
- files: [],
- folders: [],
- moreFiles: false,
- moreFolders: false,
- hierarchyFolders: [],
- selectedFiles: [],
- selectedFolders: [],
- uploadings: os.uploads,
- connection: null,
+const emit = defineEmits<{
+ (e: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
+ (e: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
+ (e: 'move-root'): void;
+ (e: 'cd', v: Misskey.entities.DriveFolder | null): void;
+ (e: 'open-folder', v: Misskey.entities.DriveFolder): void;
+}>();
- /**
- * ドロップされようとしているか
- */
- draghover: false,
+const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
+const fileInput = ref<HTMLInputElement>();
- /**
- * 自信の所有するアイテムがドラッグをスタートさせたか
- * (自分自身の階層にドロップできないようにするためのフラグ)
- */
- isDragSource: false,
+const folder = ref<Misskey.entities.DriveFolder | null>(null);
+const files = ref<Misskey.entities.DriveFile[]>([]);
+const folders = ref<Misskey.entities.DriveFolder[]>([]);
+const moreFiles = ref(false);
+const moreFolders = ref(false);
+const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
+const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
+const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
+const uploadings = os.uploads;
+const connection = stream.useChannel('drive');
- fetching: true,
+// ドロップされようとしているか
+const draghover = ref(false);
- ilFilesObserver: new IntersectionObserver(
- (entries) => entries.some((entry) => entry.isIntersecting)
- && !this.fetching && this.moreFiles &&
- this.fetchMoreFiles()
- ),
- moreFilesElement: null as Element,
+// 自身の所有するアイテムがドラッグをスタートさせたか
+// (自分自身の階層にドロップできないようにするためのフラグ)
+const isDragSource = ref(false);
- };
- },
+const fetching = ref(true);
- watch: {
- folder() {
- this.$emit('cd', this.folder);
- }
- },
+const ilFilesObserver = new IntersectionObserver(
+ (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles()
+)
- mounted() {
- if (this.$store.state.enableInfiniteScroll && this.$refs.loadMoreFiles) {
- this.$nextTick(() => {
- this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
- });
- }
+watch(folder, () => emit('cd', folder.value));
- this.connection = markRaw(stream.useChannel('drive'));
-
- this.connection.on('fileCreated', this.onStreamDriveFileCreated);
- this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
- this.connection.on('fileDeleted', this.onStreamDriveFileDeleted);
- this.connection.on('folderCreated', this.onStreamDriveFolderCreated);
- this.connection.on('folderUpdated', this.onStreamDriveFolderUpdated);
- this.connection.on('folderDeleted', this.onStreamDriveFolderDeleted);
+function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
+ addFile(file, true);
+}
- if (this.initialFolder) {
- this.move(this.initialFolder);
- } else {
- this.fetch();
- }
- },
+function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) {
+ const current = folder.value ? folder.value.id : null;
+ if (current != file.folderId) {
+ removeFile(file);
+ } else {
+ addFile(file, true);
+ }
+}
- activated() {
- if (this.$store.state.enableInfiniteScroll) {
- this.$nextTick(() => {
- this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
- });
- }
- },
+function onStreamDriveFileDeleted(fileId: string) {
+ removeFile(fileId);
+}
- beforeUnmount() {
- this.connection.dispose();
- this.ilFilesObserver.disconnect();
- },
+function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) {
+ addFolder(createdFolder, true);
+}
- methods: {
- onStreamDriveFileCreated(file) {
- this.addFile(file, true);
- },
+function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
+ const current = folder.value ? folder.value.id : null;
+ if (current != updatedFolder.parentId) {
+ removeFolder(updatedFolder);
+ } else {
+ addFolder(updatedFolder, true);
+ }
+}
- onStreamDriveFileUpdated(file) {
- const current = this.folder ? this.folder.id : null;
- if (current != file.folderId) {
- this.removeFile(file);
- } else {
- this.addFile(file, true);
- }
- },
+function onStreamDriveFolderDeleted(folderId: string) {
+ removeFolder(folderId);
+}
- onStreamDriveFileDeleted(fileId) {
- this.removeFile(fileId);
- },
+function onDragover(e: DragEvent): any {
+ if (!e.dataTransfer) return;
- onStreamDriveFolderCreated(folder) {
- this.addFolder(folder, true);
- },
+ // ドラッグ元が自分自身の所有するアイテムだったら
+ if (isDragSource.value) {
+ // 自分自身にはドロップさせない
+ e.dataTransfer.dropEffect = 'none';
+ return;
+ }
- onStreamDriveFolderUpdated(folder) {
- const current = this.folder ? this.folder.id : null;
- if (current != folder.parentId) {
- this.removeFolder(folder);
- } else {
- this.addFolder(folder, true);
- }
- },
+ const isFile = e.dataTransfer.items[0].kind == 'file';
+ const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+ const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+ if (isFile || isDriveFile || isDriveFolder) {
+ e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+ } else {
+ e.dataTransfer.dropEffect = 'none';
+ }
- onStreamDriveFolderDeleted(folderId) {
- this.removeFolder(folderId);
- },
+ return false;
+}
- onDragover(e): any {
- // ドラッグ元が自分自身の所有するアイテムだったら
- if (this.isDragSource) {
- // 自分自身にはドロップさせない
- e.dataTransfer.dropEffect = 'none';
- return;
- }
+function onDragenter() {
+ if (!isDragSource.value) draghover.value = true;
+}
- const isFile = e.dataTransfer.items[0].kind == 'file';
- const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
- const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+function onDragleave() {
+ draghover.value = false;
+}
- if (isFile || isDriveFile || isDriveFolder) {
- e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
- } else {
- e.dataTransfer.dropEffect = 'none';
- }
+function onDrop(e: DragEvent): any {
+ draghover.value = false;
- return false;
- },
+ if (!e.dataTransfer) return;
- onDragenter(e) {
- if (!this.isDragSource) this.draghover = true;
- },
+ // ドロップされてきたものがファイルだったら
+ if (e.dataTransfer.files.length > 0) {
+ for (const file of Array.from(e.dataTransfer.files)) {
+ upload(file, folder.value);
+ }
+ return;
+ }
- onDragleave(e) {
- this.draghover = false;
- },
+ //#region ドライブのファイル
+ const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+ if (driveFile != null && driveFile != '') {
+ const file = JSON.parse(driveFile);
+ if (files.value.some(f => f.id == file.id)) return;
+ removeFile(file.id);
+ os.api('drive/files/update', {
+ fileId: file.id,
+ folderId: folder.value ? folder.value.id : null
+ });
+ }
+ //#endregion
- onDrop(e): any {
- this.draghover = false;
+ //#region ドライブのフォルダ
+ const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+ if (driveFolder != null && driveFolder != '') {
+ const droppedFolder = JSON.parse(driveFolder);
- // ドロップされてきたものがファイルだったら
- if (e.dataTransfer.files.length > 0) {
- for (const file of Array.from(e.dataTransfer.files)) {
- this.upload(file, this.folder);
- }
- return;
+ // 移動先が自分自身ならreject
+ if (folder.value && droppedFolder.id == folder.value.id) return false;
+ if (folders.value.some(f => f.id == droppedFolder.id)) return false;
+ removeFolder(droppedFolder.id);
+ os.api('drive/folders/update', {
+ folderId: droppedFolder.id,
+ parentId: folder.value ? folder.value.id : null
+ }).then(() => {
+ // noop
+ }).catch(err => {
+ switch (err) {
+ case 'detected-circular-definition':
+ os.alert({
+ title: i18n.locale.unableToProcess,
+ text: i18n.locale.circularReferenceFolder
+ });
+ break;
+ default:
+ os.alert({
+ type: 'error',
+ text: i18n.locale.somethingHappened
+ });
}
+ });
+ }
+ //#endregion
+}
- //#region ドライブのファイル
- const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
- if (driveFile != null && driveFile != '') {
- const file = JSON.parse(driveFile);
- if (this.files.some(f => f.id == file.id)) return;
- this.removeFile(file.id);
- os.api('drive/files/update', {
- fileId: file.id,
- folderId: this.folder ? this.folder.id : null
- });
- }
- //#endregion
+function selectLocalFile() {
+ fileInput.value?.click();
+}
- //#region ドライブのフォルダ
- const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
- if (driveFolder != null && driveFolder != '') {
- const folder = JSON.parse(driveFolder);
+function urlUpload() {
+ os.inputText({
+ title: i18n.locale.uploadFromUrl,
+ type: 'url',
+ placeholder: i18n.locale.uploadFromUrlDescription
+ }).then(({ canceled, result: url }) => {
+ if (canceled || !url) return;
+ os.api('drive/files/upload-from-url', {
+ url: url,
+ folderId: folder.value ? folder.value.id : undefined
+ });
- // 移動先が自分自身ならreject
- if (this.folder && folder.id == this.folder.id) return false;
- if (this.folders.some(f => f.id == folder.id)) return false;
- this.removeFolder(folder.id);
- os.api('drive/folders/update', {
- folderId: folder.id,
- parentId: this.folder ? this.folder.id : null
- }).then(() => {
- // noop
- }).catch(err => {
- switch (err) {
- case 'detected-circular-definition':
- os.alert({
- title: this.$ts.unableToProcess,
- text: this.$ts.circularReferenceFolder
- });
- break;
- default:
- os.alert({
- type: 'error',
- text: this.$ts.somethingHappened
- });
- }
- });
- }
- //#endregion
- },
+ os.alert({
+ title: i18n.locale.uploadFromUrlRequested,
+ text: i18n.locale.uploadFromUrlMayTakeTime
+ });
+ });
+}
- selectLocalFile() {
- (this.$refs.fileInput as any).click();
- },
+function createFolder() {
+ os.inputText({
+ title: i18n.locale.createFolder,
+ placeholder: i18n.locale.folderName
+ }).then(({ canceled, result: name }) => {
+ if (canceled) return;
+ os.api('drive/folders/create', {
+ name: name,
+ parentId: folder.value ? folder.value.id : undefined
+ }).then(createdFolder => {
+ addFolder(createdFolder, true);
+ });
+ });
+}
- urlUpload() {
- os.inputText({
- title: this.$ts.uploadFromUrl,
- type: 'url',
- placeholder: this.$ts.uploadFromUrlDescription
- }).then(({ canceled, result: url }) => {
- if (canceled) return;
- os.api('drive/files/upload-from-url', {
- url: url,
- folderId: this.folder ? this.folder.id : undefined
- });
+function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
+ os.inputText({
+ title: i18n.locale.renameFolder,
+ placeholder: i18n.locale.inputNewFolderName,
+ default: folderToRename.name
+ }).then(({ canceled, result: name }) => {
+ if (canceled) return;
+ os.api('drive/folders/update', {
+ folderId: folderToRename.id,
+ name: name
+ }).then(updatedFolder => {
+ // FIXME: 画面を更新するために自分自身に移動
+ move(updatedFolder);
+ });
+ });
+}
+function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
+ os.api('drive/folders/delete', {
+ folderId: folderToDelete.id
+ }).then(() => {
+ // 削除時に親フォルダに移動
+ move(folderToDelete.parentId);
+ }).catch(err => {
+ switch(err.id) {
+ case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({
- title: this.$ts.uploadFromUrlRequested,
- text: this.$ts.uploadFromUrlMayTakeTime
+ type: 'error',
+ title: i18n.locale.unableToDelete,
+ text: i18n.locale.hasChildFilesOrFolders
});
- });
- },
-
- createFolder() {
- os.inputText({
- title: this.$ts.createFolder,
- placeholder: this.$ts.folderName
- }).then(({ canceled, result: name }) => {
- if (canceled) return;
- os.api('drive/folders/create', {
- name: name,
- parentId: this.folder ? this.folder.id : undefined
- }).then(folder => {
- this.addFolder(folder, true);
+ break;
+ default:
+ os.alert({
+ type: 'error',
+ text: i18n.locale.unableToDelete
});
- });
- },
+ }
+ });
+}
- renameFolder(folder) {
- os.inputText({
- title: this.$ts.renameFolder,
- placeholder: this.$ts.inputNewFolderName,
- default: folder.name
- }).then(({ canceled, result: name }) => {
- if (canceled) return;
- os.api('drive/folders/update', {
- folderId: folder.id,
- name: name
- }).then(folder => {
- // FIXME: 画面を更新するために自分自身に移動
- this.move(folder);
- });
- });
- },
+function onChangeFileInput() {
+ if (!fileInput.value?.files) return;
+ for (const file of Array.from(fileInput.value.files)) {
+ upload(file, folder.value);
+ }
+}
- deleteFolder(folder) {
- os.api('drive/folders/delete', {
- folderId: folder.id
- }).then(() => {
- // 削除時に親フォルダに移動
- this.move(folder.parentId);
- }).catch(err => {
- switch(err.id) {
- case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
- os.alert({
- type: 'error',
- title: this.$ts.unableToDelete,
- text: this.$ts.hasChildFilesOrFolders
- });
- break;
- default:
- os.alert({
- type: 'error',
- text: this.$ts.unableToDelete
- });
- }
- });
- },
+function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
+ os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).then(res => {
+ addFile(res, true);
+ });
+}
- onChangeFileInput() {
- for (const file of Array.from((this.$refs.fileInput as any).files)) {
- this.upload(file, this.folder);
- }
- },
+function chooseFile(file: Misskey.entities.DriveFile) {
+ const isAlreadySelected = selectedFiles.value.some(f => f.id == file.id);
+ if (props.multiple) {
+ if (isAlreadySelected) {
+ selectedFiles.value = selectedFiles.value.filter(f => f.id != file.id);
+ } else {
+ selectedFiles.value.push(file);
+ }
+ emit('change-selection', selectedFiles.value);
+ } else {
+ if (isAlreadySelected) {
+ emit('selected', file);
+ } else {
+ selectedFiles.value = [file];
+ emit('change-selection', [file]);
+ }
+ }
+}
- upload(file, folder) {
- if (folder && typeof folder == 'object') folder = folder.id;
- os.upload(file, folder).then(res => {
- this.addFile(res, true);
- });
- },
+function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
+ const isAlreadySelected = selectedFolders.value.some(f => f.id == folderToChoose.id);
+ if (props.multiple) {
+ if (isAlreadySelected) {
+ selectedFolders.value = selectedFolders.value.filter(f => f.id != folderToChoose.id);
+ } else {
+ selectedFolders.value.push(folderToChoose);
+ }
+ emit('change-selection', selectedFolders.value);
+ } else {
+ if (isAlreadySelected) {
+ emit('selected', folderToChoose);
+ } else {
+ selectedFolders.value = [folderToChoose];
+ emit('change-selection', [folderToChoose]);
+ }
+ }
+}
- chooseFile(file) {
- const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id);
- if (this.multiple) {
- if (isAlreadySelected) {
- this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id);
- } else {
- this.selectedFiles.push(file);
- }
- this.$emit('change-selection', this.selectedFiles);
- } else {
- if (isAlreadySelected) {
- this.$emit('selected', file);
- } else {
- this.selectedFiles = [file];
- this.$emit('change-selection', [file]);
- }
- }
- },
+function move(target?: Misskey.entities.DriveFolder) {
+ if (!target) {
+ goRoot();
+ return;
+ } else if (typeof target == 'object') {
+ target = target.id;
+ }
- chooseFolder(folder) {
- const isAlreadySelected = this.selectedFolders.some(f => f.id == folder.id);
- if (this.multiple) {
- if (isAlreadySelected) {
- this.selectedFolders = this.selectedFolders.filter(f => f.id != folder.id);
- } else {
- this.selectedFolders.push(folder);
- }
- this.$emit('change-selection', this.selectedFolders);
- } else {
- if (isAlreadySelected) {
- this.$emit('selected', folder);
- } else {
- this.selectedFolders = [folder];
- this.$emit('change-selection', [folder]);
- }
- }
- },
+ fetching.value = true;
- move(target) {
- if (target == null) {
- this.goRoot();
- return;
- } else if (typeof target == 'object') {
- target = target.id;
- }
+ os.api('drive/folders/show', {
+ folderId: target
+ }).then(folderToMove => {
+ folder.value = folderToMove;
+ hierarchyFolders.value = [];
- this.fetching = true;
+ const dive = folderToDive => {
+ hierarchyFolders.value.unshift(folderToDive);
+ if (folderToDive.parent) dive(folderToDive.parent);
+ };
+
+ if (folderToMove.parent) dive(folderToMove.parent);
- os.api('drive/folders/show', {
- folderId: target
- }).then(folder => {
- this.folder = folder;
- this.hierarchyFolders = [];
+ emit('open-folder', folderToMove);
+ fetch();
+ });
+}
- const dive = folder => {
- this.hierarchyFolders.unshift(folder);
- if (folder.parent) dive(folder.parent);
- };
+function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
+ const current = folder.value ? folder.value.id : null;
+ if (current != folderToAdd.parentId) return;
- if (folder.parent) dive(folder.parent);
+ if (folders.value.some(f => f.id == folderToAdd.id)) {
+ const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id);
+ folders.value[exist] = folderToAdd;
+ return;
+ }
- this.$emit('open-folder', folder);
- this.fetch();
- });
- },
+ if (unshift) {
+ folders.value.unshift(folderToAdd);
+ } else {
+ folders.value.push(folderToAdd);
+ }
+}
- addFolder(folder, unshift = false) {
- const current = this.folder ? this.folder.id : null;
- if (current != folder.parentId) return;
+function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
+ const current = folder.value ? folder.value.id : null;
+ if (current != fileToAdd.folderId) return;
- if (this.folders.some(f => f.id == folder.id)) {
- const exist = this.folders.map(f => f.id).indexOf(folder.id);
- this.folders[exist] = folder;
- return;
- }
+ if (files.value.some(f => f.id == fileToAdd.id)) {
+ const exist = files.value.map(f => f.id).indexOf(fileToAdd.id);
+ files.value[exist] = fileToAdd;
+ return;
+ }
- if (unshift) {
- this.folders.unshift(folder);
- } else {
- this.folders.push(folder);
- }
- },
+ if (unshift) {
+ files.value.unshift(fileToAdd);
+ } else {
+ files.value.push(fileToAdd);
+ }
+}
- addFile(file, unshift = false) {
- const current = this.folder ? this.folder.id : null;
- if (current != file.folderId) return;
+function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
+ const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove;
+ folders.value = folders.value.filter(f => f.id != folderIdToRemove);
+}
- if (this.files.some(f => f.id == file.id)) {
- const exist = this.files.map(f => f.id).indexOf(file.id);
- this.files[exist] = file;
- return;
- }
+function removeFile(file: Misskey.entities.DriveFile | string) {
+ const fileId = typeof file === 'object' ? file.id : file;
+ files.value = files.value.filter(f => f.id != fileId);
+}
- if (unshift) {
- this.files.unshift(file);
- } else {
- this.files.push(file);
- }
- },
+function appendFile(file: Misskey.entities.DriveFile) {
+ addFile(file);
+}
- removeFolder(folder) {
- if (typeof folder == 'object') folder = folder.id;
- this.folders = this.folders.filter(f => f.id != folder);
- },
+function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
+ addFolder(folderToAppend);
+}
+/*
+function prependFile(file: Misskey.entities.DriveFile) {
+ addFile(file, true);
+}
- removeFile(file) {
- if (typeof file == 'object') file = file.id;
- this.files = this.files.filter(f => f.id != file);
- },
+function prependFolder(folderToPrepend: Misskey.entities.DriveFolder) {
+ addFolder(folderToPrepend, true);
+}
+*/
+function goRoot() {
+ // 既にrootにいるなら何もしない
+ if (folder.value == null) return;
- appendFile(file) {
- this.addFile(file);
- },
+ folder.value = null;
+ hierarchyFolders.value = [];
+ emit('move-root');
+ fetch();
+}
- appendFolder(folder) {
- this.addFolder(folder);
- },
+async function fetch() {
+ folders.value = [];
+ files.value = [];
+ moreFolders.value = false;
+ moreFiles.value = false;
+ fetching.value = true;
- prependFile(file) {
- this.addFile(file, true);
- },
+ const foldersMax = 30;
+ const filesMax = 30;
- prependFolder(folder) {
- this.addFolder(folder, true);
- },
+ const foldersPromise = os.api('drive/folders', {
+ folderId: folder.value ? folder.value.id : null,
+ limit: foldersMax + 1
+ }).then(fetchedFolders => {
+ if (fetchedFolders.length == foldersMax + 1) {
+ moreFolders.value = true;
+ fetchedFolders.pop();
+ }
+ return fetchedFolders;
+ });
- goRoot() {
- // 既にrootにいるなら何もしない
- if (this.folder == null) return;
+ const filesPromise = os.api('drive/files', {
+ folderId: folder.value ? folder.value.id : null,
+ type: props.type,
+ limit: filesMax + 1
+ }).then(fetchedFiles => {
+ if (fetchedFiles.length == filesMax + 1) {
+ moreFiles.value = true;
+ fetchedFiles.pop();
+ }
+ return fetchedFiles;
+ });
- this.folder = null;
- this.hierarchyFolders = [];
- this.$emit('move-root');
- this.fetch();
- },
+ const [fetchedFolders, fetchedFiles] = await Promise.all([foldersPromise, filesPromise]);
- fetch() {
- this.folders = [];
- this.files = [];
- this.moreFolders = false;
- this.moreFiles = false;
- this.fetching = true;
+ for (const x of fetchedFolders) appendFolder(x);
+ for (const x of fetchedFiles) appendFile(x);
- let fetchedFolders = null;
- let fetchedFiles = null;
+ fetching.value = false;
+}
- const foldersMax = 30;
- const filesMax = 30;
+function fetchMoreFiles() {
+ fetching.value = true;
- // フォルダ一覧取得
- os.api('drive/folders', {
- folderId: this.folder ? this.folder.id : null,
- limit: foldersMax + 1
- }).then(folders => {
- if (folders.length == foldersMax + 1) {
- this.moreFolders = true;
- folders.pop();
- }
- fetchedFolders = folders;
- complete();
- });
+ const max = 30;
- // ファイル一覧取得
- os.api('drive/files', {
- folderId: this.folder ? this.folder.id : null,
- type: this.type,
- limit: filesMax + 1
- }).then(files => {
- if (files.length == filesMax + 1) {
- this.moreFiles = true;
- files.pop();
- }
- fetchedFiles = files;
- complete();
- });
+ // ファイル一覧取得
+ os.api('drive/files', {
+ folderId: folder.value ? folder.value.id : null,
+ type: props.type,
+ untilId: files.value[files.value.length - 1].id,
+ limit: max + 1
+ }).then(files => {
+ if (files.length == max + 1) {
+ moreFiles.value = true;
+ files.pop();
+ } else {
+ moreFiles.value = false;
+ }
+ for (const x of files) appendFile(x);
+ fetching.value = false;
+ });
+}
- let flag = false;
- const complete = () => {
- if (flag) {
- for (const x of fetchedFolders) this.appendFolder(x);
- for (const x of fetchedFiles) this.appendFile(x);
- this.fetching = false;
- } else {
- flag = true;
- }
- };
- },
+function getMenu() {
+ return [{
+ text: i18n.locale.addFile,
+ type: 'label'
+ }, {
+ text: i18n.locale.upload,
+ icon: 'fas fa-upload',
+ action: () => { selectLocalFile(); }
+ }, {
+ text: i18n.locale.fromUrl,
+ icon: 'fas fa-link',
+ action: () => { urlUpload(); }
+ }, null, {
+ text: folder.value ? folder.value.name : i18n.locale.drive,
+ type: 'label'
+ }, folder.value ? {
+ text: i18n.locale.renameFolder,
+ icon: 'fas fa-i-cursor',
+ action: () => { renameFolder(folder.value); }
+ } : undefined, folder.value ? {
+ text: i18n.locale.deleteFolder,
+ icon: 'fas fa-trash-alt',
+ action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
+ } : undefined, {
+ text: i18n.locale.createFolder,
+ icon: 'fas fa-folder-plus',
+ action: () => { createFolder(); }
+ }];
+}
- fetchMoreFiles() {
- this.fetching = true;
+function showMenu(ev: MouseEvent) {
+ os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
+}
- const max = 30;
+function onContextmenu(ev: MouseEvent) {
+ os.contextMenu(getMenu(), ev);
+}
- // ファイル一覧取得
- os.api('drive/files', {
- folderId: this.folder ? this.folder.id : null,
- type: this.type,
- untilId: this.files[this.files.length - 1].id,
- limit: max + 1
- }).then(files => {
- if (files.length == max + 1) {
- this.moreFiles = true;
- files.pop();
- } else {
- this.moreFiles = false;
- }
- for (const x of files) this.appendFile(x);
- this.fetching = false;
- });
- },
+onMounted(() => {
+ if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
+ nextTick(() => {
+ ilFilesObserver.observe(loadMoreFiles.value?.$el)
+ });
+ }
- getMenu() {
- return [{
- text: this.$ts.addFile,
- type: 'label'
- }, {
- text: this.$ts.upload,
- icon: 'fas fa-upload',
- action: () => { this.selectLocalFile(); }
- }, {
- text: this.$ts.fromUrl,
- icon: 'fas fa-link',
- action: () => { this.urlUpload(); }
- }, null, {
- text: this.folder ? this.folder.name : this.$ts.drive,
- type: 'label'
- }, this.folder ? {
- text: this.$ts.renameFolder,
- icon: 'fas fa-i-cursor',
- action: () => { this.renameFolder(this.folder); }
- } : undefined, this.folder ? {
- text: this.$ts.deleteFolder,
- icon: 'fas fa-trash-alt',
- action: () => { this.deleteFolder(this.folder); }
- } : undefined, {
- text: this.$ts.createFolder,
- icon: 'fas fa-folder-plus',
- action: () => { this.createFolder(); }
- }];
- },
+ connection.on('fileCreated', onStreamDriveFileCreated);
+ connection.on('fileUpdated', onStreamDriveFileUpdated);
+ connection.on('fileDeleted', onStreamDriveFileDeleted);
+ connection.on('folderCreated', onStreamDriveFolderCreated);
+ connection.on('folderUpdated', onStreamDriveFolderUpdated);
+ connection.on('folderDeleted', onStreamDriveFolderDeleted);
- showMenu(ev) {
- os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
- },
+ if (props.initialFolder) {
+ move(props.initialFolder);
+ } else {
+ fetch();
+ }
+});
- onContextmenu(ev) {
- os.contextMenu(this.getMenu(), ev);
- },
+onActivated(() => {
+ if (defaultStore.state.enableInfiniteScroll) {
+ nextTick(() => {
+ ilFilesObserver.observe(loadMoreFiles.value?.$el)
+ });
}
});
+
+onBeforeUnmount(() => {
+ connection.dispose();
+ ilFilesObserver.disconnect();
+});
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/emoji-picker-dialog.vue b/packages/client/src/components/emoji-picker-dialog.vue
index 51c634dd8e..f06a24636c 100644
--- a/packages/client/src/components/emoji-picker-dialog.vue
+++ b/packages/client/src/components/emoji-picker-dialog.vue
@@ -1,58 +1,65 @@
<template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'middle'" :prefer-type="asReactionPicker && $store.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" :transparent-bg="true" :manual-showing="manualShowing" :src="src" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')">
- <MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ drawer: type === 'drawer' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" :as-drawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen"/>
+<MkModal
+ ref="modal"
+ v-slot="{ type, maxHeight }"
+ :z-priority="'middle'"
+ :prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
+ :transparent-bg="true"
+ :manual-showing="manualShowing"
+ :src="src"
+ @click="modal?.close()"
+ @opening="opening"
+ @close="emit('close')"
+ @closed="emit('closed')"
+>
+ <MkEmojiPicker
+ ref="picker"
+ class="ryghynhb _popup _shadow"
+ :class="{ drawer: type === 'drawer' }"
+ :show-pinned="showPinned"
+ :as-reaction-picker="asReactionPicker"
+ :as-drawer="type === 'drawer'"
+ :max-height="maxHeight"
+ @chosen="chosen"
+ />
</MkModal>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
import MkModal from '@/components/ui/modal.vue';
import MkEmojiPicker from '@/components/emoji-picker.vue';
+import { defaultStore } from '@/store';
-export default defineComponent({
- components: {
- MkModal,
- MkEmojiPicker,
- },
-
- props: {
- manualShowing: {
- type: Boolean,
- required: false,
- default: null,
- },
- src: {
- required: false
- },
- showPinned: {
- required: false,
- default: true
- },
- asReactionPicker: {
- required: false
- },
- },
-
- emits: ['done', 'close', 'closed'],
+withDefaults(defineProps<{
+ manualShowing?: boolean;
+ src?: HTMLElement;
+ showPinned?: boolean;
+ asReactionPicker?: boolean;
+}>(), {
+ manualShowing: false,
+ showPinned: true,
+ asReactionPicker: false,
+});
- data() {
- return {
+const emit = defineEmits<{
+ (e: 'done', v: any): void;
+ (e: 'close'): void;
+ (e: 'closed'): void;
+}>();
- };
- },
+const modal = ref<InstanceType<typeof MkModal>>();
+const picker = ref<InstanceType<typeof MkEmojiPicker>>();
- methods: {
- chosen(emoji: any) {
- this.$emit('done', emoji);
- this.$refs.modal.close();
- },
+function chosen(emoji: any) {
+ emit('done', emoji);
+ modal.value?.close();
+}
- opening() {
- this.$refs.picker.reset();
- this.$refs.picker.focus();
- }
- }
-});
+function opening() {
+ picker.value?.reset();
+ picker.value?.focus();
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/emoji-picker-window.vue b/packages/client/src/components/emoji-picker-window.vue
index 0ffa0c1187..4d27fb48ba 100644
--- a/packages/client/src/components/emoji-picker-window.vue
+++ b/packages/client/src/components/emoji-picker-window.vue
@@ -5,50 +5,33 @@
:can-resize="false"
:mini="true"
:front="true"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
</MkWindow>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import MkWindow from '@/components/ui/window.vue';
import MkEmojiPicker from '@/components/emoji-picker.vue';
-export default defineComponent({
- components: {
- MkWindow,
- MkEmojiPicker,
- },
-
- props: {
- src: {
- required: false
- },
- showPinned: {
- required: false,
- default: true
- },
- asReactionPicker: {
- required: false
- },
- },
-
- emits: ['chosen', 'closed'],
-
- data() {
- return {
+withDefaults(defineProps<{
+ src?: HTMLElement;
+ showPinned?: boolean;
+ asReactionPicker?: boolean;
+}>(), {
+ showPinned: true,
+});
- };
- },
+const emit = defineEmits<{
+ (e: 'chosen', v: any): void;
+ (e: 'closed'): void;
+}>();
- methods: {
- chosen(emoji: any) {
- this.$emit('chosen', emoji);
- },
- }
-});
+function chosen(emoji: any) {
+ emit('chosen', emoji);
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/emoji-picker.section.vue b/packages/client/src/components/emoji-picker.section.vue
index 08c4f6813d..1026e894d1 100644
--- a/packages/client/src/components/emoji-picker.section.vue
+++ b/packages/client/src/components/emoji-picker.section.vue
@@ -7,7 +7,7 @@
<button v-for="emoji in emojis"
:key="emoji"
class="_button"
- @click="chosen(emoji, $event)"
+ @click="emit('chosen', emoji, $event)"
>
<MkEmoji :emoji="emoji" :normal="true"/>
</button>
@@ -15,35 +15,19 @@
</section>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+<script lang="ts" setup>
+import { ref } from 'vue';
-export default defineComponent({
- props: {
- emojis: {
- required: true,
- },
- initialShown: {
- required: false
- }
- },
+const props = defineProps<{
+ emojis: string[];
+ initialShown?: boolean;
+}>();
- emits: ['chosen'],
+const emit = defineEmits<{
+ (e: 'chosen', v: string, ev: MouseEvent): void;
+}>();
- data() {
- return {
- getStaticImageUrl,
- shown: this.initialShown,
- };
- },
-
- methods: {
- chosen(emoji: any, ev) {
- this.$parent.chosen(emoji, ev);
- },
- }
-});
+const shown = ref(!!props.initialShown);
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue
index a8eed1ca21..96670fa58c 100644
--- a/packages/client/src/components/emoji-picker.vue
+++ b/packages/client/src/components/emoji-picker.vue
@@ -1,18 +1,18 @@
<template>
-<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : null }">
- <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()">
+<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
+ <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.locale.search" @paste.stop="paste" @keyup.enter="done()">
<div ref="emojis" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0">
<button v-for="emoji in searchResultCustom"
- :key="emoji"
+ :key="emoji.id"
class="_button"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
- <MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
- <img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+ <!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
+ <img :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
</button>
</div>
<div v-if="searchResultUnicode.length > 0">
@@ -43,9 +43,9 @@
</section>
<section>
- <header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ $ts.recentUsed }}</header>
+ <header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header>
<div>
- <button v-for="emoji in $store.state.recentlyUsedEmojis"
+ <button v-for="emoji in recentlyUsedEmojis"
:key="emoji"
class="_button"
@click="chosen(emoji, $event)"
@@ -56,12 +56,12 @@
</section>
</div>
<div>
- <header class="_acrylic">{{ $ts.customEmojis }}</header>
- <XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection>
+ <header class="_acrylic">{{ i18n.locale.customEmojis }}</header>
+ <XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection>
</div>
<div>
- <header class="_acrylic">{{ $ts.emoji }}</header>
- <XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
+ <header class="_acrylic">{{ i18n.locale.emoji }}</header>
+ <XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
</div>
</div>
<div class="tabs">
@@ -73,277 +73,272 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import { emojilist } from '@/scripts/emojilist';
+<script lang="ts" setup>
+import { ref, computed, watch, onMounted } from 'vue';
+import * as Misskey from 'misskey-js';
+import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import Ripple from '@/components/ripple.vue';
import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch';
import { isMobile } from '@/scripts/is-mobile';
-import { emojiCategories } from '@/instance';
+import { emojiCategories, instance } from '@/instance';
import XSection from './emoji-picker.section.vue';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
-export default defineComponent({
- components: {
- XSection
- },
-
- props: {
- showPinned: {
- required: false,
- default: true,
- },
- asReactionPicker: {
- required: false,
- },
- maxHeight: {
- type: Number,
- required: false,
- },
- asDrawer: {
- type: Boolean,
- required: false
- },
- },
+const props = withDefaults(defineProps<{
+ showPinned?: boolean;
+ asReactionPicker?: boolean;
+ maxHeight?: number;
+ asDrawer?: boolean;
+}>(), {
+ showPinned: true,
+});
- emits: ['chosen'],
+const emit = defineEmits<{
+ (e: 'chosen', v: string): void;
+}>();
- data() {
- return {
- emojilist: markRaw(emojilist),
- getStaticImageUrl,
- pinned: this.$store.reactiveState.reactions,
- width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
- height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
- big: this.asReactionPicker ? isTouchUsing : false,
- customEmojiCategories: emojiCategories,
- customEmojis: this.$instance.emojis,
- q: null,
- searchResultCustom: [],
- searchResultUnicode: [],
- tab: 'index',
- categories: ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'],
- };
- },
+const search = ref<HTMLInputElement>();
+const emojis = ref<HTMLDivElement>();
- watch: {
- q() {
- this.$refs.emojis.scrollTop = 0;
+const {
+ reactions: pinned,
+ reactionPickerWidth,
+ reactionPickerHeight,
+ disableShowingAnimatedImages,
+ recentlyUsedEmojis,
+} = defaultStore.reactiveState;
- if (this.q == null || this.q === '') {
- this.searchResultCustom = [];
- this.searchResultUnicode = [];
- return;
- }
+const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
+const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
+const big = props.asReactionPicker ? isTouchUsing : false;
+const customEmojiCategories = emojiCategories;
+const customEmojis = instance.emojis;
+const q = ref<string | null>(null);
+const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
+const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
+const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
- const q = this.q.replace(/:/g, '');
+watch(q, () => {
+ if (emojis.value) emojis.value.scrollTop = 0;
- const searchCustom = () => {
- const max = 8;
- const emojis = this.customEmojis;
- const matches = new Set();
+ if (q.value == null || q.value === '') {
+ searchResultCustom.value = [];
+ searchResultUnicode.value = [];
+ return;
+ }
- const exactMatch = emojis.find(e => e.name === q);
- if (exactMatch) matches.add(exactMatch);
+ const newQ = q.value.replace(/:/g, '');
- if (q.includes(' ')) { // AND検索
- const keywords = q.split(' ');
+ const searchCustom = () => {
+ const max = 8;
+ const emojis = customEmojis;
+ const matches = new Set<Misskey.entities.CustomEmoji>();
- // 名前にキーワードが含まれている
- for (const emoji of emojis) {
- if (keywords.every(keyword => emoji.name.includes(keyword))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ const exactMatch = emojis.find(e => e.name === newQ);
+ if (exactMatch) matches.add(exactMatch);
- // 名前またはエイリアスにキーワードが含まれている
- for (const emoji of emojis) {
- if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- } else {
- for (const emoji of emojis) {
- if (emoji.name.startsWith(q)) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ if (newQ.includes(' ')) { // AND検索
+ const keywords = newQ.split(' ');
- for (const emoji of emojis) {
- if (emoji.aliases.some(alias => alias.startsWith(q))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ // 名前にキーワードが含まれている
+ for (const emoji of emojis) {
+ if (keywords.every(keyword => emoji.name.includes(keyword))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- for (const emoji of emojis) {
- if (emoji.name.includes(q)) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ // 名前またはエイリアスにキーワードが含まれている
+ for (const emoji of emojis) {
+ if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ } else {
+ for (const emoji of emojis) {
+ if (emoji.name.startsWith(newQ)) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- for (const emoji of emojis) {
- if (emoji.aliases.some(alias => alias.includes(q))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
+ for (const emoji of emojis) {
+ if (emoji.aliases.some(alias => alias.startsWith(newQ))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
}
+ }
+ if (matches.size >= max) return matches;
- return matches;
- };
+ for (const emoji of emojis) {
+ if (emoji.name.includes(newQ)) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- const searchUnicode = () => {
- const max = 8;
- const emojis = this.emojilist;
- const matches = new Set();
+ for (const emoji of emojis) {
+ if (emoji.aliases.some(alias => alias.includes(newQ))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ }
- const exactMatch = emojis.find(e => e.name === q);
- if (exactMatch) matches.add(exactMatch);
+ return matches;
+ };
- if (q.includes(' ')) { // AND検索
- const keywords = q.split(' ');
+ const searchUnicode = () => {
+ const max = 8;
+ const emojis = emojilist;
+ const matches = new Set<UnicodeEmojiDef>();
- // 名前にキーワードが含まれている
- for (const emoji of emojis) {
- if (keywords.every(keyword => emoji.name.includes(keyword))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ const exactMatch = emojis.find(e => e.name === newQ);
+ if (exactMatch) matches.add(exactMatch);
- // 名前またはエイリアスにキーワードが含まれている
- for (const emoji of emojis) {
- if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- } else {
- for (const emoji of emojis) {
- if (emoji.name.startsWith(q)) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ if (newQ.includes(' ')) { // AND検索
+ const keywords = newQ.split(' ');
- for (const emoji of emojis) {
- if (emoji.keywords.some(keyword => keyword.startsWith(q))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ // 名前にキーワードが含まれている
+ for (const emoji of emojis) {
+ if (keywords.every(keyword => emoji.name.includes(keyword))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- for (const emoji of emojis) {
- if (emoji.name.includes(q)) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ // 名前またはエイリアスにキーワードが含まれている
+ for (const emoji of emojis) {
+ if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ } else {
+ for (const emoji of emojis) {
+ if (emoji.name.startsWith(newQ)) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- for (const emoji of emojis) {
- if (emoji.keywords.some(keyword => keyword.includes(q))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
+ for (const emoji of emojis) {
+ if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
}
+ }
+ if (matches.size >= max) return matches;
- return matches;
- };
+ for (const emoji of emojis) {
+ if (emoji.name.includes(newQ)) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- this.searchResultCustom = Array.from(searchCustom());
- this.searchResultUnicode = Array.from(searchUnicode());
+ for (const emoji of emojis) {
+ if (emoji.keywords.some(keyword => keyword.includes(newQ))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
}
- },
- mounted() {
- this.focus();
- },
+ return matches;
+ };
- methods: {
- focus() {
- if (!isMobile && !isTouchUsing) {
- this.$refs.search.focus({
- preventScroll: true
- });
- }
- },
+ searchResultCustom.value = Array.from(searchCustom());
+ searchResultUnicode.value = Array.from(searchUnicode());
+});
- reset() {
- this.$refs.emojis.scrollTop = 0;
- this.q = '';
- },
+function focus() {
+ if (!isMobile && !isTouchUsing) {
+ search.value?.focus({
+ preventScroll: true
+ });
+ }
+}
- getKey(emoji: any) {
- return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
- },
+function reset() {
+ if (emojis.value) emojis.value.scrollTop = 0;
+ q.value = '';
+}
- chosen(emoji: any, ev) {
- if (ev) {
- const el = ev.currentTarget || ev.target;
- const rect = el.getBoundingClientRect();
- const x = rect.left + (el.offsetWidth / 2);
- const y = rect.top + (el.offsetHeight / 2);
- os.popup(Ripple, { x, y }, {}, 'end');
- }
+function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
+ return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
+}
- const key = this.getKey(emoji);
- this.$emit('chosen', key);
+function chosen(emoji: any, ev?: MouseEvent) {
+ const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined;
+ if (el) {
+ const rect = el.getBoundingClientRect();
+ const x = rect.left + (el.offsetWidth / 2);
+ const y = rect.top + (el.offsetHeight / 2);
+ os.popup(Ripple, { x, y }, {}, 'end');
+ }
- // 最近使った絵文字更新
- if (!this.pinned.includes(key)) {
- let recents = this.$store.state.recentlyUsedEmojis;
- recents = recents.filter((e: any) => e !== key);
- recents.unshift(key);
- this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
- }
- },
+ const key = getKey(emoji);
+ emit('chosen', key);
- paste(event) {
- const paste = (event.clipboardData || window.clipboardData).getData('text');
- if (this.done(paste)) {
- event.preventDefault();
- }
- },
+ // 最近使った絵文字更新
+ if (!pinned.value.includes(key)) {
+ let recents = defaultStore.state.recentlyUsedEmojis;
+ recents = recents.filter((e: any) => e !== key);
+ recents.unshift(key);
+ defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
+ }
+}
- done(query) {
- if (query == null) query = this.q;
- if (query == null) return;
- const q = query.replace(/:/g, '');
- const exactMatchCustom = this.customEmojis.find(e => e.name === q);
- if (exactMatchCustom) {
- this.chosen(exactMatchCustom);
- return true;
- }
- const exactMatchUnicode = this.emojilist.find(e => e.char === q || e.name === q);
- if (exactMatchUnicode) {
- this.chosen(exactMatchUnicode);
- return true;
- }
- if (this.searchResultCustom.length > 0) {
- this.chosen(this.searchResultCustom[0]);
- return true;
- }
- if (this.searchResultUnicode.length > 0) {
- this.chosen(this.searchResultUnicode[0]);
- return true;
- }
- },
+function paste(event: ClipboardEvent) {
+ const paste = (event.clipboardData || window.clipboardData).getData('text');
+ if (done(paste)) {
+ event.preventDefault();
}
+}
+
+function done(query?: any): boolean | void {
+ if (query == null) query = q.value;
+ if (query == null || typeof query !== 'string') return;
+
+ const q2 = query.replace(/:/g, '');
+ const exactMatchCustom = customEmojis.find(e => e.name === q2);
+ if (exactMatchCustom) {
+ chosen(exactMatchCustom);
+ return true;
+ }
+ const exactMatchUnicode = emojilist.find(e => e.char === q2 || e.name === q2);
+ if (exactMatchUnicode) {
+ chosen(exactMatchUnicode);
+ return true;
+ }
+ if (searchResultCustom.value.length > 0) {
+ chosen(searchResultCustom.value[0]);
+ return true;
+ }
+ if (searchResultUnicode.value.length > 0) {
+ chosen(searchResultUnicode.value[0]);
+ return true;
+ }
+}
+
+onMounted(() => {
+ focus();
+});
+
+defineExpose({
+ focus,
+ reset,
});
</script>
diff --git a/packages/client/src/components/featured-photos.vue b/packages/client/src/components/featured-photos.vue
index af5892c98e..e58b5d2849 100644
--- a/packages/client/src/components/featured-photos.vue
+++ b/packages/client/src/components/featured-photos.vue
@@ -2,25 +2,15 @@
<div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
import * as os from '@/os';
-export default defineComponent({
- components: {
- },
+const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
- data() {
- return {
- meta: null,
- };
- },
-
- created() {
- os.api('meta', { detail: true }).then(meta => {
- this.meta = meta;
- });
- },
+os.api('meta', { detail: true }).then(gotMeta => {
+ meta.value = gotMeta;
});
</script>
diff --git a/packages/client/src/components/follow-button.vue b/packages/client/src/components/follow-button.vue
index b16b22f26f..345edb6441 100644
--- a/packages/client/src/components/follow-button.vue
+++ b/packages/client/src/components/follow-button.vue
@@ -6,129 +6,110 @@
>
<template v-if="!wait">
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
- <span v-if="full">{{ $ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
+ <span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
</template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 -->
- <span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
+ <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
</template>
<template v-else-if="isFollowing">
- <span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i>
+ <span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
</template>
<template v-else-if="!isFollowing && user.isLocked">
- <span v-if="full">{{ $ts.followRequest }}</span><i class="fas fa-plus"></i>
+ <span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i>
</template>
<template v-else-if="!isFollowing && !user.isLocked">
- <span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i>
+ <span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
</template>
</template>
<template v-else>
- <span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
+ <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
</template>
</button>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { onBeforeUnmount, onMounted, ref } from 'vue';
+import * as Misskey from 'misskey-js';
import * as os from '@/os';
import { stream } from '@/stream';
+import { i18n } from '@/i18n';
-export default defineComponent({
- props: {
- user: {
- type: Object,
- required: true
- },
- full: {
- type: Boolean,
- required: false,
- default: false,
- },
- large: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- data() {
- return {
- isFollowing: this.user.isFollowing,
- hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou,
- wait: false,
- connection: null,
- };
- },
-
- created() {
- // 渡されたユーザー情報が不完全な場合
- if (this.user.isFollowing == null) {
- os.api('users/show', {
- userId: this.user.id
- }).then(u => {
- this.isFollowing = u.isFollowing;
- this.hasPendingFollowRequestFromYou = u.hasPendingFollowRequestFromYou;
- });
- }
- },
-
- mounted() {
- this.connection = markRaw(stream.useChannel('main'));
+const props = withDefaults(defineProps<{
+ user: Misskey.entities.UserDetailed,
+ full?: boolean,
+ large?: boolean,
+}>(), {
+ full: false,
+ large: false,
+});
- this.connection.on('follow', this.onFollowChange);
- this.connection.on('unfollow', this.onFollowChange);
- },
+const isFollowing = ref(props.user.isFollowing);
+const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
+const wait = ref(false);
+const connection = stream.useChannel('main');
- beforeUnmount() {
- this.connection.dispose();
- },
+if (props.user.isFollowing == null) {
+ os.api('users/show', {
+ userId: props.user.id
+ }).then(u => {
+ isFollowing.value = u.isFollowing;
+ hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou;
+ });
+}
- methods: {
- onFollowChange(user) {
- if (user.id == this.user.id) {
- this.isFollowing = user.isFollowing;
- this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
- }
- },
+function onFollowChange(user: Misskey.entities.UserDetailed) {
+ if (user.id == props.user.id) {
+ isFollowing.value = user.isFollowing;
+ hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
+ }
+}
- async onClick() {
- this.wait = true;
+async function onClick() {
+ wait.value = true;
- try {
- if (this.isFollowing) {
- const { canceled } = await os.confirm({
- type: 'warning',
- text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }),
- });
+ try {
+ if (isFollowing.value) {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
+ });
- if (canceled) return;
+ if (canceled) return;
- await os.api('following/delete', {
- userId: this.user.id
- });
- } else {
- if (this.hasPendingFollowRequestFromYou) {
- await os.api('following/requests/cancel', {
- userId: this.user.id
- });
- } else if (this.user.isLocked) {
- await os.api('following/create', {
- userId: this.user.id
- });
- this.hasPendingFollowRequestFromYou = true;
- } else {
- await os.api('following/create', {
- userId: this.user.id
- });
- this.hasPendingFollowRequestFromYou = true;
- }
- }
- } catch (e) {
- console.error(e);
- } finally {
- this.wait = false;
+ await os.api('following/delete', {
+ userId: props.user.id
+ });
+ } else {
+ if (hasPendingFollowRequestFromYou.value) {
+ await os.api('following/requests/cancel', {
+ userId: props.user.id
+ });
+ } else if (props.user.isLocked) {
+ await os.api('following/create', {
+ userId: props.user.id
+ });
+ hasPendingFollowRequestFromYou.value = true;
+ } else {
+ await os.api('following/create', {
+ userId: props.user.id
+ });
+ hasPendingFollowRequestFromYou.value = true;
}
}
+ } catch (e) {
+ console.error(e);
+ } finally {
+ wait.value = false;
}
+}
+
+onMounted(() => {
+ connection.on('follow', onFollowChange);
+ connection.on('unfollow', onFollowChange);
+});
+
+onBeforeUnmount(() => {
+ connection.dispose();
});
</script>
diff --git a/packages/client/src/components/forgot-password.vue b/packages/client/src/components/forgot-password.vue
index b03a6133b4..c74e1ac75e 100644
--- a/packages/client/src/components/forgot-password.vue
+++ b/packages/client/src/components/forgot-password.vue
@@ -2,72 +2,64 @@
<XModalWindow ref="dialog"
:width="370"
:height="400"
- @close="$refs.dialog.close()"
- @closed="$emit('closed')"
+ @close="dialog.close()"
+ @closed="emit('closed')"
>
- <template #header>{{ $ts.forgotPassword }}</template>
+ <template #header>{{ i18n.locale.forgotPassword }}</template>
- <form v-if="$instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
+ <form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
<div class="main _formRoot">
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
- <template #label>{{ $ts.username }}</template>
+ <template #label>{{ i18n.locale.username }}</template>
<template #prefix>@</template>
</MkInput>
<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
- <template #label>{{ $ts.emailAddress }}</template>
- <template #caption>{{ $ts._forgotPassword.enterEmail }}</template>
+ <template #label>{{ i18n.locale.emailAddress }}</template>
+ <template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template>
</MkInput>
- <MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
+ <MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton>
</div>
<div class="sub">
- <MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
+ <MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA>
</div>
</form>
- <div v-else>
- {{ $ts._forgotPassword.contactAdmin }}
+ <div v-else class="bafecedb">
+ {{ i18n.locale._forgotPassword.contactAdmin }}
</div>
</XModalWindow>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import * as os from '@/os';
+import { instance } from '@/instance';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XModalWindow,
- MkButton,
- MkInput,
- },
+const emit = defineEmits<{
+ (e: 'done'): void;
+ (e: 'closed'): void;
+}>();
- emits: ['done', 'closed'],
+let dialog: InstanceType<typeof XModalWindow> = $ref();
- data() {
- return {
- username: '',
- email: '',
- processing: false,
- };
- },
+let username = $ref('');
+let email = $ref('');
+let processing = $ref(false);
- methods: {
- async onSubmit() {
- this.processing = true;
- await os.apiWithDialog('request-reset-password', {
- username: this.username,
- email: this.email,
- });
-
- this.$emit('done');
- this.$refs.dialog.close();
- }
- }
-});
+async function onSubmit() {
+ processing = true;
+ await os.apiWithDialog('request-reset-password', {
+ username,
+ email,
+ });
+ emit('done');
+ dialog.close();
+}
</script>
<style lang="scss" scoped>
@@ -81,4 +73,8 @@ export default defineComponent({
padding: 24px;
}
}
+
+.bafecedb {
+ padding: 24px;
+}
</style>
diff --git a/packages/client/src/components/global/avatar.vue b/packages/client/src/components/global/avatar.vue
index 9e8979fe56..27cfb6e4d4 100644
--- a/packages/client/src/components/global/avatar.vue
+++ b/packages/client/src/components/global/avatar.vue
@@ -35,9 +35,9 @@ const emit = defineEmits<{
(e: 'click', ev: MouseEvent): void;
}>();
-const url = defaultStore.state.disableShowingAnimatedImages
+const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.user.avatarUrl)
- : props.user.avatarUrl;
+ : props.user.avatarUrl);
function onClick(ev: MouseEvent) {
emit('click', ev);
diff --git a/packages/client/src/components/modal-page-window.vue b/packages/client/src/components/modal-page-window.vue
index 3de1980820..2e17d5d030 100644
--- a/packages/client/src/components/modal-page-window.vue
+++ b/packages/client/src/components/modal-page-window.vue
@@ -153,8 +153,8 @@ export default defineComponent({
this.$refs.window.close();
},
- onContextmenu(e) {
- os.contextMenu(this.contextmenu, e);
+ onContextmenu(ev: MouseEvent) {
+ os.contextMenu(this.contextmenu, ev);
}
},
});
diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue
index 07e9920f65..a3b30f726e 100644
--- a/packages/client/src/components/note-detailed.vue
+++ b/packages/client/src/components/note-detailed.vue
@@ -4,6 +4,7 @@
v-show="!isDeleted"
v-hotkey="keymap"
v-size="{ max: [500, 450, 350, 300] }"
+ ref="el"
class="lxwezrsl _block"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
@@ -222,21 +223,21 @@ function undoReact(note): void {
});
}
-function onContextmenu(e): void {
+function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
- if (isLink(e.target)) return;
+ if (isLink(ev.target)) return;
if (window.getSelection().toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) {
- e.preventDefault();
+ ev.preventDefault();
react();
} else {
- os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), e).then(focus);
+ os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), ev).then(focus);
}
}
diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue
index b309afe051..fc89c2777b 100644
--- a/packages/client/src/components/note.vue
+++ b/packages/client/src/components/note.vue
@@ -211,21 +211,21 @@ function undoReact(note): void {
});
}
-function onContextmenu(e): void {
+function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
- if (isLink(e.target)) return;
+ if (isLink(ev.target)) return;
if (window.getSelection().toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) {
- e.preventDefault();
+ ev.preventDefault();
react();
} else {
- os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), e).then(focus);
+ os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), ev).then(focus);
}
}
diff --git a/packages/client/src/components/notes.vue b/packages/client/src/components/notes.vue
index aec478ac95..41bec5a579 100644
--- a/packages/client/src/components/notes.vue
+++ b/packages/client/src/components/notes.vue
@@ -32,9 +32,7 @@ const props = defineProps<{
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
defineExpose({
- prepend: (note) => {
- pagingComponent.value?.prepend(note);
- },
+ pagingComponent,
});
</script>
diff --git a/packages/client/src/components/post-form-attaches.vue b/packages/client/src/components/post-form-attaches.vue
index 0782ce22e5..0c8181b481 100644
--- a/packages/client/src/components/post-form-attaches.vue
+++ b/packages/client/src/components/post-form-attaches.vue
@@ -10,7 +10,7 @@
</div>
</template>
</XDraggable>
- <p class="remain">{{ 4 - files.length }}/4</p>
+ <p class="remain">{{ 16 - files.length }}/16</p>
</div>
</template>
@@ -41,7 +41,6 @@ export default defineComponent({
data() {
return {
menu: null as Promise<null> | null,
-
};
},
@@ -99,10 +98,12 @@ export default defineComponent({
}, {
done: result => {
if (!result || result.canceled) return;
- let comment = result.result;
+ let comment = result.result.length == 0 ? null : result.result;
os.api('drive/files/update', {
fileId: file.id,
- comment: comment.length == 0 ? null : comment
+ comment: comment,
+ }).then(() => {
+ file.comment = comment;
});
}
}, 'closed');
diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue
index 3fcb1d906b..0dcec26932 100644
--- a/packages/client/src/components/post-form.vue
+++ b/packages/client/src/components/post-form.vue
@@ -8,25 +8,28 @@
>
<header>
<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button>
+ <button v-click-anime v-tooltip="i18n.locale.switchAccount" class="account _button" @click="openAccountMenu">
+ <MkAvatar :user="postAccount ?? $i" class="avatar"/>
+ </button>
<div>
<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
- <button ref="visibilityButton" v-tooltip="$ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
+ <button ref="visibilityButton" v-tooltip="i18n.locale.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
</button>
- <button v-tooltip="$ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
+ <button v-tooltip="i18n.locale.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
</div>
</header>
<div class="form" :class="{ fixed }">
<XNoteSimple v-if="reply" class="preview" :note="reply"/>
<XNoteSimple v-if="renote" class="preview" :note="renote"/>
- <div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
+ <div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.locale.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
<div v-if="visibility === 'specified'" class="to-specified">
- <span style="margin-right: 8px;">{{ $ts.recipient }}</span>
+ <span style="margin-right: 8px;">{{ i18n.locale.recipient }}</span>
<div class="visibleUsers">
<span v-for="u in visibleUsers" :key="u.id">
<MkAcct :user="u"/>
@@ -35,21 +38,21 @@
<button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button>
</div>
</div>
- <MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ $ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ $ts.add }}</button></MkInfo>
- <input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
+ <MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.locale.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.locale.add }}</button></MkInfo>
+ <input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.locale.annotation" @keydown="onKeydown">
<textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
- <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="$ts.hashtags" list="hashtags">
+ <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.locale.hashtags" list="hashtags">
<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
<XNotePreview v-if="showPreview" class="preview" :text="text"/>
<footer>
- <button v-tooltip="$ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
- <button v-tooltip="$ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
- <button v-tooltip="$ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
- <button v-tooltip="$ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
- <button v-tooltip="$ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
- <button v-tooltip="$ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
- <button v-if="postFormActions.length > 0" v-tooltip="$ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
+ <button v-tooltip="i18n.locale.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
+ <button v-tooltip="i18n.locale.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
+ <button v-tooltip="i18n.locale.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
+ <button v-tooltip="i18n.locale.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
+ <button v-tooltip="i18n.locale.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
+ <button v-tooltip="i18n.locale.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
+ <button v-if="postFormActions.length > 0" v-tooltip="i18n.locale.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
</footer>
<datalist id="hashtags">
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
@@ -83,7 +86,7 @@ import { throttle } from 'throttle-debounce';
import MkInfo from '@/components/ui/info.vue';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
-import { $i } from '@/account';
+import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
const modal = inject('modal');
@@ -339,8 +342,8 @@ function focus() {
}
function chooseFileFrom(ev) {
- selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files => {
- for (const file of files) {
+ selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files_ => {
+ for (const file of files_) {
files.push(file);
}
});
@@ -350,8 +353,8 @@ function detachFile(id) {
files = files.filter(x => x.id != id);
}
-function updateFiles(files) {
- files = files;
+function updateFiles(_files) {
+ files = _files;
}
function updateFileSensitive(file, sensitive) {
@@ -553,8 +556,15 @@ async function post() {
}
}
+ let token = undefined;
+
+ if (postAccount) {
+ const storedAccounts = await getAccounts();
+ token = storedAccounts.find(x => x.id === postAccount.id)?.token;
+ }
+
posting = true;
- os.api('notes/create', data).then(() => {
+ os.api('notes/create', data, token).then(() => {
clear();
nextTick(() => {
deleteDraft();
@@ -565,6 +575,7 @@ async function post() {
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
posting = false;
+ postAccount = null;
});
}).catch(err => {
posting = false;
@@ -585,7 +596,7 @@ function insertMention() {
});
}
-async function insertEmoji(ev) {
+async function insertEmoji(ev: MouseEvent) {
os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl);
}
@@ -602,6 +613,23 @@ function showActions(ev) {
})), ev.currentTarget || ev.target);
}
+let postAccount = $ref<misskey.entities.UserDetailed | null>(null);
+
+function openAccountMenu(ev: MouseEvent) {
+ openAccountMenu_({
+ withExtraOperation: false,
+ includeCurrentAccount: true,
+ active: postAccount != null ? postAccount.id : $i.id,
+ onChoose: (account) => {
+ if (account.id === $i.id) {
+ postAccount = null;
+ } else {
+ postAccount = account;
+ }
+ },
+ }, ev);
+}
+
onMounted(() => {
if (props.autofocus) {
focus();
@@ -678,6 +706,19 @@ onMounted(() => {
line-height: 66px;
}
+ > .account {
+ height: 100%;
+ aspect-ratio: 1/1;
+ display: inline-flex;
+ vertical-align: bottom;
+
+ > .avatar {
+ width: 28px;
+ height: 28px;
+ margin: auto;
+ }
+ }
+
> div {
position: absolute;
top: 0;
diff --git a/packages/client/src/components/timeline.vue b/packages/client/src/components/timeline.vue
index a7af02c30b..59956b9526 100644
--- a/packages/client/src/components/timeline.vue
+++ b/packages/client/src/components/timeline.vue
@@ -25,10 +25,10 @@ const emit = defineEmits<{
provide('inChannel', computed(() => props.src === 'channel'));
-const tlComponent = ref<InstanceType<typeof XNotes>>();
+const tlComponent: InstanceType<typeof XNotes> = $ref();
const prepend = note => {
- tlComponent.value.prepend(note);
+ tlComponent.pagingComponent?.prepend(note);
emit('note');
@@ -38,16 +38,16 @@ const prepend = note => {
};
const onUserAdded = () => {
- tlComponent.value.reload();
+ tlComponent.pagingComponent?.reload();
};
const onUserRemoved = () => {
- tlComponent.value.reload();
+ tlComponent.pagingComponent?.reload();
};
const onChangeFollowing = () => {
- if (!tlComponent.value.backed) {
- tlComponent.value.reload();
+ if (!tlComponent.pagingComponent?.backed) {
+ tlComponent.pagingComponent?.reload();
}
};
diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue
index 6f3f277b11..41165c8d33 100644
--- a/packages/client/src/components/ui/menu.vue
+++ b/packages/client/src/components/ui/menu.vue
@@ -24,7 +24,7 @@
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</a>
- <button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" @click="clicked(item.action, $event)">
+ <button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button>
diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue
index 571ef71eab..9c18fc5ce5 100644
--- a/packages/client/src/components/ui/pagination.vue
+++ b/packages/client/src/components/ui/pagination.vue
@@ -73,12 +73,11 @@ const queue = ref<Item[]>([]);
const offset = ref(0);
const fetching = ref(true);
const moreFetching = ref(false);
-const inited = ref(false);
const more = ref(false);
const backed = ref(false); // 遡り中か否か
const isBackTop = ref(false);
-const empty = computed(() => items.value.length === 0 && !fetching.value && inited.value);
-const error = computed(() => !fetching.value && !inited.value);
+const empty = computed(() => items.value.length === 0);
+const error = ref(false);
const init = async (): Promise<void> => {
queue.value = [];
@@ -105,9 +104,10 @@ const init = async (): Promise<void> => {
more.value = false;
}
offset.value = res.length;
- inited.value = true;
+ error.value = false;
fetching.value = false;
}, e => {
+ error.value = true;
fetching.value = false;
});
};
@@ -183,30 +183,36 @@ const fetchMoreAhead = async (): Promise<void> => {
};
const prepend = (item: Item): void => {
- if (rootEl.value == null) return;
-
if (props.pagination.reversed) {
- const container = getScrollContainer(rootEl.value);
- if (container == null) return; // TODO?
+ if (rootEl.value) {
+ const container = getScrollContainer(rootEl.value);
+ if (container == null) return; // TODO?
- const pos = getScrollPosition(rootEl.value);
- const viewHeight = container.clientHeight;
- const height = container.scrollHeight;
- const isBottom = (pos + viewHeight > height - 32);
- if (isBottom) {
- // オーバーフローしたら古いアイテムは捨てる
- if (items.value.length >= props.displayLimit) {
- // このやり方だとVue 3.2以降アニメーションが動かなくなる
- //items.value = items.value.slice(-props.displayLimit);
- while (items.value.length >= props.displayLimit) {
- items.value.shift();
+ const pos = getScrollPosition(rootEl.value);
+ const viewHeight = container.clientHeight;
+ const height = container.scrollHeight;
+ const isBottom = (pos + viewHeight > height - 32);
+ if (isBottom) {
+ // オーバーフローしたら古いアイテムは捨てる
+ if (items.value.length >= props.displayLimit) {
+ // このやり方だとVue 3.2以降アニメーションが動かなくなる
+ //items.value = items.value.slice(-props.displayLimit);
+ while (items.value.length >= props.displayLimit) {
+ items.value.shift();
+ }
+ more.value = true;
}
- more.value = true;
}
}
items.value.push(item);
// TODO
} else {
+ // 初回表示時はunshiftだけでOK
+ if (!rootEl.value) {
+ items.value.unshift(item);
+ return;
+ }
+
const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
if (isTop) {
@@ -264,6 +270,7 @@ onDeactivated(() => {
defineExpose({
items,
+ backed,
reload,
fetchMoreAhead,
prepend,
diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue
index bd33289ccc..fa32ecfdef 100644
--- a/packages/client/src/components/ui/window.vue
+++ b/packages/client/src/components/ui/window.vue
@@ -147,9 +147,9 @@ export default defineComponent({
}
},
- onContextmenu(e) {
+ onContextmenu(ev: MouseEvent) {
if (this.contextmenu) {
- os.contextMenu(this.contextmenu, e);
+ os.contextMenu(this.contextmenu, ev);
}
},