@@ -238,10 +238,9 @@ const filesPaginator = markRaw(new Paginator('drive/files', {
params: () => ({ // 自動でリロードしたくないためcomputedParamsは使わない
folderId: folder.value ? folder.value.id : null,
type: props.type,
- sort: sortModeSelect.value,
+ sort: ['-createdAt', '+createdAt'].includes(sortModeSelect.value) ? null : sortModeSelect.value,
}),
}));
-
const foldersPaginator = markRaw(new Paginator('drive/folders', {
limit: 30,
canFetchDetection: 'limit',
@@ -250,6 +249,16 @@ const foldersPaginator = markRaw(new Paginator('drive/folders', {
}),
}));
+const canFetchFiles = computed(() => !fetching.value && (filesPaginator.order.value === 'oldest' ? filesPaginator.canFetchNewer.value : filesPaginator.canFetchOlder.value));
+
+async function fetchMoreFiles() {
+ if (filesPaginator.order.value === 'oldest') {
+ filesPaginator.fetchNewer();
+ } else {
+ filesPaginator.fetchOlder();
+ }
+}
+
const filesTimeline = makeDateGroupedTimelineComputedRef(filesPaginator.items, 'month');
const shouldBeGroupedByDate = computed(() => ['+createdAt', '-createdAt'].includes(sortModeSelect.value));
@@ -260,10 +269,10 @@ watch(sortModeSelect, () => {
async function initialize() {
fetching.value = true;
- await Promise.all([
- foldersPaginator.init(),
- filesPaginator.init(),
- ]);
+ await foldersPaginator.reload();
+ filesPaginator.initialDirection = sortModeSelect.value === '-createdAt' ? 'newer' : 'older';
+ filesPaginator.order.value = sortModeSelect.value === '-createdAt' ? 'oldest' : 'newest';
+ await filesPaginator.reload();
fetching.value = false;
}
From 443e1ed29e11dfed85a7a40c58ac2901c0183f88 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 2 Jan 2026 21:34:43 +0900
Subject: [PATCH 06/16] =?UTF-8?q?refactor(frontend):=20prefer.model,=20sto?=
=?UTF-8?q?re.model=E3=81=A7=E3=81=AFcustomRef=E3=82=92=E4=BD=BF=E7=94=A8?=
=?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#17058)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* refactor(frontend): prefer.model, store.modelではcustomRefを使用するように
* fix: watchの解除に失敗してもエラーで落ちないように
* Update packages/frontend/src/lib/pizzax.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../frontend/src/components/MkPostForm.vue | 4 +-
packages/frontend/src/lib/pizzax.ts | 63 +++++++++----------
.../frontend/src/pages/settings/navbar.vue | 2 +-
.../src/pages/settings/preferences.vue | 2 +-
.../frontend/src/pages/settings/profile.vue | 2 +-
packages/frontend/src/preferences/manager.ts | 57 +++++++++--------
.../frontend/src/ui/_common_/navbar-h.vue | 2 +-
7 files changed, 67 insertions(+), 65 deletions(-)
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 52684bc815..4b027cf105 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -329,8 +329,8 @@ const canSaveAsServerDraft = computed((): boolean => {
return canPost.value && (textLength.value > 0 || files.value.length > 0 || poll.value != null);
});
-const withHashtags = computed(store.makeGetterSetter('postFormWithHashtags'));
-const hashtags = computed(store.makeGetterSetter('postFormHashtags'));
+const withHashtags = store.model('postFormWithHashtags');
+const hashtags = store.model('postFormHashtags');
watch(text, () => {
checkMissingMention();
diff --git a/packages/frontend/src/lib/pizzax.ts b/packages/frontend/src/lib/pizzax.ts
index 80543d10e4..0dd8a82957 100644
--- a/packages/frontend/src/lib/pizzax.ts
+++ b/packages/frontend/src/lib/pizzax.ts
@@ -7,7 +7,7 @@
// TODO: Misskeyのドメイン知識があるのでutilityなどに移動する
-import { onUnmounted, ref, watch } from 'vue';
+import { customRef, ref, watch, onScopeDispose } from 'vue';
import { BroadcastChannel } from 'broadcast-channel';
import type { Ref } from 'vue';
import { $i } from '@/i.js';
@@ -223,44 +223,43 @@ export class Pizzax
{
}
/**
- * 特定のキーの、簡易的なgetter/setterを作ります
+ * 特定のキーの、簡易的なcomputed refを作ります
* 主にvue上で設定コントロールのmodelとして使う用
*/
- // TODO: 廃止
- public makeGetterSetter(
+ public model(
+ key: K,
+ ): Ref;
+ public model>(
+ key: K,
+ getter: (v: T[K]['default']) => R,
+ setter: (v: R) => T[K]['default'],
+ ): Ref;
+
+ public model(
key: K,
getter?: (v: T[K]['default']) => R,
setter?: (v: R) => T[K]['default'],
- ): {
- get: () => R;
- set: (value: R) => void;
- } {
- const valueRef = ref(this.s[key]);
+ ): Ref {
+ return customRef((track, trigger) => {
+ const watchStop = watch(this.r[key], () => {
+ trigger();
+ });
- const stop = watch(this.r[key], val => {
- valueRef.value = val;
+ onScopeDispose(() => {
+ watchStop();
+ }, true);
+
+ return {
+ get: () => {
+ track();
+ return (getter != null ? getter(this.s[key]) : this.s[key]) as R;
+ },
+ set: (value) => {
+ const val = setter != null ? setter(value) : value;
+ this.set(key, val as T[K]['default']);
+ },
+ };
});
-
- // NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする
- onUnmounted(() => {
- stop();
- });
-
- // TODO: VueのcustomRef使うと良い感じになるかも
- return {
- get: () => {
- if (getter) {
- return getter(valueRef.value);
- } else {
- return valueRef.value;
- }
- },
- set: (value) => {
- const val = setter ? setter(value) : value;
- this.set(key, val);
- valueRef.value = val;
- },
- };
}
// localStorage => indexedDBのマイグレーション
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index d25708dcb4..baa8fdc967 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -78,7 +78,7 @@ const items = ref(prefer.s.menu.map(x => ({
})));
const itemTypeValues = computed(() => items.value.map(x => x.type));
-const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
+const menuDisplay = store.model('menuDisplay');
const showNavbarSubButtons = prefer.model('showNavbarSubButtons');
async function addItem() {
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 972b50f8cd..aa7f0dabbb 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -855,7 +855,7 @@ const $i = ensureSignin();
const lang = ref(miLocalStorage.getItem('lang'));
const dataSaver = ref(prefer.s.dataSaver);
-const realtimeMode = computed(store.makeGetterSetter('realtimeMode'));
+const realtimeMode = store.model('realtimeMode');
const overridedDeviceKind = prefer.model('overridedDeviceKind');
const pollingInterval = prefer.model('pollingInterval');
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 7d3da470d6..8e4c39c8bb 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -190,7 +190,7 @@ const $i = ensureSignin();
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
-const reactionAcceptance = computed(store.makeGetterSetter('reactionAcceptance'));
+const reactionAcceptance = store.model('reactionAcceptance');
function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
return lang != null && lang in langmap;
diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts
index 13ba0000e4..1a1ec2b345 100644
--- a/packages/frontend/src/preferences/manager.ts
+++ b/packages/frontend/src/preferences/manager.ts
@@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { computed, onUnmounted, ref, watch } from 'vue';
+import { customRef, ref, watch, onScopeDispose } from 'vue';
import { EventEmitter } from 'eventemitter3';
import { host, version } from '@@/js/config.js';
import { PREF_DEF } from './def.js';
-import type { Ref, WritableComputedRef } from 'vue';
+import type { Ref } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import { genId } from '@/utility/id.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
@@ -299,36 +299,39 @@ export class PreferencesManager extends EventEmitter {
* 特定のキーの、簡易的なcomputed refを作ります
* 主にvue上で設定コントロールのmodelとして使う用
*/
- public model = ValueOf>(
+ public model>(
+ key: K,
+ ): Ref;
+ public model>>(
+ key: K,
+ getter: (v: ValueOf) => V,
+ setter: (v: V) => ValueOf,
+ ): Ref;
+
+ public model(
key: K,
getter?: (v: ValueOf) => V,
setter?: (v: V) => ValueOf,
- ): WritableComputedRef {
- const valueRef = ref(this.s[key]);
+ ): Ref {
+ return customRef((track, trigger) => {
+ const watchStop = watch(this.r[key], () => {
+ trigger();
+ });
- const stop = watch(this.r[key], val => {
- valueRef.value = val;
- });
+ onScopeDispose(() => {
+ watchStop();
+ }, true);
- // NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする
- onUnmounted(() => {
- stop();
- });
-
- // TODO: VueのcustomRef使うと良い感じになるかも
- return computed({
- get: () => {
- if (getter) {
- return getter(valueRef.value);
- } else {
- return valueRef.value;
- }
- },
- set: (value) => {
- const val = setter ? setter(value) : value;
- this.commit(key, val);
- valueRef.value = val;
- },
+ return {
+ get: () => {
+ track();
+ return (getter != null ? getter(this.s[key]) : this.s[key]) as V;
+ },
+ set: (value) => {
+ const val = setter != null ? setter(value) : value;
+ this.commit(key, val as ValueOf);
+ },
+ };
});
}
diff --git a/packages/frontend/src/ui/_common_/navbar-h.vue b/packages/frontend/src/ui/_common_/navbar-h.vue
index ad0632965b..594c398d8b 100644
--- a/packages/frontend/src/ui/_common_/navbar-h.vue
+++ b/packages/frontend/src/ui/_common_/navbar-h.vue
@@ -67,7 +67,7 @@ const props = defineProps<{
const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD);
const menu = ref(prefer.s.menu);
-// const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
+// const menuDisplay = store.model('menuDisplay');
const otherNavItemIndicated = computed(() => {
for (const def in navbarItemDef) {
if (menu.value.includes(def)) continue;
From a1ba403f9ae4b08107a10203997f7a790370e2a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 2 Jan 2026 21:38:53 +0900
Subject: [PATCH 07/16] =?UTF-8?q?fix(frontend):=20=E3=83=AD=E3=82=B0?=
=?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0?=
=?UTF-8?q?=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C=E3=81=9F=E3=81=82?=
=?UTF-8?q?=E3=81=A8=E3=81=AE=E5=87=A6=E7=90=86=E3=81=8C=E3=81=8A=E3=81=8B?=
=?UTF-8?q?=E3=81=97=E3=81=8F=E3=81=AA=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92?=
=?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#17038)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(frontend): ログインダイアログが表示されたあとの処理がおかしくなる問題を修正
* Update Changelog
---
CHANGELOG.md | 1 +
.../src/components/MkFollowButton.vue | 8 ++++-
packages/frontend/src/components/MkNote.vue | 30 ++++++++++++-------
.../src/components/MkNoteDetailed.vue | 25 +++++++++++-----
packages/frontend/src/components/MkPoll.vue | 3 +-
packages/frontend/src/os.ts | 5 ++--
packages/frontend/src/pages/flash/flash.vue | 10 +++++--
packages/frontend/src/pages/reversi/index.vue | 8 +++--
packages/frontend/src/utility/please-login.ts | 6 ++--
9 files changed, 64 insertions(+), 32 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 88f3981473..e22dfba72a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
- Enhance: ウィジェットの表示設定をプレビューを見ながら行えるように
- Enhance: ウィジェットの設定項目のラベルの多言語対応
- Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061
+- Fix: 非ログイン時にログインを求めるダイアログが表示された後にダイアログのぼかしが解除されず操作不能になることがある問題を修正
- Fix: ドライブのソートが「登録日(昇順)」の場合に正しく動作しない問題を修正
### Server
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index ba21fe82e4..72a24411c1 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -81,7 +81,13 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
}
async function onClick() {
- pleaseLogin({ openOnRemote: { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` } });
+ const isLoggedIn = await pleaseLogin({
+ openOnRemote: {
+ type: 'web',
+ path: `/@${props.user.username}@${props.user.host ?? host}`,
+ },
+ });
+ if (!isLoggedIn) return;
wait.value = true;
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index a7299d2961..56def64d3d 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -468,8 +468,12 @@ if (!props.mock) {
}
}
-function renote() {
- pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+async function renote() {
+ if (props.mock) return;
+
+ const isLoggedIn = await pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+ if (!isLoggedIn) return;
+
showMovedDialog();
const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock });
@@ -478,11 +482,12 @@ function renote() {
subscribeManuallyToNoteCapture();
}
-function reply(): void {
- pleaseLogin({ openOnRemote: pleaseLoginContext.value });
- if (props.mock) {
- return;
- }
+async function reply() {
+ if (props.mock) return;
+
+ const isLoggedIn = await pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+ if (!isLoggedIn) return;
+
os.post({
reply: appearNote,
channel: appearNote.channel,
@@ -491,8 +496,10 @@ function reply(): void {
});
}
-function react(): void {
- pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+async function react() {
+ const isLoggedIn = await pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+ if (!isLoggedIn) return;
+
showMovedDialog();
if (appearNote.reactionAcceptance === 'likeOnly') {
sound.playMisskeySfx('reaction');
@@ -621,10 +628,12 @@ async function clip(): Promise {
os.popupMenu(await getNoteClipMenu({ note: note, currentClip: currentClip?.value }), clipButton.value).then(focus);
}
-function showRenoteMenu(): void {
+async function showRenoteMenu() {
if (props.mock) {
return;
}
+ const isLoggedIn = await pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+ if (!isLoggedIn) return;
function getUnrenote(): MenuItem {
return {
@@ -649,7 +658,6 @@ function showRenoteMenu(): void {
};
if (isMyRenote) {
- pleaseLogin({ openOnRemote: pleaseLoginContext.value });
os.popupMenu([
renoteDetailsMenu,
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 47bf365877..febf909f42 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -448,8 +448,10 @@ if (appearNote.reactionAcceptance === 'likeOnly') {
});
}
-function renote() {
- pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+async function renote() {
+ const isLoggedIn = await pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+ if (!isLoggedIn) return;
+
showMovedDialog();
const { menu } = getRenoteMenu({ note: note, renoteButton });
@@ -459,8 +461,10 @@ function renote() {
subscribeManuallyToNoteCapture();
}
-function reply(): void {
- pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+async function reply() {
+ const isLoggedIn = await pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+ if (!isLoggedIn) return;
+
showMovedDialog();
os.post({
reply: appearNote,
@@ -470,8 +474,10 @@ function reply(): void {
});
}
-function react(): void {
- pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+async function react() {
+ const isLoggedIn = await pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+ if (!isLoggedIn) return;
+
showMovedDialog();
if (appearNote.reactionAcceptance === 'likeOnly') {
sound.playMisskeySfx('reaction');
@@ -569,9 +575,12 @@ async function clip(): Promise {
os.popupMenu(await getNoteClipMenu({ note: note }), clipButton.value).then(focus);
}
-function showRenoteMenu(): void {
+async function showRenoteMenu() {
if (!isMyRenote) return;
- pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+
+ const isLoggedIn = await pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+ if (!isLoggedIn) return;
+
os.popupMenu([{
text: i18n.ts.unrenote,
icon: 'ti ti-trash',
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 305e9b5c4f..31567d2b84 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -90,7 +90,8 @@ const pleaseLoginContext = computed(() => ({
const vote = async (id: number) => {
if (props.readOnly || closed.value || isVoted.value) return;
- pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+ const isLoggedIn = await pleaseLogin({ openOnRemote: pleaseLoginContext.value });
+ if (!isLoggedIn) return;
const { canceled } = await os.confirm({
type: 'question',
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index aafa1c4b21..59ed3dc948 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -709,8 +709,8 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise {
}));
}
-export function post(props: PostFormProps = {}): Promise {
- pleaseLogin({
+export async function post(props: PostFormProps = {}): Promise {
+ const isLoggedIn = await pleaseLogin({
openOnRemote: (props.initialText || props.initialNote ? {
type: 'share',
params: {
@@ -720,6 +720,7 @@ export function post(props: PostFormProps = {}): Promise {
},
} : undefined),
});
+ if (!isLoggedIn) return;
showMovedDialog();
return new Promise(resolve => {
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index efc9ee014f..f5e51dc72f 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -151,9 +151,11 @@ function shareWithNote() {
});
}
-function like() {
+async function like() {
if (!flash.value) return;
- pleaseLogin();
+
+ const isLoggedIn = await pleaseLogin();
+ if (!isLoggedIn) return;
os.apiWithDialog('flash/like', {
flashId: flash.value.id,
@@ -165,7 +167,9 @@ function like() {
async function unlike() {
if (!flash.value) return;
- pleaseLogin();
+
+ const isLoggedIn = await pleaseLogin();
+ if (!isLoggedIn) return;
const confirm = await os.confirm({
type: 'warning',
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index 0ae374649d..8438943126 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -197,7 +197,8 @@ async function matchHeatbeat() {
}
async function matchUser() {
- pleaseLogin();
+ const isLoggedIn = await pleaseLogin();
+ if (!isLoggedIn) return;
const user = await os.selectUser({ includeSelf: false, localOnly: true });
if (user == null) return;
@@ -207,8 +208,9 @@ async function matchUser() {
matchHeatbeat();
}
-function matchAny(ev: MouseEvent) {
- pleaseLogin();
+async function matchAny(ev: MouseEvent) {
+ const isLoggedIn = await pleaseLogin();
+ if (!isLoggedIn) return;
os.popupMenu([{
text: i18n.ts._reversi.allowIrregularRules,
diff --git a/packages/frontend/src/utility/please-login.ts b/packages/frontend/src/utility/please-login.ts
index 737e7d7c6e..8120a8d1af 100644
--- a/packages/frontend/src/utility/please-login.ts
+++ b/packages/frontend/src/utility/please-login.ts
@@ -48,8 +48,8 @@ export async function pleaseLogin(opts: {
path?: string;
message?: string;
openOnRemote?: OpenOnRemoteOptions;
-} = {}) {
- if ($i) return;
+} = {}): Promise {
+ if ($i != null) return true;
let _openOnRemote: OpenOnRemoteOptions | undefined = undefined;
@@ -71,5 +71,5 @@ export async function pleaseLogin(opts: {
closed: () => dispose(),
});
- throw new Error('signin required');
+ return false;
}
From 9c225384541192bbd83da94ac2f6c09ade3a25e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 2 Jan 2026 21:41:32 +0900
Subject: [PATCH 08/16] =?UTF-8?q?fix(frontend):=20=E3=83=95=E3=82=A1?=
=?UTF-8?q?=E3=82=A4=E3=83=AB=E3=82=BF=E3=83=96=E3=81=AE=E3=82=BB=E3=83=B3?=
=?UTF-8?q?=E3=82=B7=E3=83=86=E3=82=A3=E3=83=96=E3=83=A1=E3=83=87=E3=82=A3?=
=?UTF-8?q?=E3=82=A2=E3=82=92=E9=96=8B=E3=81=8F=E9=9A=9B=E3=81=AB=E7=A2=BA?=
=?UTF-8?q?=E8=AA=8D=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0=E3=82=92?=
=?UTF-8?q?=E5=87=BA=E3=81=99=E8=A8=AD=E5=AE=9A=E3=81=8C=E9=81=A9=E7=94=A8?=
=?UTF-8?q?=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92?=
=?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#17019)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(frontend): ファイルタブのセンシティブメディアを開く際に確認ダイアログを出す設定が適用されない問題を修正
* Update Changelog
* refactor
* Update Changelog
---
CHANGELOG.md | 1 +
.../frontend/src/components/MkMediaAudio.vue | 12 +++----
.../frontend/src/components/MkMediaBanner.vue | 15 +++------
.../frontend/src/components/MkMediaImage.vue | 13 +++-----
.../frontend/src/components/MkMediaVideo.vue | 11 +++----
.../src/components/MkNoteMediaGrid.vue | 28 ++++++++++++----
.../frontend/src/utility/sensitive-file.ts | 33 +++++++++++++++++++
7 files changed, 73 insertions(+), 40 deletions(-)
create mode 100644 packages/frontend/src/utility/sensitive-file.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e22dfba72a..669e6778ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@
- Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061
- Fix: 非ログイン時にログインを求めるダイアログが表示された後にダイアログのぼかしが解除されず操作不能になることがある問題を修正
- Fix: ドライブのソートが「登録日(昇順)」の場合に正しく動作しない問題を修正
+- Fix: ファイルタブのセンシティブメディアを開く際に確認ダイアログを出す設定が適用されない問題を修正
### Server
- Enhance: OAuthのクライアント情報取得(Client Information Discovery)において、IndieWeb Living Standard 11 July 2024で定義されているJSONドキュメント形式に対応しました
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index e3bb39549f..efcbf26a29 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -100,6 +100,7 @@ import { hms } from '@/filters/hms.js';
import MkMediaRange from '@/components/MkMediaRange.vue';
import { $i, iAmModerator } from '@/i.js';
import { prefer } from '@/preferences.js';
+import { canRevealFile, shouldHideFileByDefault } from '@/utility/sensitive-file.js';
const props = defineProps<{
audio: Misskey.entities.DriveFile;
@@ -154,16 +155,11 @@ function hasFocus() {
const playerEl = useTemplateRef('playerEl');
const audioEl = useTemplateRef('audioEl');
-// eslint-disable-next-line vue/no-setup-props-reactivity-loss
-const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore'));
+const hide = ref(shouldHideFileByDefault(props.audio));
async function reveal() {
- if (props.audio.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
- const { canceled } = await os.confirm({
- type: 'question',
- text: i18n.ts.sensitiveMediaRevealConfirm,
- });
- if (canceled) return;
+ if (!(await canRevealFile(props.audio))) {
+ return;
}
hide.value = false;
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index 7730e01a9f..fd86b61b87 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
{{ i18n.ts.sensitive }}
{{ i18n.ts.clickToShow }}
@@ -27,23 +27,18 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
-import * as os from '@/os.js';
import MkMediaAudio from '@/components/MkMediaAudio.vue';
-import { prefer } from '@/preferences.js';
+import { shouldHideFileByDefault, canRevealFile } from '@/utility/sensitive-file.js';
const props = defineProps<{
media: Misskey.entities.DriveFile;
}>();
-const hide = ref(true);
+const hide = ref(shouldHideFileByDefault(props.media));
async function reveal() {
- if (props.media.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
- const { canceled } = await os.confirm({
- type: 'question',
- text: i18n.ts.sensitiveMediaRevealConfirm,
- });
- if (canceled) return;
+ if (!(await canRevealFile(props.media))) {
+ return;
}
hide.value = false;
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index f59d15d9a2..345c261776 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -77,6 +77,7 @@ import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { $i, iAmModerator } from '@/i.js';
import { prefer } from '@/preferences.js';
+import { shouldHideFileByDefault, canRevealFile } from '@/utility/sensitive-file.js';
const props = withDefaults(defineProps<{
image: Misskey.entities.DriveFile;
@@ -106,12 +107,8 @@ async function reveal(ev: MouseEvent) {
if (hide.value) {
ev.stopPropagation();
- if (props.image.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
- const { canceled } = await os.confirm({
- type: 'question',
- text: i18n.ts.sensitiveMediaRevealConfirm,
- });
- if (canceled) return;
+ if (!(await canRevealFile(props.image))) {
+ return;
}
hide.value = false;
@@ -119,8 +116,8 @@ async function reveal(ev: MouseEvent) {
}
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
-watch(() => props.image, () => {
- hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.image.isSensitive && prefer.s.nsfw !== 'ignore');
+watch(() => props.image, (newImage) => {
+ hide.value = shouldHideFileByDefault(newImage);
}, {
deep: true,
immediate: true,
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index b0f7a909d3..63db3c3ab5 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -124,6 +124,7 @@ import hasAudio from '@/utility/media-has-audio.js';
import MkMediaRange from '@/components/MkMediaRange.vue';
import { $i, iAmModerator } from '@/i.js';
import { prefer } from '@/preferences.js';
+import { shouldHideFileByDefault, canRevealFile } from '@/utility/sensitive-file.js';
const props = defineProps<{
video: Misskey.entities.DriveFile;
@@ -176,15 +177,11 @@ function hasFocus() {
}
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
-const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore'));
+const hide = ref(shouldHideFileByDefault(props.video));
async function reveal() {
- if (props.video.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
- const { canceled } = await os.confirm({
- type: 'question',
- text: i18n.ts.sensitiveMediaRevealConfirm,
- });
- if (canceled) return;
+ if (!(await canRevealFile(props.video))) {
+ return;
}
hide.value = false;
diff --git a/packages/frontend/src/components/MkNoteMediaGrid.vue b/packages/frontend/src/components/MkNoteMediaGrid.vue
index 7e900b28fa..e46456d614 100644
--- a/packages/frontend/src/components/MkNoteMediaGrid.vue
+++ b/packages/frontend/src/components/MkNoteMediaGrid.vue
@@ -6,14 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only
();
const showingFiles = ref>(new Set());
+
+function isHiding(file: Misskey.entities.DriveFile) {
+ if (shouldHideFileByDefault(file) && !showingFiles.value.has(file.id)) {
+ if (!file.isSensitive && !file.type.startsWith('image/')) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+}
+
+async function reveal(file: Misskey.entities.DriveFile) {
+ if (!(await canRevealFile(file))) {
+ return;
+ }
+
+ showingFiles.value.add(file.id);
+}