-
+
+
![]()
+
{{ instanceName }}
+
{{ i18n.ts.signup }}
+
@@ -38,6 +40,7 @@ import { i18n } from '@/i18n.js';
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
import { mainRouter } from '@/router.js';
import { DI } from '@/di.js';
+import MkButton from '@/components/MkButton.vue';
const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
@@ -93,16 +96,26 @@ onMounted(() => {
min-width: 0;
}
-.homeButton {
- position: fixed;
- z-index: 1000;
- bottom: 16px;
- right: 16px;
- width: 60px;
- height: 60px;
+.header {
+ padding: 16px;
+ display: flex;
+ align-items: center;
background: var(--MI_THEME-panel);
- border-radius: 999px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.headerIcon {
+ width: 48px;
+ vertical-align: bottom;
+ border-radius: 8px;
+}
+
+.headerTitle {
+ margin: 0 16px;
+ font-weight: bold;
+}
+
+.headerButton {
+ margin-left: auto;
}
.side {
@@ -112,7 +125,7 @@ onMounted(() => {
background: var(--MI_THEME-accent);
}
-.banner {
+.sideBanner {
position: absolute;
top: 0;
left: 0;
@@ -124,7 +137,7 @@ onMounted(() => {
mask-image: linear-gradient(rgba(0, 0, 0, 1.0), transparent);
}
-.dashboard {
+.sideDashboard {
padding: 32px;
}
From a0d34940ff91688241f76cfd210ea88bbc68976d Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 9 Nov 2025 19:43:19 +0900
Subject: [PATCH 03/11] fix type
---
packages/frontend/src/pages/scratchpad.vue | 3 ++-
packages/frontend/src/widgets/WidgetAiscript.vue | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index d73363d058..4e02556c83 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -60,6 +60,7 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript';
import type { Ref } from 'vue';
import type { AsUiComponent } from '@/aiscript/ui.js';
import type { AsUiRoot } from '@/aiscript/ui.js';
+import type { Value } from '@syuilo/aiscript/interpreter/value.js';
import MkContainer from '@/components/MkContainer.vue';
import MkButton from '@/components/MkButton.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@@ -141,7 +142,7 @@ async function run() {
switch (type) {
case 'end': logs.value.push({
id: Math.random(),
- text: utils.valToString(params.val, true),
+ text: utils.valToString(params.val as Value, true),
print: false,
}); break;
default: break;
diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index a2d964718e..795c5a2cfa 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -24,6 +24,7 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript';
import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
+import type { Value } from '@syuilo/aiscript/interpreter/value.js';
import * as os from '@/os.js';
import MkContainer from '@/components/MkContainer.vue';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
@@ -83,7 +84,7 @@ const run = async () => {
switch (type) {
case 'end': logs.value.push({
id: genId(),
- text: utils.valToString(params.val, true),
+ text: utils.valToString(params.val as Value, true),
print: false,
}); break;
default: break;
From 4e38f218ec53a6a58df31a5a034a2796a5874d75 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 9 Nov 2025 19:44:07 +0900
Subject: [PATCH 04/11] fix type
---
.../frontend/src/components/MkCropperDialog.stories.impl.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts
index 7ac3e2a2cd..a97cb4fd2f 100644
--- a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts
+++ b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts
@@ -38,7 +38,7 @@ export const Default = {
};
},
args: {
- imageFile: file(),
+ imageFile: new File([], 'image.webp', { type: 'image/webp' }),
aspectRatio: NaN,
},
parameters: {
From 1ffc53f596125a98a3dd49155efe7ee0d0c06442 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 9 Nov 2025 19:49:27 +0900
Subject: [PATCH 05/11] use esnext to avoid type error
---
packages/frontend/tsconfig.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json
index 135bcc04cb..125a393417 100644
--- a/packages/frontend/tsconfig.json
+++ b/packages/frontend/tsconfig.json
@@ -10,7 +10,7 @@
"declaration": false,
"sourceMap": false,
"target": "ES2022",
- "module": "ES2022",
+ "module": "esnext",
"moduleResolution": "Bundler",
"removeComments": false,
"noLib": false,
From ca1bf21dcf654577f9831ed89b24740bfeeaf3f1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?=
<46447427+samunohito@users.noreply.github.com>
Date: Mon, 10 Nov 2025 10:31:49 +0900
Subject: [PATCH 06/11] =?UTF-8?q?chore:=20RoleService=E3=81=AEunit-test?=
=?UTF-8?q?=E8=BF=BD=E5=8A=A0=20(#16777)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/backend/test/unit/RoleService.ts | 255 ++++++++++++++++++++--
1 file changed, 237 insertions(+), 18 deletions(-)
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index 306836ea43..62789d28a2 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test';
import { setTimeout } from 'node:timers/promises';
-import { jest } from '@jest/globals';
+import { describe, jest } from '@jest/globals';
import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
@@ -168,6 +168,61 @@ describe('RoleService', () => {
await app.close();
});
+ describe('getUserAssigns', () => {
+ test('アサインされたロールを取得できる', async () => {
+ const user = await createUser();
+ const role1 = await createRole({ name: 'a' });
+ const role2 = await createRole({ name: 'b' });
+
+ await roleService.assign(user.id, role1.id);
+ await roleService.assign(user.id, role2.id);
+
+ const assigns = await roleService.getUserAssigns(user.id);
+ expect(assigns).toHaveLength(2);
+ expect(assigns.some(a => a.roleId === role1.id)).toBe(true);
+ expect(assigns.some(a => a.roleId === role2.id)).toBe(true);
+ });
+
+ test('アサインされたロールの有効/期限切れパターンを取得できる', async () => {
+ const user = await createUser();
+ const roleNoExpiry = await createRole({ name: 'no-expires' });
+ const roleNotExpired = await createRole({ name: 'not-expired' });
+ const roleExpired = await createRole({ name: 'expired' });
+
+ // expiresAtなし
+ await roleService.assign(user.id, roleNoExpiry.id);
+
+ // expiresAtあり(期限切れでない)
+ const future = new Date(Date.now() + 1000 * 60 * 60); // +1 hour
+ await roleService.assign(user.id, roleNotExpired.id, future);
+
+ // expiresAtあり(期限切れ)
+ await assignRole({ userId: user.id, roleId: roleExpired.id, expiresAt: new Date(Date.now() - 1000) });
+
+ const assigns = await roleService.getUserAssigns(user.id);
+ expect(assigns.some(a => a.roleId === roleNoExpiry.id)).toBe(true);
+ expect(assigns.some(a => a.roleId === roleNotExpired.id)).toBe(true);
+ expect(assigns.some(a => a.roleId === roleExpired.id)).toBe(false);
+ });
+ });
+
+ describe('getUserRoles', () => {
+ test('アサインされたロールとコンディショナルロールの両方が取得できる', async () => {
+ const user = await createUser();
+ const manualRole = await createRole({ name: 'manual role' });
+ const conditionalRole = await createConditionalRole({
+ id: aidx(),
+ type: 'isBot',
+ });
+ await roleService.assign(user.id, manualRole.id);
+ await roleService.assign(user.id, conditionalRole.id);
+
+ const roles = await roleService.getUserRoles(user.id);
+ expect(roles.some(r => r.id === manualRole.id)).toBe(true);
+ expect(roles.some(r => r.id === conditionalRole.id)).toBe(true);
+ });
+ });
+
describe('getUserPolicies', () => {
test('instance default policies', async () => {
const user = await createUser();
@@ -280,6 +335,112 @@ describe('RoleService', () => {
const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
});
+
+ test('role with no policy set', async () => {
+ const user = await createUser();
+ const roleWithPolicy = await createRole({
+ name: 'roleWithPolicy',
+ policies: {
+ pinLimit: {
+ useDefault: false,
+ priority: 0,
+ value: 10,
+ },
+ },
+ });
+ const roleWithoutPolicy = await createRole({
+ name: 'roleWithoutPolicy',
+ policies: {}, // ポリシーが空
+ });
+ await roleService.assign(user.id, roleWithPolicy.id);
+ await roleService.assign(user.id, roleWithoutPolicy.id);
+ meta.policies = {
+ pinLimit: 5,
+ };
+
+ const result = await roleService.getUserPolicies(user.id);
+
+ // roleWithoutPolicy は default 値 (5) を使い、roleWithPolicy の 10 と比較して大きい方が採用される
+ expect(result.pinLimit).toBe(10);
+ });
+ });
+
+ describe('getUserBadgeRoles', () => {
+ test('手動アサイン済みのバッジロールのみが返る', async () => {
+ const user = await createUser();
+ const badgeRole = await createRole({ name: 'badge', asBadge: true });
+ const normalRole = await createRole({ name: 'normal', asBadge: false });
+
+ await roleService.assign(user.id, badgeRole.id);
+ await roleService.assign(user.id, normalRole.id);
+
+ const roles = await roleService.getUserBadgeRoles(user.id);
+ expect(roles.some(r => r.id === badgeRole.id)).toBe(true);
+ expect(roles.some(r => r.id === normalRole.id)).toBe(false);
+ });
+
+ test('コンディショナルなバッジロールが条件一致で返る', async () => {
+ const user = await createUser({ isBot: true });
+ const condBadgeRole = await createConditionalRole({
+ id: aidx(),
+ type: 'isBot',
+ }, { asBadge: true, name: 'cond-badge' });
+ const condNonBadgeRole = await createConditionalRole({
+ id: aidx(),
+ type: 'isBot',
+ }, { asBadge: false, name: 'cond-non-badge' });
+
+ const roles = await roleService.getUserBadgeRoles(user.id);
+ expect(roles.some(r => r.id === condBadgeRole.id)).toBe(true);
+ expect(roles.some(r => r.id === condNonBadgeRole.id)).toBe(false);
+ });
+
+ test('roleAssignedTo 条件のバッジロール: アサイン有無で変化する', async () => {
+ const [user1, user2] = await Promise.all([createUser(), createUser()]);
+ const manualRole = await createRole({ name: 'manual' });
+ const condBadgeRole = await createConditionalRole({
+ id: aidx(),
+ type: 'roleAssignedTo',
+ roleId: manualRole.id,
+ }, { asBadge: true, name: 'assigned-badge' });
+
+ await roleService.assign(user2.id, manualRole.id);
+
+ const [roles1, roles2] = await Promise.all([
+ roleService.getUserBadgeRoles(user1.id),
+ roleService.getUserBadgeRoles(user2.id),
+ ]);
+ expect(roles1.some(r => r.id === condBadgeRole.id)).toBe(false);
+ expect(roles2.some(r => r.id === condBadgeRole.id)).toBe(true);
+ });
+
+ test('期限切れのバッジロールは除外される', async () => {
+ const user = await createUser();
+ const roleNoExpiry = await createRole({ name: 'no-exp', asBadge: true });
+ const roleNotExpired = await createRole({ name: 'not-expired', asBadge: true });
+ const roleExpired = await createRole({ name: 'expired', asBadge: true });
+
+ // expiresAt なし
+ await roleService.assign(user.id, roleNoExpiry.id);
+
+ // expiresAt あり(期限切れでない)
+ const future = new Date(Date.now() + 1000 * 60 * 60); // +1 hour
+ await roleService.assign(user.id, roleNotExpired.id, future);
+
+ // expiresAt あり(期限切れ)
+ await assignRole({ userId: user.id, roleId: roleExpired.id, expiresAt: new Date(Date.now() - 1000) });
+
+ const rolesBefore = await roleService.getUserBadgeRoles(user.id);
+ expect(rolesBefore.some(r => r.id === roleNoExpiry.id)).toBe(true);
+ expect(rolesBefore.some(r => r.id === roleNotExpired.id)).toBe(true);
+ expect(rolesBefore.some(r => r.id === roleExpired.id)).toBe(false);
+
+ // 時間経過で roleNotExpired を失効させる
+ clock.tick('02:00:00');
+ const rolesAfter = await roleService.getUserBadgeRoles(user.id);
+ expect(rolesAfter.some(r => r.id === roleNoExpiry.id)).toBe(true);
+ expect(rolesAfter.some(r => r.id === roleNotExpired.id)).toBe(false);
+ });
});
describe('getModeratorIds', () => {
@@ -413,9 +574,9 @@ describe('RoleService', () => {
expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]);
});
- test('root has moderator role', async () => {
- const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
- createUser(), createUser(), createUser(), createRoot(),
+ test('includeAdmins = false, includeRoot = true, excludeExpire = true', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -424,9 +585,11 @@ describe('RoleService', () => {
await Promise.all([
assignRole({ userId: adminUser1.id, roleId: role1.id }),
+ assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
assignRole({ userId: modeUser1.id, roleId: role2.id }),
- assignRole({ userId: rootUser.id, roleId: role2.id }),
+ assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
assignRole({ userId: normalUser1.id, roleId: role3.id }),
+ assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
const result = await roleService.getModeratorIds({
@@ -434,12 +597,12 @@ describe('RoleService', () => {
includeRoot: true,
excludeExpire: false,
});
- expect(result).toEqual([modeUser1.id, rootUser.id]);
+ expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]);
});
- test('root has administrator role', async () => {
- const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
- createUser(), createUser(), createUser(), createRoot(),
+ test('includeAdmins = true, includeRoot = true, excludeExpire = false', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -448,9 +611,11 @@ describe('RoleService', () => {
await Promise.all([
assignRole({ userId: adminUser1.id, roleId: role1.id }),
- assignRole({ userId: rootUser.id, roleId: role1.id }),
+ assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
assignRole({ userId: modeUser1.id, roleId: role2.id }),
+ assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
assignRole({ userId: normalUser1.id, roleId: role3.id }),
+ assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
const result = await roleService.getModeratorIds({
@@ -458,12 +623,12 @@ describe('RoleService', () => {
includeRoot: true,
excludeExpire: false,
});
- expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]);
+ expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id, rootUser.id]);
});
- test('root has moderator role(expire)', async () => {
- const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
- createUser(), createUser(), createUser(), createRoot(),
+ test('includeAdmins = true, includeRoot = true, excludeExpire = true', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -472,17 +637,71 @@ describe('RoleService', () => {
await Promise.all([
assignRole({ userId: adminUser1.id, roleId: role1.id }),
- assignRole({ userId: modeUser1.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
- assignRole({ userId: rootUser.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+ assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
+ assignRole({ userId: modeUser1.id, roleId: role2.id }),
+ assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
assignRole({ userId: normalUser1.id, roleId: role3.id }),
+ assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
const result = await roleService.getModeratorIds({
- includeAdmins: false,
+ includeAdmins: true,
includeRoot: true,
excludeExpire: true,
});
- expect(result).toEqual([rootUser.id]);
+ expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]);
+ });
+ });
+
+ describe('getAdministratorIds', () => {
+ test('should return only user IDs with administrator roles', async () => {
+ const adminUser1 = await createUser();
+ const adminUser2 = await createUser();
+ const normalUser = await createUser();
+ const moderatorUser = await createUser();
+
+ const adminRole = await createRole({ name: 'admin', isAdministrator: true, isModerator: false });
+ const moderatorRole = await createRole({ name: 'moderator', isModerator: true, isAdministrator: false });
+ const normalRole = await createRole({ name: 'normal', isAdministrator: false, isModerator: false });
+
+ await roleService.assign(adminUser1.id, adminRole.id);
+ await roleService.assign(adminUser2.id, adminRole.id);
+ await roleService.assign(moderatorUser.id, moderatorRole.id);
+ await roleService.assign(normalUser.id, normalRole.id);
+
+ const adminIds = await roleService.getAdministratorIds();
+
+ // sort for deterministic order
+ adminIds.sort();
+ const expectedIds = [adminUser1.id, adminUser2.id].sort();
+
+ expect(adminIds).toEqual(expectedIds);
+ });
+
+ test('should return an empty array if no users have administrator roles', async () => {
+ const normalUser = await createUser();
+ const normalRole = await createRole({ name: 'normal', isAdministrator: false });
+ await roleService.assign(normalUser.id, normalRole.id);
+
+ const adminIds = await roleService.getAdministratorIds();
+
+ expect(adminIds).toHaveLength(0);
+ });
+
+ test('should return an empty array if there are no administrator roles defined', async () => {
+ await createUser(); // create user to ensure not empty db
+ const adminIds = await roleService.getAdministratorIds();
+ expect(adminIds).toHaveLength(0);
+ });
+
+ // TODO: rootユーザーは現在実装に含まれていないため、テストもそれに倣う
+ test('should not include the root user', async () => {
+ const rootUser = await createUser();
+ meta.rootUserId = rootUser.id;
+
+ const adminIds = await roleService.getAdministratorIds();
+
+ expect(adminIds).not.toContain(rootUser.id);
});
});
From 23d2d191a0b0dfc8ce3a369d0077dc5991ea42f4 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 10 Nov 2025 13:23:23 +0900
Subject: [PATCH 07/11] =?UTF-8?q?chore(frontend):=20=E3=82=A2=E3=83=8B?=
=?UTF-8?q?=E3=83=A1=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E7=94=BB=E5=83=8F?=
=?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=82=92=E3=83=91=E3=83=95=E3=82=A9=E3=83=BC?=
=?UTF-8?q?=E3=83=9E=E3=83=B3=E3=82=B9=E3=82=BB=E3=82=AF=E3=82=B7=E3=83=A7?=
=?UTF-8?q?=E3=83=B3=E3=81=AB=E3=82=82=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
locales/index.d.ts | 4 ++++
locales/ja-JP.yml | 1 +
packages/frontend/src/pages/settings/preferences.vue | 12 ++++++++++++
3 files changed, 17 insertions(+)
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 0d0c1cfc53..0c93055b39 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -6187,6 +6187,10 @@ export interface Locale extends ILocale {
* 絵文字ピッカーに固定表示するプリセットをパレットとして登録したり、ピッカーの表示方法をカスタマイズしたりできます。
*/
"emojiPaletteBanner": string;
+ /**
+ * アニメーション画像を有効にする
+ */
+ "enableAnimatedImages": string;
"_chat": {
/**
* 送信者の名前を表示
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index e40c083cff..bcd2b9add6 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1554,6 +1554,7 @@ _settings:
showAvailableReactionsFirstInNote: "利用できるリアクションを先頭に表示"
showPageTabBarBottom: "ページのタブバーを下部に表示"
emojiPaletteBanner: "絵文字ピッカーに固定表示するプリセットをパレットとして登録したり、ピッカーの表示方法をカスタマイズしたりできます。"
+ enableAnimatedImages: "アニメーション画像を有効にする"
_chat:
showSenderName: "送信者の名前を表示"
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 5e3f148710..972b50f8cd 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -603,6 +603,18 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+ disableShowingAnimatedImages = !v">
+ {{ i18n.ts._settings.enableAnimatedImages }}
+
+ {{ i18n.ts.turnOffToImprovePerformance }}
+ {{ i18n.ts.disableShowingAnimatedImages_caption }}
+
+
+
+
+
From 73bcd330f7c409f444e2ec34d088dbe5f0c543ca Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 10 Nov 2025 14:09:15 +0900
Subject: [PATCH 08/11] fix(backend): improve isFederationAllowedHost
---
packages/backend/src/core/UtilityService.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index 67ec6cc7b0..21ea9b9983 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -133,6 +133,7 @@ export class UtilityService {
@bindThis
public isFederationAllowedHost(host: string): boolean {
+ if (this.isSelfHost(host)) return true;
if (this.meta.federation === 'none') return false;
if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
if (this.isBlockedHost(this.meta.blockedHosts, host)) return false;
From fd2fe34270ebd406d12a55f33b74028dec31cfd2 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: Mon, 10 Nov 2025 15:33:54 +0900
Subject: [PATCH 09/11] =?UTF-8?q?refactor8frontend9:=20any=E3=82=92?=
=?UTF-8?q?=E9=99=A4=E5=8E=BB=20(#16778)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../frontend/src/components/MkChannelList.vue | 9 +--
.../frontend/src/components/MkUserList.vue | 8 +-
packages/frontend/src/composables/use-form.ts | 11 ++-
packages/frontend/src/di.ts | 3 +-
.../src/directives/adaptive-border.ts | 2 +-
.../frontend/src/directives/user-preview.ts | 6 +-
packages/frontend/src/lib/pizzax.ts | 2 +-
packages/frontend/src/pages/admin-user.vue | 2 +-
packages/frontend/src/pages/admin/relays.vue | 2 +-
.../frontend/src/pages/install-extensions.vue | 6 +-
packages/frontend/src/stream.ts | 2 -
packages/frontend/src/utility/drive.ts | 2 +-
.../utility/image-effector/ImageEffector.ts | 2 +-
packages/frontend/src/utility/paginator.ts | 11 ++-
packages/frontend/src/utility/stream-mock.ts | 81 -------------------
.../utility/watermark/WatermarkRenderer.ts | 3 +-
16 files changed, 42 insertions(+), 110 deletions(-)
delete mode 100644 packages/frontend/src/utility/stream-mock.ts
diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue
index 394dcb6bd1..23bb32c6b9 100644
--- a/packages/frontend/src/components/MkChannelList.vue
+++ b/packages/frontend/src/components/MkChannelList.vue
@@ -13,17 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only
-