Merge remote-tracking branch 'misskey/release/2024.5.0' into future
This commit is contained in:
commit
3372e0ffe1
181 changed files with 4057 additions and 891 deletions
|
|
@ -4,77 +4,81 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
|
||||
<template #header>:{{ emoji.name }}:</template>
|
||||
<template #default>
|
||||
<MkSpacer>
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
<div :class="$style.emojiImgWrapper">
|
||||
<MkCustomEmoji :name="emoji.name" :normal="true" :useOriginalSize="true" style="height: 100%;"></MkCustomEmoji>
|
||||
</div>
|
||||
<MkKeyValue :copy="`:${emoji.name}:`">
|
||||
<template #key>{{ i18n.ts.name }}</template>
|
||||
<template #value>{{ emoji.name }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.tags }}</template>
|
||||
<template #value>
|
||||
<div v-if="emoji.aliases.length === 0">{{ i18n.ts.none }}</div>
|
||||
<div v-else :class="$style.aliases">
|
||||
<span v-for="alias in emoji.aliases" :key="alias" :class="$style.alias">
|
||||
{{ alias }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.category }}</template>
|
||||
<template #value>{{ emoji.category ?? i18n.ts.none }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.sensitive }}</template>
|
||||
<template #value>{{ emoji.isSensitive ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.localOnly }}</template>
|
||||
<template #value>{{ emoji.localOnly ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.license }}</template>
|
||||
<template #value><Mfm :text="emoji.license ?? i18n.ts.none" /></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue :copy="emoji.url">
|
||||
<template #key>{{ i18n.ts.emojiUrl }}</template>
|
||||
<template #value>
|
||||
<MkLink :url="emoji.url" target="_blank">{{ emoji.url }}</MkLink>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
</MkModalWindow>
|
||||
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
|
||||
<template #header>:{{ emoji.name }}:</template>
|
||||
<template #default>
|
||||
<MkSpacer>
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
<div :class="$style.emojiImgWrapper">
|
||||
<MkCustomEmoji :name="emoji.name" :normal="true" :useOriginalSize="true" style="height: 100%;"></MkCustomEmoji>
|
||||
</div>
|
||||
<MkKeyValue :copy="`:${emoji.name}:`">
|
||||
<template #key>{{ i18n.ts.name }}</template>
|
||||
<template #value>{{ emoji.name }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.tags }}</template>
|
||||
<template #value>
|
||||
<div v-if="emoji.aliases.length === 0">{{ i18n.ts.none }}</div>
|
||||
<div v-else :class="$style.aliases">
|
||||
<span v-for="alias in emoji.aliases" :key="alias" :class="$style.alias">
|
||||
{{ alias }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.category }}</template>
|
||||
<template #value>{{ emoji.category ?? i18n.ts.none }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.sensitive }}</template>
|
||||
<template #value>{{ emoji.isSensitive ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.localOnly }}</template>
|
||||
<template #value>{{ emoji.localOnly ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.license }}</template>
|
||||
<template #value><Mfm :text="emoji.license ?? i18n.ts.none"/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue :copy="emoji.url">
|
||||
<template #key>{{ i18n.ts.emojiUrl }}</template>
|
||||
<template #value>
|
||||
<MkLink :url="emoji.url" target="_blank">{{ emoji.url }}</MkLink>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { defineProps, shallowRef } from 'vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkLink from './MkLink.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji: Misskey.entities.EmojiDetailed,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'ok', cropped: Misskey.entities.DriveFile): void;
|
||||
(ev: 'cancel'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const cancel = () => {
|
||||
|
||||
function cancel() {
|
||||
emit('cancel');
|
||||
dialogEl.value!.close();
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
71
packages/frontend/src/components/MkFormDialog.file.vue
Normal file
71
packages/frontend/src/components/MkFormDialog.file.vue
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MkButton inline rounded primary @click="selectButton($event)">{{ i18n.ts.selectFile }}</MkButton>
|
||||
<div :class="['_nowrap', !fileName && $style.fileNotSelected]">{{ friendlyFileName }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computed, ref } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
||||
const props = defineProps<{
|
||||
fileId?: string | null;
|
||||
validate?: (file: Misskey.entities.DriveFile) => Promise<boolean>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update', result: Misskey.entities.DriveFile): void;
|
||||
}>();
|
||||
|
||||
const fileUrl = ref('');
|
||||
const fileName = ref<string>('');
|
||||
|
||||
const friendlyFileName = computed<string>(() => {
|
||||
if (fileName.value) {
|
||||
return fileName.value;
|
||||
}
|
||||
if (fileUrl.value) {
|
||||
return fileUrl.value;
|
||||
}
|
||||
|
||||
return i18n.ts.fileNotSelected;
|
||||
});
|
||||
|
||||
if (props.fileId) {
|
||||
misskeyApi('drive/files/show', {
|
||||
fileId: props.fileId,
|
||||
}).then((apiRes) => {
|
||||
fileName.value = apiRes.name;
|
||||
fileUrl.value = apiRes.url;
|
||||
});
|
||||
}
|
||||
|
||||
function selectButton(ev: MouseEvent) {
|
||||
selectFile(ev.currentTarget ?? ev.target).then(async (file) => {
|
||||
if (!file) return;
|
||||
if (props.validate && !await props.validate(file)) return;
|
||||
|
||||
emit('update', file);
|
||||
fileName.value = file.name;
|
||||
fileUrl.value = file.url;
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
.fileNotSelected {
|
||||
font-weight: 700;
|
||||
color: var(--infoWarnFg);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<MkSpacer :marginMin="20" :marginMax="32">
|
||||
<div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
|
||||
<template v-for="(v, k) in Object.fromEntries(Object.entries(form).filter(([_, v]) => !('hidden' in v) || 'hidden' in v && !v.hidden))">
|
||||
<MkInput v-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1">
|
||||
<template v-for="(v, k) in Object.fromEntries(Object.entries(form))">
|
||||
<template v-if="typeof v.hidden == 'function' ? v.hidden(values) : v.hidden"></template>
|
||||
<MkInput v-else-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1">
|
||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||
<template v-if="v.description" #caption>{{ v.description }}</template>
|
||||
</MkInput>
|
||||
|
|
@ -53,6 +54,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton v-else-if="v.type === 'button'" @click="v.action($event, values)">
|
||||
<span v-text="v.content || k"></span>
|
||||
</MkButton>
|
||||
<XFile
|
||||
v-else-if="v.type === 'drive-file'"
|
||||
:fileId="v.defaultFileId"
|
||||
:validate="async f => !v.validate || await v.validate(f)"
|
||||
@update="f => values[k] = f"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="_fullinfo">
|
||||
|
|
@ -72,6 +79,7 @@ import MkSelect from './MkSelect.vue';
|
|||
import MkRange from './MkRange.vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkRadios from './MkRadios.vue';
|
||||
import XFile from './MkFormDialog.file.vue';
|
||||
import type { Form } from '@/scripts/form.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
|
|||
|
|
@ -276,8 +276,11 @@ const align = () => {
|
|||
const onOpened = () => {
|
||||
emit('opened');
|
||||
|
||||
// NOTE: Chromatic テストの際に undefined になる場合がある
|
||||
if (content.value == null) return;
|
||||
|
||||
// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
|
||||
const el = content.value!.children[0];
|
||||
const el = content.value.children[0];
|
||||
el.addEventListener('mousedown', ev => {
|
||||
contentClicking = true;
|
||||
window.addEventListener('mouseup', ev => {
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ const localOnly = ref(props.initialLocalOnly ?? (defaultStore.state.rememberNote
|
|||
const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility));
|
||||
const visibleUsers = ref<Misskey.entities.UserDetailed[]>([]);
|
||||
if (props.initialVisibleUsers) {
|
||||
props.initialVisibleUsers.forEach(pushVisibleUser);
|
||||
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
|
||||
}
|
||||
const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
|
||||
const autocomplete = ref(null);
|
||||
|
|
@ -338,7 +338,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
|
|||
misskeyApi('users/show', {
|
||||
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply?.userId),
|
||||
}).then(users => {
|
||||
users.forEach(pushVisibleUser);
|
||||
users.forEach(u => pushVisibleUser(u));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -618,6 +618,23 @@ async function onPaste(ev: ClipboardEvent) {
|
|||
quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)?.[1] ?? null;
|
||||
});
|
||||
}
|
||||
|
||||
if (paste.length > 1000) {
|
||||
ev.preventDefault();
|
||||
os.confirm({
|
||||
type: 'info',
|
||||
text: i18n.ts.attachAsFileQuestion,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) {
|
||||
insertTextAtCursor(textareaEl.value, paste);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, "0");
|
||||
const file = new File([paste], `${fileName}.txt`, { type: "text/plain" });
|
||||
upload(file, `${fileName}.txt`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onDragover(ev) {
|
||||
|
|
@ -1004,11 +1021,7 @@ onMounted(() => {
|
|||
}
|
||||
if (draft.data.visibleUserIds) {
|
||||
misskeyApi('users/show', { userIds: draft.data.visibleUserIds }).then(users => {
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
if (users[i].id === draft.data.visibleUserIds[i]) {
|
||||
pushVisibleUser(users[i]);
|
||||
}
|
||||
}
|
||||
users.forEach(u => pushVisibleUser(u));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ import { i18n } from '@/i18n.js';
|
|||
|
||||
let lock: Promise<undefined> | undefined;
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const common = {
|
||||
render(args) {
|
||||
return {
|
||||
|
|
@ -43,6 +47,8 @@ const common = {
|
|||
lock = new Promise(r => resolve = r);
|
||||
|
||||
try {
|
||||
// NOTE: sleep しないと何故か落ちる
|
||||
await sleep(100);
|
||||
const canvas = within(canvasElement);
|
||||
const a = canvas.getByRole<HTMLAnchorElement>('link');
|
||||
// await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
|
||||
|
|
@ -53,7 +59,7 @@ const common = {
|
|||
const i = buttons[0];
|
||||
await expect(i).toBeInTheDocument();
|
||||
await userEvent.click(i);
|
||||
// await expect(canvasElement).toHaveTextContent(i18n.ts._ad.back);
|
||||
await expect(canvasElement).toHaveTextContent(i18n.ts._ad.back);
|
||||
await expect(a).not.toBeInTheDocument();
|
||||
await expect(i).not.toBeInTheDocument();
|
||||
buttons = canvas.getAllByRole<HTMLButtonElement>('button');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue