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
@ -31,6 +31,7 @@
|
||||
- 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: TL上からノートが見えなくなるワードミュートであるハードミュートを追加
|
||||
- Enhance: 公開ロールにアサインされたときに通知が作成されるように
|
||||
- Enhance: アイコンデコレーションを複数設定できるように
|
||||
- Enhance: アイコンデコレーションの位置を微調整できるように
|
||||
- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072
|
||||
|
1
locales/index.d.ts
vendored
1
locales/index.d.ts
vendored
@ -2325,6 +2325,7 @@ export interface Locale {
|
||||
"pollEnded": string;
|
||||
"newNote": string;
|
||||
"unreadAntennaNote": string;
|
||||
"roleAssigned": string;
|
||||
"emptyPushNotificationMessage": string;
|
||||
"achievementEarned": string;
|
||||
"testNotification": string;
|
||||
|
@ -2227,6 +2227,7 @@ _notification:
|
||||
pollEnded: "アンケートの結果が出ました"
|
||||
newNote: "新しい投稿"
|
||||
unreadAntennaNote: "アンテナ {name}"
|
||||
roleAssigned: "ロールが付与されました"
|
||||
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
|
||||
achievementEarned: "実績を獲得"
|
||||
testNotification: "通知テスト"
|
||||
|
@ -6,7 +6,14 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
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 type { MiUser } from '@/models/User.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import type { Packed } from '@/misc/json-schema.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 = {
|
||||
gtlAvailable: boolean;
|
||||
@ -78,14 +86,17 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class RoleService implements OnApplicationShutdown {
|
||||
export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
private rolesCache: MemorySingleCache<MiRole[]>;
|
||||
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
|
||||
private notificationService: NotificationService;
|
||||
|
||||
public static AlreadyAssignedError = class extends Error {};
|
||||
public static NotAssignedError = class extends Error {};
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@ -120,6 +131,10 @@ export class RoleService implements OnApplicationShutdown {
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.notificationService = this.moduleRef.get(NotificationService.name);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
@ -427,6 +442,12 @@ export class RoleService implements OnApplicationShutdown {
|
||||
|
||||
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
|
||||
|
||||
if (role.isPublic) {
|
||||
this.notificationService.createNotification(userId, 'roleAssigned', {
|
||||
roleId: roleId,
|
||||
});
|
||||
}
|
||||
|
||||
if (moderator) {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||
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 { MiUserListMembership } from '@/models/UserListMembership.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { RedisKVCache } from '@/misc/cache.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserListService implements OnApplicationShutdown {
|
||||
|
@ -15,8 +15,8 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { FilterUnionByProperty, notificationTypes } from '@/types.js';
|
||||
import { RoleEntityService } from './RoleEntityService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||
import type { UserEntityService } from './UserEntityService.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 {
|
||||
private userEntityService: UserEntityService;
|
||||
private noteEntityService: NoteEntityService;
|
||||
private customEmojiService: CustomEmojiService;
|
||||
private roleEntityService: RoleEntityService;
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
@ -43,14 +43,13 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
|
||||
//private userEntityService: UserEntityService,
|
||||
//private noteEntityService: NoteEntityService,
|
||||
//private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.userEntityService = this.moduleRef.get('UserEntityService');
|
||||
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
||||
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
||||
this.roleEntityService = this.moduleRef.get('RoleEntityService');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@ -81,6 +80,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
detail: false,
|
||||
})
|
||||
) : undefined;
|
||||
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
|
||||
|
||||
return await awaitAll({
|
||||
id: notification.id,
|
||||
@ -92,6 +92,9 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
...(notification.type === 'reaction' ? {
|
||||
reaction: notification.reaction,
|
||||
} : {}),
|
||||
...(notification.type === 'roleAssigned' ? {
|
||||
role: role,
|
||||
} : {}),
|
||||
...(notification.type === 'achievementEarned' ? {
|
||||
achievement: notification.achievement,
|
||||
} : {}),
|
||||
|
@ -3,11 +3,10 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { notificationTypes } from '@/types.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiNote } from './Note.js';
|
||||
import { MiFollowRequest } from './FollowRequest.js';
|
||||
import { MiAccessToken } from './AccessToken.js';
|
||||
import { MiRole } from './Role.js';
|
||||
|
||||
export type MiNotification = {
|
||||
type: 'note';
|
||||
@ -68,6 +67,11 @@ export type MiNotification = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
notifierId: MiUser['id'];
|
||||
} | {
|
||||
type: 'roleAssigned';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
roleId: MiRole['id'];
|
||||
} | {
|
||||
type: 'achievementEarned';
|
||||
id: string;
|
||||
|
@ -554,9 +554,7 @@ export const packedMeDetailedOnlySchema = {
|
||||
mention: notificationRecieveConfig,
|
||||
reaction: notificationRecieveConfig,
|
||||
pollEnded: notificationRecieveConfig,
|
||||
achievementEarned: notificationRecieveConfig,
|
||||
receiveFollowRequest: notificationRecieveConfig,
|
||||
followRequestAccepted: notificationRecieveConfig,
|
||||
},
|
||||
},
|
||||
emailNotificationTypes: {
|
||||
|
@ -14,11 +14,26 @@
|
||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||
* receiveFollowRequest - フォローリクエストされた
|
||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||
* roleAssigned - ロールが付与された
|
||||
* achievementEarned - 実績を獲得
|
||||
* app - アプリ通知
|
||||
* 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 noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||
|
@ -19,6 +19,7 @@ import { CacheService } from '@/core/CacheService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { sleep } from '../utils.js';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import type { MockFunctionMetadata } from 'jest-mock';
|
||||
@ -32,6 +33,7 @@ describe('RoleService', () => {
|
||||
let rolesRepository: RolesRepository;
|
||||
let roleAssignmentsRepository: RoleAssignmentsRepository;
|
||||
let metaService: jest.Mocked<MetaService>;
|
||||
let notificationService: jest.Mocked<NotificationService>;
|
||||
let clock: lolex.InstalledClock;
|
||||
|
||||
function createUser(data: Partial<MiUser> = {}) {
|
||||
@ -76,6 +78,8 @@ describe('RoleService', () => {
|
||||
.useMocker((token) => {
|
||||
if (token === MetaService) {
|
||||
return { fetch: jest.fn() };
|
||||
} else if (token === NotificationService) {
|
||||
return { createNotification: jest.fn() };
|
||||
}
|
||||
if (typeof token === 'function') {
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
||||
@ -93,6 +97,7 @@ describe('RoleService', () => {
|
||||
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
|
||||
|
||||
metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>;
|
||||
notificationService = app.get<NotificationService>(NotificationService) as jest.Mocked<NotificationService>;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@ -273,4 +278,53 @@ describe('RoleService', () => {
|
||||
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">
|
||||
<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 === 'roleAssigned'" :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 === '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 === 'pollEnded'" class="ti ti-chart-arrows"></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使うと一部ブラウザで刺さるので念の為 -->
|
||||
<MkReactionIcon
|
||||
v-else-if="notification.type === 'reaction'"
|
||||
@ -50,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<header :class="$style.header">
|
||||
<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 === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</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>
|
||||
<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"/>
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
</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">
|
||||
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
|
||||
</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
|
||||
*/
|
||||
|
||||
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 ROLE_POLICIES = [
|
||||
|
@ -68,7 +68,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||
import { notificationTypes } from '@/const.js';
|
||||
|
||||
const nonConfigurableNotificationTypes = ['note'];
|
||||
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned'];
|
||||
|
||||
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
|
||||
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
|
||||
|
@ -1635,9 +1635,6 @@ type FetchLike = (input: string, init?: {
|
||||
// @public (undocumented)
|
||||
type FetchRssRequest = operations['fetch-rss']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
export const ffVisibility: readonly ["public", "followers", "private"];
|
||||
|
||||
// @public (undocumented)
|
||||
type Flash = components['schemas']['Flash'];
|
||||
|
||||
@ -1677,6 +1674,9 @@ type FlashUnlikeRequest = operations['flash/unlike']['requestBody']['content']['
|
||||
// @public (undocumented)
|
||||
type FlashUpdateRequest = operations['flash/update']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
export const followersVisibilities: readonly ["public", "followers", "private"];
|
||||
|
||||
// @public (undocumented)
|
||||
type Following = components['schemas']['Following'];
|
||||
|
||||
@ -1725,6 +1725,9 @@ type FollowingUpdateRequest = operations['following/update']['requestBody']['con
|
||||
// @public (undocumented)
|
||||
type FollowingUpdateResponse = operations['following/update']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
export const followingVisibilities: readonly ["public", "followers", "private"];
|
||||
|
||||
// @public (undocumented)
|
||||
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'];
|
||||
|
||||
// @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)
|
||||
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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user