summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2024-09-29 11:42:24 +0000
committerGitHub <noreply@github.com>2024-09-29 11:42:24 +0000
commit5fc8b3bc5018a2cb553f114a570cc33ef6831163 (patch)
tree40edc874ae384548fd13e55fff6e317d1ef84fbb /packages/frontend/src/components
parentMerge pull request #14391 from misskey-dev/develop (diff)
parentRelease: 2024.9.0 (diff)
downloadsharkey-5fc8b3bc5018a2cb553f114a570cc33ef6831163.tar.gz
sharkey-5fc8b3bc5018a2cb553f114a570cc33ef6831163.tar.bz2
sharkey-5fc8b3bc5018a2cb553f114a570cc33ef6831163.zip
Merge pull request #14580 from misskey-dev/develop
Release: 2024.9.0
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkAccountMoved.vue2
-rw-r--r--packages/frontend/src/components/MkAsUi.vue27
-rw-r--r--packages/frontend/src/components/MkAutocomplete.vue6
-rw-r--r--packages/frontend/src/components/MkButton.vue11
-rw-r--r--packages/frontend/src/components/MkChannelPreview.vue2
-rw-r--r--packages/frontend/src/components/MkChart.vue50
-rw-r--r--packages/frontend/src/components/MkClickerGame.vue2
-rw-r--r--packages/frontend/src/components/MkCode.vue13
-rw-r--r--packages/frontend/src/components/MkContainer.vue2
-rw-r--r--packages/frontend/src/components/MkContextMenu.vue2
-rw-r--r--packages/frontend/src/components/MkCropperDialog.vue2
-rw-r--r--packages/frontend/src/components/MkDateSeparatedList.vue19
-rw-r--r--packages/frontend/src/components/MkDonation.vue2
-rw-r--r--packages/frontend/src/components/MkDrive.folder.vue2
-rw-r--r--packages/frontend/src/components/MkDrive.vue30
-rw-r--r--packages/frontend/src/components/MkDriveFileThumbnail.vue21
-rw-r--r--packages/frontend/src/components/MkEmbedCodeGenDialog.vue414
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.section.vue2
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.vue7
-rw-r--r--packages/frontend/src/components/MkEmojiPickerDialog.vue2
-rw-r--r--packages/frontend/src/components/MkFileListForAdmin.vue2
-rw-r--r--packages/frontend/src/components/MkFolder.vue23
-rw-r--r--packages/frontend/src/components/MkFollowButton.vue4
-rw-r--r--packages/frontend/src/components/MkFormFooter.vue49
-rw-r--r--packages/frontend/src/components/MkImgWithBlurhash.vue4
-rw-r--r--packages/frontend/src/components/MkInput.vue2
-rw-r--r--packages/frontend/src/components/MkInstanceTicker.vue2
-rw-r--r--packages/frontend/src/components/MkInviteCode.vue17
-rw-r--r--packages/frontend/src/components/MkLink.vue2
-rw-r--r--packages/frontend/src/components/MkMediaAudio.vue6
-rw-r--r--packages/frontend/src/components/MkMediaImage.vue59
-rw-r--r--packages/frontend/src/components/MkMediaList.vue2
-rw-r--r--packages/frontend/src/components/MkMediaVideo.vue6
-rw-r--r--packages/frontend/src/components/MkMention.vue2
-rw-r--r--packages/frontend/src/components/MkMenu.child.vue2
-rw-r--r--packages/frontend/src/components/MkMenu.vue98
-rw-r--r--packages/frontend/src/components/MkMiniChart.vue2
-rw-r--r--packages/frontend/src/components/MkModal.vue2
-rw-r--r--packages/frontend/src/components/MkModalWindow.vue6
-rw-r--r--packages/frontend/src/components/MkNote.vue32
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue22
-rw-r--r--packages/frontend/src/components/MkNoteHeader.vue21
-rw-r--r--packages/frontend/src/components/MkNotification.vue50
-rw-r--r--packages/frontend/src/components/MkNotificationSelectWindow.vue2
-rw-r--r--packages/frontend/src/components/MkNotifications.vue2
-rw-r--r--packages/frontend/src/components/MkOmit.vue2
-rw-r--r--packages/frontend/src/components/MkPageWindow.vue4
-rw-r--r--packages/frontend/src/components/MkPagination.vue17
-rw-r--r--packages/frontend/src/components/MkPoll.vue12
-rw-r--r--packages/frontend/src/components/MkPopupMenu.vue2
-rw-r--r--packages/frontend/src/components/MkPostForm.vue17
-rw-r--r--packages/frontend/src/components/MkPostFormAttaches.vue28
-rw-r--r--packages/frontend/src/components/MkPreview.vue2
-rw-r--r--packages/frontend/src/components/MkPullToRefresh.vue2
-rw-r--r--packages/frontend/src/components/MkRange.vue54
-rw-r--r--packages/frontend/src/components/MkReactionTooltip.vue1
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.details.vue3
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.reaction.vue2
-rw-r--r--packages/frontend/src/components/MkSelect.vue4
-rw-r--r--packages/frontend/src/components/MkSignin.vue97
-rw-r--r--packages/frontend/src/components/MkSigninDialog.vue2
-rw-r--r--packages/frontend/src/components/MkSignupDialog.form.vue2
-rw-r--r--packages/frontend/src/components/MkSourceCodeAvailablePopup.vue2
-rw-r--r--packages/frontend/src/components/MkSubNoteContent.vue4
-rw-r--r--packages/frontend/src/components/MkSuperMenu.vue6
-rw-r--r--packages/frontend/src/components/MkSystemWebhookEditor.impl.ts3
-rw-r--r--packages/frontend/src/components/MkSystemWebhookEditor.vue76
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.vue2
-rw-r--r--packages/frontend/src/components/MkUpdated.vue2
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue4
-rw-r--r--packages/frontend/src/components/MkUserSelectDialog.vue2
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.vue2
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.vue2
-rw-r--r--packages/frontend/src/components/MkWidgets.vue8
-rw-r--r--packages/frontend/src/components/MkWindow.vue10
-rw-r--r--packages/frontend/src/components/MkYouTubePlayer.vue2
-rw-r--r--packages/frontend/src/components/form/link.vue6
-rw-r--r--packages/frontend/src/components/global/MkA.vue2
-rw-r--r--packages/frontend/src/components/global/MkAcct.vue8
-rw-r--r--packages/frontend/src/components/global/MkAd.vue2
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue19
-rw-r--r--packages/frontend/src/components/global/MkCondensedLine.vue2
-rw-r--r--packages/frontend/src/components/global/MkCustomEmoji.vue31
-rw-r--r--packages/frontend/src/components/global/MkEmoji.vue31
-rw-r--r--packages/frontend/src/components/global/MkMfm.stories.impl.ts (renamed from packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts)17
-rw-r--r--packages/frontend/src/components/global/MkMfm.ts (renamed from packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts)18
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue2
-rw-r--r--packages/frontend/src/components/global/MkStickyContainer.vue7
-rw-r--r--packages/frontend/src/components/global/MkTime.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkTime.vue2
-rw-r--r--packages/frontend/src/components/global/MkUrl.vue11
-rw-r--r--packages/frontend/src/components/index.ts2
92 files changed, 1216 insertions, 359 deletions
diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue
index 6c0774b634..796524fce9 100644
--- a/packages/frontend/src/components/MkAccountMoved.vue
+++ b/packages/frontend/src/components/MkAccountMoved.vue
@@ -16,7 +16,7 @@ import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkMention from './MkMention.vue';
import { i18n } from '@/i18n.js';
-import { host as localHost } from '@/config.js';
+import { host as localHost } from '@@/js/config.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
const user = ref<Misskey.entities.UserLite>();
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index 18e8e7542e..b50a7fea5c 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
</template>
</MkFolder>
- <div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align, backgroundColor: c.bgColor, color: c.fgColor, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
+ <div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="containerStyle">
<template v-for="child in c.children" :key="child">
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
</template>
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { Ref, ref } from 'vue';
+import { Ref, ref, computed } from 'vue';
import * as os from '@/os.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -97,6 +97,29 @@ function g(id) {
} as AsUiRoot;
}
+const containerStyle = computed(() => {
+ if (c.type !== 'container') return undefined;
+
+ // width, color, styleのうち一つでも指定があれば、枠線がちゃんと表示されるようにwidthとstyleのデフォルト値を設定
+ // radiusは単に角を丸める用途もあるため除外
+ const isBordered = c.borderWidth ?? c.borderColor ?? c.borderStyle;
+
+ const border = isBordered ? {
+ borderWidth: c.borderWidth ?? '1px',
+ borderColor: c.borderColor ?? 'var(--divider)',
+ borderStyle: c.borderStyle ?? 'solid',
+ } : undefined;
+
+ return {
+ textAlign: c.align,
+ backgroundColor: c.bgColor,
+ color: c.fgColor,
+ padding: c.padding ? `${c.padding}px` : 0,
+ borderRadius: (c.borderRadius ?? (c.rounded ? 8 : 0)) + 'px',
+ ...border,
+ };
+});
+
const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false);
function onSwitchUpdate(v) {
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 932c4ecb2e..f547991369 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -46,17 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts">
import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import sanitizeHtml from 'sanitize-html';
+import { emojilist, getEmojiName } from '@@/js/emojilist.js';
import contains from '@/scripts/contains.js';
-import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
+import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
import { acct } from '@/filters/user.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
-import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js';
-import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
+import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js';
const lib = emojilist.filter(x => x.category !== 'flags');
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 9560efb7d9..1156b3f2b8 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -171,11 +171,11 @@ function onMousedown(evt: MouseEvent): void {
background: var(--accent);
&:not(:disabled):hover {
- background: var(--X8);
+ background: hsl(from var(--accent) h s calc(l + 5));
}
&:not(:disabled):active {
- background: var(--X8);
+ background: hsl(from var(--accent) h s calc(l + 5));
}
}
@@ -220,15 +220,16 @@ function onMousedown(evt: MouseEvent): void {
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
&:not(:disabled):hover {
- background: linear-gradient(90deg, var(--X8), var(--X8));
+ background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
&:not(:disabled):active {
- background: linear-gradient(90deg, var(--X8), var(--X8));
+ background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
}
&.danger {
+ font-weight: bold;
color: #ff2a2a;
&.primary {
@@ -246,7 +247,7 @@ function onMousedown(evt: MouseEvent): void {
}
&:disabled {
- opacity: 0.7;
+ opacity: 0.5;
}
&:focus-visible {
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index c30cb66c07..3c0874a1eb 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -117,7 +117,7 @@ const bannerStyle = computed(() => {
left: 0;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), var(--X15));
+ background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
}
> .name {
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index 4b24562249..57d325b11a 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -13,29 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
-<script lang="ts" setup>
-/* eslint-disable id-denylist --
- Chart.js has a `data` attribute in most chart definitions, which triggers the
- id-denylist violation when setting it. This is causing about 60+ lint issues.
- As this is part of Chart.js's API it makes sense to disable the check here.
-*/
-import { onMounted, ref, shallowRef, watch } from 'vue';
-import { Chart } from 'chart.js';
-import * as Misskey from 'misskey-js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { alpha } from '@/scripts/color.js';
-import date from '@/filters/date.js';
-import bytes from '@/filters/bytes.js';
-import { initChart } from '@/scripts/init-chart.js';
-import { chartLegend } from '@/scripts/chart-legend.js';
-import MkChartLegend from '@/components/MkChartLegend.vue';
-
-initChart();
-
-type ChartSrc =
+<script lang="ts">
+export type ChartSrc =
| 'federation'
| 'ap-request'
| 'users'
@@ -62,7 +41,30 @@ type ChartSrc =
| 'per-user-pv'
| 'per-user-following'
| 'per-user-followers'
- | 'per-user-drive'
+ | 'per-user-drive';
+</script>
+
+<script lang="ts" setup>
+/* eslint-disable id-denylist --
+ Chart.js has a `data` attribute in most chart definitions, which triggers the
+ id-denylist violation when setting it. This is causing about 60+ lint issues.
+ As this is part of Chart.js's API it makes sense to disable the check here.
+*/
+import { onMounted, ref, shallowRef, watch } from 'vue';
+import { Chart } from 'chart.js';
+import * as Misskey from 'misskey-js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { defaultStore } from '@/store.js';
+import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
+import { chartVLine } from '@/scripts/chart-vline.js';
+import { alpha } from '@/scripts/color.js';
+import date from '@/filters/date.js';
+import bytes from '@/filters/bytes.js';
+import { initChart } from '@/scripts/init-chart.js';
+import { chartLegend } from '@/scripts/chart-legend.js';
+import MkChartLegend from '@/components/MkChartLegend.vue';
+
+initChart();
const props = withDefaults(defineProps<{
src: ChartSrc;
diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue
index 00506fb735..9a0a9fba05 100644
--- a/packages/frontend/src/components/MkClickerGame.vue
+++ b/packages/frontend/src/components/MkClickerGame.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, onMounted, onUnmounted, ref } from 'vue';
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
import * as os from '@/os.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
import * as game from '@/scripts/clicker-game.js';
import number from '@/filters/number.js';
import { claimAchievement } from '@/scripts/achievements.js';
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index 1d4c0b6366..716dd92678 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.codeBlockRoot">
- <button :class="$style.codeBlockCopyButton" class="_button" @click="copy">
+ <button v-if="copyButton" :class="$style.codeBlockCopyButton" class="_button" @click="copy">
<i class="ti ti-copy"></i>
</button>
<Suspense>
@@ -32,12 +32,17 @@ import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-const props = defineProps<{
+const props = withDefaults(defineProps<{
code: string;
+ forceShow?: boolean;
+ copyButton?: boolean;
lang?: string;
-}>();
+}>(), {
+ copyButton: true,
+ forceShow: false,
+});
-const show = ref(!defaultStore.state.dataSaver.code);
+const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code);
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index a399acd47f..8ad653a0bf 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -216,7 +216,7 @@ onUnmounted(() => {
left: 0;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), var(--X15));
+ background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index 8ea8fa6cf3..f51fefa0c0 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
import MkMenu from './MkMenu.vue';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
import contains from '@/scripts/contains.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 54f6f39c9d..2e1e92cbdf 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -39,7 +39,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
import * as os from '@/os.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
-import { apiUrl } from '@/config.js';
+import { apiUrl } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index f16981716c..4b94bef4b6 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -43,9 +43,9 @@ export default defineComponent({
setup(props, { slots, expose }) {
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
- function getDateText(time: string) {
- const date = new Date(time).getDate();
- const month = new Date(time).getMonth() + 1;
+ function getDateText(dateInstance: Date) {
+ const date = dateInstance.getDate();
+ const month = dateInstance.getMonth() + 1;
return i18n.tsx.monthAndDay({
month: month.toString(),
day: date.toString(),
@@ -62,9 +62,16 @@ export default defineComponent({
})[0];
if (el.key == null && item.id) el.key = item.id;
+ const date = new Date(item.createdAt);
+ const nextDate = props.items[i + 1] ? new Date(props.items[i + 1].createdAt) : null;
+
if (
i !== props.items.length - 1 &&
- new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
+ nextDate != null && (
+ date.getFullYear() !== nextDate.getFullYear() ||
+ date.getMonth() !== nextDate.getMonth() ||
+ date.getDate() !== nextDate.getDate()
+ )
) {
const separator = h('div', {
class: $style['separator'],
@@ -78,12 +85,12 @@ export default defineComponent({
h('i', {
class: `ti ti-chevron-up ${$style['date-1-icon']}`,
}),
- getDateText(item.createdAt),
+ getDateText(date),
]),
h('span', {
class: $style['date-2'],
}, [
- getDateText(props.items[i + 1].createdAt),
+ getDateText(nextDate),
h('i', {
class: `ti ti-chevron-down ${$style['date-2-icon']}`,
}),
diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue
index 434fc81582..098be07a8c 100644
--- a/packages/frontend/src/components/MkDonation.vue
+++ b/packages/frontend/src/components/MkDonation.vue
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import MkButton from '@/components/MkButton.vue';
import MkLink from '@/components/MkLink.vue';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { miLocalStorage } from '@/local-storage.js';
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index d6dfaf34e5..92b3a23662 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -42,7 +42,7 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
const props = withDefaults(defineProps<{
folder: Misskey.entities.DriveFolder;
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index dbb4917069..d9ca0a72a0 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -620,7 +620,9 @@ function fetchMoreFiles() {
}
function getMenu() {
- const menu: MenuItem[] = [{
+ const menu: MenuItem[] = [];
+
+ menu.push({
type: 'switch',
text: i18n.ts.keepOriginalUploading,
ref: keepOriginal,
@@ -638,19 +640,25 @@ function getMenu() {
}, { type: 'divider' }, {
text: folder.value ? folder.value.name : i18n.ts.drive,
type: 'label',
- }, folder.value ? {
- text: i18n.ts.renameFolder,
- icon: 'ti ti-forms',
- action: () => { if (folder.value) renameFolder(folder.value); },
- } : undefined, folder.value ? {
- text: i18n.ts.deleteFolder,
- icon: 'ti ti-trash',
- action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
- } : undefined, {
+ });
+
+ if (folder.value) {
+ menu.push({
+ text: i18n.ts.renameFolder,
+ icon: 'ti ti-forms',
+ action: () => { if (folder.value) renameFolder(folder.value); },
+ }, {
+ text: i18n.ts.deleteFolder,
+ icon: 'ti ti-trash',
+ action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); },
+ });
+ }
+
+ menu.push({
text: i18n.ts.createFolder,
icon: 'ti ti-folder-plus',
action: () => { createFolder(); },
- }];
+ });
return menu;
}
diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue
index 2c47a70970..eb93aaab6e 100644
--- a/packages/frontend/src/components/MkDriveFileThumbnail.vue
+++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue
@@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div ref="thumbnail" :class="$style.root">
+<div
+ ref="thumbnail"
+ :class="[
+ $style.root,
+ { [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive },
+ ]"
+>
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
<i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i>
<i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i>
@@ -27,6 +33,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
const props = defineProps<{
file: Misskey.entities.DriveFile;
fit: 'cover' | 'contain';
+ highlightWhenSensitive?: boolean;
}>();
const is = computed(() => {
@@ -67,6 +74,18 @@ const isThumbnailAvailable = computed(() => {
overflow: clip;
}
+.sensitiveHighlight::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ border-radius: inherit;
+ box-shadow: inset 0 0 0 4px var(--warn);
+}
+
.iconSub {
position: absolute;
width: 30%;
diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
new file mode 100644
index 0000000000..c060c3a659
--- /dev/null
+++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
@@ -0,0 +1,414 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkModalWindow
+ ref="dialogEl"
+ :width="1000"
+ :height="600"
+ :scroll="false"
+ :withOkButton="false"
+ @close="cancel()"
+ @closed="$emit('closed')"
+>
+ <template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>
+
+ <div :class="$style.embedCodeGenRoot">
+ <Transition
+ mode="out-in"
+ :enterActiveClass="$style.transition_x_enterActive"
+ :leaveActiveClass="$style.transition_x_leaveActive"
+ :enterFromClass="$style.transition_x_enterFrom"
+ :leaveToClass="$style.transition_x_leaveTo"
+ >
+ <div v-if="phase === 'input'" key="input" :class="$style.embedCodeGenInputRoot">
+ <div
+ :class="$style.embedCodeGenPreviewRoot"
+ >
+ <MkLoading v-if="iframeLoading" :class="$style.embedCodeGenPreviewSpinner"/>
+ <div :class="$style.embedCodeGenPreviewWrapper">
+ <div class="_acrylic" :class="$style.embedCodeGenPreviewTitle">{{ i18n.ts.preview }}</div>
+ <div ref="resizerRootEl" :class="$style.embedCodeGenPreviewResizerRoot" inert>
+ <div
+ :class="$style.embedCodeGenPreviewResizer"
+ :style="{ transform: iframeStyle }"
+ >
+ <iframe
+ ref="iframeEl"
+ :src="embedPreviewUrl"
+ :class="$style.embedCodeGenPreviewIframe"
+ :style="{ height: `${iframeHeight}px` }"
+ @load="iframeOnLoad"
+ ></iframe>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div :class="$style.embedCodeGenSettings" class="_gaps">
+ <MkInput v-if="isEmbedWithScrollbar" v-model="maxHeight" type="number" :min="0">
+ <template #label>{{ i18n.ts._embedCodeGen.maxHeight }}</template>
+ <template #suffix>px</template>
+ <template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
+ </MkInput>
+ <MkSelect v-model="colorMode">
+ <template #label>{{ i18n.ts.theme }}</template>
+ <option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
+ <option value="light">{{ i18n.ts.light }}</option>
+ <option value="dark">{{ i18n.ts.dark }}</option>
+ </MkSelect>
+ <MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
+ <MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
+ <MkSwitch v-model="border">{{ i18n.ts._embedCodeGen.border }}</MkSwitch>
+ <MkInfo v-if="isEmbedWithScrollbar && (!maxHeight || maxHeight <= 0)" warn>{{ i18n.ts._embedCodeGen.maxHeightWarn }}</MkInfo>
+ <MkInfo v-if="typeof maxHeight === 'number' && (maxHeight <= 0 || maxHeight > 700)">{{ i18n.ts._embedCodeGen.previewIsNotActual }}</MkInfo>
+ <div class="_buttons">
+ <MkButton :disabled="iframeLoading" @click="applyToPreview">{{ i18n.ts._embedCodeGen.applyToPreview }}</MkButton>
+ <MkButton :disabled="iframeLoading" primary @click="generate">{{ i18n.ts._embedCodeGen.generateCode }} <i class="ti ti-arrow-right"></i></MkButton>
+ </div>
+ </div>
+ </div>
+ <div v-else-if="phase === 'result'" key="result" :class="$style.embedCodeGenResultRoot">
+ <div :class="$style.embedCodeGenResultWrapper" class="_gaps">
+ <div class="_gaps_s">
+ <div :class="$style.embedCodeGenResultHeadingIcon"><i class="ti ti-check"></i></div>
+ <div :class="$style.embedCodeGenResultHeading">{{ i18n.ts._embedCodeGen.codeGenerated }}</div>
+ <div :class="$style.embedCodeGenResultDescription">{{ i18n.ts._embedCodeGen.codeGeneratedDescription }}</div>
+ </div>
+ <div class="_gaps_s">
+ <MkCode :code="result" lang="html" :forceShow="true" :copyButton="false" :class="$style.embedCodeGenResultCode"/>
+ <MkButton :class="$style.embedCodeGenResultButtons" rounded primary @click="doCopy"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+ </div>
+ <MkButton :class="$style.embedCodeGenResultButtons" rounded transparent @click="close">{{ i18n.ts.close }}</MkButton>
+ </div>
+ </div>
+ </Transition>
+ </div>
+</MkModalWindow>
+</template>
+
+<script setup lang="ts">
+import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue';
+import { url } from '@@/js/config.js';
+import { embedRouteWithScrollbar } from '@@/js/embed-page.js';
+import type { EmbeddableEntity, EmbedParams } from '@@/js/embed-page.js';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+
+import MkInput from '@/components/MkInput.vue';
+import MkSelect from '@/components/MkSelect.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkButton from '@/components/MkButton.vue';
+
+import MkCode from '@/components/MkCode.vue';
+import MkInfo from '@/components/MkInfo.vue';
+
+import * as os from '@/os.js';
+import { i18n } from '@/i18n.js';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js';
+
+const emit = defineEmits<{
+ (ev: 'ok'): void;
+ (ev: 'cancel'): void;
+ (ev: 'closed'): void;
+}>();
+
+const props = defineProps<{
+ entity: EmbeddableEntity;
+ id: string;
+ params?: EmbedParams;
+}>();
+
+//#region Modalの制御
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+
+function cancel() {
+ emit('cancel');
+ dialogEl.value?.close();
+}
+
+function close() {
+ dialogEl.value?.close();
+}
+
+const phase = ref<'input' | 'result'>('input');
+//#endregion
+
+//#region 埋め込みURL生成・カスタマイズ
+
+// 本URL生成用params
+const paramsForUrl = computed<EmbedParams>(() => ({
+ header: header.value,
+ maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
+ colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
+ rounded: rounded.value,
+ border: border.value,
+}));
+
+// プレビュー用params(手動で更新を掛けるのでref)
+const paramsForPreview = ref<EmbedParams>(props.params ?? {});
+
+const embedPreviewUrl = computed(() => {
+ const paramClass = new URLSearchParams(normalizeEmbedParams(paramsForPreview.value));
+ if (paramClass.has('maxHeight')) {
+ const maxHeight = parseInt(paramClass.get('maxHeight')!);
+ paramClass.set('maxHeight', maxHeight === 0 ? '500' : Math.min(maxHeight, 700).toString()); // プレビューであまりにも縮小されると見づらいため、700pxまでに制限
+ }
+ return `${url}/embed/${props.entity}/${props.id}${paramClass.toString() ? '?' + paramClass.toString() : ''}`;
+});
+
+const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity));
+const header = ref(props.params?.header ?? true);
+const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500);
+
+const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
+const rounded = ref(props.params?.rounded ?? true);
+const border = ref(props.params?.border ?? true);
+
+function applyToPreview() {
+ const currentPreviewUrl = embedPreviewUrl.value;
+
+ paramsForPreview.value = {
+ header: header.value,
+ maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined,
+ colorMode: colorMode.value === 'auto' ? undefined : colorMode.value,
+ rounded: rounded.value,
+ border: border.value,
+ };
+
+ nextTick(() => {
+ if (currentPreviewUrl === embedPreviewUrl.value) {
+ // URLが変わらなくてもリロード
+ iframeEl.value?.contentWindow?.location.reload();
+ }
+ });
+}
+
+const result = ref('');
+
+function generate() {
+ result.value = getEmbedCode(`/embed/${props.entity}/${props.id}`, paramsForUrl.value);
+ phase.value = 'result';
+}
+
+function doCopy() {
+ copyToClipboard(result.value);
+ os.success();
+}
+//#endregion
+
+//#region プレビューのリサイズ
+const resizerRootEl = shallowRef<HTMLDivElement>();
+const iframeLoading = ref(true);
+const iframeEl = shallowRef<HTMLIFrameElement>();
+const iframeHeight = ref(0);
+const iframeScale = ref(1);
+const iframeStyle = computed(() => {
+ return `translate(-50%, -50%) scale(${iframeScale.value})`;
+});
+const resizeObserver = new ResizeObserver(() => {
+ calcScale();
+});
+
+function iframeOnLoad() {
+ iframeEl.value?.contentWindow?.addEventListener('beforeunload', () => {
+ iframeLoading.value = true;
+ nextTick(() => {
+ iframeHeight.value = 0;
+ iframeScale.value = 1;
+ });
+ });
+}
+
+function windowEventHandler(event: MessageEvent) {
+ if (event.source !== iframeEl.value?.contentWindow) {
+ return;
+ }
+ if (event.data.type === 'misskey:embed:ready') {
+ iframeEl.value!.contentWindow?.postMessage({
+ type: 'misskey:embedParent:registerIframeId',
+ payload: {
+ iframeId: 'embedCodeGen', // 同じタイミングで複数のembed iframeがある際の区別用なのでここではなんでもいい
+ },
+ });
+ }
+ if (event.data.type === 'misskey:embed:changeHeight') {
+ iframeHeight.value = event.data.payload.height;
+ nextTick(() => {
+ calcScale();
+ iframeLoading.value = false; // 初回の高さ変更まで待つ
+ });
+ }
+}
+
+function calcScale() {
+ if (!resizerRootEl.value) return;
+ const previewWidth = resizerRootEl.value.clientWidth - 40; // 左右の余白 20pxずつ
+ const previewHeight = resizerRootEl.value.clientHeight - 40; // 上下の余白 20pxずつ
+ const iframeWidth = 500;
+ const scale = Math.min(previewWidth / iframeWidth, previewHeight / iframeHeight.value, 1); // 拡大はしないので1を上限に
+ iframeScale.value = scale;
+}
+
+onMounted(() => {
+ window.addEventListener('message', windowEventHandler);
+ if (!resizerRootEl.value) return;
+ resizeObserver.observe(resizerRootEl.value);
+});
+
+function reset() {
+ window.removeEventListener('message', windowEventHandler);
+ resizeObserver.disconnect();
+
+ // プレビューのリセット
+ iframeHeight.value = 0;
+ iframeScale.value = 1;
+ iframeLoading.value = true;
+ result.value = '';
+ phase.value = 'input';
+}
+
+onDeactivated(() => {
+ reset();
+});
+
+onUnmounted(() => {
+ reset();
+});
+//#endregion
+</script>
+
+<style module>
+.transition_x_enterActive,
+.transition_x_leaveActive {
+ transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
+}
+.transition_x_enterFrom {
+ opacity: 0;
+ transform: translateX(50px);
+}
+.transition_x_leaveTo {
+ opacity: 0;
+ transform: translateX(-50px);
+}
+
+.embedCodeGenRoot {
+ container-type: inline-size;
+ height: 100%;
+}
+
+.embedCodeGenInputRoot {
+ height: 100%;
+ display: grid;
+ grid-template-columns: 1fr 400px;
+}
+
+.embedCodeGenPreviewRoot {
+ position: relative;
+ background-color: var(--bg);
+ background-size: auto auto;
+ background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--panel) 6px, var(--panel) 12px);
+ cursor: not-allowed;
+}
+
+.embedCodeGenPreviewWrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ pointer-events: none;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+.embedCodeGenPreviewTitle {
+ position: absolute;
+ z-index: 100;
+ top: 8px;
+ left: 8px;
+ padding: 6px 10px;
+ border-radius: 6px;
+ font-size: 85%;
+}
+
+.embedCodeGenPreviewSpinner {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ user-select: none;
+ -webkit-user-drag: none;
+}
+
+.embedCodeGenPreviewResizerRoot {
+ position: relative;
+ flex: 1 0;
+}
+
+.embedCodeGenPreviewResizer {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+}
+
+.embedCodeGenPreviewIframe {
+ display: block;
+ border: none;
+ width: 500px;
+ color-scheme: light dark;
+}
+
+.embedCodeGenSettings {
+ padding: 24px;
+ overflow-y: scroll;
+}
+
+.embedCodeGenResultRoot {
+ box-sizing: border-box;
+ padding: 24px;
+ height: 100%;
+ max-width: 700px;
+ margin: 0 auto;
+ display: flex;
+ align-items: center;
+}
+
+.embedCodeGenResultHeading {
+ text-align: center;
+ font-size: 1.2em;
+}
+
+.embedCodeGenResultHeadingIcon {
+ margin: 0 auto;
+ background-color: var(--accentedBg);
+ color: var(--accent);
+ text-align: center;
+ height: 64px;
+ width: 64px;
+ font-size: 24px;
+ line-height: 64px;
+ border-radius: 50%;
+}
+
+.embedCodeGenResultDescription {
+ text-align: center;
+ white-space: pre-wrap;
+}
+
+.embedCodeGenResultWrapper,
+.embedCodeGenResultCode {
+ width: 100%;
+}
+
+.embedCodeGenResultButtons {
+ margin: 0 auto;
+}
+
+@container (max-width: 800px) {
+ .embedCodeGenInputRoot {
+ grid-template-columns: 1fr;
+ grid-template-rows: 1fr 1fr;
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index c13164c296..fca7aa2f4e 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, Ref } from 'vue';
-import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
+import { CustomEmojiFolderTree, getEmojiName } from '@@/js/emojilist.js';
import { i18n } from '@/i18n.js';
import { customEmojis } from '@/custom-emojis.js';
import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 4a3ed69f47..3bad8da06f 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -117,7 +117,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
-import XSection from '@/components/MkEmojiPicker.section.vue';
import {
emojilist,
emojiCharByCategory,
@@ -126,7 +125,8 @@ import {
getEmojiName,
CustomEmojiFolderTree,
getUnicodeEmoji,
-} from '@/scripts/emojilist.js';
+} from '@@/js/emojilist.js';
+import XSection from '@/components/MkEmojiPicker.section.vue';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import * as os from '@/os.js';
import { isTouchUsing } from '@/scripts/touch.js';
@@ -611,6 +611,7 @@ defineExpose({
width: auto;
height: auto;
min-width: 0;
+ padding: 0;
&:disabled {
cursor: not-allowed;
@@ -717,7 +718,7 @@ defineExpose({
> .item {
position: relative;
- padding: 0;
+ padding: 0 3px;
width: var(--eachSize);
height: var(--eachSize);
contain: strict;
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index 7e1ffbfa9e..21c712b441 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="modal"
v-slot="{ type, maxHeight }"
:zPriority="'middle'"
- :preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
+ :preferType="defaultStore.state.emojiPickerStyle"
:hasInteractionWithOtherFocusTrappedEls="true"
:transparentBg="true"
:manualShowing="manualShowing"
diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue
index 30822ef655..13295c455b 100644
--- a/packages/frontend/src/components/MkFileListForAdmin.vue
+++ b/packages/frontend/src/components/MkFileListForAdmin.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
class="file _button"
>
<div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div>
- <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
+ <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain" :highlightWhenSensitive="true"/>
<div v-if="viewMode === 'list'" class="body">
<div>
<small style="opacity: 0.7;">{{ file.name }}</small>
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index f805be7b57..a5f3069d45 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -41,6 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :marginMin="14" :marginMax="22">
<slot></slot>
</MkSpacer>
+ <div v-if="$slots.footer" :class="$style.footer">
+ <slot name="footer"></slot>
+ </div>
</div>
</KeepAlive>
</Transition>
@@ -136,7 +139,7 @@ onMounted(() => {
width: 100%;
box-sizing: border-box;
padding: 9px 12px 9px 12px;
- background: var(--buttonBg);
+ background: var(--folderHeaderBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
border-radius: 6px;
@@ -144,7 +147,7 @@ onMounted(() => {
&:hover {
text-decoration: none;
- background: var(--buttonHoverBg);
+ background: var(--folderHeaderHoverBg);
}
&:focus-within {
@@ -153,7 +156,7 @@ onMounted(() => {
&.active {
color: var(--accent);
- background: var(--buttonHoverBg);
+ background: var(--folderHeaderHoverBg);
}
&.opened {
@@ -224,4 +227,18 @@ onMounted(() => {
background: var(--bg);
}
}
+
+.footer {
+ position: sticky !important;
+ z-index: 1;
+ bottom: var(--stickyBottom, 0px);
+ left: 0;
+ padding: 12px;
+ background: var(--acrylicBg);
+ -webkit-backdrop-filter: var(--blur, blur(15px));
+ backdrop-filter: var(--blur, blur(15px));
+ background-size: auto auto;
+ background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--panel) 5px, var(--panel) 10px);
+ border-radius: 0 0 6px 6px;
+}
</style>
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index d8ac8024b4..0de52906ed 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
</template>
<template v-else-if="isFollowing">
- <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
+ <span v-if="full" :class="$style.text">{{ i18n.ts.youFollowing }}</span><i class="ti ti-minus"></i>
</template>
<template v-else-if="!isFollowing && user.isLocked">
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
@@ -43,7 +43,7 @@ import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { pleaseLogin } from '@/scripts/please-login.js';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue
new file mode 100644
index 0000000000..1e88d59d8e
--- /dev/null
+++ b/packages/frontend/src/components/MkFormFooter.vue
@@ -0,0 +1,49 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root">
+ <div :class="$style.text">{{ i18n.tsx.thereAreNChanges({ n: form.modifiedCount.value }) }}</div>
+ <div style="margin-left: auto;" class="_buttons">
+ <MkButton danger rounded @click="form.discard"><i class="ti ti-x"></i> {{ i18n.ts.discard }}</MkButton>
+ <MkButton primary rounded @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import MkButton from './MkButton.vue';
+import { i18n } from '@/i18n.js';
+
+const props = defineProps<{
+ form: {
+ modifiedCount: {
+ value: number;
+ };
+ discard: () => void;
+ save: () => void;
+ };
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+ display: flex;
+ align-items: center;
+}
+
+.text {
+ color: var(--warn);
+ font-size: 90%;
+ animation: modified-blink 2s infinite;
+}
+
+@keyframes modified-blink {
+ 0% { opacity: 1; }
+ 50% { opacity: 0.5; }
+ 100% { opacity: 1; }
+}
+</style>
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 8d301f16bd..c04d0864fb 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts">
import DrawBlurhash from '@/workers/draw-blurhash?worker';
import TestWebGL2 from '@/workers/test-webgl2?worker';
-import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js';
-import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js';
+import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js';
+import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
// テスト環境で Web Worker インスタンスは作成できない
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index e695564f92..4c2fc1ba00 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
import { i18n } from '@/i18n.js';
import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue
index 82c82199b5..fae22baa3f 100644
--- a/packages/frontend/src/components/MkInstanceTicker.vue
+++ b/packages/frontend/src/components/MkInstanceTicker.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed } from 'vue';
-import { instanceName } from '@/config.js';
+import { instanceName } from '@@/js/config.js';
import { instance as Instance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue
index de51a98789..4aee64f78e 100644
--- a/packages/frontend/src/components/MkInviteCode.vue
+++ b/packages/frontend/src/components/MkInviteCode.vue
@@ -11,8 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span>
<span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span>
</template>
+ <template #footer>
+ <div class="_buttons">
+ <MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+ <MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+ </div>
+ </template>
- <div class="_gaps_s" :class="$style.root">
+ <div :class="$style.root">
<div :class="$style.items">
<div>
<div :class="$style.label">{{ i18n.ts.invitationCode }}</div>
@@ -49,10 +55,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div><MkTime :time="invite.createdAt" mode="absolute"/></div>
</div>
</div>
- <div :class="$style.buttons">
- <MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
- <MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
- </div>
</div>
</MkFolder>
</template>
@@ -121,9 +123,4 @@ function copyInviteCode() {
width: var(--height);
height: var(--height);
}
-
-.buttons {
- display: flex;
- gap: 8px;
-}
</style>
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index e842ec2d6e..bda2161eb8 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
-import { url as local } from '@/config.js';
+import { url as local } from '@@/js/config.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js';
import { isEnabledUrlPreview } from '@/instance.js';
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index a080550ddf..b41705d5e6 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -172,9 +172,7 @@ async function show() {
const menuShowing = ref(false);
function showMenu(ev: MouseEvent) {
- let menu: MenuItem[] = [];
-
- menu = [
+ const menu: MenuItem[] = [
// TODO: 再生キューに追加
{
type: 'switch',
@@ -222,7 +220,7 @@ function showMenu(ev: MouseEvent) {
menu.push({
type: 'divider',
}, {
- type: 'link' as const,
+ type: 'link',
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
to: `/my/drive/file/${props.audio.id}`,
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 0d1409e2c8..0c5c8fd9de 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
+<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" @click="onclick">
<component
:is="disableImageLink ? 'div' : 'a'"
v-bind="disableImageLink ? {
@@ -53,6 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
+import type { MenuItem } from '@/types/menu.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import bytes from '@/filters/bytes.js';
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
@@ -74,7 +75,6 @@ const props = withDefaults(defineProps<{
});
const hide = ref(true);
-const darkMode = ref<boolean>(defaultStore.state.darkMode);
const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
? props.image.url
@@ -111,27 +111,39 @@ watch(() => props.image, () => {
});
function showMenu(ev: MouseEvent) {
- os.popupMenu([{
+ const menuItems: MenuItem[] = [];
+
+ menuItems.push({
text: i18n.ts.hide,
icon: 'ti ti-eye-off',
action: () => {
hide.value = true;
},
- }, ...(iAmModerator ? [{
- text: i18n.ts.markAsSensitive,
- icon: 'ti ti-eye-exclamation',
- danger: true,
- action: () => {
- os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
- },
- }] : []), ...($i?.id === props.image.userId ? [{
- type: 'divider' as const,
- }, {
- type: 'link' as const,
- text: i18n.ts._fileViewer.title,
- icon: 'ti ti-info-circle',
- to: `/my/drive/file/${props.image.id}`,
- }] : [])], ev.currentTarget ?? ev.target);
+ });
+
+ if (iAmModerator) {
+ menuItems.push({
+ text: i18n.ts.markAsSensitive,
+ icon: 'ti ti-eye-exclamation',
+ danger: true,
+ action: () => {
+ os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
+ },
+ });
+ }
+
+ if ($i?.id === props.image.userId) {
+ menuItems.push({
+ type: 'divider',
+ }, {
+ type: 'link',
+ text: i18n.ts._fileViewer.title,
+ icon: 'ti ti-info-circle',
+ to: `/my/drive/file/${props.image.id}`,
+ });
+ }
+
+ os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
</script>
@@ -196,10 +208,19 @@ function showMenu(ev: MouseEvent) {
position: relative;
//box-shadow: 0 0 0 1px var(--divider) inset;
background: var(--bg);
- background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
background-size: 16px 16px;
}
+html[data-color-scheme=dark] .visible {
+ --c: rgb(255 255 255 / 2%);
+ background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
+}
+
+html[data-color-scheme=light] .visible {
+ --c: rgb(0 0 0 / 2%);
+ background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
+}
+
.menu {
display: block;
position: absolute;
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 2300802dcf..4a4a99be25 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -37,7 +37,7 @@ import XBanner from '@/components/MkMediaBanner.vue';
import XImage from '@/components/MkMediaImage.vue';
import XVideo from '@/components/MkMediaVideo.vue';
import * as os from '@/os.js';
-import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
+import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js';
import { defaultStore } from '@/store.js';
import { focusParent } from '@/scripts/focus.js';
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 7c5a365148..1b1915e6c8 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -192,9 +192,7 @@ async function show() {
const menuShowing = ref(false);
function showMenu(ev: MouseEvent) {
- let menu: MenuItem[] = [];
-
- menu = [
+ const menu: MenuItem[] = [
// TODO: 再生キューに追加
{
type: 'switch',
@@ -247,7 +245,7 @@ function showMenu(ev: MouseEvent) {
menu.push({
type: 'divider',
}, {
- type: 'link' as const,
+ type: 'link',
text: i18n.ts._fileViewer.title,
icon: 'ti ti-info-circle',
to: `/my/drive/file/${props.video.id}`,
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index bfb49a416e..9d9661e816 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { toUnicode } from 'punycode';
import { computed } from 'vue';
import tinycolor from 'tinycolor2';
-import { host as localHost } from '@/config.js';
+import { host as localHost } from '@@/js/config.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue
index 235790556c..086573ba6d 100644
--- a/packages/frontend/src/components/MkMenu.child.vue
+++ b/packages/frontend/src/components/MkMenu.child.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue';
import MkMenu from './MkMenu.vue';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
const props = defineProps<{
items: MenuItem[];
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index c0728d56fa..890b99fcc2 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -4,17 +4,22 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div role="menu" @focusin.passive.stop="() => {}">
+<div
+ role="menu"
+ :class="{
+ [$style.root]: true,
+ [$style.center]: align === 'center',
+ [$style.big]: big,
+ [$style.asDrawer]: asDrawer,
+ }"
+ @focusin.passive.stop="() => {}"
+>
<div
ref="itemsEl"
v-hotkey="keymap"
tabindex="0"
class="_popup _shadow"
- :class="{
- [$style.root]: true,
- [$style.center]: align === 'center',
- [$style.asDrawer]: asDrawer,
- }"
+ :class="$style.menu"
:style="{
width: (width && !asDrawer) ? `${width}px` : '',
maxHeight: maxHeight ? `min(${maxHeight}px, calc(100dvh - 32px))` : 'calc(100dvh - 32px)',
@@ -200,6 +205,8 @@ const emit = defineEmits<{
(ev: 'hide'): void;
}>();
+const big = isTouchUsing;
+
const isNestingMenu = inject<boolean>('isNestingMenu', false);
const itemsEl = shallowRef<HTMLElement>();
@@ -297,6 +304,8 @@ async function showRadioOptions(item: MenuRadio, ev: Event) {
}
async function showChildren(item: MenuParent, ev: Event) {
+ ev.stopPropagation();
+
const children: MenuItem[] = await (async () => {
if (childrenCache.has(item)) {
return childrenCache.get(item)!;
@@ -418,48 +427,67 @@ onBeforeUnmount(() => {
<style lang="scss" module>
.root {
- padding: 8px 0;
- box-sizing: border-box;
- max-width: 100vw;
- min-width: 200px;
- overflow: auto;
- overscroll-behavior: contain;
-
- &:focus-visible {
- outline: none;
+ &.center {
+ > .menu {
+ > .item {
+ text-align: center;
+ }
+ }
}
- &.center {
- > .item {
- text-align: center;
+ &.big:not(.asDrawer) {
+ > .menu {
+ > .item {
+ padding: 6px 20px;
+ font-size: 1em;
+ line-height: 24px;
+ }
}
}
&.asDrawer {
- padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
- width: 100%;
- border-radius: 24px;
- border-bottom-right-radius: 0;
- border-bottom-left-radius: 0;
+ max-width: 600px;
+ margin: auto;
- > .item {
- font-size: 1em;
- padding: 12px 24px;
+ > .menu {
+ padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
+ width: 100%;
+ border-radius: 24px;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
- &::before {
- width: calc(100% - 24px);
- border-radius: 12px;
+ > .item {
+ font-size: 1em;
+ padding: 12px 24px;
+
+ &::before {
+ width: calc(100% - 24px);
+ border-radius: 12px;
+ }
+
+ > .icon {
+ margin-right: 14px;
+ width: 24px;
+ }
}
- > .icon {
- margin-right: 14px;
- width: 24px;
+ > .divider {
+ margin: 12px 0;
}
}
+ }
+}
- > .divider {
- margin: 12px 0;
- }
+.menu {
+ padding: 8px 0;
+ box-sizing: border-box;
+ max-width: 100vw;
+ min-width: 200px;
+ overflow: auto;
+ overscroll-behavior: contain;
+
+ &:focus-visible {
+ outline: none;
}
}
diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue
index f2f2bf47a8..1b6f6cef31 100644
--- a/packages/frontend/src/components/MkMiniChart.vue
+++ b/packages/frontend/src/components/MkMiniChart.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { watch, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import tinycolor from 'tinycolor2';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
const props = defineProps<{
src: number[];
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index f8032f9b43..c766a33823 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -106,7 +106,7 @@ const zIndex = os.claimZIndex(props.zPriority);
const useSendAnime = ref(false);
const type = computed<ModalTypes>(() => {
if (props.preferType === 'auto') {
- if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
+ if ((defaultStore.state.menuStyle === 'drawer') || (defaultStore.state.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) {
return 'drawer';
} else {
return props.src != null ? 'popup' : 'dialog';
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index c3c7812036..f26959888b 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -94,12 +94,12 @@ defineExpose({
--root-margin: 24px;
+ --headerHeight: 46px;
+ --headerHeightNarrow: 42px;
+
@media (max-width: 500px) {
--root-margin: 16px;
}
-
- --headerHeight: 46px;
- --headerHeightNarrow: 42px;
}
.header {
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 32d1cc5640..e8ff743bf2 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -53,7 +53,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
<div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw">
- <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
+ <Mfm
+ v-if="appearNote.cw != ''"
+ :text="appearNote.cw"
+ :author="appearNote.user"
+ :nyaize="'respect'"
+ :enableEmojiMenu="true"
+ :enableEmojiMenuReaction="true"
+ />
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;"/>
</p>
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
@@ -119,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i>
</button>
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
- <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
+ <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>
@@ -163,6 +170,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
+import { isLink } from '@@/js/is-link.js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
@@ -192,11 +200,11 @@ import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { getNoteSummary } from '@/scripts/get-note-summary.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
-import { shouldCollapsed } from '@/scripts/collapsed.js';
-import { host } from '@/config.js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import { host } from '@@/js/config.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { type Keymap } from '@/scripts/hotkey.js';
import { focusPrev, focusNext } from '@/scripts/focus.js';
@@ -506,16 +514,6 @@ function onContextmenu(ev: MouseEvent): void {
return;
}
- const isLink = (el: HTMLElement): boolean => {
- if (el.tagName === 'A') return true;
- // 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
- if (el.tagName === 'AUDIO') return true;
- if (el.parentElement) {
- return isLink(el.parentElement);
- }
- return false;
- };
-
if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== '') return;
@@ -627,7 +625,7 @@ function emitUpdReaction(emoji: string, delta: number) {
// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
//content-visibility: auto;
- //contain-intrinsic-size: 0 128px;
+ //contain-intrinsic-size: 0 128px;
&:focus-visible {
outline: none;
@@ -859,7 +857,7 @@ function emitUpdReaction(emoji: string, delta: number) {
z-index: 2;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), var(--X15));
+ background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
&:hover > .collapsedLabel {
background: var(--panelHighlight);
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 2b7d2afa04..bdb800b32a 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -68,7 +68,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</header>
<div :class="$style.noteContent">
<p v-if="appearNote.cw != null" :class="$style.cw">
- <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
+ <Mfm
+ v-if="appearNote.cw != ''"
+ :text="appearNote.cw"
+ :author="appearNote.user"
+ :nyaize="'respect'"
+ :enableEmojiMenu="true"
+ :enableEmojiMenuReaction="true"
+ />
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/>
</p>
<div v-show="appearNote.cw == null || showContent">
@@ -128,7 +135,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i>
</button>
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
- <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
+ <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>
@@ -199,6 +206,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, inject, onMounted, provide, ref, shallowRef } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
+import { isLink } from '@@/js/is-link.js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@@ -222,7 +230,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js';
@@ -468,14 +476,6 @@ function toggleReact() {
}
function onContextmenu(ev: MouseEvent): void {
- const isLink = (el: HTMLElement): boolean => {
- if (el.tagName === 'A') return true;
- if (el.parentElement) {
- return isLink(el.parentElement);
- }
- return false;
- };
-
if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== '') return;
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index be5829d92f..888c570571 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -5,14 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<header :class="$style.root">
- <div v-if="mock" :class="$style.name">
- <MkUserName :user="note.user"/>
- </div>
- <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
- <MkUserName :user="note.user"/>
- </MkA>
- <div v-if="note.user.isBot" :class="$style.isBot">bot</div>
- <div :class="$style.username"><MkAcct :user="note.user"/></div>
+ <component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.5" style="min-width: 0;">
+ <div style="display: flex; white-space: nowrap; align-items: baseline;">
+ <div v-if="mock" :class="$style.name">
+ <MkUserName :user="note.user"/>
+ </div>
+ <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
+ <MkUserName :user="note.user"/>
+ </MkA>
+ <div v-if="note.user.isBot" :class="$style.isBot">bot</div>
+ <div :class="$style.username"><MkAcct :user="note.user"/></div>
+ </div>
+ </component>
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
</div>
@@ -40,6 +44,7 @@ import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import { notePage } from '@/filters/note.js';
import { userPage } from '@/filters/user.js';
+import { defaultStore } from '@/store.js';
defineProps<{
note: Misskey.entities.Note;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index ee65743574..12c2974de4 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -13,7 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
- <img v-else-if="'icon' in notification" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
+ <MkAvatar v-else-if="notification.type === 'exportCompleted'" :class="$style.icon" :user="$i" link preview/>
+ <img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
<div
:class="[$style.subIcon, {
[$style.t_follow]: notification.type === 'follow',
@@ -25,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
+ [$style.t_exportCompleted]: notification.type === 'exportCompleted',
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
}]"
>
@@ -37,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
+ <i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
<template v-else-if="notification.type === 'roleAssigned'">
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
<i v-else class="ti ti-badges"></i>
@@ -46,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:withTooltip="true"
:reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')"
:noStyle="true"
- style="width: 100%; height: 100%;"
+ style="width: 100%; height: 100% !important; object-fit: contain;"
/>
</div>
</div>
@@ -57,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
+ <span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
@@ -98,10 +102,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
</MkA>
+ <MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
+ {{ i18n.ts.showFile }}
+ </MkA>
<template v-else-if="notification.type === 'follow'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
</template>
- <span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
+ <template v-else-if="notification.type === 'followRequestAccepted'">
+ <div :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</div>
+ <div v-if="notification.message" :class="$style.text" style="opacity: 0.6; font-style: oblique;">
+ <i class="ti ti-quote" :class="$style.quote"></i>
+ <span>{{ notification.message }}</span>
+ <i class="ti ti-quote" :class="$style.quote"></i>
+ </div>
+ </template>
<template v-else-if="notification.type === 'receiveFollowRequest'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
@@ -122,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:withTooltip="true"
:reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')"
:noStyle="true"
- style="width: 100%; height: 100%;"
+ style="width: 100%; height: 100% !important; object-fit: contain;"
/>
</div>
</div>
@@ -161,6 +175,20 @@ const props = withDefaults(defineProps<{
full: false,
});
+type ExportCompletedNotification = Misskey.entities.Notification & { type: 'exportCompleted' };
+
+const exportEntityName = {
+ antenna: i18n.ts.antennas,
+ blocking: i18n.ts.blockedUsers,
+ clip: i18n.ts.clips,
+ customEmoji: i18n.ts.customEmojis,
+ favorite: i18n.ts.favorites,
+ following: i18n.ts.following,
+ muting: i18n.ts.mutedUsers,
+ note: i18n.ts.notes,
+ userList: i18n.ts.lists,
+} as const satisfies Record<ExportCompletedNotification['exportedEntity'], string>;
+
const followRequestDone = ref(false);
const acceptFollowRequest = () => {
@@ -190,6 +218,14 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
overflow-wrap: break-word;
display: flex;
contain: content;
+
+ --eventFollow: #36aed2;
+ --eventRenote: #36d298;
+ --eventReply: #007aff;
+ --eventReactionHeart: var(--love);
+ --eventReaction: #e99a0b;
+ --eventAchievement: #cb9a11;
+ --eventOther: #88a6b7;
}
.head {
@@ -298,6 +334,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none;
}
+.t_exportCompleted {
+ padding: 3px;
+ background: var(--eventOther);
+ pointer-events: none;
+}
+
.t_roleAssigned {
padding: 3px;
background: var(--eventOther);
diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue
index 71b38d99ed..47a9c79e45 100644
--- a/packages/frontend/src/components/MkNotificationSelectWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue
@@ -35,7 +35,7 @@ import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
-import { notificationTypes } from '@/const.js';
+import { notificationTypes } from '@@/js/const.js';
import { i18n } from '@/i18n.js';
type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 389987338d..d67616e6b2 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -31,7 +31,7 @@ import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkNote from '@/components/MkNote.vue';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
-import { notificationTypes } from '@/const.js';
+import { notificationTypes } from '@@/js/const.js';
import { infoImageUrl } from '@/instance.js';
import { defaultStore } from '@/store.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index 100a025653..ee1f15c189 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -62,7 +62,7 @@ onUnmounted(() => {
left: 0;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), var(--X15));
+ background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index bd86b01591..2b993ab12f 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -34,13 +34,13 @@ import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { url } from '@/config.js';
+import { url } from '@@/js/config.js';
import { useScrollPositionManager } from '@/nirax.js';
import { i18n } from '@/i18n.js';
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { openingWindowsCount } from '@/os.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import { getScrollContainer } from '@/scripts/scroll.js';
+import { getScrollContainer } from '@@/js/scroll.js';
import { useRouterFactory } from '@/router/supplier.js';
import { mainRouter } from '@/router/main.js';
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 62a85389ad..ea299c319e 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -45,10 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts">
import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
+import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
+import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
-import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js';
-import { useDocumentVisibility } from '@/scripts/use-document-visibility.js';
import { defaultStore } from '@/store.js';
import { MisskeyEntity } from '@/types/date-separated-list.js';
import { i18n } from '@/i18n.js';
@@ -125,8 +125,6 @@ const items = ref<MisskeyEntityMap>(new Map());
*/
const queue = ref<MisskeyEntityMap>(new Map());
-const offset = ref(0);
-
/**
* 初期化中かどうか(trueならMkLoadingで全て隠す)
*/
@@ -179,7 +177,9 @@ watch([backed, contentEl], () => {
if (!backed.value) {
if (!contentEl.value) return;
- scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE);
+ scrollRemove.value = props.pagination.reversed
+ ? onScrollBottom(contentEl.value, executeQueue, TOLERANCE)
+ : onScrollTop(contentEl.value, (topVisible) => { if (topVisible) executeQueue(); }, TOLERANCE);
} else {
if (scrollRemove.value) scrollRemove.value();
scrollRemove.value = null;
@@ -223,7 +223,6 @@ async function init(): Promise<void> {
more.value = true;
}
- offset.value = res.length;
error.value = false;
fetching.value = false;
}, err => {
@@ -244,7 +243,7 @@ const fetchMore = async (): Promise<void> => {
...params,
limit: SECOND_FETCH_LIMIT,
...(props.pagination.offsetMode ? {
- offset: offset.value,
+ offset: items.value.size,
} : {
untilId: Array.from(items.value.keys()).at(-1),
}),
@@ -294,7 +293,6 @@ const fetchMore = async (): Promise<void> => {
moreFetching.value = false;
}
}
- offset.value += res.length;
}, err => {
moreFetching.value = false;
});
@@ -308,7 +306,7 @@ const fetchMoreAhead = async (): Promise<void> => {
...params,
limit: SECOND_FETCH_LIMIT,
...(props.pagination.offsetMode ? {
- offset: offset.value,
+ offset: items.value.size,
} : {
sinceId: Array.from(items.value.keys()).at(-1),
}),
@@ -320,7 +318,6 @@ const fetchMoreAhead = async (): Promise<void> => {
items.value = concatMapWithArray(items.value, res);
more.value = true;
}
- offset.value += res.length;
moreFetching.value = false;
}, err => {
moreFetching.value = false;
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 72bd8f4f6c..e1d5db2730 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { sum } from '@/scripts/array.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
-import { host } from '@/config.js';
-import { useInterval } from '@/scripts/use-interval.js';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
+import { host } from '@@/js/config.js';
+import { useInterval } from '@@/js/use-interval.js';
const props = defineProps<{
noteId: string;
@@ -83,10 +83,10 @@ if (props.poll.expiresAt) {
}
const vote = async (id) => {
- pleaseLogin(undefined, pleaseLoginContext.value);
-
if (props.readOnly || closed.value || isVoted.value) return;
+ pleaseLogin(undefined, pleaseLoginContext.value);
+
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.voteConfirm({ choice: props.poll.choices[id].text }),
@@ -145,7 +145,7 @@ const vote = async (id) => {
.done {
.choice {
- cursor: default;
+ cursor: initial;
}
}
</style>
diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue
index 8a0c7b1e54..26c251a8d2 100644
--- a/packages/frontend/src/components/MkPopupMenu.vue
+++ b/packages/frontend/src/components/MkPopupMenu.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, shallowRef } from 'vue';
import MkModal from './MkModal.vue';
import MkMenu from './MkMenu.vue';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
defineProps<{
items: MenuItem[];
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 51ec941c97..471ffdd896 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -105,11 +105,11 @@ import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
import { toASCII } from 'punycode/';
+import { host, url } from '@@/js/config.js';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
-import { host, url } from '@/config.js';
import { erase, unique } from '@/scripts/array.js';
import { extractMentions } from '@/scripts/extract-mentions.js';
import { formatTimeString } from '@/scripts/format-time-string.js';
@@ -245,7 +245,7 @@ const submitText = computed((): string => {
});
const textLength = computed((): number => {
- return (text.value + imeText.value).trim().length;
+ return (text.value + imeText.value).length;
});
const maxTextLength = computed((): number => {
@@ -1128,13 +1128,13 @@ defineExpose({
&:not(:disabled):hover {
> .inner {
- background: linear-gradient(90deg, var(--X8), var(--X8));
+ background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
}
&:not(:disabled):active {
> .inner {
- background: linear-gradient(90deg, var(--X8), var(--X8));
+ background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
}
}
@@ -1201,6 +1201,15 @@ defineExpose({
min-height: 75px;
max-height: 150px;
overflow: auto;
+ background-size: auto auto;
+}
+
+html[data-color-scheme=dark] .preview {
+ background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, #0004 5px, #0004 10px);
+}
+
+html[data-color-scheme=light] .preview {
+ background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, #00000005 5px, #00000005 10px);
}
.targetNote {
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index 8854babb6b..80b75a0875 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -26,6 +26,7 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
+import type { MenuItem } from '@/types/menu.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -63,7 +64,7 @@ async function detachAndDeleteMedia(file: Misskey.entities.DriveFile) {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('driveFileDeleteConfirm', { name: file.name }),
+ text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }),
});
if (canceled) return;
@@ -136,7 +137,10 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
if (menuShowing) return;
const isImage = file.type.startsWith('image/');
- os.popupMenu([{
+
+ const menuItems: MenuItem[] = [];
+
+ menuItems.push({
text: i18n.ts.renameFile,
icon: 'ti ti-forms',
action: () => { rename(file); },
@@ -148,11 +152,17 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
text: i18n.ts.describeFile,
icon: 'ti ti-text-caption',
action: () => { describe(file); },
- }, ...isImage ? [{
- text: i18n.ts.cropImage,
- icon: 'ti ti-crop',
- action: () : void => { crop(file); },
- }] : [], {
+ });
+
+ if (isImage) {
+ menuItems.push({
+ text: i18n.ts.cropImage,
+ icon: 'ti ti-crop',
+ action: () : void => { crop(file); },
+ });
+ }
+
+ menuItems.push({
type: 'divider',
}, {
text: i18n.ts.attachCancel,
@@ -163,7 +173,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
icon: 'ti ti-trash',
danger: true,
action: () => { detachAndDeleteMedia(file); },
- }], ev.currentTarget ?? ev.target).then(() => menuShowing = false);
+ });
+
+ os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => menuShowing = false);
menuShowing = true;
}
</script>
diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue
index 649dee2fdb..6efd99d14b 100644
--- a/packages/frontend/src/components/MkPreview.vue
+++ b/packages/frontend/src/components/MkPreview.vue
@@ -42,7 +42,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkRadio from '@/components/MkRadio.vue';
import * as os from '@/os.js';
-import * as config from '@/config.js';
+import * as config from '@@/js/config.js';
import { $i } from '@/account.js';
const text = ref('');
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index e0d0b561be..4fb4c6fe56 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
import { i18n } from '@/i18n.js';
-import { getScrollContainer } from '@/scripts/scroll.js';
+import { getScrollContainer } from '@@/js/scroll.js';
import { isHorizontalSwipeSwiping } from '@/scripts/touch.js';
const SCROLL_STOP = 10;
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index 1eae642937..cfaaa67d58 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -5,7 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="timctyfi" :class="{ disabled, easing }">
- <div class="label"><slot name="label"></slot></div>
+ <div class="label">
+ <slot name="label"></slot>
+ </div>
<div v-adaptive-border class="body">
<div ref="containerEl" class="container">
<div class="track">
@@ -14,15 +16,25 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="steps && showTicks" class="ticks">
<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
</div>
- <div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
+ <div
+ ref="thumbEl"
+ class="thumb"
+ :style="{ left: thumbPosition + 'px' }"
+ @mouseenter.passive="onMouseenter"
+ @mousedown="onMousedown"
+ @touchstart="onMousedown"
+ ></div>
</div>
</div>
- <div class="caption"><slot name="caption"></slot></div>
+ <div class="caption">
+ <slot name="caption"></slot>
+ </div>
</div>
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch, shallowRef } from 'vue';
+import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
+import { isTouchUsing } from '@/scripts/touch.js';
import * as os from '@/os.js';
const props = withDefaults(defineProps<{
@@ -101,12 +113,36 @@ const steps = computed(() => {
}
});
+const tooltipForDragShowing = ref(false);
+const tooltipForHoverShowing = ref(false);
+
+function onMouseenter() {
+ if (isTouchUsing) return;
+
+ tooltipForHoverShowing.value = true;
+
+ const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
+ showing: computed(() => tooltipForHoverShowing.value && !tooltipForDragShowing.value),
+ text: computed(() => {
+ return props.textConverter(finalValue.value);
+ }),
+ targetElement: thumbEl,
+ }, {
+ closed: () => dispose(),
+ });
+
+ thumbEl.value!.addEventListener('mouseleave', () => {
+ tooltipForHoverShowing.value = false;
+ }, { once: true, passive: true });
+}
+
function onMousedown(ev: MouseEvent | TouchEvent) {
ev.preventDefault();
- const tooltipShowing = ref(true);
+ tooltipForDragShowing.value = true;
+
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
- showing: tooltipShowing,
+ showing: tooltipForDragShowing,
text: computed(() => {
return props.textConverter(finalValue.value);
}),
@@ -137,7 +173,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
const onMouseup = () => {
document.head.removeChild(style);
- tooltipShowing.value = false;
+ tooltipForDragShowing.value = false;
window.removeEventListener('mousemove', onDrag);
window.removeEventListener('touchmove', onDrag);
window.removeEventListener('mouseup', onMouseup);
@@ -261,12 +297,12 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
> .container {
> .track {
> .highlight {
- transition: width 0.2s cubic-bezier(0,0,0,1);
+ transition: width 0.2s cubic-bezier(0, 0, 0, 1);
}
}
> .thumb {
- transition: left 0.2s cubic-bezier(0,0,0,1);
+ transition: left 0.2s cubic-bezier(0, 0, 0, 1);
}
}
}
diff --git a/packages/frontend/src/components/MkReactionTooltip.vue b/packages/frontend/src/components/MkReactionTooltip.vue
index 15409a216a..77ca841ad0 100644
--- a/packages/frontend/src/components/MkReactionTooltip.vue
+++ b/packages/frontend/src/components/MkReactionTooltip.vue
@@ -36,6 +36,7 @@ const emit = defineEmits<{
.icon {
display: block;
width: 60px;
+ max-height: 60px;
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
margin: 0 auto;
object-fit: contain;
diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue
index 60118fadd2..8038ec7429 100644
--- a/packages/frontend/src/components/MkReactionsViewer.details.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.details.vue
@@ -23,9 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { } from 'vue';
+import { getEmojiName } from '@@/js/emojilist.js';
import MkTooltip from './MkTooltip.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
-import { getEmojiName } from '@/scripts/emojilist.js';
defineProps<{
showing: boolean;
@@ -63,6 +63,7 @@ function getReactionName(reaction: string): string {
.reactionIcon {
display: block;
width: 60px;
+ max-height: 60px;
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
object-fit: contain;
margin: 0 auto;
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 26223364ab..f42a0b3227 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, inject, onMounted, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
+import { getUnicodeEmoji } from '@@/js/emojilist.js';
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
import XDetails from '@/components/MkReactionsViewer.details.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
@@ -34,7 +35,6 @@ import { i18n } from '@/i18n.js';
import * as sound from '@/scripts/sound.js';
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
import { customEmojisMap } from '@/custom-emojis.js';
-import { getUnicodeEmoji } from '@/scripts/emojilist.js';
const props = defineProps<{
reaction: string;
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 0eba8d6a9c..343524fc82 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -44,9 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
import { i18n } from '@/i18n.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
const props = defineProps<{
modelValue: string | null;
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 781145e1bc..7942a84d66 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
- <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
+ <MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
<p>{{ i18n.ts.useSecurityKey }}</p>
- <MkButton v-if="!queryingKey" @click="queryKey">
+ <MkButton v-if="!queryingKey" @click="query2FaKey">
{{ i18n.ts.retry }}
</MkButton>
</div>
@@ -45,10 +45,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
</div>
<div class="twofa-group totp-group _gaps">
- <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
- <template #label>{{ i18n.ts.password }}</template>
- <template #prefix><i class="ti ti-lock"></i></template>
- </MkInput>
<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
@@ -57,6 +53,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
</div>
+ <div v-if="!totpLogin && usePasswordLessLogin" :class="$style.orHr">
+ <p :class="$style.orMsg">{{ i18n.ts.or }}</p>
+ </div>
+ <div v-if="!totpLogin && usePasswordLessLogin" class="twofa-group tap-group">
+ <MkButton v-if="!queryingKey" type="submit" :disabled="signing" style="margin: auto auto;" rounded large primary @click="onPasskeyLogin">
+ <i class="ti ti-device-usb" style="font-size: medium;"></i>
+ {{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }}
+ </MkButton>
+ <p v-if="queryingKey">{{ i18n.ts.useSecurityKey }}</p>
+ </div>
</div>
</form>
</template>
@@ -66,20 +72,23 @@ import { defineAsyncComponent, ref } from 'vue';
import { toUnicode } from 'punycode/';
import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+import { SigninWithPasskeyResponse } from 'misskey-js/entities.js';
+import { query, extractDomain } from '@@/js/url.js';
+import { host as configHost } from '@@/js/config.js';
+import MkDivider from './MkDivider.vue';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
-import { host as configHost } from '@/config.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
-import { query, extractDomain } from '@/scripts/url.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
const signing = ref(false);
const user = ref<Misskey.entities.UserDetailed | null>(null);
+const usePasswordLessLogin = ref<Misskey.entities.UserDetailed['usePasswordLessLogin']>(true);
const username = ref('');
const password = ref('');
const token = ref('');
@@ -88,6 +97,7 @@ const totpLogin = ref(false);
const isBackupCode = ref(false);
const queryingKey = ref(false);
let credentialRequest: CredentialRequestOptions | null = null;
+const passkey_context = ref('');
const emit = defineEmits<{
(ev: 'login', v: any): void;
@@ -110,8 +120,10 @@ function onUsernameChange(): void {
username: username.value,
}).then(userResponse => {
user.value = userResponse;
+ usePasswordLessLogin.value = userResponse.usePasswordLessLogin;
}, () => {
user.value = null;
+ usePasswordLessLogin.value = true;
});
}
@@ -121,7 +133,7 @@ function onLogin(res: any): Promise<void> | void {
}
}
-async function queryKey(): Promise<void> {
+async function query2FaKey(): Promise<void> {
if (credentialRequest == null) return;
queryingKey.value = true;
await webAuthnRequest(credentialRequest)
@@ -150,6 +162,47 @@ async function queryKey(): Promise<void> {
});
}
+function onPasskeyLogin(): void {
+ signing.value = true;
+ if (webAuthnSupported()) {
+ misskeyApi('signin-with-passkey', {})
+ .then((res: SigninWithPasskeyResponse) => {
+ totpLogin.value = false;
+ signing.value = false;
+ queryingKey.value = true;
+ passkey_context.value = res.context ?? '';
+ credentialRequest = parseRequestOptionsFromJSON({
+ publicKey: res.option,
+ });
+ })
+ .then(() => queryPasskey())
+ .catch(loginFailed);
+ }
+}
+
+async function queryPasskey(): Promise<void> {
+ if (credentialRequest == null) return;
+ queryingKey.value = true;
+ console.log('Waiting passkey auth...');
+ await webAuthnRequest(credentialRequest)
+ .catch((err) => {
+ console.warn('Passkey Auth fail!: ', err);
+ queryingKey.value = false;
+ return Promise.reject(null);
+ }).then(credential => {
+ credentialRequest = null;
+ queryingKey.value = false;
+ signing.value = true;
+ return misskeyApi('signin-with-passkey', {
+ credential: credential.toJSON(),
+ context: passkey_context.value,
+ });
+ }).then((res: SigninWithPasskeyResponse) => {
+ emit('login', res.signinResponse);
+ return onLogin(res.signinResponse);
+ });
+}
+
function onSubmit(): void {
signing.value = true;
if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
@@ -164,7 +217,7 @@ function onSubmit(): void {
publicKey: res,
});
})
- .then(() => queryKey())
+ .then(() => query2FaKey())
.catch(loginFailed);
} else {
totpLogin.value = true;
@@ -212,6 +265,30 @@ function loginFailed(err: any): void {
});
break;
}
+ case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.loginFailed,
+ text: i18n.ts.unknownWebAuthnKey,
+ });
+ break;
+ }
+ case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.loginFailed,
+ text: i18n.ts.passkeyVerificationFailed,
+ });
+ break;
+ }
+ case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.loginFailed,
+ text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled,
+ });
+ break;
+ }
default: {
console.error(err);
os.alert({
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 524c62b4d3..d48780e9de 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkModalWindow
ref="dialog"
:width="400"
- :height="430"
+ :height="450"
@close="onClose"
@closed="emit('closed')"
>
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 5f08e416c1..4ab4380ad5 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -84,7 +84,7 @@ import * as Misskey from 'misskey-js';
import MkButton from './MkButton.vue';
import MkInput from './MkInput.vue';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
-import * as config from '@/config.js';
+import * as config from '@@/js/config.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { login } from '@/account.js';
diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
index 80f3a6709c..1845b01b69 100644
--- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
+++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import MkButton from '@/components/MkButton.vue';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { miLocalStorage } from '@/local-storage.js';
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index 9a07826f1a..3bbb163f0f 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -35,7 +35,7 @@ import * as Misskey from 'misskey-js';
import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue';
import { i18n } from '@/i18n.js';
-import { shouldCollapsed } from '@/scripts/collapsed.js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
const props = defineProps<{
note: Misskey.entities.Note;
@@ -62,7 +62,7 @@ const collapsed = ref(isLong);
left: 0;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), var(--X15));
+ background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 1a880170be..3746ffd8f3 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -100,14 +100,14 @@ defineProps<{
&.grid {
> .group {
+ margin-left: 0;
+ margin-right: 0;
+
& + .group {
padding-top: 0;
border-top: none;
}
- margin-left: 0;
- margin-right: 0;
-
> .title {
font-size: 1em;
opacity: 0.7;
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
index 69b8edd85a..19e4eea733 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
@@ -4,9 +4,10 @@
*/
import { defineAsyncComponent } from 'vue';
+import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
-export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved';
+export type SystemWebhookEventType = Misskey.entities.SystemWebhook['on'][number];
export type MkSystemWebhookEditorProps = {
mode: 'create' | 'edit';
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index f5c7a3160b..ec3b1c90ca 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -35,16 +35,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts._webhookSettings.trigger }}</template>
- <div class="_gaps_s">
- <MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
- <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
- </MkSwitch>
- <MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
- <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
- </MkSwitch>
- <MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
- <template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
- </MkSwitch>
+ <div class="_gaps">
+ <div class="_gaps_s">
+ <div :class="$style.switchBox">
+ <MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
+ <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
+ </MkSwitch>
+ <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReport)" @click="test('abuseReport')"><i class="ti ti-send"></i></MkButton>
+ </div>
+ <div :class="$style.switchBox">
+ <MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
+ <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
+ </MkSwitch>
+ <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReportResolved)" @click="test('abuseReportResolved')"><i class="ti ti-send"></i></MkButton>
+ </div>
+ <div :class="$style.switchBox">
+ <MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
+ <template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
+ </MkSwitch>
+ <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.userCreated)" @click="test('userCreated')"><i class="ti ti-send"></i></MkButton>
+ </div>
+ </div>
+
+ <div v-show="mode === 'edit'" :class="$style.description">
+ {{ i18n.ts._webhookSettings.testRemarks }}
+ </div>
</div>
</MkFolder>
@@ -66,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
+import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import {
@@ -180,6 +196,21 @@ async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
}
}
+async function test(type: Misskey.entities.SystemWebhook['on'][number]): Promise<void> {
+ if (!id.value) {
+ return Promise.resolve();
+ }
+
+ await os.apiWithDialog('admin/system-webhook/test', {
+ webhookId: id.value,
+ type,
+ override: {
+ secret: secret.value,
+ url: url.value,
+ },
+ });
+}
+
onMounted(async () => {
await loadingScope(async () => {
switch (mode.value) {
@@ -235,4 +266,29 @@ onMounted(async () => {
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
+
+.switchBox {
+ display: flex;
+ align-items: center;
+ justify-content: start;
+
+ .testButton {
+ $buttonSize: 28px;
+ padding: 0;
+ width: $buttonSize;
+ min-width: $buttonSize;
+ max-width: $buttonSize;
+ height: $buttonSize;
+ margin-left: auto;
+ line-height: normal;
+ font-size: 90%;
+ border-radius: 9999px;
+ }
+}
+
+.description {
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
+}
</style>
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 9adc8d466c..1f5a2b9381 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -158,7 +158,7 @@ import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue';
import MkAnimBg from '@/components/MkAnimBg.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
import { claimAchievement } from '@/scripts/achievements.js';
import * as os from '@/os.js';
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index 188cc37f41..f8af276836 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -19,7 +19,7 @@ import { onMounted, shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkSparkle from '@/components/MkSparkle.vue';
-import { version } from '@/config.js';
+import { version } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { confetti } from '@/scripts/confetti.js';
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index c868a22045..f5f9b43197 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -85,12 +85,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
import type { summaly } from '@misskey-dev/summaly';
-import { url as local } from '@/config.js';
+import { url as local } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { deviceKind } from '@/scripts/device-kind.js';
import MkButton from '@/components/MkButton.vue';
-import { versatileLang } from '@/scripts/intl-const.js';
+import { versatileLang } from '@@/js/intl-const.js';
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
import { defaultStore } from '@/store.js';
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index cbb40924f6..1374817c72 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -70,7 +70,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
-import { host as currentHost, hostname } from '@/config.js';
+import { host as currentHost, hostname } from '@@/js/config.js';
const emit = defineEmits<{
(ev: 'ok', selected: Misskey.entities.UserDetailed): void;
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index 514350c930..1fb1eda039 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -137,7 +137,7 @@ import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
import MkAnimBg from '@/components/MkAnimBg.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 445780eca7..a6c8baeaaa 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -58,7 +58,7 @@ import XSignupDialog from '@/components/MkSignupDialog.vue';
import MkButton from '@/components/MkButton.vue';
import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue';
-import { instanceName } from '@/config.js';
+import { instanceName } from '@@/js/config.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index 7550edd120..0c51cfa9ce 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -57,6 +57,7 @@ import MkButton from '@/components/MkButton.vue';
import { widgets as widgetDefs } from '@/widgets/index.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
+import { isLink } from '@@/js/is-link.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -98,13 +99,6 @@ const updateWidget = (id, data) => {
function onContextmenu(widget: Widget, ev: MouseEvent) {
const element = ev.target as HTMLElement | null;
- const isLink = (el: HTMLElement): boolean => {
- if (el.tagName === 'A') return true;
- if (el.parentElement) {
- return isLink(el.parentElement);
- }
- return false;
- };
if (element && isLink(element)) return;
if (element && (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(element.tagName) || element.attributes['contenteditable'])) return;
if (window.getSelection()?.toString() !== '') return;
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 303e49de00..08906a1205 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
import contains from '@/scripts/contains.js';
import * as os from '@/os.js';
-import { MenuItem } from '@/types/menu.js';
+import type { MenuItem } from '@/types/menu.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
@@ -508,10 +508,6 @@ defineExpose({
.header {
--height: 39px;
- &.mini {
- --height: 32px;
- }
-
display: flex;
position: relative;
z-index: 1;
@@ -524,6 +520,10 @@ defineExpose({
//border-bottom: solid 1px var(--divider);
font-size: 90%;
font-weight: bold;
+
+ &.mini {
+ --height: 32px;
+ }
}
.headerButton {
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index e3711b3463..1122976436 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
import MkWindow from '@/components/MkWindow.vue';
-import { versatileLang } from '@/scripts/intl-const.js';
+import { versatileLang } from '@@/js/intl-const.js';
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
import { defaultStore } from '@/store.js';
diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue
index e76ed9a849..d6585bf4a5 100644
--- a/packages/frontend/src/components/form/link.vue
+++ b/packages/frontend/src/components/form/link.vue
@@ -51,18 +51,18 @@ const props = defineProps<{
width: 100%;
box-sizing: border-box;
padding: 10px 14px;
- background: var(--buttonBg);
+ background: var(--folderHeaderBg);
border-radius: 6px;
font-size: 0.9em;
&:hover {
text-decoration: none;
- background: var(--buttonHoverBg);
+ background: var(--folderHeaderHoverBg);
}
&.active {
color: var(--accent);
- background: var(--buttonHoverBg);
+ background: var(--folderHeaderHoverBg);
}
}
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index 3a45ca429f..87fa9c8252 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -17,7 +17,7 @@ export type MkABehavior = 'window' | 'browser' | null;
import { computed, inject, shallowRef } from 'vue';
import * as os from '@/os.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { url } from '@/config.js';
+import { url } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router/supplier.js';
diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue
index bbcb070803..9a1ac3aca2 100644
--- a/packages/frontend/src/components/global/MkAcct.vue
+++ b/packages/frontend/src/components/global/MkAcct.vue
@@ -4,11 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :minScale="2 / 3">
- <span>@{{ user.username }}</span>
- <span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
-</MkCondensedLine>
-<span v-else>
+<span>
<span>@{{ user.username }}</span>
<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
</span>
@@ -17,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { toUnicode } from 'punycode/';
-import { host as hostRaw } from '@/config.js';
+import { host as hostRaw } from '@@/js/config.js';
import { defaultStore } from '@/store.js';
defineProps<{
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index bdaa8a809f..f0e943960d 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
-import { url as local, host } from '@/config.js';
+import { url as local, host } from '@@/js/config.js';
import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index e8e1bc696b..35c07bc80c 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="showDecoration">
<img
v-for="decoration in decorations ?? user.avatarDecorations"
- :class="[$style.decoration]"
+ :class="[$style.decoration, { [$style.decorationBlink]: decoration.blink }]"
:src="getDecorationUrl(decoration)"
:style="{
rotate: getDecorationAngle(decoration),
@@ -42,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
+import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
import MkA from './MkA.vue';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
-import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js';
import { acct, userPage } from '@/filters/user.js';
import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
import { defaultStore } from '@/store.js';
@@ -60,7 +60,7 @@ const props = withDefaults(defineProps<{
link?: boolean;
preview?: boolean;
indicator?: boolean;
- decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[];
+ decorations?: (Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'> & { blink?: boolean; })[];
forceShowDecoration?: boolean;
}>(), {
target: null,
@@ -330,4 +330,17 @@ watch(() => props.user.avatarBlurhash, () => {
width: 200%;
pointer-events: none;
}
+
+.decorationBlink {
+ animation: blink 1s infinite;
+}
+
+@keyframes blink {
+ 0%, 100% {
+ filter: brightness(2);
+ }
+ 50% {
+ filter: brightness(1);
+ }
+}
</style>
diff --git a/packages/frontend/src/components/global/MkCondensedLine.vue b/packages/frontend/src/components/global/MkCondensedLine.vue
index 7c4957d77f..473d444c16 100644
--- a/packages/frontend/src/components/global/MkCondensedLine.vue
+++ b/packages/frontend/src/components/global/MkCondensedLine.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<span :class="$style.container">
- <span ref="content" :class="$style.content">
+ <span ref="content" :class="$style.content" :style="{ maxWidth: `${100 / minScale}%` }">
<slot/>
</span>
</span>
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index dff56cd7f0..66f82a7898 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -35,6 +35,7 @@ import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import * as sound from '@/scripts/sound.js';
import { i18n } from '@/i18n.js';
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
+import type { MenuItem } from '@/types/menu.js';
const props = defineProps<{
name: string;
@@ -85,7 +86,9 @@ const errored = ref(url.value == null);
function onClick(ev: MouseEvent) {
if (props.menu) {
- os.popupMenu([{
+ const menuItems: MenuItem[] = [];
+
+ menuItems.push({
type: 'label',
text: `:${props.name}:`,
}, {
@@ -95,14 +98,20 @@ function onClick(ev: MouseEvent) {
copyToClipboard(`:${props.name}:`);
os.success();
},
- }, ...(props.menuReaction && react ? [{
- text: i18n.ts.doReaction,
- icon: 'ti ti-plus',
- action: () => {
- react(`:${props.name}:`);
- sound.playMisskeySfx('reaction');
- },
- }] : []), {
+ });
+
+ if (props.menuReaction && react) {
+ menuItems.push({
+ text: i18n.ts.doReaction,
+ icon: 'ti ti-plus',
+ action: () => {
+ react(`:${props.name}:`);
+ sound.playMisskeySfx('reaction');
+ },
+ });
+ }
+
+ menuItems.push({
text: i18n.ts.info,
icon: 'ti ti-info-circle',
action: async () => {
@@ -114,7 +123,9 @@ function onClick(ev: MouseEvent) {
closed: () => dispose(),
});
},
- }], ev.currentTarget ?? ev.target);
+ });
+
+ os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
}
</script>
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index fa780d4ad3..f0acd3bc27 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -10,13 +10,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, inject } from 'vue';
-import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@/scripts/emoji-base.js';
+import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js';
+import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@@/js/emoji-base.js';
import { defaultStore } from '@/store.js';
-import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
import * as os from '@/os.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import * as sound from '@/scripts/sound.js';
import { i18n } from '@/i18n.js';
+import type { MenuItem } from '@/types/menu.js';
const props = defineProps<{
emoji: string;
@@ -39,7 +40,9 @@ function computeTitle(event: PointerEvent): void {
function onClick(ev: MouseEvent) {
if (props.menu) {
- os.popupMenu([{
+ const menuItems: MenuItem[] = [];
+
+ menuItems.push({
type: 'label',
text: props.emoji,
}, {
@@ -49,14 +52,20 @@ function onClick(ev: MouseEvent) {
copyToClipboard(props.emoji);
os.success();
},
- }, ...(props.menuReaction && react ? [{
- text: i18n.ts.doReaction,
- icon: 'ti ti-plus',
- action: () => {
- react(props.emoji);
- sound.playMisskeySfx('reaction');
- },
- }] : [])], ev.currentTarget ?? ev.target);
+ });
+
+ if (props.menuReaction && react) {
+ menuItems.push({
+ text: i18n.ts.doReaction,
+ icon: 'ti ti-plus',
+ action: () => {
+ react(props.emoji);
+ sound.playMisskeySfx('reaction');
+ },
+ });
+ }
+
+ os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
}
</script>
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMfm.stories.impl.ts
index 730351f795..1daf7a29cb 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkMfm.stories.impl.ts
@@ -2,16 +2,15 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-
-/* eslint-disable @typescript-eslint/explicit-function-return-type */
+
import { StoryObj } from '@storybook/vue3';
import { expect, within } from '@storybook/test';
-import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.js';
+import MkMfm from './MkMfm.js';
export const Default = {
render(args) {
return {
components: {
- MkMisskeyFlavoredMarkdown,
+ MkMfm,
},
setup() {
return {
@@ -25,7 +24,7 @@ export const Default = {
};
},
},
- template: '<MkMisskeyFlavoredMarkdown v-bind="props" />',
+ template: '<MkMfm v-bind="props" />',
};
},
async play({ canvasElement, args }) {
@@ -54,25 +53,25 @@ export const Default = {
parameters: {
layout: 'centered',
},
-} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
+} satisfies StoryObj<typeof MkMfm>;
export const Plain = {
...Default,
args: {
...Default.args,
plain: true,
},
-} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
+} satisfies StoryObj<typeof MkMfm>;
export const Nowrap = {
...Default,
args: {
...Default.args,
nowrap: true,
},
-} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
+} satisfies StoryObj<typeof MkMfm>;
export const IsNotNote = {
...Default,
args: {
...Default.args,
isNote: false,
},
-} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>;
+} satisfies StoryObj<typeof MkMfm>;
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMfm.ts
index 0d869892bd..d914492231 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
+++ b/packages/frontend/src/components/global/MkMfm.ts
@@ -17,10 +17,15 @@ import MkCodeInline from '@/components/MkCodeInline.vue';
import MkGoogle from '@/components/MkGoogle.vue';
import MkSparkle from '@/components/MkSparkle.vue';
import MkA, { MkABehavior } from '@/components/global/MkA.vue';
-import { host } from '@/config.js';
+import { host } from '@@/js/config.js';
import { defaultStore } from '@/store.js';
-import { nyaize as doNyaize } from '@/scripts/nyaize.js';
-import { safeParseFloat } from '@/scripts/safe-parse.js';
+
+function safeParseFloat(str: unknown): number | null {
+ if (typeof str !== 'string' || str === '') return null;
+ const num = parseFloat(str);
+ if (isNaN(num)) return null;
+ return num;
+}
const QUOTE_STYLE = `
display: block;
@@ -86,7 +91,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
case 'text': {
let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (!disableNyaize && shouldNyaize) {
- text = doNyaize(text);
+ text = Misskey.nyaize(text);
}
if (!props.plain) {
@@ -281,14 +286,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
const child = token.children[0];
let text = child.type === 'text' ? child.props.text : '';
if (!disableNyaize && shouldNyaize) {
- text = doNyaize(text);
+ text = Misskey.nyaize(text);
}
return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]);
} else {
const rt = token.children.at(-1)!;
let text = rt.type === 'text' ? rt.props.text : '';
if (!disableNyaize && shouldNyaize) {
- text = doNyaize(text);
+ text = Misskey.nyaize(text);
}
return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
}
@@ -400,7 +405,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
}
case 'emojiCode': {
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (props.author?.host == null) {
return [h(MkCustomEmoji, {
key: Math.random(),
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index f16d951679..f1a451808f 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue';
import tinycolor from 'tinycolor2';
import XTabs, { Tab } from './MkPageHeader.tabs.vue';
-import { scrollToTop } from '@/scripts/scroll.js';
+import { scrollToTop } from '@@/js/scroll.js';
import { globalEvents } from '@/events.js';
import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index b12dc8cb31..72993991ce 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="bodyEl"
:data-sticky-container-header-height="headerHeight"
:data-sticky-container-footer-height="footerHeight"
+ style="position: relative; z-index: 0;"
>
<slot></slot>
</div>
@@ -24,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue';
-import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const.js';
+import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js';
const rootEl = shallowRef<HTMLElement>();
const headerEl = shallowRef<HTMLElement>();
@@ -83,14 +84,14 @@ onMounted(() => {
if (headerEl.value != null) {
headerEl.value.style.position = 'sticky';
headerEl.value.style.top = 'var(--stickyTop, 0)';
- headerEl.value.style.zIndex = '1000';
+ headerEl.value.style.zIndex = '1';
observer.observe(headerEl.value);
}
if (footerEl.value != null) {
footerEl.value.style.position = 'sticky';
footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
- footerEl.value.style.zIndex = '1000';
+ footerEl.value.style.zIndex = '1';
observer.observe(footerEl.value);
}
});
diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts
index ffd4a849a2..ccf7f200b5 100644
--- a/packages/frontend/src/components/global/MkTime.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts
@@ -8,7 +8,7 @@ import { expect } from '@storybook/test';
import { StoryObj } from '@storybook/vue3';
import MkTime from './MkTime.vue';
import { i18n } from '@/i18n.js';
-import { dateTimeFormat } from '@/scripts/intl-const.js';
+import { dateTimeFormat } from '@@/js/intl-const.js';
const now = new Date('2023-04-01T00:00:00.000Z');
const future = new Date('2024-04-01T00:00:00.000Z');
const oneHourAgo = new Date(now.getTime() - 3600000);
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index 027b226f3f..50bec990a1 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import isChromatic from 'chromatic/isChromatic';
import { onMounted, onUnmounted, ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
-import { dateTimeFormat } from '@/scripts/intl-const.js';
+import { dateTimeFormat } from '@@/js/intl-const.js';
const props = withDefaults(defineProps<{
time: Date | string | number | null;
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index d2ddd4aa85..e789251659 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -27,13 +27,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import { toUnicode as decodePunycode } from 'punycode/';
-import { url as local } from '@/config.js';
+import { url as local } from '@@/js/config.js';
import * as os from '@/os.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
-import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { MkABehavior } from '@/components/global/MkA.vue';
+function safeURIDecode(str: string): string {
+ try {
+ return decodeURIComponent(str);
+ } catch {
+ return str;
+ }
+}
+
const props = withDefaults(defineProps<{
url: string;
rel?: string;
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index 44d8d59941..b36625ed1b 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -5,7 +5,7 @@
import { App } from 'vue';
-import Mfm from './global/MkMisskeyFlavoredMarkdown.js';
+import Mfm from './global/MkMfm.js';
import MkA from './global/MkA.vue';
import MkAcct from './global/MkAcct.vue';
import MkAvatar from './global/MkAvatar.vue';