summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-03-30 01:58:17 -0400
committerHazelnoot <acomputerdog@gmail.com>2025-03-30 01:58:17 -0400
commit4ae26e6e185e52ac186ac10ccd4eda7718bf6e26 (patch)
tree8d556072f3876f0bfbab9d24e7cd209ca6bd091a /packages/frontend/src/components
parentrestore following feed deck UI (diff)
parentNew Crowdin updates (#15721) (diff)
downloadsharkey-4ae26e6e185e52ac186ac10ccd4eda7718bf6e26.tar.gz
sharkey-4ae26e6e185e52ac186ac10ccd4eda7718bf6e26.tar.bz2
sharkey-4ae26e6e185e52ac186ac10ccd4eda7718bf6e26.zip
Merge branch 'misskey-develop' into merge/2025-03-24
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts8
-rw-r--r--packages/frontend/src/components/MkClickerGame.stories.impl.ts8
-rw-r--r--packages/frontend/src/components/MkDrive.vue106
-rw-r--r--packages/frontend/src/components/MkEmojiPickerDialog.vue2
-rw-r--r--packages/frontend/src/components/MkHorizontalSwipe.vue2
-rw-r--r--packages/frontend/src/components/MkMediaVideo.vue2
-rw-r--r--packages/frontend/src/components/MkModal.vue3
-rw-r--r--packages/frontend/src/components/MkNote.vue21
-rw-r--r--packages/frontend/src/components/MkNotes.vue85
-rw-r--r--packages/frontend/src/components/MkNotifications.vue60
-rw-r--r--packages/frontend/src/components/MkPagination.vue20
-rw-r--r--packages/frontend/src/components/MkPostForm.vue4
-rw-r--r--packages/frontend/src/components/MkPullToRefresh.vue13
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.vue16
-rw-r--r--packages/frontend/src/components/MkTooltip.vue3
-rw-r--r--packages/frontend/src/components/global/MkAd.vue5
-rw-r--r--packages/frontend/src/components/global/MkEmoji.vue2
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.stories.impl.ts7
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.tabs.vue2
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue18
-rw-r--r--packages/frontend/src/components/global/PageWithHeader.vue11
-rw-r--r--packages/frontend/src/components/global/RouterView.vue4
-rw-r--r--packages/frontend/src/components/grid/MkGrid.vue15
23 files changed, 217 insertions, 200 deletions
diff --git a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
index a42e80c27a..4304c2e2b7 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
+++ b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts
@@ -2,20 +2,18 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-
-/* eslint-disable @typescript-eslint/explicit-function-return-type */
-/* eslint-disable import/no-default-export */
-import type { StoryObj } from '@storybook/vue3';
+
import { HttpResponse, http } from 'msw';
import { action } from '@storybook/addon-actions';
import { expect, userEvent, within } from '@storybook/test';
import { channel } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkChannelFollowButton from './MkChannelFollowButton.vue';
+import type { StoryObj } from '@storybook/vue3';
import { i18n } from '@/i18n.js';
function sleep(ms: number) {
- return new Promise(resolve => setTimeout(resolve, ms));
+ return new Promise(resolve => window.setTimeout(resolve, ms));
}
export const Default = {
diff --git a/packages/frontend/src/components/MkClickerGame.stories.impl.ts b/packages/frontend/src/components/MkClickerGame.stories.impl.ts
index eb7e61f294..6e1eb13d61 100644
--- a/packages/frontend/src/components/MkClickerGame.stories.impl.ts
+++ b/packages/frontend/src/components/MkClickerGame.stories.impl.ts
@@ -2,18 +2,16 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-
-/* eslint-disable @typescript-eslint/explicit-function-return-type */
-/* eslint-disable import/no-default-export */
-import type { StoryObj } from '@storybook/vue3';
+
import { HttpResponse, http } from 'msw';
import { action } from '@storybook/addon-actions';
import { expect, userEvent, within } from '@storybook/test';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkClickerGame from './MkClickerGame.vue';
+import type { StoryObj } from '@storybook/vue3';
function sleep(ms: number) {
- return new Promise(resolve => setTimeout(resolve, ms));
+ return new Promise(resolve => window.setTimeout(resolve, ms));
}
export const Default = {
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 730dd0d55e..0aeb65d69c 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -4,41 +4,45 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div :class="$style.root">
- <nav :class="$style.nav">
- <div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
- <XNavFolder
- :class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
- :parentFolder="folder"
- @move="move"
- @upload="upload"
- @removeFile="removeFile"
- @removeFolder="removeFolder"
- />
- <template v-for="f in hierarchyFolders">
- <span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
+<MkStickyContainer>
+ <template #header>
+ <nav :class="$style.nav">
+ <div :class="$style.navPath" @contextmenu.prevent.stop="() => {}">
<XNavFolder
- :folder="f"
+ :class="[$style.navPathItem, { [$style.navCurrent]: folder == null }]"
:parentFolder="folder"
- :class="[$style.navPathItem]"
@move="move"
@upload="upload"
@removeFile="removeFile"
@removeFolder="removeFolder"
/>
- </template>
- <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
- <span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
- </div>
- <div :class="$style.navMenu">
- <!-- "Search drive via alt text or file names" -->
- <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" :placeholder="i18n.ts.driveSearchbarPlaceholder" @enter="fetch">
- <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
- </MkInput>
-
+ <template v-for="f in hierarchyFolders">
+ <span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
+ <XNavFolder
+ :folder="f"
+ :parentFolder="folder"
+ :class="[$style.navPathItem]"
+ @move="move"
+ @upload="upload"
+ @removeFile="removeFile"
+ @removeFolder="removeFolder"
+ />
+ </template>
+ <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span>
+ <span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span>
+ </div>
<button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
- </div>
- </nav>
+ <div :class="$style.navMenu">
+ <!-- "Search drive via alt text or file names" -->
+ <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" :placeholder="i18n.ts.driveSearchbarPlaceholder" @enter="fetch">
+ <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template>
+ </MkInput>
+
+ <button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
+ </div>
+ </nav>
+ </template>
+
<div
ref="main"
:class="[$style.main, { [$style.uploading]: uploadings.length > 0, [$style.fetching]: fetching }]"
@@ -98,8 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLoading v-if="fetching"/>
</div>
<div v-if="draghover" :class="$style.dropzone"></div>
- <input ref="fileInput" style="display: none;" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
-</div>
+</MkStickyContainer>
</template>
<script lang="ts" setup>
@@ -118,6 +121,7 @@ import { i18n } from '@/i18n.js';
import { uploadFile, uploads } from '@/utility/upload.js';
import { claimAchievement } from '@/utility/achievements.js';
import { prefer } from '@/preferences.js';
+import { chooseFileFromPc } from '@/utility/select-file.js';
const searchQuery = ref('');
@@ -140,7 +144,6 @@ const emit = defineEmits<{
}>();
const loadMoreFiles = useTemplateRef('loadMoreFiles');
-const fileInput = useTemplateRef('fileInput');
const folder = ref<Misskey.entities.DriveFolder | null>(null);
const files = ref<Misskey.entities.DriveFile[]>([]);
@@ -152,7 +155,6 @@ const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
const uploadings = uploads;
const connection = useStream().useChannel('drive');
-const keepOriginal = ref<boolean>(prefer.s.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
// ドロップされようとしているか
const draghover = ref(false);
@@ -314,10 +316,6 @@ function onDrop(ev: DragEvent) {
//#endregion
}
-function selectLocalFile() {
- fileInput.value?.click();
-}
-
function urlUpload() {
os.inputText({
title: i18n.ts.uploadFromUrl,
@@ -393,15 +391,8 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
});
}
-function onChangeFileInput() {
- if (!fileInput.value?.files) return;
- for (const file of Array.from(fileInput.value.files)) {
- upload(file, folder.value);
- }
-}
-
-function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
- uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
+function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null, keepOriginal?: boolean) {
+ uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal).then(res => {
addFile(res, true);
});
}
@@ -644,16 +635,20 @@ function getMenu() {
const menu: MenuItem[] = [];
menu.push({
- type: 'switch',
- text: i18n.ts.keepOriginalUploading,
- ref: keepOriginal,
- }, { type: 'divider' }, {
text: i18n.ts.addFile,
type: 'label',
}, {
+ text: i18n.ts.upload + ' (' + i18n.ts.compress + ')',
+ icon: 'ti ti-upload',
+ action: () => {
+ chooseFileFromPc(true, { keepOriginal: false });
+ },
+ }, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
- action: () => { selectLocalFile(); },
+ action: () => {
+ chooseFileFromPc(true, { keepOriginal: true });
+ },
}, {
text: i18n.ts.fromUrl,
icon: 'ti ti-link',
@@ -765,22 +760,17 @@ onBeforeUnmount(() => {
</script>
<style lang="scss" module>
-.root {
- display: flex;
- flex-direction: column;
- height: 100%;
-}
-
.nav {
display: flex;
- z-index: 2;
width: 100%;
padding: 0 8px;
box-sizing: border-box;
overflow: auto;
font-size: 0.9em;
- box-shadow: 0 1px 0 var(--MI_THEME-divider);
- user-select: none;
+ background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
}
.navPath {
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index 5c4400eaf8..118eb8ecc0 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -79,7 +79,7 @@ function opening() {
picker.value?.focus();
// 何故かちょっと待たないとフォーカスされない
- setTimeout(() => {
+ window.setTimeout(() => {
picker.value?.focus();
}, 10);
}
diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue
index bc63bef0b6..1d0ffaea11 100644
--- a/packages/frontend/src/components/MkHorizontalSwipe.vue
+++ b/packages/frontend/src/components/MkHorizontalSwipe.vue
@@ -100,7 +100,7 @@ function touchMove(event: TouchEvent) {
pullDistance.value = 0;
isSwiping.value = false;
- setTimeout(() => {
+ window.setTimeout(() => {
isSwipingForClass.value = false;
}, 400);
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index a60e100969..8db3924fbd 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -342,7 +342,7 @@ const bufferedDataRatio = computed(() => {
// MediaControl Events
function onMouseOver() {
if (controlStateTimer) {
- clearTimeout(controlStateTimer);
+ window.clearTimeout(controlStateTimer);
}
isHoverring.value = true;
}
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index b5c93df4ed..3bcf835ec9 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -50,6 +50,7 @@ import { deviceKind } from '@/utility/device-kind.js';
import { focusTrap } from '@/utility/focus-trap.js';
import { focusParent } from '@/utility/focus.js';
import { prefer } from '@/preferences.js';
+import { DI } from '@/di.js';
function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === 'BODY') return null;
@@ -94,7 +95,7 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-provide('modal', true);
+provide(DI.inModal, true);
const maxHeight = ref<number>();
const fixed = ref(false);
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 2e2693d319..5fcb91b338 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -19,8 +19,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkNoteSub v-if="appearNote.reply" v-show="!renoteCollapsed && !inReplyToCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
- <!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>-->
- <!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>-->
<div v-if="isRenote" :class="$style.renote">
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
@@ -53,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
- <MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
+ <MkAvatar :class="[$style.avatar, prefer.s.useStickyIcons ? $style.useSticky : null]" :user="appearNote.user" :link="!mock" :preview="!mock"/>
<div :class="[$style.main, { [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click.stop="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined">
<MkNoteHeader :note="appearNote" :mini="true" @click.stop/>
<MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
@@ -922,7 +920,6 @@ function emitUpdReaction(emoji: string, delta: number) {
<style lang="scss" module>
.root {
position: relative;
- transition: box-shadow 0.1s ease;
font-size: 1.05em;
overflow: clip;
contain: content;
@@ -993,6 +990,8 @@ function emitUpdReaction(emoji: string, delta: number) {
}
.skipRender {
+ // TODO: これが有効だとTransitionGroupでnoteを追加するときに一瞬がくっとなってしまうのをどうにかしたい
+ // Transitionが完了するのを待ってからskipRenderを付与すれば解決しそうだけどパフォーマンス的な影響が不明
content-visibility: auto;
contain-intrinsic-size: 0 150px;
}
@@ -1127,9 +1126,12 @@ function emitUpdReaction(emoji: string, delta: number) {
margin: 0 14px 0 0;
width: var(--MI-avatar);
height: var(--MI-avatar);
- position: sticky !important;
- top: calc(22px + var(--MI-stickyTop, 0px));
- left: 0;
+
+ &.useSticky {
+ position: sticky !important;
+ top: calc(22px + var(--MI-stickyTop, 0px));
+ left: 0;
+ }
}
.main {
@@ -1324,7 +1326,10 @@ function emitUpdReaction(emoji: string, delta: number) {
margin: 0 10px 0 0;
width: 46px;
height: 46px;
- top: calc(14px + var(--MI-stickyTop, 0px));
+
+ &.useSticky {
+ top: calc(14px + var(--MI-stickyTop, 0px));
+ }
}
}
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 928a143a72..bc72351552 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -13,38 +13,34 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #default="{ items: notes }">
- <div :class="[$style.root, { [$style.noGap]: noGap }]">
- <MkDateSeparatedList
- ref="notes"
- v-slot="{ item: note }"
- :items="notes"
- :direction="pagination.reversed ? 'up' : 'down'"
- :reversed="pagination.reversed"
- :noGap="noGap"
- :ad="true"
- :class="$style.notes"
- >
- <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
- </MkDateSeparatedList>
- </div>
+ <component
+ :is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]"
+ :enterActiveClass="$style.transition_x_enterActive"
+ :leaveActiveClass="$style.transition_x_leaveActive"
+ :enterFromClass="$style.transition_x_enterFrom"
+ :leaveToClass="$style.transition_x_leaveTo"
+ :moveClass=" $style.transition_x_move"
+ tag="div"
+ >
+ <template v-for="(note, i) in notes" :key="note.id">
+ <DynamicNote :class="$style.note" :note="note" :withHardMute="true"/>
+ <div v-if="note._shouldInsertAd_" :class="$style.ad">
+ <MkAd :preferForms="['horizontal', 'horizontal-big']"/>
+ </div>
+ </template>
+ </component>
</template>
</MkPagination>
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, useTemplateRef } from 'vue';
+import { useTemplateRef, TransitionGroup } from 'vue';
import type { Paging } from '@/components/MkPagination.vue';
-import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
+import DynamicNote from '@/components/DynamicNote.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
-import { defaultStore } from '@/store.js';
-
-const MkNote = defineAsyncComponent(() =>
- (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') :
- (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') :
- null
-);
+import { prefer } from '@/preferences.js';
const props = defineProps<{
pagination: Paging;
@@ -60,24 +56,49 @@ defineExpose({
</script>
<style lang="scss" module>
+.transition_x_move,
+.transition_x_enterActive,
+.transition_x_leaveActive {
+ transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
+}
+.transition_x_enterFrom,
+.transition_x_leaveTo {
+ opacity: 0;
+ transform: translateY(-50%);
+}
+.transition_x_leaveActive {
+ position: absolute;
+}
+
.root {
+ container-type: inline-size;
+
&.noGap {
- border-radius: var(--MI-radius);
+ background: var(--MI_THEME-panel);
+
+ .note {
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
+ }
- > .notes {
- background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent);
+ .ad {
+ padding: 8px;
+ background-size: auto auto;
+ background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
}
}
&:not(.noGap) {
- > .notes {
- background: var(--MI_THEME-bg);
+ background: var(--MI_THEME-bg);
- .note {
- background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent);
- border-radius: var(--MI-radius);
- }
+ .note {
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
}
}
}
+
+.ad:empty {
+ display: none;
+}
</style>
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 4a1377655f..453a6c83b3 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -14,34 +14,38 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #default="{ items: notifications }">
- <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
- <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
- <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
- </MkDateSeparatedList>
+ <component
+ :is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.notifications]"
+ :enterActiveClass="$style.transition_x_enterActive"
+ :leaveActiveClass="$style.transition_x_leaveActive"
+ :enterFromClass="$style.transition_x_enterFrom"
+ :leaveToClass="$style.transition_x_leaveTo"
+ :moveClass=" $style.transition_x_move"
+ tag="div"
+ >
+ <template v-for="(notification, i) in notifications" :key="notification.id">
+ <DynamicNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.item" :note="notification.note" :withHardMute="true"/>
+ <XNotification v-else :class="$style.item" :notification="notification" :withTime="true" :full="true"/>
+ </template>
+ </component>
</template>
</MkPagination>
</MkPullToRefresh>
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, onUnmounted, onDeactivated, onMounted, computed, useTemplateRef, onActivated } from 'vue';
+import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup } from 'vue';
import * as Misskey from 'misskey-js';
import type { notificationTypes } from '@@/js/const.js';
import MkPagination from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue';
-import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
+import DynamicNote from '@/components/DynamicNote.vue';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { prefer } from '@/preferences.js';
-const MkNote = defineAsyncComponent(() =>
- (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') :
- (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') :
- null
-);
-
const props = defineProps<{
excludeTypes?: typeof notificationTypes[number][];
}>();
@@ -89,28 +93,36 @@ onMounted(() => {
connection.on('notificationFlushed', reload);
});
-onActivated(() => {
- pagingComponent.value?.reload();
- connection = useStream().useChannel('main');
- connection.on('notification', onNotification);
- connection.on('notificationFlushed', reload);
-});
-
onUnmounted(() => {
if (connection) connection.dispose();
});
-onDeactivated(() => {
- if (connection) connection.dispose();
-});
-
defineExpose({
reload,
});
</script>
<style lang="scss" module>
-.list {
+.transition_x_move,
+.transition_x_enterActive,
+.transition_x_leaveActive {
+ transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
+}
+.transition_x_enterFrom,
+.transition_x_leaveTo {
+ opacity: 0;
+ transform: translateY(-50%);
+}
+.transition_x_leaveActive {
+ position: absolute;
+}
+
+.notifications {
+ container-type: inline-size;
background: var(--MI_THEME-panel);
}
+
+.item {
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
+}
</style>
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 71f7fc513b..ee128e5991 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkError v-else-if="error" @retry="init()"/>
- <div v-else-if="empty" key="_empty_" class="empty">
+ <div v-else-if="empty" key="_empty_">
<slot name="empty">
<div class="_fullinfo">
<img :src="infoImageUrl" draggable="false"/>
@@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMoreAhead">
{{ i18n.ts.loadMore }}
</MkButton>
- <MkLoading v-else class="loading"/>
+ <MkLoading v-else/>
</div>
<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
<div v-show="!pagination.reversed && more" key="_more_">
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">
{{ i18n.ts.loadMore }}
</MkButton>
- <MkLoading v-else class="loading"/>
+ <MkLoading v-else/>
</div>
</div>
</Transition>
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, useTemplateRef, watch, type Ref } from 'vue';
import * as Misskey from 'misskey-js';
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
-import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible } from '@@/js/scroll.js';
+import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scrollInContainer, isTailVisible } from '@@/js/scroll.js';
import type { ComputedRef } from 'vue';
import type { MisskeyEntity } from '@/types/date-separated-list.js';
import { misskeyApi } from '@/utility/misskey-api.js';
@@ -268,7 +268,7 @@ const fetchMore = async (): Promise<void> => {
return nextTick(() => {
if (scrollableElement.value) {
- scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
+ scrollInContainer(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
} else {
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
}
@@ -368,7 +368,7 @@ watch(visibility, () => {
BACKGROUND_PAUSE_WAIT_SEC * 1000);
} else { // 'visible'
if (timerForSetPause) {
- clearTimeout(timerForSetPause);
+ window.clearTimeout(timerForSetPause);
timerForSetPause = null;
} else {
isPausingUpdate = false;
@@ -464,11 +464,11 @@ onBeforeMount(() => {
init().then(() => {
if (props.pagination.reversed) {
nextTick(() => {
- setTimeout(toBottom, 800);
+ window.setTimeout(toBottom, 800);
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
// more = trueを遅らせる
- setTimeout(() => {
+ window.setTimeout(() => {
moreFetching.value = false;
}, 2000);
});
@@ -478,11 +478,11 @@ onBeforeMount(() => {
onBeforeUnmount(() => {
if (timerForSetPause) {
- clearTimeout(timerForSetPause);
+ window.clearTimeout(timerForSetPause);
timerForSetPause = null;
}
if (preventAppearFetchMoreTimer.value) {
- clearTimeout(preventAppearFetchMoreTimer.value);
+ window.clearTimeout(preventAppearFetchMoreTimer.value);
preventAppearFetchMoreTimer.value = null;
}
scrollObserver.value?.disconnect();
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 921829c41e..c70d0d5581 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -150,7 +150,7 @@ import { DI } from '@/di.js';
const $i = ensureSignin();
-const modal = inject('modal');
+const modal = inject(DI.inModal, false);
const props = withDefaults(defineProps<PostFormProps & {
fixed?: boolean;
@@ -1431,7 +1431,7 @@ defineExpose({
padding: 0 24px;
margin: 0;
width: 100%;
- font-size: 16px;
+ font-size: 110%;
border: none;
border-radius: 0;
background: transparent;
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index 1fbf00d212..22ae563d13 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -16,9 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
- <div :class="{ [$style.slotClip]: isPullStart }">
- <slot/>
- </div>
+
+ <slot/>
</div>
</template>
@@ -82,11 +81,11 @@ function moveBySystem(to: number): Promise<void> {
return;
}
const startTime = Date.now();
- let intervalId = setInterval(() => {
+ let intervalId = window.setInterval(() => {
const time = Date.now() - startTime;
if (time > RELEASE_TRANSITION_DURATION) {
pullDistance.value = to;
- clearInterval(intervalId);
+ window.clearInterval(intervalId);
r();
return;
}
@@ -261,8 +260,4 @@ defineExpose({
margin: 5px 0;
}
}
-
-.slotClip {
- overflow-y: clip;
-}
</style>
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index cf0bb7b1f2..6b19f4a55c 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -4,22 +4,24 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<TransitionGroup
- :enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''"
- :leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''"
- :enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''"
- :leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''"
- :moveClass="prefer.s.animation ? $style.transition_x_move : ''"
+<component
+ :is="prefer.s.animation ? TransitionGroup : 'div'"
+ :enterActiveClass="$style.transition_x_enterActive"
+ :leaveActiveClass="$style.transition_x_leaveActive"
+ :enterFromClass="$style.transition_x_enterFrom"
+ :leaveToClass="$style.transition_x_leaveTo"
+ :moveClass="$style.transition_x_move"
tag="div" :class="$style.root"
>
<XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
<slot v-if="hasMoreReactions" name="more"/>
-</TransitionGroup>
+</component>
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { inject, watch, ref } from 'vue';
+import { TransitionGroup } from 'vue';
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index 955c24b6ef..16913386c1 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -9,7 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveActiveClass="prefer.s.animation ? $style.transition_tooltip_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_tooltip_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_tooltip_leaveTo : ''"
- appear @afterLeave="emit('closed')"
+ appear :css="prefer.s.animation"
+ @afterLeave="emit('closed')"
>
<div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
<slot>
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index b29bbb6392..525c47da45 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -36,7 +36,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button>
</div>
</div>
-<div v-else></div>
</template>
<script lang="ts" setup>
@@ -53,7 +52,7 @@ import { prefer } from '@/preferences.js';
type Ad = (typeof instance)['ads'][number];
const props = defineProps<{
- prefer: string[];
+ preferForms: string[];
specify?: Ad;
}>();
@@ -72,7 +71,7 @@ const choseAd = (): Ad | null => {
ratio: 0,
} : ad);
- let ads = allAds.filter(ad => props.prefer.includes(ad.place));
+ let ads = allAds.filter(ad => props.preferForms.includes(ad.place));
if (ads.length === 0) {
ads = allAds.filter(ad => ad.place === 'square');
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index f5323690d0..6abe1f30b6 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -25,7 +25,7 @@ const props = defineProps<{
menuReaction?: boolean;
}>();
-const react = inject(DI.mfmEmojiReactCallback);
+const react = inject(DI.mfmEmojiReactCallback, null);
const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : prefer.s.emojiStyle === 'tossface' ? char2tossfaceFilePath : char2fluentEmojiFilePath;
diff --git a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
index c9af5f4ea4..15938d0495 100644
--- a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
@@ -2,11 +2,10 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-
-/* eslint-disable @typescript-eslint/explicit-function-return-type */
+
import { waitFor } from '@storybook/test';
-import type { StoryObj } from '@storybook/vue3';
import MkPageHeader from './MkPageHeader.vue';
+import type { StoryObj } from '@storybook/vue3';
export const Empty = {
render(args) {
return {
@@ -29,7 +28,7 @@ export const Empty = {
};
},
async play() {
- const wait = new Promise((resolve) => setTimeout(resolve, 800));
+ const wait = new Promise((resolve) => window.setTimeout(resolve, 800));
await waitFor(async () => await wait);
},
args: {
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index 832c7887cb..b54e683179 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -133,7 +133,7 @@ async function enter(el: Element) {
entering = false;
});
- setTimeout(renderTab, 170);
+ window.setTimeout(renderTab, 170);
}
function afterEnter(el: Element) {
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 54ce0f85eb..1dda723fd2 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div v-if="show" ref="el" :class="[$style.root]" :style="{ background: bg }">
+<div v-if="show" ref="el" :class="[$style.root]">
<div :class="[$style.upper, { [$style.slim]: narrow, [$style.thin]: thin_ }]">
<div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu">
<MkAvatar :class="$style.avatar" :user="$i"/>
@@ -51,7 +51,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'vue';
-import tinycolor from 'tinycolor2';
import { scrollToTop } from '@@/js/scroll.js';
import XTabs from './MkPageHeader.tabs.vue';
import type { Tab } from './MkPageHeader.tabs.vue';
@@ -82,7 +81,6 @@ const emit = defineEmits<{
const displayBackButton = props.displayBackButton && history.state.key !== 'index' && history.length > 1 && inject('shouldBackButton', true);
const viewId = inject(DI.viewId);
-const viewTransitionName = computed(() => `${viewId}---pageHeader`);
const injectedPageMetadata = inject(DI.pageMetadata);
const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value);
@@ -90,7 +88,6 @@ const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props
const thin_ = props.thin || inject('shouldHeaderThin', false);
const el = useTemplateRef('el');
-const bg = ref<string | undefined>(undefined);
const narrow = ref(false);
const hasTabs = computed(() => props.tabs.length > 0);
const hasActions = computed(() => props.actions && props.actions.length > 0);
@@ -122,19 +119,9 @@ function goBack(): void {
window.history.back();
}
-const calcBg = () => {
- const rawBg = 'var(--MI_THEME-bg)';
- const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(window.document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
- tinyBg.setAlpha(0.85);
- bg.value = tinyBg.toRgbString();
-};
-
let ro: ResizeObserver | null;
onMounted(() => {
- calcBg();
- globalEvents.on('themeChanging', calcBg);
-
if (el.value && el.value.parentElement) {
narrow.value = el.value.parentElement.offsetWidth < 500;
ro = new ResizeObserver((entries, observer) => {
@@ -147,18 +134,17 @@ onMounted(() => {
});
onUnmounted(() => {
- globalEvents.off('themeChanging', calcBg);
if (ro) ro.disconnect();
});
</script>
<style lang="scss" module>
.root {
+ background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-bottom: solid 0.5px var(--MI_THEME-divider);
width: 100%;
- view-transition-name: v-bind(viewTransitionName);
}
.upper,
diff --git a/packages/frontend/src/components/global/PageWithHeader.vue b/packages/frontend/src/components/global/PageWithHeader.vue
index fb813689ba..7ea0b5c97f 100644
--- a/packages/frontend/src/components/global/PageWithHeader.vue
+++ b/packages/frontend/src/components/global/PageWithHeader.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']">
+<div ref="rootEl" :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']">
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="actions" :tabs="tabs"/></template>
<div :class="$style.body">
@@ -16,6 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { useTemplateRef } from 'vue';
+import { scrollInContainer } from '@@/js/scroll.js';
import type { PageHeaderItem } from '@/types/page-header.js';
import type { Tab } from './MkPageHeader.tabs.vue';
@@ -31,6 +33,13 @@ const props = withDefaults(defineProps<{
});
const tab = defineModel<string>('tab');
+const rootEl = useTemplateRef('rootEl');
+
+defineExpose({
+ scrollToTop: () => {
+ if (rootEl.value) scrollInContainer(rootEl.value, { top: 0, behavior: 'smooth' });
+ },
+});
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 1c0c35f34e..78ac6900a3 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -44,7 +44,9 @@ provide(DI.routerCurrentDepth, currentDepth + 1);
const rootEl = useTemplateRef('rootEl');
onMounted(() => {
- rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入
+ if (prefer.s.animation) {
+ rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入
+ }
});
// view-transition-newなどの<pt-name-selector>にはcss varが使えず、v-bindできないため直接スタイルを生成
diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue
index 94f4f3dab1..c37f3df0d3 100644
--- a/packages/frontend/src/components/grid/MkGrid.vue
+++ b/packages/frontend/src/components/grid/MkGrid.vue
@@ -50,6 +50,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { computed, onMounted, ref, toRefs, watch } from 'vue';
+import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
+import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
+import type { GridContext, GridEvent } from '@/components/grid/grid-event.js';
+import type { GridColumn } from '@/components/grid/column.js';
+import type { GridRow, GridRowSetting } from '@/components/grid/row.js';
+import type { MenuItem } from '@/types/menu.js';
import { GridEventEmitter } from '@/components/grid/grid.js';
import MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
@@ -68,13 +74,6 @@ import { createColumn } from '@/components/grid/column.js';
import { createRow, defaultGridRowSetting, resetRow } from '@/components/grid/row.js';
import { handleKeyEvent } from '@/utility/key-event.js';
-import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
-import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
-import type { GridContext, GridEvent } from '@/components/grid/grid-event.js';
-import type { GridColumn } from '@/components/grid/column.js';
-import type { GridRow, GridRowSetting } from '@/components/grid/row.js';
-import type { MenuItem } from '@/types/menu.js';
-
type RowHolder = {
row: GridRow,
cells: GridCell[],
@@ -130,7 +129,7 @@ const bus = new GridEventEmitter();
*
* @see {@link onResize}
*/
-const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries)));
+const resizeObserver = new ResizeObserver((entries) => window.setTimeout(() => onResize(entries)));
const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
/**