parent
							
								
									f195fa4ab9
								
							
						
					
					
						commit
						e3f151e230
					
				
					 25 changed files with 238 additions and 31 deletions
				
			
		| 
						 | 
					@ -26,6 +26,7 @@
 | 
				
			||||||
- Feat: 二要素認証のバックアップコードが生成されるようになりました
 | 
					- Feat: 二要素認証のバックアップコードが生成されるようになりました
 | 
				
			||||||
	- ref. https://github.com/MisskeyIO/misskey/pull/121
 | 
						- ref. https://github.com/MisskeyIO/misskey/pull/121
 | 
				
			||||||
- Feat: 二要素認証でパスキーをサポートするようになりました
 | 
					- Feat: 二要素認証でパスキーをサポートするようになりました
 | 
				
			||||||
 | 
					- Feat: 指定したユーザーが投稿したときに通知できるようになりました
 | 
				
			||||||
- Feat: プロフィールでのリンク検証
 | 
					- Feat: プロフィールでのリンク検証
 | 
				
			||||||
- Feat: 通知をテストできるようになりました
 | 
					- Feat: 通知をテストできるようになりました
 | 
				
			||||||
- Feat: PWAのアイコンが設定できるようになりました
 | 
					- Feat: PWAのアイコンが設定できるようになりました
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										3
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1117,6 +1117,8 @@ export interface Locale {
 | 
				
			||||||
    "pinnedList": string;
 | 
					    "pinnedList": string;
 | 
				
			||||||
    "keepScreenOn": string;
 | 
					    "keepScreenOn": string;
 | 
				
			||||||
    "verifiedLink": string;
 | 
					    "verifiedLink": string;
 | 
				
			||||||
 | 
					    "notifyNotes": string;
 | 
				
			||||||
 | 
					    "unnotifyNotes": string;
 | 
				
			||||||
    "_announcement": {
 | 
					    "_announcement": {
 | 
				
			||||||
        "forExistingUsers": string;
 | 
					        "forExistingUsers": string;
 | 
				
			||||||
        "forExistingUsersDescription": string;
 | 
					        "forExistingUsersDescription": string;
 | 
				
			||||||
| 
						 | 
					@ -2150,6 +2152,7 @@ export interface Locale {
 | 
				
			||||||
        "youReceivedFollowRequest": string;
 | 
					        "youReceivedFollowRequest": string;
 | 
				
			||||||
        "yourFollowRequestAccepted": string;
 | 
					        "yourFollowRequestAccepted": string;
 | 
				
			||||||
        "pollEnded": string;
 | 
					        "pollEnded": string;
 | 
				
			||||||
 | 
					        "newNote": string;
 | 
				
			||||||
        "unreadAntennaNote": string;
 | 
					        "unreadAntennaNote": string;
 | 
				
			||||||
        "emptyPushNotificationMessage": string;
 | 
					        "emptyPushNotificationMessage": string;
 | 
				
			||||||
        "achievementEarned": string;
 | 
					        "achievementEarned": string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1114,6 +1114,8 @@ loadConversation: "会話を見る"
 | 
				
			||||||
pinnedList: "ピン留めされたリスト"
 | 
					pinnedList: "ピン留めされたリスト"
 | 
				
			||||||
keepScreenOn: "デバイスの画面を常にオンにする"
 | 
					keepScreenOn: "デバイスの画面を常にオンにする"
 | 
				
			||||||
verifiedLink: "このリンク先の所有者であることが確認されました"
 | 
					verifiedLink: "このリンク先の所有者であることが確認されました"
 | 
				
			||||||
 | 
					notifyNotes: "投稿を通知"
 | 
				
			||||||
 | 
					unnotifyNotes: "投稿の通知を解除"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_announcement:
 | 
					_announcement:
 | 
				
			||||||
  forExistingUsers: "既存ユーザーのみ"
 | 
					  forExistingUsers: "既存ユーザーのみ"
 | 
				
			||||||
| 
						 | 
					@ -2064,6 +2066,7 @@ _notification:
 | 
				
			||||||
  youReceivedFollowRequest: "フォローリクエストが来ました"
 | 
					  youReceivedFollowRequest: "フォローリクエストが来ました"
 | 
				
			||||||
  yourFollowRequestAccepted: "フォローリクエストが承認されました"
 | 
					  yourFollowRequestAccepted: "フォローリクエストが承認されました"
 | 
				
			||||||
  pollEnded: "アンケートの結果が出ました"
 | 
					  pollEnded: "アンケートの結果が出ました"
 | 
				
			||||||
 | 
					  newNote: "新しい投稿"
 | 
				
			||||||
  unreadAntennaNote: "アンテナ {name}"
 | 
					  unreadAntennaNote: "アンテナ {name}"
 | 
				
			||||||
  emptyPushNotificationMessage: "プッシュ通知の更新をしました"
 | 
					  emptyPushNotificationMessage: "プッシュ通知の更新をしました"
 | 
				
			||||||
  achievementEarned: "実績を獲得"
 | 
					  achievementEarned: "実績を獲得"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								packages/backend/migration/1695288787870-following-notify.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/backend/migration/1695288787870-following-notify.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					export class FollowingNotify1695288787870 {
 | 
				
			||||||
 | 
					    name = 'FollowingNotify1695288787870'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async up(queryRunner) {
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "following" ADD "notify" character varying(32)`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`CREATE INDEX "IDX_5108098457488634a4768e1d12" ON "following" ("notify") `);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async down(queryRunner) {
 | 
				
			||||||
 | 
					        await queryRunner.query(`DROP INDEX "public"."IDX_5108098457488634a4768e1d12"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "notify"`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
 | 
				
			||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
 | 
					import { extractHashtags } from '@/misc/extract-hashtags.js';
 | 
				
			||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
 | 
					import type { IMentionedRemoteUsers } from '@/models/Note.js';
 | 
				
			||||||
import { MiNote } from '@/models/Note.js';
 | 
					import { MiNote } from '@/models/Note.js';
 | 
				
			||||||
import type { ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 | 
					import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 | 
				
			||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
 | 
					import type { MiDriveFile } from '@/models/DriveFile.js';
 | 
				
			||||||
import type { MiApp } from '@/models/App.js';
 | 
					import type { MiApp } from '@/models/App.js';
 | 
				
			||||||
import { concat } from '@/misc/prelude/array.js';
 | 
					import { concat } from '@/misc/prelude/array.js';
 | 
				
			||||||
| 
						 | 
					@ -185,6 +185,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
				
			||||||
		@Inject(DI.noteThreadMutingsRepository)
 | 
							@Inject(DI.noteThreadMutingsRepository)
 | 
				
			||||||
		private noteThreadMutingsRepository: NoteThreadMutingsRepository,
 | 
							private noteThreadMutingsRepository: NoteThreadMutingsRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@Inject(DI.followingsRepository)
 | 
				
			||||||
 | 
							private followingsRepository: FollowingsRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private userEntityService: UserEntityService,
 | 
							private userEntityService: UserEntityService,
 | 
				
			||||||
		private noteEntityService: NoteEntityService,
 | 
							private noteEntityService: NoteEntityService,
 | 
				
			||||||
		private idService: IdService,
 | 
							private idService: IdService,
 | 
				
			||||||
| 
						 | 
					@ -505,6 +508,20 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
				
			||||||
			this.saveReply(data.reply, note);
 | 
								this.saveReply(data.reply, note);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (data.reply == null) {
 | 
				
			||||||
 | 
								this.followingsRepository.findBy({
 | 
				
			||||||
 | 
									followeeId: user.id,
 | 
				
			||||||
 | 
									notify: 'normal',
 | 
				
			||||||
 | 
								}).then(followings => {
 | 
				
			||||||
 | 
									for (const following of followings) {
 | 
				
			||||||
 | 
										this.notificationService.createNotification(following.followerId, 'note', {
 | 
				
			||||||
 | 
											notifierId: user.id,
 | 
				
			||||||
 | 
											noteId: note.id,
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
 | 
							// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
 | 
				
			||||||
		if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
 | 
							if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
 | 
				
			||||||
			if (!user.isBot) this.incRenoteCount(data.renote);
 | 
								if (!user.isBot) this.incRenoteCount(data.renote);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,7 @@ 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';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
 | 
					const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class NotificationEntityService implements OnModuleInit {
 | 
					export class NotificationEntityService implements OnModuleInit {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -146,15 +146,14 @@ export class UserEntityService implements OnModuleInit {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
	public async getRelation(me: MiUser['id'], target: MiUser['id']) {
 | 
						public async getRelation(me: MiUser['id'], target: MiUser['id']) {
 | 
				
			||||||
 | 
							const following = await this.followingsRepository.findOneBy({
 | 
				
			||||||
 | 
								followerId: me,
 | 
				
			||||||
 | 
								followeeId: target,
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
		return awaitAll({
 | 
							return awaitAll({
 | 
				
			||||||
			id: target,
 | 
								id: target,
 | 
				
			||||||
			isFollowing: this.followingsRepository.count({
 | 
								following,
 | 
				
			||||||
				where: {
 | 
								isFollowing: following != null,
 | 
				
			||||||
					followerId: me,
 | 
					 | 
				
			||||||
					followeeId: target,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				take: 1,
 | 
					 | 
				
			||||||
			}).then(n => n > 0),
 | 
					 | 
				
			||||||
			isFollowed: this.followingsRepository.count({
 | 
								isFollowed: this.followingsRepository.count({
 | 
				
			||||||
				where: {
 | 
									where: {
 | 
				
			||||||
					followerId: target,
 | 
										followerId: target,
 | 
				
			||||||
| 
						 | 
					@ -486,6 +485,7 @@ export class UserEntityService implements OnModuleInit {
 | 
				
			||||||
				isBlocked: relation.isBlocked,
 | 
									isBlocked: relation.isBlocked,
 | 
				
			||||||
				isMuted: relation.isMuted,
 | 
									isMuted: relation.isMuted,
 | 
				
			||||||
				isRenoteMuted: relation.isRenoteMuted,
 | 
									isRenoteMuted: relation.isRenoteMuted,
 | 
				
			||||||
 | 
									notify: relation.following?.notify ?? 'none',
 | 
				
			||||||
			} : {}),
 | 
								} : {}),
 | 
				
			||||||
		} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
 | 
							} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,6 +45,13 @@ export class MiFollowing {
 | 
				
			||||||
	@JoinColumn()
 | 
						@JoinColumn()
 | 
				
			||||||
	public follower: MiUser | null;
 | 
						public follower: MiUser | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Index()
 | 
				
			||||||
 | 
						@Column('varchar', {
 | 
				
			||||||
 | 
							length: 32,
 | 
				
			||||||
 | 
							nullable: true,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public notify: 'normal' | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//#region Denormalized fields
 | 
						//#region Denormalized fields
 | 
				
			||||||
	@Index()
 | 
						@Index()
 | 
				
			||||||
	@Column('varchar', {
 | 
						@Column('varchar', {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,18 +22,6 @@ export type MiNotification = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * 通知の種類。
 | 
						 * 通知の種類。
 | 
				
			||||||
	 * follow - フォローされた
 | 
					 | 
				
			||||||
	 * mention - 投稿で自分が言及された
 | 
					 | 
				
			||||||
	 * reply - 投稿に返信された
 | 
					 | 
				
			||||||
	 * renote - 投稿がRenoteされた
 | 
					 | 
				
			||||||
	 * quote - 投稿が引用Renoteされた
 | 
					 | 
				
			||||||
	 * reaction - 投稿にリアクションされた
 | 
					 | 
				
			||||||
	 * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
 | 
					 | 
				
			||||||
	 * receiveFollowRequest - フォローリクエストされた
 | 
					 | 
				
			||||||
	 * followRequestAccepted - 自分の送ったフォローリクエストが承認された
 | 
					 | 
				
			||||||
	 * achievementEarned - 実績を獲得
 | 
					 | 
				
			||||||
	 * app - アプリ通知
 | 
					 | 
				
			||||||
	 * test - テスト通知(サーバー側)
 | 
					 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	type: typeof notificationTypes[number];
 | 
						type: typeof notificationTypes[number];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -273,6 +273,10 @@ export const packedUserDetailedNotMeOnlySchema = {
 | 
				
			||||||
			type: 'string',
 | 
								type: 'string',
 | 
				
			||||||
			nullable: false, optional: true,
 | 
								nullable: false, optional: true,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							notify: {
 | 
				
			||||||
 | 
								type: 'string',
 | 
				
			||||||
 | 
								nullable: false, optional: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		//#endregion
 | 
							//#endregion
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -160,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js';
 | 
				
			||||||
import * as ep___federation_stats from './endpoints/federation/stats.js';
 | 
					import * as ep___federation_stats from './endpoints/federation/stats.js';
 | 
				
			||||||
import * as ep___following_create from './endpoints/following/create.js';
 | 
					import * as ep___following_create from './endpoints/following/create.js';
 | 
				
			||||||
import * as ep___following_delete from './endpoints/following/delete.js';
 | 
					import * as ep___following_delete from './endpoints/following/delete.js';
 | 
				
			||||||
 | 
					import * as ep___following_update from './endpoints/following/update.js';
 | 
				
			||||||
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
 | 
					import * as ep___following_invalidate from './endpoints/following/invalidate.js';
 | 
				
			||||||
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
 | 
					import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
 | 
				
			||||||
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
 | 
					import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
 | 
				
			||||||
| 
						 | 
					@ -507,6 +508,7 @@ const $federation_users: Provider = { provide: 'ep:federation/users', useClass:
 | 
				
			||||||
const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default };
 | 
					const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default };
 | 
				
			||||||
const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default };
 | 
					const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default };
 | 
				
			||||||
const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default };
 | 
					const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default };
 | 
				
			||||||
 | 
					const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default };
 | 
				
			||||||
const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default };
 | 
					const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default };
 | 
				
			||||||
const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
 | 
					const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
 | 
				
			||||||
const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
 | 
					const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
 | 
				
			||||||
| 
						 | 
					@ -858,6 +860,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 | 
				
			||||||
		$federation_stats,
 | 
							$federation_stats,
 | 
				
			||||||
		$following_create,
 | 
							$following_create,
 | 
				
			||||||
		$following_delete,
 | 
							$following_delete,
 | 
				
			||||||
 | 
							$following_update,
 | 
				
			||||||
		$following_invalidate,
 | 
							$following_invalidate,
 | 
				
			||||||
		$following_requests_accept,
 | 
							$following_requests_accept,
 | 
				
			||||||
		$following_requests_cancel,
 | 
							$following_requests_cancel,
 | 
				
			||||||
| 
						 | 
					@ -1203,6 +1206,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 | 
				
			||||||
		$federation_stats,
 | 
							$federation_stats,
 | 
				
			||||||
		$following_create,
 | 
							$following_create,
 | 
				
			||||||
		$following_delete,
 | 
							$following_delete,
 | 
				
			||||||
 | 
							$following_update,
 | 
				
			||||||
		$following_invalidate,
 | 
							$following_invalidate,
 | 
				
			||||||
		$following_requests_accept,
 | 
							$following_requests_accept,
 | 
				
			||||||
		$following_requests_cancel,
 | 
							$following_requests_cancel,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -160,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js';
 | 
				
			||||||
import * as ep___federation_stats from './endpoints/federation/stats.js';
 | 
					import * as ep___federation_stats from './endpoints/federation/stats.js';
 | 
				
			||||||
import * as ep___following_create from './endpoints/following/create.js';
 | 
					import * as ep___following_create from './endpoints/following/create.js';
 | 
				
			||||||
import * as ep___following_delete from './endpoints/following/delete.js';
 | 
					import * as ep___following_delete from './endpoints/following/delete.js';
 | 
				
			||||||
 | 
					import * as ep___following_update from './endpoints/following/update.js';
 | 
				
			||||||
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
 | 
					import * as ep___following_invalidate from './endpoints/following/invalidate.js';
 | 
				
			||||||
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
 | 
					import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
 | 
				
			||||||
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
 | 
					import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
 | 
				
			||||||
| 
						 | 
					@ -505,6 +506,7 @@ const eps = [
 | 
				
			||||||
	['federation/stats', ep___federation_stats],
 | 
						['federation/stats', ep___federation_stats],
 | 
				
			||||||
	['following/create', ep___following_create],
 | 
						['following/create', ep___following_create],
 | 
				
			||||||
	['following/delete', ep___following_delete],
 | 
						['following/delete', ep___following_delete],
 | 
				
			||||||
 | 
						['following/update', ep___following_update],
 | 
				
			||||||
	['following/invalidate', ep___following_invalidate],
 | 
						['following/invalidate', ep___following_invalidate],
 | 
				
			||||||
	['following/requests/accept', ep___following_requests_accept],
 | 
						['following/requests/accept', ep___following_requests_accept],
 | 
				
			||||||
	['following/requests/cancel', ep___following_requests_cancel],
 | 
						['following/requests/cancel', ep___following_requests_cancel],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,7 @@ export const meta = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	limit: {
 | 
						limit: {
 | 
				
			||||||
		duration: ms('1hour'),
 | 
							duration: ms('1hour'),
 | 
				
			||||||
		max: 50,
 | 
							max: 100,
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	requireCredential: true,
 | 
						requireCredential: true,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,7 +29,7 @@ export const meta = {
 | 
				
			||||||
		noSuchUser: {
 | 
							noSuchUser: {
 | 
				
			||||||
			message: 'No such user.',
 | 
								message: 'No such user.',
 | 
				
			||||||
			code: 'NO_SUCH_USER',
 | 
								code: 'NO_SUCH_USER',
 | 
				
			||||||
			id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8',
 | 
								id: 'b77e6ae6-a3e5-40da-9cc8-c240115479cc',
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		followerIsYourself: {
 | 
							followerIsYourself: {
 | 
				
			||||||
| 
						 | 
					@ -41,7 +41,7 @@ export const meta = {
 | 
				
			||||||
		notFollowing: {
 | 
							notFollowing: {
 | 
				
			||||||
			message: 'The other use is not following you.',
 | 
								message: 'The other use is not following you.',
 | 
				
			||||||
			code: 'NOT_FOLLOWING',
 | 
								code: 'NOT_FOLLOWING',
 | 
				
			||||||
			id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09',
 | 
								id: '918faac3-074f-41ae-9c43-ed5d2946770d',
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										107
									
								
								packages/backend/src/server/api/endpoints/following/update.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								packages/backend/src/server/api/endpoints/following/update.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,107 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ms from 'ms';
 | 
				
			||||||
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
				
			||||||
 | 
					import type { FollowingsRepository } from '@/models/_.js';
 | 
				
			||||||
 | 
					import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
				
			||||||
 | 
					import { UserFollowingService } from '@/core/UserFollowingService.js';
 | 
				
			||||||
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
 | 
					import { GetterService } from '@/server/api/GetterService.js';
 | 
				
			||||||
 | 
					import { ApiError } from '../../error.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						tags: ['following', 'users'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						limit: {
 | 
				
			||||||
 | 
							duration: ms('1hour'),
 | 
				
			||||||
 | 
							max: 100,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						kind: 'write:following',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						errors: {
 | 
				
			||||||
 | 
							noSuchUser: {
 | 
				
			||||||
 | 
								message: 'No such user.',
 | 
				
			||||||
 | 
								code: 'NO_SUCH_USER',
 | 
				
			||||||
 | 
								id: '14318698-f67e-492a-99da-5353a5ac52be',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							followeeIsYourself: {
 | 
				
			||||||
 | 
								message: 'Followee is yourself.',
 | 
				
			||||||
 | 
								code: 'FOLLOWEE_IS_YOURSELF',
 | 
				
			||||||
 | 
								id: '4c4cbaf9-962a-463b-8418-a5e365dbf2eb',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							notFollowing: {
 | 
				
			||||||
 | 
								message: 'You are not following that user.',
 | 
				
			||||||
 | 
								code: 'NOT_FOLLOWING',
 | 
				
			||||||
 | 
								id: 'b8dc75cf-1cb5-46c9-b14b-5f1ffbd782c9',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						res: {
 | 
				
			||||||
 | 
							type: 'object',
 | 
				
			||||||
 | 
							optional: false, nullable: false,
 | 
				
			||||||
 | 
							ref: 'UserLite',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const paramDef = {
 | 
				
			||||||
 | 
						type: 'object',
 | 
				
			||||||
 | 
						properties: {
 | 
				
			||||||
 | 
							userId: { type: 'string', format: 'misskey:id' },
 | 
				
			||||||
 | 
							notify: { type: 'string', enum: ['normal', 'none'] },
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						required: ['userId', 'notify'],
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
				
			||||||
 | 
						constructor(
 | 
				
			||||||
 | 
							@Inject(DI.followingsRepository)
 | 
				
			||||||
 | 
							private followingsRepository: FollowingsRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							private userEntityService: UserEntityService,
 | 
				
			||||||
 | 
							private getterService: GetterService,
 | 
				
			||||||
 | 
							private userFollowingService: UserFollowingService,
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							super(meta, paramDef, async (ps, me) => {
 | 
				
			||||||
 | 
								const follower = me;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Check if the follower is yourself
 | 
				
			||||||
 | 
								if (me.id === ps.userId) {
 | 
				
			||||||
 | 
									throw new ApiError(meta.errors.followeeIsYourself);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Get followee
 | 
				
			||||||
 | 
								const followee = await this.getterService.getUser(ps.userId).catch(err => {
 | 
				
			||||||
 | 
									if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
 | 
				
			||||||
 | 
									throw err;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Check not following
 | 
				
			||||||
 | 
								const exist = await this.followingsRepository.findOneBy({
 | 
				
			||||||
 | 
									followerId: follower.id,
 | 
				
			||||||
 | 
									followeeId: followee.id,
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (exist == null) {
 | 
				
			||||||
 | 
									throw new ApiError(meta.errors.notFollowing);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								await this.followingsRepository.update({
 | 
				
			||||||
 | 
									id: exist.id,
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									notify: ps.notify === 'none' ? null : ps.notify,
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return await this.userEntityService.pack(follower.id, me);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,22 @@
 | 
				
			||||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const;
 | 
					/**
 | 
				
			||||||
 | 
					 * note - 通知オンにしているユーザーが投稿した
 | 
				
			||||||
 | 
					 * follow - フォローされた
 | 
				
			||||||
 | 
					 * mention - 投稿で自分が言及された
 | 
				
			||||||
 | 
					 * reply - 投稿に返信された
 | 
				
			||||||
 | 
					 * renote - 投稿がRenoteされた
 | 
				
			||||||
 | 
					 * quote - 投稿が引用Renoteされた
 | 
				
			||||||
 | 
					 * reaction - 投稿にリアクションされた
 | 
				
			||||||
 | 
					 * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
 | 
				
			||||||
 | 
					 * receiveFollowRequest - フォローリクエストされた
 | 
				
			||||||
 | 
					 * followRequestAccepted - 自分の送ったフォローリクエストが承認された
 | 
				
			||||||
 | 
					 * achievementEarned - 実績を獲得
 | 
				
			||||||
 | 
					 * app - アプリ通知
 | 
				
			||||||
 | 
					 * test - テスト通知(サーバー側)
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', '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;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -494,7 +494,7 @@ describe('ユーザー', () => {
 | 
				
			||||||
		{ parameters: (): object => ({ mutedWords: [] }) },
 | 
							{ parameters: (): object => ({ mutedWords: [] }) },
 | 
				
			||||||
		{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
 | 
							{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
 | 
				
			||||||
		{ parameters: (): object => ({ mutedInstances: [] }) },
 | 
							{ parameters: (): object => ({ mutedInstances: [] }) },
 | 
				
			||||||
		{ parameters: (): object => ({ mutingNotificationTypes: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
 | 
							{ parameters: (): object => ({ mutingNotificationTypes: ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
 | 
				
			||||||
		{ parameters: (): object => ({ mutingNotificationTypes: [] }) },
 | 
							{ parameters: (): object => ({ mutingNotificationTypes: [] }) },
 | 
				
			||||||
		{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
 | 
							{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
 | 
				
			||||||
		{ parameters: (): object => ({ emailNotificationTypes: [] }) },
 | 
							{ parameters: (): object => ({ emailNotificationTypes: [] }) },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -120,6 +120,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
 | 
				
			||||||
		updatedAt: null,
 | 
							updatedAt: null,
 | 
				
			||||||
		uri: null,
 | 
							uri: null,
 | 
				
			||||||
		url: null,
 | 
							url: null,
 | 
				
			||||||
 | 
							notify: 'none',
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
<div ref="elRef" :class="$style.root">
 | 
					<div ref="elRef" :class="$style.root">
 | 
				
			||||||
	<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 === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
 | 
							<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
 | 
				
			||||||
		<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
 | 
							<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
 | 
				
			||||||
		<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
 | 
							<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user" link preview/>
 | 
				
			||||||
| 
						 | 
					@ -47,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
	<div :class="$style.tail">
 | 
						<div :class="$style.tail">
 | 
				
			||||||
		<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 }}: {{ notification.note.user.name ?? notification.note.user.username }}</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>
 | 
				
			||||||
| 
						 | 
					@ -73,6 +75,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
			<MkA v-else-if="notification.type === 'quote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
 | 
								<MkA v-else-if="notification.type === 'quote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
 | 
				
			||||||
				<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"/>
 | 
				
			||||||
			</MkA>
 | 
								</MkA>
 | 
				
			||||||
 | 
								<MkA v-else-if="notification.type === 'note'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
 | 
				
			||||||
 | 
									<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
 | 
				
			||||||
 | 
								</MkA>
 | 
				
			||||||
			<MkA v-else-if="notification.type === 'pollEnded'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
 | 
								<MkA v-else-if="notification.type === 'pollEnded'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
 | 
				
			||||||
				<i class="ti ti-quote" :class="$style.quote"></i>
 | 
									<i class="ti ti-quote" :class="$style.quote"></i>
 | 
				
			||||||
				<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"/>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -54,7 +54,7 @@ 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 = ['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', '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 = [
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -80,6 +80,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function toggleNotify() {
 | 
				
			||||||
 | 
							os.apiWithDialog('following/update', {
 | 
				
			||||||
 | 
								userId: user.id,
 | 
				
			||||||
 | 
								notify: user.notify === 'normal' ? 'none' : 'normal',
 | 
				
			||||||
 | 
							}).then(() => {
 | 
				
			||||||
 | 
								user.notify = user.notify === 'normal' ? 'none' : 'normal';
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function reportAbuse() {
 | 
						function reportAbuse() {
 | 
				
			||||||
		os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
 | 
							os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
 | 
				
			||||||
			user: user,
 | 
								user: user,
 | 
				
			||||||
| 
						 | 
					@ -270,6 +279,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
 | 
				
			||||||
			}]);
 | 
								}]);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため
 | 
				
			||||||
 | 
							//if (user.isFollowing) {
 | 
				
			||||||
 | 
							menu = menu.concat([{
 | 
				
			||||||
 | 
								icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off',
 | 
				
			||||||
 | 
								text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes,
 | 
				
			||||||
 | 
								action: toggleNotify,
 | 
				
			||||||
 | 
							}]);
 | 
				
			||||||
 | 
							//}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		menu = menu.concat([null, {
 | 
							menu = menu.concat([null, {
 | 
				
			||||||
			icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
 | 
								icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
 | 
				
			||||||
			text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
 | 
								text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2609,7 +2609,12 @@ type Notification_2 = {
 | 
				
			||||||
    userId: User['id'];
 | 
					    userId: User['id'];
 | 
				
			||||||
    note: Note;
 | 
					    note: Note;
 | 
				
			||||||
} | {
 | 
					} | {
 | 
				
			||||||
    type: 'pollVote';
 | 
					    type: 'note';
 | 
				
			||||||
 | 
					    user: User;
 | 
				
			||||||
 | 
					    userId: User['id'];
 | 
				
			||||||
 | 
					    note: Note;
 | 
				
			||||||
 | 
					} | {
 | 
				
			||||||
 | 
					    type: 'pollEnded';
 | 
				
			||||||
    user: User;
 | 
					    user: User;
 | 
				
			||||||
    userId: User['id'];
 | 
					    userId: User['id'];
 | 
				
			||||||
    note: Note;
 | 
					    note: Note;
 | 
				
			||||||
| 
						 | 
					@ -2640,7 +2645,7 @@ type Notification_2 = {
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// @public (undocumented)
 | 
					// @public (undocumented)
 | 
				
			||||||
export const notificationTypes: readonly ["follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app"];
 | 
					export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// @public (undocumented)
 | 
					// @public (undocumented)
 | 
				
			||||||
type OriginType = 'combined' | 'local' | 'remote';
 | 
					type OriginType = 'combined' | 'local' | 'remote';
 | 
				
			||||||
| 
						 | 
					@ -2810,6 +2815,7 @@ type UserDetailed = UserLite & {
 | 
				
			||||||
    updatedAt: DateString | null;
 | 
					    updatedAt: DateString | null;
 | 
				
			||||||
    uri: string | null;
 | 
					    uri: string | null;
 | 
				
			||||||
    url: string | null;
 | 
					    url: string | null;
 | 
				
			||||||
 | 
					    notify: 'normal' | 'none';
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// @public (undocumented)
 | 
					// @public (undocumented)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
 | 
					export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
 | 
					export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,6 +70,7 @@ export type UserDetailed = UserLite & {
 | 
				
			||||||
	updatedAt: DateString | null;
 | 
						updatedAt: DateString | null;
 | 
				
			||||||
	uri: string | null;
 | 
						uri: string | null;
 | 
				
			||||||
	url: string | null;
 | 
						url: string | null;
 | 
				
			||||||
 | 
						notify: 'normal' | 'none';
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type UserGroup = TODO;
 | 
					export type UserGroup = TODO;
 | 
				
			||||||
| 
						 | 
					@ -233,7 +234,12 @@ export type Notification = {
 | 
				
			||||||
	userId: User['id'];
 | 
						userId: User['id'];
 | 
				
			||||||
	note: Note;
 | 
						note: Note;
 | 
				
			||||||
} | {
 | 
					} | {
 | 
				
			||||||
	type: 'pollVote';
 | 
						type: 'note';
 | 
				
			||||||
 | 
						user: User;
 | 
				
			||||||
 | 
						userId: User['id'];
 | 
				
			||||||
 | 
						note: Note;
 | 
				
			||||||
 | 
					} | {
 | 
				
			||||||
 | 
						type: 'pollEnded';
 | 
				
			||||||
	user: User;
 | 
						user: User;
 | 
				
			||||||
	userId: User['id'];
 | 
						userId: User['id'];
 | 
				
			||||||
	note: Note;
 | 
						note: Note;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -134,6 +134,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
 | 
				
			||||||
						],
 | 
											],
 | 
				
			||||||
					}];
 | 
										}];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									case 'note':
 | 
				
			||||||
 | 
										return [t('_notification.newNote') + ': ' + getUserName(data.body.user), {
 | 
				
			||||||
 | 
											body: data.body.note.text ?? '',
 | 
				
			||||||
 | 
											icon: data.body.user.avatarUrl,
 | 
				
			||||||
 | 
											data,
 | 
				
			||||||
 | 
										}];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				case 'reaction': {
 | 
									case 'reaction': {
 | 
				
			||||||
					let reaction = data.body.reaction;
 | 
										let reaction = data.body.reaction;
 | 
				
			||||||
					let badge: string | undefined;
 | 
										let badge: string | undefined;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue