diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d0cc4e73..9cdafe6b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ - Enhance: プッシュ通知を行うための権限確認をより確実に行うように - Enhance: 投稿フォームのチュートリアルを追加 - Enhance: 「自動でもっと見る」をほとんどの箇所で利用可能に +- Enhance: アンテナ・リスト設定画面とタイムラインの動線を改善 + - アンテナ・リスト一覧画面の項目を選択すると、設定画面ではなくタイムラインに移動するようになりました + - アンテナ・リストの設定画面の右上にタイムラインに移動するボタンを追加しました - Enhance: ミュートの付与期間を自由に設定できるように - Enhance: ロールの付与期間を自由に設定できるように - Fix: 紙吹雪エフェクトがアニメーション設定を考慮せず常に表示される問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index 652902e51d..841bfc5d0b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -6207,6 +6207,10 @@ export interface Locale extends ILocale { * 絵文字ピッカーに固定表示するプリセットをパレットとして登録したり、ピッカーの表示方法をカスタマイズしたりできます。 */ "emojiPaletteBanner": string; + /** + * アニメーション画像を有効にする + */ + "enableAnimatedImages": string; "_chat": { /** * 送信者の名前を表示 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 14eb74fb2e..6dcc4914e0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1559,6 +1559,7 @@ _settings: showAvailableReactionsFirstInNote: "利用できるリアクションを先頭に表示" showPageTabBarBottom: "ページのタブバーを下部に表示" emojiPaletteBanner: "絵文字ピッカーに固定表示するプリセットをパレットとして登録したり、ピッカーの表示方法をカスタマイズしたりできます。" + enableAnimatedImages: "アニメーション画像を有効にする" _chat: showSenderName: "送信者の名前を表示" 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; 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); }); }); 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 -