enhance: ロールにアサインされたときの通知 (#12607)
* wip * Update misskey-js.api.md * Update CHANGELOG.md * Update RoleService.ts * Update locales/ja-JP.yml Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> * Update UserListService.ts * Update misskey-js.api.md * fix (#12724) --------- Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Co-authored-by: おさむのひと <46447427+samunohito@users.noreply.github.com>
This commit is contained in:
parent
d14eb20122
commit
15b0d2aff2
15 changed files with 143 additions and 22 deletions
|
@ -31,6 +31,7 @@
|
||||||
- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
|
- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
|
||||||
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
|
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
|
||||||
- Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加
|
- Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加
|
||||||
|
- Enhance: 公開ロールにアサインされたときに通知が作成されるように
|
||||||
- Enhance: アイコンデコレーションを複数設定できるように
|
- Enhance: アイコンデコレーションを複数設定できるように
|
||||||
- Enhance: アイコンデコレーションの位置を微調整できるように
|
- Enhance: アイコンデコレーションの位置を微調整できるように
|
||||||
- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072
|
- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072
|
||||||
|
|
1
locales/index.d.ts
vendored
1
locales/index.d.ts
vendored
|
@ -2325,6 +2325,7 @@ export interface Locale {
|
||||||
"pollEnded": string;
|
"pollEnded": string;
|
||||||
"newNote": string;
|
"newNote": string;
|
||||||
"unreadAntennaNote": string;
|
"unreadAntennaNote": string;
|
||||||
|
"roleAssigned": string;
|
||||||
"emptyPushNotificationMessage": string;
|
"emptyPushNotificationMessage": string;
|
||||||
"achievementEarned": string;
|
"achievementEarned": string;
|
||||||
"testNotification": string;
|
"testNotification": string;
|
||||||
|
|
|
@ -2227,6 +2227,7 @@ _notification:
|
||||||
pollEnded: "アンケートの結果が出ました"
|
pollEnded: "アンケートの結果が出ました"
|
||||||
newNote: "新しい投稿"
|
newNote: "新しい投稿"
|
||||||
unreadAntennaNote: "アンテナ {name}"
|
unreadAntennaNote: "アンテナ {name}"
|
||||||
|
roleAssigned: "ロールが付与されました"
|
||||||
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
||||||
achievementEarned: "実績を獲得"
|
achievementEarned: "実績を獲得"
|
||||||
testNotification: "通知テスト"
|
testNotification: "通知テスト"
|
||||||
|
|
|
@ -6,7 +6,14 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import type { MiRole, MiRoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
import type {
|
||||||
|
MiRole,
|
||||||
|
MiRoleAssignment,
|
||||||
|
RoleAssignmentsRepository,
|
||||||
|
RolesRepository,
|
||||||
|
UsersRepository,
|
||||||
|
} from '@/models/_.js';
|
||||||
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
|
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -16,12 +23,13 @@ import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
import type { RoleCondFormulaValue } from '@/models/Role.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
|
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||||
|
|
||||||
export type RolePolicies = {
|
export type RolePolicies = {
|
||||||
gtlAvailable: boolean;
|
gtlAvailable: boolean;
|
||||||
|
@ -78,14 +86,17 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RoleService implements OnApplicationShutdown {
|
export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
private rolesCache: MemorySingleCache<MiRole[]>;
|
private rolesCache: MemorySingleCache<MiRole[]>;
|
||||||
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
|
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
|
||||||
|
private notificationService: NotificationService;
|
||||||
|
|
||||||
public static AlreadyAssignedError = class extends Error {};
|
public static AlreadyAssignedError = class extends Error {};
|
||||||
public static NotAssignedError = class extends Error {};
|
public static NotAssignedError = class extends Error {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private moduleRef: ModuleRef,
|
||||||
|
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redis)
|
||||||
private redisClient: Redis.Redis,
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
|
@ -120,6 +131,10 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
this.redisForSub.on('message', this.onMessage);
|
this.redisForSub.on('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
this.notificationService = this.moduleRef.get(NotificationService.name);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onMessage(_: string, data: string): Promise<void> {
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
const obj = JSON.parse(data);
|
const obj = JSON.parse(data);
|
||||||
|
@ -427,6 +442,12 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
|
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
|
||||||
|
|
||||||
|
if (role.isPublic) {
|
||||||
|
this.notificationService.createNotification(userId, 'roleAssigned', {
|
||||||
|
roleId: roleId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (moderator) {
|
if (moderator) {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||||
this.moderationLogService.log(moderator, 'assignRole', {
|
this.moderationLogService.log(moderator, 'assignRole', {
|
||||||
|
|
|
@ -10,15 +10,15 @@ import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiUserList } from '@/models/UserList.js';
|
import type { MiUserList } from '@/models/UserList.js';
|
||||||
import type { MiUserListMembership } from '@/models/UserListMembership.js';
|
import type { MiUserListMembership } from '@/models/UserListMembership.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { RedisKVCache } from '@/misc/cache.js';
|
import { RedisKVCache } from '@/misc/cache.js';
|
||||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserListService implements OnApplicationShutdown {
|
export class UserListService implements OnApplicationShutdown {
|
||||||
|
|
|
@ -15,8 +15,8 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isNotNull } from '@/misc/is-not-null.js';
|
import { isNotNull } from '@/misc/is-not-null.js';
|
||||||
import { FilterUnionByProperty, notificationTypes } from '@/types.js';
|
import { FilterUnionByProperty, notificationTypes } from '@/types.js';
|
||||||
|
import { RoleEntityService } from './RoleEntityService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
|
||||||
import type { UserEntityService } from './UserEntityService.js';
|
import type { UserEntityService } from './UserEntityService.js';
|
||||||
import type { NoteEntityService } from './NoteEntityService.js';
|
import type { NoteEntityService } from './NoteEntityService.js';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 're
|
||||||
export class NotificationEntityService implements OnModuleInit {
|
export class NotificationEntityService implements OnModuleInit {
|
||||||
private userEntityService: UserEntityService;
|
private userEntityService: UserEntityService;
|
||||||
private noteEntityService: NoteEntityService;
|
private noteEntityService: NoteEntityService;
|
||||||
private customEmojiService: CustomEmojiService;
|
private roleEntityService: RoleEntityService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
@ -43,14 +43,13 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
|
|
||||||
//private userEntityService: UserEntityService,
|
//private userEntityService: UserEntityService,
|
||||||
//private noteEntityService: NoteEntityService,
|
//private noteEntityService: NoteEntityService,
|
||||||
//private customEmojiService: CustomEmojiService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
this.userEntityService = this.moduleRef.get('UserEntityService');
|
this.userEntityService = this.moduleRef.get('UserEntityService');
|
||||||
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
||||||
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
this.roleEntityService = this.moduleRef.get('RoleEntityService');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -81,6 +80,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
detail: false,
|
detail: false,
|
||||||
})
|
})
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
|
||||||
|
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
|
@ -92,6 +92,9 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
...(notification.type === 'reaction' ? {
|
...(notification.type === 'reaction' ? {
|
||||||
reaction: notification.reaction,
|
reaction: notification.reaction,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
...(notification.type === 'roleAssigned' ? {
|
||||||
|
role: role,
|
||||||
|
} : {}),
|
||||||
...(notification.type === 'achievementEarned' ? {
|
...(notification.type === 'achievementEarned' ? {
|
||||||
achievement: notification.achievement,
|
achievement: notification.achievement,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
|
@ -3,11 +3,10 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { notificationTypes } from '@/types.js';
|
|
||||||
import { MiUser } from './User.js';
|
import { MiUser } from './User.js';
|
||||||
import { MiNote } from './Note.js';
|
import { MiNote } from './Note.js';
|
||||||
import { MiFollowRequest } from './FollowRequest.js';
|
|
||||||
import { MiAccessToken } from './AccessToken.js';
|
import { MiAccessToken } from './AccessToken.js';
|
||||||
|
import { MiRole } from './Role.js';
|
||||||
|
|
||||||
export type MiNotification = {
|
export type MiNotification = {
|
||||||
type: 'note';
|
type: 'note';
|
||||||
|
@ -68,6 +67,11 @@ export type MiNotification = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
notifierId: MiUser['id'];
|
notifierId: MiUser['id'];
|
||||||
|
} | {
|
||||||
|
type: 'roleAssigned';
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
roleId: MiRole['id'];
|
||||||
} | {
|
} | {
|
||||||
type: 'achievementEarned';
|
type: 'achievementEarned';
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -554,9 +554,7 @@ export const packedMeDetailedOnlySchema = {
|
||||||
mention: notificationRecieveConfig,
|
mention: notificationRecieveConfig,
|
||||||
reaction: notificationRecieveConfig,
|
reaction: notificationRecieveConfig,
|
||||||
pollEnded: notificationRecieveConfig,
|
pollEnded: notificationRecieveConfig,
|
||||||
achievementEarned: notificationRecieveConfig,
|
|
||||||
receiveFollowRequest: notificationRecieveConfig,
|
receiveFollowRequest: notificationRecieveConfig,
|
||||||
followRequestAccepted: notificationRecieveConfig,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emailNotificationTypes: {
|
emailNotificationTypes: {
|
||||||
|
|
|
@ -14,11 +14,26 @@
|
||||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||||
* receiveFollowRequest - フォローリクエストされた
|
* receiveFollowRequest - フォローリクエストされた
|
||||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||||
|
* roleAssigned - ロールが付与された
|
||||||
* achievementEarned - 実績を獲得
|
* achievementEarned - 実績を獲得
|
||||||
* app - アプリ通知
|
* app - アプリ通知
|
||||||
* test - テスト通知(サーバー側)
|
* test - テスト通知(サーバー側)
|
||||||
*/
|
*/
|
||||||
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const;
|
export const notificationTypes = [
|
||||||
|
'note',
|
||||||
|
'follow',
|
||||||
|
'mention',
|
||||||
|
'reply',
|
||||||
|
'renote',
|
||||||
|
'quote',
|
||||||
|
'reaction',
|
||||||
|
'pollEnded',
|
||||||
|
'receiveFollowRequest',
|
||||||
|
'followRequestAccepted',
|
||||||
|
'roleAssigned',
|
||||||
|
'achievementEarned',
|
||||||
|
'app',
|
||||||
|
'test'] as const;
|
||||||
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
||||||
|
|
||||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { CacheService } from '@/core/CacheService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { sleep } from '../utils.js';
|
import { sleep } from '../utils.js';
|
||||||
import type { TestingModule } from '@nestjs/testing';
|
import type { TestingModule } from '@nestjs/testing';
|
||||||
import type { MockFunctionMetadata } from 'jest-mock';
|
import type { MockFunctionMetadata } from 'jest-mock';
|
||||||
|
@ -32,6 +33,7 @@ describe('RoleService', () => {
|
||||||
let rolesRepository: RolesRepository;
|
let rolesRepository: RolesRepository;
|
||||||
let roleAssignmentsRepository: RoleAssignmentsRepository;
|
let roleAssignmentsRepository: RoleAssignmentsRepository;
|
||||||
let metaService: jest.Mocked<MetaService>;
|
let metaService: jest.Mocked<MetaService>;
|
||||||
|
let notificationService: jest.Mocked<NotificationService>;
|
||||||
let clock: lolex.InstalledClock;
|
let clock: lolex.InstalledClock;
|
||||||
|
|
||||||
function createUser(data: Partial<MiUser> = {}) {
|
function createUser(data: Partial<MiUser> = {}) {
|
||||||
|
@ -76,6 +78,8 @@ describe('RoleService', () => {
|
||||||
.useMocker((token) => {
|
.useMocker((token) => {
|
||||||
if (token === MetaService) {
|
if (token === MetaService) {
|
||||||
return { fetch: jest.fn() };
|
return { fetch: jest.fn() };
|
||||||
|
} else if (token === NotificationService) {
|
||||||
|
return { createNotification: jest.fn() };
|
||||||
}
|
}
|
||||||
if (typeof token === 'function') {
|
if (typeof token === 'function') {
|
||||||
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
||||||
|
@ -93,6 +97,7 @@ describe('RoleService', () => {
|
||||||
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
|
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
|
||||||
|
|
||||||
metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>;
|
metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>;
|
||||||
|
notificationService = app.get<NotificationService>(NotificationService) as jest.Mocked<NotificationService>;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
@ -273,4 +278,53 @@ describe('RoleService', () => {
|
||||||
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
|
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('assign', () => {
|
||||||
|
test('公開ロールの場合は通知される', async () => {
|
||||||
|
const user = await createUser();
|
||||||
|
const role = await createRole({
|
||||||
|
isPublic: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await roleService.assign(user.id, role.id);
|
||||||
|
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
const assignments = await roleAssignmentsRepository.find({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
roleId: role.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(assignments).toHaveLength(1);
|
||||||
|
|
||||||
|
expect(notificationService.createNotification).toHaveBeenCalled();
|
||||||
|
expect(notificationService.createNotification.mock.lastCall![0]).toBe(user.id);
|
||||||
|
expect(notificationService.createNotification.mock.lastCall![1]).toBe('roleAssigned');
|
||||||
|
expect(notificationService.createNotification.mock.lastCall![2]).toBe({
|
||||||
|
roleId: role.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('非公開ロールの場合は通知されない', async () => {
|
||||||
|
const user = await createUser();
|
||||||
|
const role = await createRole({
|
||||||
|
isPublic: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await roleService.assign(user.id, role.id);
|
||||||
|
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
const assignments = await roleAssignmentsRepository.find({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
roleId: role.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(assignments).toHaveLength(1);
|
||||||
|
|
||||||
|
expect(notificationService.createNotification).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.head">
|
<div :class="$style.head">
|
||||||
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
|
<MkAvatar v-else-if="notification.type === 'roleAssigned'" :class="$style.icon" :user="$i" link preview/>
|
||||||
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
|
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
|
||||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||||
|
@ -36,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||||
|
<img v-else-if="notification.type === 'roleAssigned'" :src="notification.role.iconUrl" alt=""/>
|
||||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||||
<MkReactionIcon
|
<MkReactionIcon
|
||||||
v-else-if="notification.type === 'reaction'"
|
v-else-if="notification.type === 'reaction'"
|
||||||
|
@ -50,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<header :class="$style.header">
|
<header :class="$style.header">
|
||||||
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
||||||
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
|
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
|
||||||
|
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
||||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||||
|
@ -86,6 +89,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
|
||||||
|
{{ notification.role.name }}
|
||||||
|
</div>
|
||||||
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
||||||
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
|
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
|
||||||
</MkA>
|
</MkA>
|
||||||
|
|
|
@ -54,7 +54,21 @@ https://github.com/sindresorhus/file-type/blob/main/core.js
|
||||||
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
|
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const;
|
export const notificationTypes = [
|
||||||
|
'note',
|
||||||
|
'follow',
|
||||||
|
'mention',
|
||||||
|
'reply',
|
||||||
|
'renote',
|
||||||
|
'quote',
|
||||||
|
'reaction',
|
||||||
|
'pollEnded',
|
||||||
|
'receiveFollowRequest',
|
||||||
|
'followRequestAccepted',
|
||||||
|
'roleAssigned',
|
||||||
|
'achievementEarned',
|
||||||
|
'app',
|
||||||
|
] as const;
|
||||||
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
||||||
|
|
||||||
export const ROLE_POLICIES = [
|
export const ROLE_POLICIES = [
|
||||||
|
|
|
@ -68,7 +68,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||||
import { notificationTypes } from '@/const.js';
|
import { notificationTypes } from '@/const.js';
|
||||||
|
|
||||||
const nonConfigurableNotificationTypes = ['note'];
|
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned'];
|
||||||
|
|
||||||
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
|
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
|
||||||
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
|
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
|
||||||
|
|
|
@ -1635,9 +1635,6 @@ type FetchLike = (input: string, init?: {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type FetchRssRequest = operations['fetch-rss']['requestBody']['content']['application/json'];
|
type FetchRssRequest = operations['fetch-rss']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export const ffVisibility: readonly ["public", "followers", "private"];
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type Flash = components['schemas']['Flash'];
|
type Flash = components['schemas']['Flash'];
|
||||||
|
|
||||||
|
@ -1677,6 +1674,9 @@ type FlashUnlikeRequest = operations['flash/unlike']['requestBody']['content']['
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type FlashUpdateRequest = operations['flash/update']['requestBody']['content']['application/json'];
|
type FlashUpdateRequest = operations['flash/update']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const followersVisibilities: readonly ["public", "followers", "private"];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type Following = components['schemas']['Following'];
|
type Following = components['schemas']['Following'];
|
||||||
|
|
||||||
|
@ -1725,6 +1725,9 @@ type FollowingUpdateRequest = operations['following/update']['requestBody']['con
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type FollowingUpdateResponse = operations['following/update']['responses']['200']['content']['application/json'];
|
type FollowingUpdateResponse = operations['following/update']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const followingVisibilities: readonly ["public", "followers", "private"];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type GalleryFeaturedRequest = operations['gallery/featured']['requestBody']['content']['application/json'];
|
type GalleryFeaturedRequest = operations['gallery/featured']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
@ -2337,7 +2340,7 @@ type Notification_2 = components['schemas']['Notification'];
|
||||||
type NotificationsCreateRequest = operations['notifications/create']['requestBody']['content']['application/json'];
|
type NotificationsCreateRequest = operations['notifications/create']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "achievementEarned"];
|
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned"];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type Page = components['schemas']['Page'];
|
type Page = components['schemas']['Page'];
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'achievementEarned'] as const;
|
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const;
|
||||||
|
|
||||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue