mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-03 23:14:13 +00:00 
			
		
		
		
	
							parent
							
								
									d0aba46ee3
								
							
						
					
					
						commit
						8f2049bcd2
					
				
					 47 changed files with 18 additions and 3292 deletions
				
			
		| 
						 | 
				
			
			@ -22,7 +22,6 @@ import { IdService } from './IdService.js';
 | 
			
		|||
import { ImageProcessingService } from './ImageProcessingService.js';
 | 
			
		||||
import { InstanceActorService } from './InstanceActorService.js';
 | 
			
		||||
import { InternalStorageService } from './InternalStorageService.js';
 | 
			
		||||
import { MessagingService } from './MessagingService.js';
 | 
			
		||||
import { MetaService } from './MetaService.js';
 | 
			
		||||
import { MfmService } from './MfmService.js';
 | 
			
		||||
import { ModerationLogService } from './ModerationLogService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +81,6 @@ import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js
 | 
			
		|||
import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js';
 | 
			
		||||
import { HashtagEntityService } from './entities/HashtagEntityService.js';
 | 
			
		||||
import { InstanceEntityService } from './entities/InstanceEntityService.js';
 | 
			
		||||
import { MessagingMessageEntityService } from './entities/MessagingMessageEntityService.js';
 | 
			
		||||
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
 | 
			
		||||
import { MutingEntityService } from './entities/MutingEntityService.js';
 | 
			
		||||
import { NoteEntityService } from './entities/NoteEntityService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -146,7 +144,6 @@ const $IdService: Provider = { provide: 'IdService', useExisting: IdService };
 | 
			
		|||
const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService };
 | 
			
		||||
const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService };
 | 
			
		||||
const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService };
 | 
			
		||||
const $MessagingService: Provider = { provide: 'MessagingService', useExisting: MessagingService };
 | 
			
		||||
const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService };
 | 
			
		||||
const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService };
 | 
			
		||||
const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService };
 | 
			
		||||
| 
						 | 
				
			
			@ -207,7 +204,6 @@ const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService
 | 
			
		|||
const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService };
 | 
			
		||||
const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService };
 | 
			
		||||
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
 | 
			
		||||
const $MessagingMessageEntityService: Provider = { provide: 'MessagingMessageEntityService', useExisting: MessagingMessageEntityService };
 | 
			
		||||
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
 | 
			
		||||
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
 | 
			
		||||
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
 | 
			
		||||
| 
						 | 
				
			
			@ -273,7 +269,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		|||
		ImageProcessingService,
 | 
			
		||||
		InstanceActorService,
 | 
			
		||||
		InternalStorageService,
 | 
			
		||||
		MessagingService,
 | 
			
		||||
		MetaService,
 | 
			
		||||
		MfmService,
 | 
			
		||||
		ModerationLogService,
 | 
			
		||||
| 
						 | 
				
			
			@ -333,7 +328,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		|||
		GalleryPostEntityService,
 | 
			
		||||
		HashtagEntityService,
 | 
			
		||||
		InstanceEntityService,
 | 
			
		||||
		MessagingMessageEntityService,
 | 
			
		||||
		ModerationLogEntityService,
 | 
			
		||||
		MutingEntityService,
 | 
			
		||||
		NoteEntityService,
 | 
			
		||||
| 
						 | 
				
			
			@ -394,7 +388,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		|||
		$ImageProcessingService,
 | 
			
		||||
		$InstanceActorService,
 | 
			
		||||
		$InternalStorageService,
 | 
			
		||||
		$MessagingService,
 | 
			
		||||
		$MetaService,
 | 
			
		||||
		$MfmService,
 | 
			
		||||
		$ModerationLogService,
 | 
			
		||||
| 
						 | 
				
			
			@ -454,7 +447,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		|||
		$GalleryPostEntityService,
 | 
			
		||||
		$HashtagEntityService,
 | 
			
		||||
		$InstanceEntityService,
 | 
			
		||||
		$MessagingMessageEntityService,
 | 
			
		||||
		$ModerationLogEntityService,
 | 
			
		||||
		$MutingEntityService,
 | 
			
		||||
		$NoteEntityService,
 | 
			
		||||
| 
						 | 
				
			
			@ -516,7 +508,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		|||
		ImageProcessingService,
 | 
			
		||||
		InstanceActorService,
 | 
			
		||||
		InternalStorageService,
 | 
			
		||||
		MessagingService,
 | 
			
		||||
		MetaService,
 | 
			
		||||
		MfmService,
 | 
			
		||||
		ModerationLogService,
 | 
			
		||||
| 
						 | 
				
			
			@ -575,7 +566,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		|||
		GalleryPostEntityService,
 | 
			
		||||
		HashtagEntityService,
 | 
			
		||||
		InstanceEntityService,
 | 
			
		||||
		MessagingMessageEntityService,
 | 
			
		||||
		ModerationLogEntityService,
 | 
			
		||||
		MutingEntityService,
 | 
			
		||||
		NoteEntityService,
 | 
			
		||||
| 
						 | 
				
			
			@ -636,7 +626,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		|||
		$ImageProcessingService,
 | 
			
		||||
		$InstanceActorService,
 | 
			
		||||
		$InternalStorageService,
 | 
			
		||||
		$MessagingService,
 | 
			
		||||
		$MetaService,
 | 
			
		||||
		$MfmService,
 | 
			
		||||
		$ModerationLogService,
 | 
			
		||||
| 
						 | 
				
			
			@ -695,7 +684,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		|||
		$GalleryPostEntityService,
 | 
			
		||||
		$HashtagEntityService,
 | 
			
		||||
		$InstanceEntityService,
 | 
			
		||||
		$MessagingMessageEntityService,
 | 
			
		||||
		$ModerationLogEntityService,
 | 
			
		||||
		$MutingEntityService,
 | 
			
		||||
		$NoteEntityService,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,13 +11,9 @@ import type {
 | 
			
		|||
	AdminStreamTypes,
 | 
			
		||||
	AntennaStreamTypes,
 | 
			
		||||
	BroadcastTypes,
 | 
			
		||||
	ChannelStreamTypes,
 | 
			
		||||
	DriveStreamTypes,
 | 
			
		||||
	GroupMessagingStreamTypes,
 | 
			
		||||
	InternalStreamTypes,
 | 
			
		||||
	MainStreamTypes,
 | 
			
		||||
	MessagingIndexStreamTypes,
 | 
			
		||||
	MessagingStreamTypes,
 | 
			
		||||
	NoteStreamTypes,
 | 
			
		||||
	UserListStreamTypes,
 | 
			
		||||
	UserStreamTypes,
 | 
			
		||||
| 
						 | 
				
			
			@ -83,11 +79,6 @@ export class GlobalEventService {
 | 
			
		|||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public publishChannelStream<K extends keyof ChannelStreamTypes>(channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void {
 | 
			
		||||
		this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public publishUserListStream<K extends keyof UserListStreamTypes>(listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void {
 | 
			
		||||
		this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
| 
						 | 
				
			
			@ -98,21 +89,6 @@ export class GlobalEventService {
 | 
			
		|||
		this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public publishMessagingStream<K extends keyof MessagingStreamTypes>(userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void {
 | 
			
		||||
		this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public publishGroupMessagingStream<K extends keyof GroupMessagingStreamTypes>(groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void {
 | 
			
		||||
		this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public publishMessagingIndexStream<K extends keyof MessagingIndexStreamTypes>(userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void {
 | 
			
		||||
		this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public publishNotesStream(note: Packed<'Note'>): void {
 | 
			
		||||
		this.publish('notesStream', null, note);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,307 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { In, Not } from 'typeorm';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
 | 
			
		||||
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
 | 
			
		||||
import type { Note } from '@/models/entities/Note.js';
 | 
			
		||||
import type { User, RemoteUser } from '@/models/entities/User.js';
 | 
			
		||||
import type { UserGroup } from '@/models/entities/UserGroup.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
import { toArray } from '@/misc/prelude/array.js';
 | 
			
		||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
 | 
			
		||||
import type { MessagingMessagesRepository, MutingsRepository, UserGroupJoiningsRepository, UsersRepository } from '@/models/index.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 | 
			
		||||
import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js';
 | 
			
		||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class MessagingService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userGroupJoiningsRepository)
 | 
			
		||||
		private userGroupJoiningsRepository: UserGroupJoiningsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.mutingsRepository)
 | 
			
		||||
		private mutingsRepository: MutingsRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private messagingMessageEntityService: MessagingMessageEntityService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
		private apRendererService: ApRendererService,
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
		private pushNotificationService: PushNotificationService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) {
 | 
			
		||||
		const message = {
 | 
			
		||||
			id: this.idService.genId(),
 | 
			
		||||
			createdAt: new Date(),
 | 
			
		||||
			fileId: file ? file.id : null,
 | 
			
		||||
			recipientId: recipientUser ? recipientUser.id : null,
 | 
			
		||||
			groupId: recipientGroup ? recipientGroup.id : null,
 | 
			
		||||
			text: text ? text.trim() : null,
 | 
			
		||||
			userId: user.id,
 | 
			
		||||
			isRead: false,
 | 
			
		||||
			reads: [] as any[],
 | 
			
		||||
			uri,
 | 
			
		||||
		} as MessagingMessage;
 | 
			
		||||
	
 | 
			
		||||
		await this.messagingMessagesRepository.insert(message);
 | 
			
		||||
	
 | 
			
		||||
		const messageObj = await this.messagingMessageEntityService.pack(message);
 | 
			
		||||
	
 | 
			
		||||
		if (recipientUser) {
 | 
			
		||||
			if (this.userEntityService.isLocalUser(user)) {
 | 
			
		||||
				// 自分のストリーム
 | 
			
		||||
				this.globalEventService.publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj);
 | 
			
		||||
				this.globalEventService.publishMessagingIndexStream(message.userId, 'message', messageObj);
 | 
			
		||||
				this.globalEventService.publishMainStream(message.userId, 'messagingMessage', messageObj);
 | 
			
		||||
			}
 | 
			
		||||
	
 | 
			
		||||
			if (this.userEntityService.isLocalUser(recipientUser)) {
 | 
			
		||||
				// 相手のストリーム
 | 
			
		||||
				this.globalEventService.publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj);
 | 
			
		||||
				this.globalEventService.publishMessagingIndexStream(recipientUser.id, 'message', messageObj);
 | 
			
		||||
				this.globalEventService.publishMainStream(recipientUser.id, 'messagingMessage', messageObj);
 | 
			
		||||
			}
 | 
			
		||||
		} else if (recipientGroup) {
 | 
			
		||||
			// グループのストリーム
 | 
			
		||||
			this.globalEventService.publishGroupMessagingStream(recipientGroup.id, 'message', messageObj);
 | 
			
		||||
	
 | 
			
		||||
			// メンバーのストリーム
 | 
			
		||||
			const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: recipientGroup.id });
 | 
			
		||||
			for (const joining of joinings) {
 | 
			
		||||
				this.globalEventService.publishMessagingIndexStream(joining.userId, 'message', messageObj);
 | 
			
		||||
				this.globalEventService.publishMainStream(joining.userId, 'messagingMessage', messageObj);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	
 | 
			
		||||
		// 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
 | 
			
		||||
		setTimeout(async () => {
 | 
			
		||||
			const freshMessage = await this.messagingMessagesRepository.findOneBy({ id: message.id });
 | 
			
		||||
			if (freshMessage == null) return; // メッセージが削除されている場合もある
 | 
			
		||||
	
 | 
			
		||||
			if (recipientUser && this.userEntityService.isLocalUser(recipientUser)) {
 | 
			
		||||
				if (freshMessage.isRead) return; // 既読
 | 
			
		||||
	
 | 
			
		||||
				//#region ただしミュートされているなら発行しない
 | 
			
		||||
				const mute = await this.mutingsRepository.findBy({
 | 
			
		||||
					muterId: recipientUser.id,
 | 
			
		||||
				});
 | 
			
		||||
				if (mute.map(m => m.muteeId).includes(user.id)) return;
 | 
			
		||||
				//#endregion
 | 
			
		||||
	
 | 
			
		||||
				this.globalEventService.publishMainStream(recipientUser.id, 'unreadMessagingMessage', messageObj);
 | 
			
		||||
				this.pushNotificationService.pushNotification(recipientUser.id, 'unreadMessagingMessage', messageObj);
 | 
			
		||||
			} else if (recipientGroup) {
 | 
			
		||||
				const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: recipientGroup.id, userId: Not(user.id) });
 | 
			
		||||
				for (const joining of joinings) {
 | 
			
		||||
					if (freshMessage.reads.includes(joining.userId)) return; // 既読
 | 
			
		||||
					this.globalEventService.publishMainStream(joining.userId, 'unreadMessagingMessage', messageObj);
 | 
			
		||||
					this.pushNotificationService.pushNotification(joining.userId, 'unreadMessagingMessage', messageObj);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}, 2000);
 | 
			
		||||
	
 | 
			
		||||
		if (recipientUser && this.userEntityService.isLocalUser(user) && this.userEntityService.isRemoteUser(recipientUser)) {
 | 
			
		||||
			const note = {
 | 
			
		||||
				id: message.id,
 | 
			
		||||
				createdAt: message.createdAt,
 | 
			
		||||
				fileIds: message.fileId ? [message.fileId] : [],
 | 
			
		||||
				text: message.text,
 | 
			
		||||
				userId: message.userId,
 | 
			
		||||
				visibility: 'specified',
 | 
			
		||||
				mentions: [recipientUser].map(u => u.id),
 | 
			
		||||
				mentionedRemoteUsers: JSON.stringify([recipientUser].map(u => ({
 | 
			
		||||
					uri: u.uri,
 | 
			
		||||
					username: u.username,
 | 
			
		||||
					host: u.host,
 | 
			
		||||
				}))),
 | 
			
		||||
			} as Note;
 | 
			
		||||
	
 | 
			
		||||
			const activity = this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false, true), note));
 | 
			
		||||
	
 | 
			
		||||
			this.queueService.deliver(user, activity, recipientUser.inbox);
 | 
			
		||||
		}
 | 
			
		||||
		return messageObj;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async deleteMessage(message: MessagingMessage) {
 | 
			
		||||
		await this.messagingMessagesRepository.delete(message.id);
 | 
			
		||||
		this.postDeleteMessage(message);
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async postDeleteMessage(message: MessagingMessage) {
 | 
			
		||||
		if (message.recipientId) {
 | 
			
		||||
			const user = await this.usersRepository.findOneByOrFail({ id: message.userId });
 | 
			
		||||
			const recipient = await this.usersRepository.findOneByOrFail({ id: message.recipientId });
 | 
			
		||||
	
 | 
			
		||||
			if (this.userEntityService.isLocalUser(user)) this.globalEventService.publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id);
 | 
			
		||||
			if (this.userEntityService.isLocalUser(recipient)) this.globalEventService.publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id);
 | 
			
		||||
	
 | 
			
		||||
			if (this.userEntityService.isLocalUser(user) && this.userEntityService.isRemoteUser(recipient)) {
 | 
			
		||||
				const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), user));
 | 
			
		||||
				this.queueService.deliver(user, activity, recipient.inbox);
 | 
			
		||||
			}
 | 
			
		||||
		} else if (message.groupId) {
 | 
			
		||||
			this.globalEventService.publishGroupMessagingStream(message.groupId, 'deleted', message.id);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Mark messages as read
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async readUserMessagingMessage(
 | 
			
		||||
		userId: User['id'],
 | 
			
		||||
		otherpartyId: User['id'],
 | 
			
		||||
		messageIds: MessagingMessage['id'][],
 | 
			
		||||
	) {
 | 
			
		||||
		if (messageIds.length === 0) return;
 | 
			
		||||
 | 
			
		||||
		const messages = await this.messagingMessagesRepository.findBy({
 | 
			
		||||
			id: In(messageIds),
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		for (const message of messages) {
 | 
			
		||||
			if (message.recipientId !== userId) {
 | 
			
		||||
				throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).');
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Update documents
 | 
			
		||||
		await this.messagingMessagesRepository.update({
 | 
			
		||||
			id: In(messageIds),
 | 
			
		||||
			userId: otherpartyId,
 | 
			
		||||
			recipientId: userId,
 | 
			
		||||
			isRead: false,
 | 
			
		||||
		}, {
 | 
			
		||||
			isRead: true,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// Publish event
 | 
			
		||||
		this.globalEventService.publishMessagingStream(otherpartyId, userId, 'read', messageIds);
 | 
			
		||||
		this.globalEventService.publishMessagingIndexStream(userId, 'read', messageIds);
 | 
			
		||||
 | 
			
		||||
		if (!await this.userEntityService.getHasUnreadMessagingMessage(userId)) {
 | 
			
		||||
		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 | 
			
		||||
			this.globalEventService.publishMainStream(userId, 'readAllMessagingMessages');
 | 
			
		||||
			this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessages', undefined);
 | 
			
		||||
		} else {
 | 
			
		||||
		// そのユーザーとのメッセージで未読がなければイベント発行
 | 
			
		||||
			const count = await this.messagingMessagesRepository.count({
 | 
			
		||||
				where: {
 | 
			
		||||
					userId: otherpartyId,
 | 
			
		||||
					recipientId: userId,
 | 
			
		||||
					isRead: false,
 | 
			
		||||
				},
 | 
			
		||||
				take: 1,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (!count) {
 | 
			
		||||
				this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId });
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Mark messages as read
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async readGroupMessagingMessage(
 | 
			
		||||
		userId: User['id'],
 | 
			
		||||
		groupId: UserGroup['id'],
 | 
			
		||||
		messageIds: MessagingMessage['id'][],
 | 
			
		||||
	) {
 | 
			
		||||
		if (messageIds.length === 0) return;
 | 
			
		||||
 | 
			
		||||
		// check joined
 | 
			
		||||
		const joining = await this.userGroupJoiningsRepository.findOneBy({
 | 
			
		||||
			userId: userId,
 | 
			
		||||
			userGroupId: groupId,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (joining == null) {
 | 
			
		||||
			throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const messages = await this.messagingMessagesRepository.findBy({
 | 
			
		||||
			id: In(messageIds),
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const reads: MessagingMessage['id'][] = [];
 | 
			
		||||
 | 
			
		||||
		for (const message of messages) {
 | 
			
		||||
			if (message.userId === userId) continue;
 | 
			
		||||
			if (message.reads.includes(userId)) continue;
 | 
			
		||||
 | 
			
		||||
			// Update document
 | 
			
		||||
			await this.messagingMessagesRepository.createQueryBuilder().update()
 | 
			
		||||
				.set({
 | 
			
		||||
					reads: (() => `array_append("reads", '${joining.userId}')`) as any,
 | 
			
		||||
				})
 | 
			
		||||
				.where('id = :id', { id: message.id })
 | 
			
		||||
				.execute();
 | 
			
		||||
 | 
			
		||||
			reads.push(message.id);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Publish event
 | 
			
		||||
		this.globalEventService.publishGroupMessagingStream(groupId, 'read', {
 | 
			
		||||
			ids: reads,
 | 
			
		||||
			userId: userId,
 | 
			
		||||
		});
 | 
			
		||||
		this.globalEventService.publishMessagingIndexStream(userId, 'read', reads);
 | 
			
		||||
 | 
			
		||||
		if (!await this.userEntityService.getHasUnreadMessagingMessage(userId)) {
 | 
			
		||||
		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
 | 
			
		||||
			this.globalEventService.publishMainStream(userId, 'readAllMessagingMessages');
 | 
			
		||||
			this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessages', undefined);
 | 
			
		||||
		} else {
 | 
			
		||||
		// そのグループにおいて未読がなければイベント発行
 | 
			
		||||
			const unreadExist = await this.messagingMessagesRepository.createQueryBuilder('message')
 | 
			
		||||
				.where('message.groupId = :groupId', { groupId: groupId })
 | 
			
		||||
				.andWhere('message.userId != :userId', { userId: userId })
 | 
			
		||||
				.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
 | 
			
		||||
				.andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない
 | 
			
		||||
				.getOne().then(x => x != null);
 | 
			
		||||
 | 
			
		||||
			if (!unreadExist) {
 | 
			
		||||
				this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId });
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async deliverReadActivity(user: { id: User['id']; host: null; }, recipient: RemoteUser, messages: MessagingMessage | MessagingMessage[]) {
 | 
			
		||||
		messages = toArray(messages).filter(x => x.uri);
 | 
			
		||||
		const contents = messages.map(x => this.apRendererService.renderRead(user, x));
 | 
			
		||||
 | 
			
		||||
		if (contents.length > 1) {
 | 
			
		||||
			const collection = this.apRendererService.renderOrderedCollection(null, contents.length, undefined, undefined, contents);
 | 
			
		||||
			this.queueService.deliver(user, this.apRendererService.addContext(collection), recipient.inbox);
 | 
			
		||||
		} else {
 | 
			
		||||
			for (const content of contents) {
 | 
			
		||||
				this.queueService.deliver(user, this.apRendererService.addContext(content), recipient.inbox);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -11,15 +11,12 @@ import { bindThis } from '@/decorators.js';
 | 
			
		|||
// Defined also packages/sw/types.ts#L13
 | 
			
		||||
type pushNotificationsTypes = {
 | 
			
		||||
	'notification': Packed<'Notification'>;
 | 
			
		||||
	'unreadMessagingMessage': Packed<'MessagingMessage'>;
 | 
			
		||||
	'unreadAntennaNote': {
 | 
			
		||||
		antenna: { id: string, name: string };
 | 
			
		||||
		note: Packed<'Note'>;
 | 
			
		||||
	};
 | 
			
		||||
	'readNotifications': { notificationIds: string[] };
 | 
			
		||||
	'readAllNotifications': undefined;
 | 
			
		||||
	'readAllMessagingMessages': undefined;
 | 
			
		||||
	'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string };
 | 
			
		||||
	'readAntenna': { antennaId: string };
 | 
			
		||||
	'readAllAntennas': undefined;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -40,11 +37,10 @@ function truncateBody<T extends keyof pushNotificationsTypes>(type: T, body: pus
 | 
			
		|||
				reply: undefined,
 | 
			
		||||
				renote: undefined,
 | 
			
		||||
				user: type === 'notification' ? undefined as any : body.note.user,
 | 
			
		||||
			}
 | 
			
		||||
			},
 | 
			
		||||
		} : {}),
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	return body;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
| 
						 | 
				
			
			@ -81,8 +77,6 @@ export class PushNotificationService {
 | 
			
		|||
			if ([
 | 
			
		||||
				'readNotifications',
 | 
			
		||||
				'readAllNotifications',
 | 
			
		||||
				'readAllMessagingMessages',
 | 
			
		||||
				'readAllMessagingMessagesOfARoom',
 | 
			
		||||
				'readAntenna',
 | 
			
		||||
				'readAllAntennas',
 | 
			
		||||
			].includes(type) && !subscription.sendReadMessage) continue;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,12 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import escapeRegexp from 'escape-regexp';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { MessagingMessagesRepository, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
 | 
			
		||||
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { Cache } from '@/misc/cache.js';
 | 
			
		||||
import type { UserPublickey } from '@/models/entities/UserPublickey.js';
 | 
			
		||||
import { UserCacheService } from '@/core/UserCacheService.js';
 | 
			
		||||
import type { Note } from '@/models/entities/Note.js';
 | 
			
		||||
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { RemoteUser, User } from '@/models/entities/User.js';
 | 
			
		||||
import { getApId } from './type.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -42,9 +41,6 @@ export class ApDbResolverService {
 | 
			
		|||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.notesRepository)
 | 
			
		||||
		private notesRepository: NotesRepository,
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -101,23 +97,6 @@ export class ApDbResolverService {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> {
 | 
			
		||||
		const parsed = this.parseUri(value);
 | 
			
		||||
 | 
			
		||||
		if (parsed.local) {
 | 
			
		||||
			if (parsed.type !== 'notes') return null;
 | 
			
		||||
 | 
			
		||||
			return await this.messagingMessagesRepository.findOneBy({
 | 
			
		||||
				id: parsed.id,
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
			return await this.messagingMessagesRepository.findOneBy({
 | 
			
		||||
				uri: parsed.uri,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * AP Person => Misskey User in DB
 | 
			
		||||
	 */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,8 +19,7 @@ import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
import { MessagingService } from '@/core/MessagingService.js';
 | 
			
		||||
import type { UsersRepository, NotesRepository, FollowingsRepository, MessagingMessagesRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
 | 
			
		||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import type { RemoteUser } from '@/models/entities/User.js';
 | 
			
		||||
import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -51,9 +50,6 @@ export class ApInboxService {
 | 
			
		|||
		@Inject(DI.followingsRepository)
 | 
			
		||||
		private followingsRepository: FollowingsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.abuseUserReportsRepository)
 | 
			
		||||
		private abuseUserReportsRepository: AbuseUserReportsRepository,
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -81,7 +77,6 @@ export class ApInboxService {
 | 
			
		|||
		private apPersonService: ApPersonService,
 | 
			
		||||
		private apQuestionService: ApQuestionService,
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
		private messagingService: MessagingService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.logger = this.apLoggerService.logger;
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -124,8 +119,6 @@ export class ApInboxService {
 | 
			
		|||
			await this.delete(actor, activity);
 | 
			
		||||
		} else if (isUpdate(activity)) {
 | 
			
		||||
			await this.update(actor, activity);
 | 
			
		||||
		} else if (isRead(activity)) {
 | 
			
		||||
			await this.read(actor, activity);
 | 
			
		||||
		} else if (isFollow(activity)) {
 | 
			
		||||
			await this.follow(actor, activity);
 | 
			
		||||
		} else if (isAccept(activity)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -185,29 +178,6 @@ export class ApInboxService {
 | 
			
		|||
		}).then(() => 'ok');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async read(actor: RemoteUser, activity: IRead): Promise<string> {
 | 
			
		||||
		const id = await getApId(activity.object);
 | 
			
		||||
 | 
			
		||||
		if (!this.utilityService.isSelfHost(this.utilityService.extractDbHost(id))) {
 | 
			
		||||
			return `skip: Read to foreign host (${id})`;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const messageId = id.split('/').pop();
 | 
			
		||||
 | 
			
		||||
		const message = await this.messagingMessagesRepository.findOneBy({ id: messageId });
 | 
			
		||||
		if (message == null) {
 | 
			
		||||
			return 'skip: message not found';
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (actor.id !== message.recipientId) {
 | 
			
		||||
			return 'skip: actor is not a message recipient';
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		await this.messagingService.readUserMessagingMessage(message.recipientId!, message.userId, [message.id]);
 | 
			
		||||
		return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async accept(actor: RemoteUser, activity: IAccept): Promise<string> {
 | 
			
		||||
		const uri = activity.id ?? activity;
 | 
			
		||||
| 
						 | 
				
			
			@ -504,16 +474,7 @@ export class ApInboxService {
 | 
			
		|||
			const note = await this.apDbResolverService.getNoteFromApId(uri);
 | 
			
		||||
	
 | 
			
		||||
			if (note == null) {
 | 
			
		||||
				const message = await this.apDbResolverService.getMessageFromApId(uri);
 | 
			
		||||
				if (message == null) return 'message not found';
 | 
			
		||||
	
 | 
			
		||||
				if (message.userId !== actor.id) {
 | 
			
		||||
					return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
 | 
			
		||||
				}
 | 
			
		||||
	
 | 
			
		||||
				await this.messagingService.deleteMessage(message);
 | 
			
		||||
	
 | 
			
		||||
				return 'ok: message deleted';
 | 
			
		||||
				return 'message not found';
 | 
			
		||||
			}
 | 
			
		||||
	
 | 
			
		||||
			if (note.userId !== actor.id) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,6 @@ import type { DriveFile } from '@/models/entities/DriveFile.js';
 | 
			
		|||
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
 | 
			
		||||
import type { Emoji } from '@/models/entities/Emoji.js';
 | 
			
		||||
import type { Poll } from '@/models/entities/Poll.js';
 | 
			
		||||
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
 | 
			
		||||
import type { PollVote } from '@/models/entities/PollVote.js';
 | 
			
		||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
 | 
			
		||||
import { MfmService } from '@/core/MfmService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -293,7 +292,7 @@ export class ApRendererService {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async renderNote(note: Note, dive = true, isTalk = false): Promise<IPost> {
 | 
			
		||||
	public async renderNote(note: Note, dive = true): Promise<IPost> {
 | 
			
		||||
		const getPromisedFiles = async (ids: string[]) => {
 | 
			
		||||
			if (!ids || ids.length === 0) return [];
 | 
			
		||||
			const items = await this.driveFilesRepository.findBy({ id: In(ids) });
 | 
			
		||||
| 
						 | 
				
			
			@ -408,10 +407,6 @@ export class ApRendererService {
 | 
			
		|||
			})),
 | 
			
		||||
		} as const : {};
 | 
			
		||||
 | 
			
		||||
		const asTalk = isTalk ? {
 | 
			
		||||
			_misskey_talk: true,
 | 
			
		||||
		} as const : {};
 | 
			
		||||
	
 | 
			
		||||
		return {
 | 
			
		||||
			id: `${this.config.url}/notes/${note.id}`,
 | 
			
		||||
			type: 'Note',
 | 
			
		||||
| 
						 | 
				
			
			@ -433,7 +428,6 @@ export class ApRendererService {
 | 
			
		|||
			sensitive: note.cw != null || files.some(file => file.isSensitive),
 | 
			
		||||
			tag,
 | 
			
		||||
			...asPoll,
 | 
			
		||||
			...asTalk,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -532,15 +526,6 @@ export class ApRendererService {
 | 
			
		|||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public renderRead(user: { id: User['id'] }, message: MessagingMessage): IRead {
 | 
			
		||||
		return {
 | 
			
		||||
			type: 'Read',
 | 
			
		||||
			actor: `${this.config.url}/users/${user.id}`,
 | 
			
		||||
			object: message.uri!,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public renderReject(object: any, user: { id: User['id'] }): IReject {
 | 
			
		||||
		return {
 | 
			
		||||
| 
						 | 
				
			
			@ -643,7 +628,6 @@ export class ApRendererService {
 | 
			
		|||
					'_misskey_quote': 'misskey:_misskey_quote',
 | 
			
		||||
					'_misskey_reaction': 'misskey:_misskey_reaction',
 | 
			
		||||
					'_misskey_votes': 'misskey:_misskey_votes',
 | 
			
		||||
					'_misskey_talk': 'misskey:_misskey_talk',
 | 
			
		||||
					'isCat': 'misskey:isCat',
 | 
			
		||||
					// vcard
 | 
			
		||||
					vcard: 'http://www.w3.org/2006/vcard/ns#',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import promiseLimit from 'promise-limit';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
 | 
			
		||||
import type { PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import type { RemoteUser } from '@/models/entities/User.js';
 | 
			
		||||
import type { Note } from '@/models/entities/Note.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +16,6 @@ import { IdService } from '@/core/IdService.js';
 | 
			
		|||
import { PollService } from '@/core/PollService.js';
 | 
			
		||||
import { StatusError } from '@/misc/status-error.js';
 | 
			
		||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import { MessagingService } from '@/core/MessagingService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
 | 
			
		||||
| 
						 | 
				
			
			@ -47,9 +46,6 @@ export class ApNoteService {
 | 
			
		|||
		@Inject(DI.emojisRepository)
 | 
			
		||||
		private emojisRepository: EmojisRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private apMfmService: ApMfmService,
 | 
			
		||||
		private apResolverService: ApResolverService,
 | 
			
		||||
| 
						 | 
				
			
			@ -64,7 +60,6 @@ export class ApNoteService {
 | 
			
		|||
		private apImageService: ApImageService,
 | 
			
		||||
		private apQuestionService: ApQuestionService,
 | 
			
		||||
		private metaService: MetaService,
 | 
			
		||||
		private messagingService: MessagingService,
 | 
			
		||||
		private appLockService: AppLockService,
 | 
			
		||||
		private pollService: PollService,
 | 
			
		||||
		private noteCreateService: NoteCreateService,
 | 
			
		||||
| 
						 | 
				
			
			@ -165,8 +160,6 @@ export class ApNoteService {
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
	
 | 
			
		||||
		let isMessaging = note._misskey_talk && visibility === 'specified';
 | 
			
		||||
	
 | 
			
		||||
		const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
 | 
			
		||||
		const apHashtags = await extractApHashtags(note.tag);
 | 
			
		||||
	
 | 
			
		||||
| 
						 | 
				
			
			@ -193,17 +186,6 @@ export class ApNoteService {
 | 
			
		|||
					return x;
 | 
			
		||||
				}
 | 
			
		||||
			}).catch(async err => {
 | 
			
		||||
				// トークだったらinReplyToのエラーは無視
 | 
			
		||||
				const uri = getApId(note.inReplyTo);
 | 
			
		||||
				if (uri.startsWith(this.config.url + '/')) {
 | 
			
		||||
					const id = uri.split('/').pop();
 | 
			
		||||
					const talk = await this.messagingMessagesRepository.findOneBy({ id });
 | 
			
		||||
					if (talk) {
 | 
			
		||||
						isMessaging = true;
 | 
			
		||||
						return null;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
	
 | 
			
		||||
				this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
 | 
			
		||||
				throw err;
 | 
			
		||||
			})
 | 
			
		||||
| 
						 | 
				
			
			@ -293,13 +275,6 @@ export class ApNoteService {
 | 
			
		|||
	
 | 
			
		||||
		const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
 | 
			
		||||
		
 | 
			
		||||
		if (isMessaging) {
 | 
			
		||||
			for (const recipient of visibleUsers) {
 | 
			
		||||
				await this.messagingService.createMessage(actor, recipient, undefined, text ?? undefined, (files && files.length > 0) ? files[0] : null, object.id);
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	
 | 
			
		||||
		return await this.noteCreateService.create(actor, {
 | 
			
		||||
			createdAt: note.published ? new Date(note.published) : null,
 | 
			
		||||
			files,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -113,7 +113,6 @@ export interface IPost extends IObject {
 | 
			
		|||
	_misskey_quote?: string;
 | 
			
		||||
	_misskey_content?: string;
 | 
			
		||||
	quoteUrl?: string;
 | 
			
		||||
	_misskey_talk?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IQuestion extends IObject {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,59 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { MessagingMessagesRepository } from '@/models/index.js';
 | 
			
		||||
import { awaitAll } from '@/misc/prelude/await-all.js';
 | 
			
		||||
import type { Packed } from '@/misc/schema.js';
 | 
			
		||||
import type { } from '@/models/entities/Blocking.js';
 | 
			
		||||
import type { User } from '@/models/entities/User.js';
 | 
			
		||||
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
 | 
			
		||||
import { UserEntityService } from './UserEntityService.js';
 | 
			
		||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
 | 
			
		||||
import { UserGroupEntityService } from './UserGroupEntityService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class MessagingMessageEntityService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private userGroupEntityService: UserGroupEntityService,
 | 
			
		||||
		private driveFileEntityService: DriveFileEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async pack(
 | 
			
		||||
		src: MessagingMessage['id'] | MessagingMessage,
 | 
			
		||||
		me?: { id: User['id'] } | null | undefined,
 | 
			
		||||
		options?: {
 | 
			
		||||
			populateRecipient?: boolean,
 | 
			
		||||
			populateGroup?: boolean,
 | 
			
		||||
		},
 | 
			
		||||
	): Promise<Packed<'MessagingMessage'>> {
 | 
			
		||||
		const opts = options ?? {
 | 
			
		||||
			populateRecipient: true,
 | 
			
		||||
			populateGroup: true,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const message = typeof src === 'object' ? src : await this.messagingMessagesRepository.findOneByOrFail({ id: src });
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			id: message.id,
 | 
			
		||||
			createdAt: message.createdAt.toISOString(),
 | 
			
		||||
			text: message.text,
 | 
			
		||||
			userId: message.userId,
 | 
			
		||||
			user: await this.userEntityService.pack(message.user ?? message.userId, me),
 | 
			
		||||
			recipientId: message.recipientId,
 | 
			
		||||
			recipient: message.recipientId && opts.populateRecipient ? await this.userEntityService.pack(message.recipient ?? message.recipientId, me) : undefined,
 | 
			
		||||
			groupId: message.groupId,
 | 
			
		||||
			group: message.groupId && opts.populateGroup ? await this.userGroupEntityService.pack(message.group ?? message.groupId) : undefined,
 | 
			
		||||
			fileId: message.fileId,
 | 
			
		||||
			file: message.fileId ? await this.driveFileEntityService.pack(message.fileId) : null,
 | 
			
		||||
			isRead: message.isRead,
 | 
			
		||||
			reads: message.reads,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ import { Cache } from '@/misc/cache.js';
 | 
			
		|||
import type { Instance } from '@/models/entities/Instance.js';
 | 
			
		||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
 | 
			
		||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
 | 
			
		||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js';
 | 
			
		||||
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import type { OnModuleInit } from '@nestjs/common';
 | 
			
		||||
| 
						 | 
				
			
			@ -102,9 +102,6 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		|||
		@Inject(DI.announcementReadsRepository)
 | 
			
		||||
		private announcementReadsRepository: AnnouncementReadsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userGroupJoiningsRepository)
 | 
			
		||||
		private userGroupJoiningsRepository: UserGroupJoiningsRepository,
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -204,36 +201,6 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		|||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
 | 
			
		||||
		const mute = await this.mutingsRepository.findBy({
 | 
			
		||||
			muterId: userId,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const joinings = await this.userGroupJoiningsRepository.findBy({ userId: userId });
 | 
			
		||||
 | 
			
		||||
		const groupQs = Promise.all(joinings.map(j => this.messagingMessagesRepository.createQueryBuilder('message')
 | 
			
		||||
			.where('message.groupId = :groupId', { groupId: j.userGroupId })
 | 
			
		||||
			.andWhere('message.userId != :userId', { userId: userId })
 | 
			
		||||
			.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
 | 
			
		||||
			.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
 | 
			
		||||
			.getOne().then(x => x != null)));
 | 
			
		||||
 | 
			
		||||
		const [withUser, withGroups] = await Promise.all([
 | 
			
		||||
			this.messagingMessagesRepository.count({
 | 
			
		||||
				where: {
 | 
			
		||||
					recipientId: userId,
 | 
			
		||||
					isRead: false,
 | 
			
		||||
					...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}),
 | 
			
		||||
				},
 | 
			
		||||
				take: 1,
 | 
			
		||||
			}).then(count => count > 0),
 | 
			
		||||
			groupQs,
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
		return withUser || withGroups.some(x => x);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
 | 
			
		||||
		const reads = await this.announcementReadsRepository.findBy({
 | 
			
		||||
| 
						 | 
				
			
			@ -492,7 +459,6 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		|||
				hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
 | 
			
		||||
				hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
 | 
			
		||||
				hasUnreadChannel: this.getHasUnreadChannel(user.id),
 | 
			
		||||
				hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
 | 
			
		||||
				hasUnreadNotification: this.getHasUnreadNotification(user.id),
 | 
			
		||||
				hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
 | 
			
		||||
				mutedWords: profile!.mutedWords,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,7 +47,6 @@ export const DI = {
 | 
			
		|||
	authSessionsRepository: Symbol('authSessionsRepository'),
 | 
			
		||||
	accessTokensRepository: Symbol('accessTokensRepository'),
 | 
			
		||||
	signinsRepository: Symbol('signinsRepository'),
 | 
			
		||||
	messagingMessagesRepository: Symbol('messagingMessagesRepository'),
 | 
			
		||||
	pagesRepository: Symbol('pagesRepository'),
 | 
			
		||||
	pageLikesRepository: Symbol('pageLikesRepository'),
 | 
			
		||||
	galleryPostsRepository: Symbol('galleryPostsRepository'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,6 @@ import {
 | 
			
		|||
import { packedNoteSchema } from '@/models/schema/note.js';
 | 
			
		||||
import { packedUserListSchema } from '@/models/schema/user-list.js';
 | 
			
		||||
import { packedAppSchema } from '@/models/schema/app.js';
 | 
			
		||||
import { packedMessagingMessageSchema } from '@/models/schema/messaging-message.js';
 | 
			
		||||
import { packedNotificationSchema } from '@/models/schema/notification.js';
 | 
			
		||||
import { packedDriveFileSchema } from '@/models/schema/drive-file.js';
 | 
			
		||||
import { packedDriveFolderSchema } from '@/models/schema/drive-folder.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +41,6 @@ export const refs = {
 | 
			
		|||
	UserList: packedUserListSchema,
 | 
			
		||||
	UserGroup: packedUserGroupSchema,
 | 
			
		||||
	App: packedAppSchema,
 | 
			
		||||
	MessagingMessage: packedMessagingMessageSchema,
 | 
			
		||||
	Note: packedNoteSchema,
 | 
			
		||||
	NoteReaction: packedNoteReactionSchema,
 | 
			
		||||
	NoteFavorite: packedNoteFavoriteSchema,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js';
 | 
			
		||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js';
 | 
			
		||||
import type { DataSource } from 'typeorm';
 | 
			
		||||
import type { Provider } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -256,12 +256,6 @@ const $signinsRepository: Provider = {
 | 
			
		|||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $messagingMessagesRepository: Provider = {
 | 
			
		||||
	provide: DI.messagingMessagesRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MessagingMessage),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $pagesRepository: Provider = {
 | 
			
		||||
	provide: DI.pagesRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(Page),
 | 
			
		||||
| 
						 | 
				
			
			@ -458,7 +452,6 @@ const $roleAssignmentsRepository: Provider = {
 | 
			
		|||
		$authSessionsRepository,
 | 
			
		||||
		$accessTokensRepository,
 | 
			
		||||
		$signinsRepository,
 | 
			
		||||
		$messagingMessagesRepository,
 | 
			
		||||
		$pagesRepository,
 | 
			
		||||
		$pageLikesRepository,
 | 
			
		||||
		$galleryPostsRepository,
 | 
			
		||||
| 
						 | 
				
			
			@ -528,7 +521,6 @@ const $roleAssignmentsRepository: Provider = {
 | 
			
		|||
		$authSessionsRepository,
 | 
			
		||||
		$accessTokensRepository,
 | 
			
		||||
		$signinsRepository,
 | 
			
		||||
		$messagingMessagesRepository,
 | 
			
		||||
		$pagesRepository,
 | 
			
		||||
		$pageLikesRepository,
 | 
			
		||||
		$galleryPostsRepository,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,89 +0,0 @@
 | 
			
		|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 | 
			
		||||
import { id } from '../id.js';
 | 
			
		||||
import { User } from './User.js';
 | 
			
		||||
import { DriveFile } from './DriveFile.js';
 | 
			
		||||
import { UserGroup } from './UserGroup.js';
 | 
			
		||||
 | 
			
		||||
@Entity()
 | 
			
		||||
export class MessagingMessage {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('timestamp with time zone', {
 | 
			
		||||
		comment: 'The created date of the MessagingMessage.',
 | 
			
		||||
	})
 | 
			
		||||
	public createdAt: Date;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		comment: 'The sender user ID.',
 | 
			
		||||
	})
 | 
			
		||||
	public userId: User['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => User, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public user: User | null;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(), nullable: true,
 | 
			
		||||
		comment: 'The recipient user ID.',
 | 
			
		||||
	})
 | 
			
		||||
	public recipientId: User['id'] | null;
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => User, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public recipient: User | null;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(), nullable: true,
 | 
			
		||||
		comment: 'The recipient group ID.',
 | 
			
		||||
	})
 | 
			
		||||
	public groupId: UserGroup['id'] | null;
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => UserGroup, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public group: UserGroup | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 4096, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public text: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public isRead: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 512, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public uri: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		array: true, default: '{}',
 | 
			
		||||
	})
 | 
			
		||||
	public reads: User['id'][];
 | 
			
		||||
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public fileId: DriveFile['id'] | null;
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => DriveFile, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public file: DriveFile | null;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -22,7 +22,6 @@ import { GalleryLike } from '@/models/entities/GalleryLike.js';
 | 
			
		|||
import { GalleryPost } from '@/models/entities/GalleryPost.js';
 | 
			
		||||
import { Hashtag } from '@/models/entities/Hashtag.js';
 | 
			
		||||
import { Instance } from '@/models/entities/Instance.js';
 | 
			
		||||
import { MessagingMessage } from '@/models/entities/MessagingMessage.js';
 | 
			
		||||
import { Meta } from '@/models/entities/Meta.js';
 | 
			
		||||
import { ModerationLog } from '@/models/entities/ModerationLog.js';
 | 
			
		||||
import { MutedNote } from '@/models/entities/MutedNote.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +92,6 @@ export {
 | 
			
		|||
	GalleryPost,
 | 
			
		||||
	Hashtag,
 | 
			
		||||
	Instance,
 | 
			
		||||
	MessagingMessage,
 | 
			
		||||
	Meta,
 | 
			
		||||
	ModerationLog,
 | 
			
		||||
	MutedNote,
 | 
			
		||||
| 
						 | 
				
			
			@ -163,7 +161,6 @@ export type GalleryLikesRepository = Repository<GalleryLike>;
 | 
			
		|||
export type GalleryPostsRepository = Repository<GalleryPost>;
 | 
			
		||||
export type HashtagsRepository = Repository<Hashtag>;
 | 
			
		||||
export type InstancesRepository = Repository<Instance>;
 | 
			
		||||
export type MessagingMessagesRepository = Repository<MessagingMessage>;
 | 
			
		||||
export type MetasRepository = Repository<Meta>;
 | 
			
		||||
export type ModerationLogsRepository = Repository<ModerationLog>;
 | 
			
		||||
export type MutedNotesRepository = Repository<MutedNote>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,73 +0,0 @@
 | 
			
		|||
export const packedMessagingMessageSchema = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		id: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
			example: 'xxxxxxxxxx',
 | 
			
		||||
		},
 | 
			
		||||
		createdAt: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			format: 'date-time',
 | 
			
		||||
		},
 | 
			
		||||
		userId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
		},
 | 
			
		||||
		user: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			ref: 'UserLite',
 | 
			
		||||
			optional: true, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		text: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: true,
 | 
			
		||||
		},
 | 
			
		||||
		fileId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
		},
 | 
			
		||||
		file: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
			ref: 'DriveFile',
 | 
			
		||||
		},
 | 
			
		||||
		recipientId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: true,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
		},
 | 
			
		||||
		recipient: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
			ref: 'UserLite',
 | 
			
		||||
		},
 | 
			
		||||
		groupId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: true,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
		},
 | 
			
		||||
		group: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
			ref: 'UserGroup',
 | 
			
		||||
		},
 | 
			
		||||
		isRead: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			optional: true, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		reads: {
 | 
			
		||||
			type: 'array',
 | 
			
		||||
			optional: true, nullable: false,
 | 
			
		||||
			items: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
				format: 'id',
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
| 
						 | 
				
			
			@ -311,10 +311,6 @@ export const packedMeDetailedOnlySchema = {
 | 
			
		|||
			type: 'boolean',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
		},
 | 
			
		||||
		hasUnreadMessagingMessage: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
		},
 | 
			
		||||
		hasUnreadNotification: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,6 @@ import { GalleryLike } from '@/models/entities/GalleryLike.js';
 | 
			
		|||
import { GalleryPost } from '@/models/entities/GalleryPost.js';
 | 
			
		||||
import { Hashtag } from '@/models/entities/Hashtag.js';
 | 
			
		||||
import { Instance } from '@/models/entities/Instance.js';
 | 
			
		||||
import { MessagingMessage } from '@/models/entities/MessagingMessage.js';
 | 
			
		||||
import { Meta } from '@/models/entities/Meta.js';
 | 
			
		||||
import { ModerationLog } from '@/models/entities/ModerationLog.js';
 | 
			
		||||
import { MutedNote } from '@/models/entities/MutedNote.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -167,7 +166,6 @@ export const entities = [
 | 
			
		|||
	SwSubscription,
 | 
			
		||||
	AbuseUserReport,
 | 
			
		||||
	RegistrationTicket,
 | 
			
		||||
	MessagingMessage,
 | 
			
		||||
	Signin,
 | 
			
		||||
	ModerationLog,
 | 
			
		||||
	Clip,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,8 +30,6 @@ import { HashtagChannelService } from './api/stream/channels/hashtag.js';
 | 
			
		|||
import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js';
 | 
			
		||||
import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js';
 | 
			
		||||
import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js';
 | 
			
		||||
import { MessagingIndexChannelService } from './api/stream/channels/messaging-index.js';
 | 
			
		||||
import { MessagingChannelService } from './api/stream/channels/messaging.js';
 | 
			
		||||
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
 | 
			
		||||
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
 | 
			
		||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -71,8 +69,6 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
 | 
			
		|||
		HomeTimelineChannelService,
 | 
			
		||||
		HybridTimelineChannelService,
 | 
			
		||||
		LocalTimelineChannelService,
 | 
			
		||||
		MessagingIndexChannelService,
 | 
			
		||||
		MessagingChannelService,
 | 
			
		||||
		QueueStatsChannelService,
 | 
			
		||||
		ServerStatsChannelService,
 | 
			
		||||
		UserListChannelService,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -195,7 +195,6 @@ import * as ep___i_notifications from './endpoints/i/notifications.js';
 | 
			
		|||
import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
 | 
			
		||||
import * as ep___i_pages from './endpoints/i/pages.js';
 | 
			
		||||
import * as ep___i_pin from './endpoints/i/pin.js';
 | 
			
		||||
import * as ep___i_readAllMessagingMessages from './endpoints/i/read-all-messaging-messages.js';
 | 
			
		||||
import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js';
 | 
			
		||||
import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js';
 | 
			
		||||
import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -218,11 +217,6 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
 | 
			
		|||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
 | 
			
		||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
 | 
			
		||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
 | 
			
		||||
import * as ep___messaging_history from './endpoints/messaging/history.js';
 | 
			
		||||
import * as ep___messaging_messages from './endpoints/messaging/messages.js';
 | 
			
		||||
import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js';
 | 
			
		||||
import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js';
 | 
			
		||||
import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js';
 | 
			
		||||
import * as ep___meta from './endpoints/meta.js';
 | 
			
		||||
import * as ep___emojis from './endpoints/emojis.js';
 | 
			
		||||
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -530,7 +524,6 @@ const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep
 | 
			
		|||
const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default };
 | 
			
		||||
const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default };
 | 
			
		||||
const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default };
 | 
			
		||||
const $i_readAllMessagingMessages: Provider = { provide: 'ep:i/read-all-messaging-messages', useClass: ep___i_readAllMessagingMessages.default };
 | 
			
		||||
const $i_readAllUnreadNotes: Provider = { provide: 'ep:i/read-all-unread-notes', useClass: ep___i_readAllUnreadNotes.default };
 | 
			
		||||
const $i_readAnnouncement: Provider = { provide: 'ep:i/read-announcement', useClass: ep___i_readAnnouncement.default };
 | 
			
		||||
const $i_regenerateToken: Provider = { provide: 'ep:i/regenerate-token', useClass: ep___i_regenerateToken.default };
 | 
			
		||||
| 
						 | 
				
			
			@ -553,11 +546,6 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
 | 
			
		|||
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
 | 
			
		||||
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
 | 
			
		||||
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
 | 
			
		||||
const $messaging_history: Provider = { provide: 'ep:messaging/history', useClass: ep___messaging_history.default };
 | 
			
		||||
const $messaging_messages: Provider = { provide: 'ep:messaging/messages', useClass: ep___messaging_messages.default };
 | 
			
		||||
const $messaging_messages_create: Provider = { provide: 'ep:messaging/messages/create', useClass: ep___messaging_messages_create.default };
 | 
			
		||||
const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default };
 | 
			
		||||
const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default };
 | 
			
		||||
const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
 | 
			
		||||
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
 | 
			
		||||
const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default };
 | 
			
		||||
| 
						 | 
				
			
			@ -869,7 +857,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 | 
			
		|||
		$i_pageLikes,
 | 
			
		||||
		$i_pages,
 | 
			
		||||
		$i_pin,
 | 
			
		||||
		$i_readAllMessagingMessages,
 | 
			
		||||
		$i_readAllUnreadNotes,
 | 
			
		||||
		$i_readAnnouncement,
 | 
			
		||||
		$i_regenerateToken,
 | 
			
		||||
| 
						 | 
				
			
			@ -892,11 +879,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 | 
			
		|||
		$i_webhooks_show,
 | 
			
		||||
		$i_webhooks_update,
 | 
			
		||||
		$i_webhooks_delete,
 | 
			
		||||
		$messaging_history,
 | 
			
		||||
		$messaging_messages,
 | 
			
		||||
		$messaging_messages_create,
 | 
			
		||||
		$messaging_messages_delete,
 | 
			
		||||
		$messaging_messages_read,
 | 
			
		||||
		$meta,
 | 
			
		||||
		$emojis,
 | 
			
		||||
		$miauth_genToken,
 | 
			
		||||
| 
						 | 
				
			
			@ -1202,7 +1184,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 | 
			
		|||
		$i_pageLikes,
 | 
			
		||||
		$i_pages,
 | 
			
		||||
		$i_pin,
 | 
			
		||||
		$i_readAllMessagingMessages,
 | 
			
		||||
		$i_readAllUnreadNotes,
 | 
			
		||||
		$i_readAnnouncement,
 | 
			
		||||
		$i_regenerateToken,
 | 
			
		||||
| 
						 | 
				
			
			@ -1225,11 +1206,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 | 
			
		|||
		$i_webhooks_show,
 | 
			
		||||
		$i_webhooks_update,
 | 
			
		||||
		$i_webhooks_delete,
 | 
			
		||||
		$messaging_history,
 | 
			
		||||
		$messaging_messages,
 | 
			
		||||
		$messaging_messages_create,
 | 
			
		||||
		$messaging_messages_delete,
 | 
			
		||||
		$messaging_messages_read,
 | 
			
		||||
		$meta,
 | 
			
		||||
		$emojis,
 | 
			
		||||
		$miauth_genToken,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -194,7 +194,6 @@ import * as ep___i_notifications from './endpoints/i/notifications.js';
 | 
			
		|||
import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
 | 
			
		||||
import * as ep___i_pages from './endpoints/i/pages.js';
 | 
			
		||||
import * as ep___i_pin from './endpoints/i/pin.js';
 | 
			
		||||
import * as ep___i_readAllMessagingMessages from './endpoints/i/read-all-messaging-messages.js';
 | 
			
		||||
import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js';
 | 
			
		||||
import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js';
 | 
			
		||||
import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -217,11 +216,6 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
 | 
			
		|||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
 | 
			
		||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
 | 
			
		||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
 | 
			
		||||
import * as ep___messaging_history from './endpoints/messaging/history.js';
 | 
			
		||||
import * as ep___messaging_messages from './endpoints/messaging/messages.js';
 | 
			
		||||
import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js';
 | 
			
		||||
import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js';
 | 
			
		||||
import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js';
 | 
			
		||||
import * as ep___meta from './endpoints/meta.js';
 | 
			
		||||
import * as ep___emojis from './endpoints/emojis.js';
 | 
			
		||||
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -527,7 +521,6 @@ const eps = [
 | 
			
		|||
	['i/page-likes', ep___i_pageLikes],
 | 
			
		||||
	['i/pages', ep___i_pages],
 | 
			
		||||
	['i/pin', ep___i_pin],
 | 
			
		||||
	['i/read-all-messaging-messages', ep___i_readAllMessagingMessages],
 | 
			
		||||
	['i/read-all-unread-notes', ep___i_readAllUnreadNotes],
 | 
			
		||||
	['i/read-announcement', ep___i_readAnnouncement],
 | 
			
		||||
	['i/regenerate-token', ep___i_regenerateToken],
 | 
			
		||||
| 
						 | 
				
			
			@ -550,11 +543,6 @@ const eps = [
 | 
			
		|||
	['i/webhooks/show', ep___i_webhooks_show],
 | 
			
		||||
	['i/webhooks/update', ep___i_webhooks_update],
 | 
			
		||||
	['i/webhooks/delete', ep___i_webhooks_delete],
 | 
			
		||||
	['messaging/history', ep___messaging_history],
 | 
			
		||||
	['messaging/messages', ep___messaging_messages],
 | 
			
		||||
	['messaging/messages/create', ep___messaging_messages_create],
 | 
			
		||||
	['messaging/messages/delete', ep___messaging_messages_delete],
 | 
			
		||||
	['messaging/messages/read', ep___messaging_messages_read],
 | 
			
		||||
	['meta', ep___meta],
 | 
			
		||||
	['emojis', ep___emojis],
 | 
			
		||||
	['miauth/gen-token', ep___miauth_genToken],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,56 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['account', 'messaging'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:account',
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {},
 | 
			
		||||
	required: [],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userGroupJoiningsRepository)
 | 
			
		||||
		private userGroupJoiningsRepository: UserGroupJoiningsRepository,
 | 
			
		||||
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			// Update documents
 | 
			
		||||
			await this.messagingMessagesRepository.update({
 | 
			
		||||
				recipientId: me.id,
 | 
			
		||||
				isRead: false,
 | 
			
		||||
			}, {
 | 
			
		||||
				isRead: true,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			const joinings = await this.userGroupJoiningsRepository.findBy({ userId: me.id });
 | 
			
		||||
 | 
			
		||||
			await Promise.all(joinings.map(j => this.messagingMessagesRepository.createQueryBuilder().update()
 | 
			
		||||
				.set({
 | 
			
		||||
					reads: (() => `array_append("reads", '${me.id}')`) as any,
 | 
			
		||||
				})
 | 
			
		||||
				.where('groupId = :groupId', { groupId: j.userGroupId })
 | 
			
		||||
				.andWhere('userId != :userId', { userId: me.id })
 | 
			
		||||
				.andWhere('NOT (:userId = ANY(reads))', { userId: me.id })
 | 
			
		||||
				.execute()));
 | 
			
		||||
 | 
			
		||||
			this.globalEventService.publishMainStream(me.id, 'readAllMessagingMessages');
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,110 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Brackets } from 'typeorm';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
 | 
			
		||||
import type { MutingsRepository, UserGroupJoiningsRepository, MessagingMessagesRepository } from '@/models/index.js';
 | 
			
		||||
import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['messaging'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'read:messaging',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'MessagingMessage',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
			
		||||
		group: { type: 'boolean', default: false },
 | 
			
		||||
	},
 | 
			
		||||
	required: [],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.mutingsRepository)
 | 
			
		||||
		private mutingsRepository: MutingsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userGroupJoiningsRepository)
 | 
			
		||||
		private userGroupJoiningsRepository: UserGroupJoiningsRepository,
 | 
			
		||||
 | 
			
		||||
		private messagingMessageEntityService: MessagingMessageEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const mute = await this.mutingsRepository.findBy({
 | 
			
		||||
				muterId: me.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			const groups = ps.group ? await this.userGroupJoiningsRepository.findBy({
 | 
			
		||||
				userId: me.id,
 | 
			
		||||
			}).then(xs => xs.map(x => x.userGroupId)) : [];
 | 
			
		||||
 | 
			
		||||
			if (ps.group && groups.length === 0) {
 | 
			
		||||
				return [];
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const history: MessagingMessage[] = [];
 | 
			
		||||
 | 
			
		||||
			for (let i = 0; i < ps.limit; i++) {
 | 
			
		||||
				const found = ps.group
 | 
			
		||||
					? history.map(m => m.groupId!)
 | 
			
		||||
					: history.map(m => (m.userId === me.id) ? m.recipientId! : m.userId!);
 | 
			
		||||
 | 
			
		||||
				const query = this.messagingMessagesRepository.createQueryBuilder('message')
 | 
			
		||||
					.orderBy('message.createdAt', 'DESC');
 | 
			
		||||
 | 
			
		||||
				if (ps.group) {
 | 
			
		||||
					query.where('message.groupId IN (:...groups)', { groups: groups });
 | 
			
		||||
 | 
			
		||||
					if (found.length > 0) {
 | 
			
		||||
						query.andWhere('message.groupId NOT IN (:...found)', { found: found });
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					query.where(new Brackets(qb => { qb
 | 
			
		||||
						.where('message.userId = :userId', { userId: me.id })
 | 
			
		||||
						.orWhere('message.recipientId = :userId', { userId: me.id });
 | 
			
		||||
					}));
 | 
			
		||||
					query.andWhere('message.groupId IS NULL');
 | 
			
		||||
 | 
			
		||||
					if (found.length > 0) {
 | 
			
		||||
						query.andWhere('message.userId NOT IN (:...found)', { found: found });
 | 
			
		||||
						query.andWhere('message.recipientId NOT IN (:...found)', { found: found });
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if (mute.length > 0) {
 | 
			
		||||
						query.andWhere('message.userId NOT IN (:...mute)', { mute: mute.map(m => m.muteeId) });
 | 
			
		||||
						query.andWhere('message.recipientId NOT IN (:...mute)', { mute: mute.map(m => m.muteeId) });
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const message = await query.getOne();
 | 
			
		||||
 | 
			
		||||
				if (message) {
 | 
			
		||||
					history.push(message);
 | 
			
		||||
				} else {
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return await Promise.all(history.map(h => this.messagingMessageEntityService.pack(h.id, me)));
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,165 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Brackets } from 'typeorm';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { UsersRepository, UserGroupsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js';
 | 
			
		||||
import { QueryService } from '@/core/QueryService.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js';
 | 
			
		||||
import { MessagingService } from '@/core/MessagingService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../error.js';
 | 
			
		||||
import { GetterService } from '@/server/api/GetterService.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['messaging'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'read:messaging',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'MessagingMessage',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchUser: {
 | 
			
		||||
			message: 'No such user.',
 | 
			
		||||
			code: 'NO_SUCH_USER',
 | 
			
		||||
			id: '11795c64-40ea-4198-b06e-3c873ed9039d',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noSuchGroup: {
 | 
			
		||||
			message: 'No such group.',
 | 
			
		||||
			code: 'NO_SUCH_GROUP',
 | 
			
		||||
			id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		groupAccessDenied: {
 | 
			
		||||
			message: 'You can not read messages of groups that you have not joined.',
 | 
			
		||||
			code: 'GROUP_ACCESS_DENIED',
 | 
			
		||||
			id: 'a053a8dd-a491-4718-8f87-50775aad9284',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
			
		||||
		sinceId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		untilId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		markAsRead: { type: 'boolean', default: true },
 | 
			
		||||
	},
 | 
			
		||||
	anyOf: [
 | 
			
		||||
		{
 | 
			
		||||
			properties: {
 | 
			
		||||
				userId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
			},
 | 
			
		||||
			required: ['userId'],
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			properties: {
 | 
			
		||||
				groupId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
			},
 | 
			
		||||
			required: ['groupId'],
 | 
			
		||||
		},
 | 
			
		||||
	],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userGroupsRepository)
 | 
			
		||||
		private userGroupRepository: UserGroupsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userGroupJoiningsRepository)
 | 
			
		||||
		private userGroupJoiningsRepository: UserGroupJoiningsRepository,
 | 
			
		||||
 | 
			
		||||
		private messagingMessageEntityService: MessagingMessageEntityService,
 | 
			
		||||
		private messagingService: MessagingService,
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private queryService: QueryService,
 | 
			
		||||
		private getterService: GetterService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			if (ps.userId != null) {
 | 
			
		||||
				// Fetch recipient (user)
 | 
			
		||||
				const recipient = await this.getterService.getUser(ps.userId).catch(err => {
 | 
			
		||||
					if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
 | 
			
		||||
					throw err;
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				const query = this.queryService.makePaginationQuery(this.messagingMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId)
 | 
			
		||||
					.andWhere(new Brackets(qb => { qb
 | 
			
		||||
						.where(new Brackets(qb => { qb
 | 
			
		||||
							.where('message.userId = :meId')
 | 
			
		||||
							.andWhere('message.recipientId = :recipientId');
 | 
			
		||||
						}))
 | 
			
		||||
						.orWhere(new Brackets(qb => { qb
 | 
			
		||||
							.where('message.userId = :recipientId')
 | 
			
		||||
							.andWhere('message.recipientId = :meId');
 | 
			
		||||
						}));
 | 
			
		||||
					}))
 | 
			
		||||
					.setParameter('meId', me.id)
 | 
			
		||||
					.setParameter('recipientId', recipient.id);
 | 
			
		||||
 | 
			
		||||
				const messages = await query.take(ps.limit).getMany();
 | 
			
		||||
 | 
			
		||||
				// Mark all as read
 | 
			
		||||
				if (ps.markAsRead) {
 | 
			
		||||
					this.messagingService.readUserMessagingMessage(me.id, recipient.id, messages.filter(m => m.recipientId === me.id).map(x => x.id));
 | 
			
		||||
 | 
			
		||||
					// リモートユーザーとのメッセージだったら既読配信
 | 
			
		||||
					if (this.userEntityService.isLocalUser(me) && this.userEntityService.isRemoteUser(recipient)) {
 | 
			
		||||
						this.messagingService.deliverReadActivity(me, recipient, messages);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, {
 | 
			
		||||
					populateRecipient: false,
 | 
			
		||||
				})));
 | 
			
		||||
			} else if (ps.groupId != null) {
 | 
			
		||||
				// Fetch recipient (group)
 | 
			
		||||
				const recipientGroup = await this.userGroupRepository.findOneBy({ id: ps.groupId });
 | 
			
		||||
 | 
			
		||||
				if (recipientGroup == null) {
 | 
			
		||||
					throw new ApiError(meta.errors.noSuchGroup);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// check joined
 | 
			
		||||
				const joining = await this.userGroupJoiningsRepository.findOneBy({
 | 
			
		||||
					userId: me.id,
 | 
			
		||||
					userGroupId: recipientGroup.id,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if (joining == null) {
 | 
			
		||||
					throw new ApiError(meta.errors.groupAccessDenied);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const query = this.queryService.makePaginationQuery(this.messagingMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId)
 | 
			
		||||
					.andWhere('message.groupId = :groupId', { groupId: recipientGroup.id });
 | 
			
		||||
 | 
			
		||||
				const messages = await query.take(ps.limit).getMany();
 | 
			
		||||
 | 
			
		||||
				// Mark all as read
 | 
			
		||||
				if (ps.markAsRead) {
 | 
			
		||||
					this.messagingService.readGroupMessagingMessage(me.id, recipientGroup.id, messages.map(x => x.id));
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, {
 | 
			
		||||
					populateGroup: false,
 | 
			
		||||
				})));
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,179 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { BlockingsRepository, UserGroupJoiningsRepository, DriveFilesRepository, UserGroupsRepository } from '@/models/index.js';
 | 
			
		||||
import type { User } from '@/models/entities/User.js';
 | 
			
		||||
import type { UserGroup } from '@/models/entities/UserGroup.js';
 | 
			
		||||
import { GetterService } from '@/server/api/GetterService.js';
 | 
			
		||||
import { MessagingService } from '@/core/MessagingService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['messaging'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:messaging',
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1hour'),
 | 
			
		||||
		max: 120,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		ref: 'MessagingMessage',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		recipientIsYourself: {
 | 
			
		||||
			message: 'You can not send a message to yourself.',
 | 
			
		||||
			code: 'RECIPIENT_IS_YOURSELF',
 | 
			
		||||
			id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noSuchUser: {
 | 
			
		||||
			message: 'No such user.',
 | 
			
		||||
			code: 'NO_SUCH_USER',
 | 
			
		||||
			id: '11795c64-40ea-4198-b06e-3c873ed9039d',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noSuchGroup: {
 | 
			
		||||
			message: 'No such group.',
 | 
			
		||||
			code: 'NO_SUCH_GROUP',
 | 
			
		||||
			id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		groupAccessDenied: {
 | 
			
		||||
			message: 'You can not send messages to groups that you have not joined.',
 | 
			
		||||
			code: 'GROUP_ACCESS_DENIED',
 | 
			
		||||
			id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noSuchFile: {
 | 
			
		||||
			message: 'No such file.',
 | 
			
		||||
			code: 'NO_SUCH_FILE',
 | 
			
		||||
			id: '4372b8e2-185d-4146-8749-2f68864a3e5f',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		contentRequired: {
 | 
			
		||||
			message: 'Content required. You need to set text or fileId.',
 | 
			
		||||
			code: 'CONTENT_REQUIRED',
 | 
			
		||||
			id: '25587321-b0e6-449c-9239-f8925092942c',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		youHaveBeenBlocked: {
 | 
			
		||||
			message: 'You cannot send a message because you have been blocked by this user.',
 | 
			
		||||
			code: 'YOU_HAVE_BEEN_BLOCKED',
 | 
			
		||||
			id: 'c15a5199-7422-4968-941a-2a462c478f7d',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		text: { type: 'string', nullable: true, maxLength: 3000 },
 | 
			
		||||
		fileId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	anyOf: [
 | 
			
		||||
		{
 | 
			
		||||
			properties: {
 | 
			
		||||
				userId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
			},
 | 
			
		||||
			required: ['userId'],
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			properties: {
 | 
			
		||||
				groupId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
			},
 | 
			
		||||
			required: ['groupId'],
 | 
			
		||||
		},
 | 
			
		||||
	],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.userGroupsRepository)
 | 
			
		||||
		private userGroupsRepository: UserGroupsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userGroupJoiningsRepository)
 | 
			
		||||
		private userGroupJoiningsRepository: UserGroupJoiningsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.blockingsRepository)
 | 
			
		||||
		private blockingsRepository: BlockingsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.driveFilesRepository)
 | 
			
		||||
		private driveFilesRepository: DriveFilesRepository,
 | 
			
		||||
 | 
			
		||||
		private getterService: GetterService,
 | 
			
		||||
		private messagingService: MessagingService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			let recipientUser: User | null;
 | 
			
		||||
			let recipientGroup: UserGroup | null;
 | 
			
		||||
 | 
			
		||||
			if (ps.userId != null) {
 | 
			
		||||
				// Myself
 | 
			
		||||
				if (ps.userId === me.id) {
 | 
			
		||||
					throw new ApiError(meta.errors.recipientIsYourself);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Fetch recipient (user)
 | 
			
		||||
				recipientUser = 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 blocking
 | 
			
		||||
				const block = await this.blockingsRepository.findOneBy({
 | 
			
		||||
					blockerId: recipientUser.id,
 | 
			
		||||
					blockeeId: me.id,
 | 
			
		||||
				});
 | 
			
		||||
				if (block) {
 | 
			
		||||
					throw new ApiError(meta.errors.youHaveBeenBlocked);
 | 
			
		||||
				}
 | 
			
		||||
			} else if (ps.groupId != null) {
 | 
			
		||||
				// Fetch recipient (group)
 | 
			
		||||
				recipientGroup = await this.userGroupsRepository.findOneBy({ id: ps.groupId! });
 | 
			
		||||
 | 
			
		||||
				if (recipientGroup == null) {
 | 
			
		||||
					throw new ApiError(meta.errors.noSuchGroup);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// check joined
 | 
			
		||||
				const joining = await this.userGroupJoiningsRepository.findOneBy({
 | 
			
		||||
					userId: me.id,
 | 
			
		||||
					userGroupId: recipientGroup.id,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if (joining == null) {
 | 
			
		||||
					throw new ApiError(meta.errors.groupAccessDenied);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			let file = null;
 | 
			
		||||
			if (ps.fileId != null) {
 | 
			
		||||
				file = await this.driveFilesRepository.findOneBy({
 | 
			
		||||
					id: ps.fileId,
 | 
			
		||||
					userId: me.id,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if (file == null) {
 | 
			
		||||
					throw new ApiError(meta.errors.noSuchFile);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// テキストが無いかつ添付ファイルも無かったらエラー
 | 
			
		||||
			if (ps.text == null && file == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.contentRequired);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return await this.messagingService.createMessage(me, recipientUser, recipientGroup, ps.text, file);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,61 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { MessagingMessagesRepository } from '@/models/index.js';
 | 
			
		||||
import { MessagingService } from '@/core/MessagingService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['messaging'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:messaging',
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1hour'),
 | 
			
		||||
		max: 300,
 | 
			
		||||
		minInterval: ms('1sec'),
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchMessage: {
 | 
			
		||||
			message: 'No such message.',
 | 
			
		||||
			code: 'NO_SUCH_MESSAGE',
 | 
			
		||||
			id: '54b5b326-7925-42cf-8019-130fda8b56af',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		messageId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['messageId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		private messagingService: MessagingService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const message = await this.messagingMessagesRepository.findOneBy({
 | 
			
		||||
				id: ps.messageId,
 | 
			
		||||
				userId: me.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (message == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchMessage);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			await this.messagingService.deleteMessage(message);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,61 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { MessagingMessagesRepository } from '@/models/index.js';
 | 
			
		||||
import { MessagingService } from '@/core/MessagingService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['messaging'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:messaging',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchMessage: {
 | 
			
		||||
			message: 'No such message.',
 | 
			
		||||
			code: 'NO_SUCH_MESSAGE',
 | 
			
		||||
			id: '86d56a2f-a9c3-4afb-b13c-3e9bfef9aa14',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		messageId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['messageId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		private messagingService: MessagingService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const message = await this.messagingMessagesRepository.findOneBy({ id: ps.messageId });
 | 
			
		||||
 | 
			
		||||
			if (message == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchMessage);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (message.recipientId) {
 | 
			
		||||
				await this.messagingService.readUserMessagingMessage(me.id, message.userId, [message.id]).catch(err => {
 | 
			
		||||
					if (err.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage);
 | 
			
		||||
					throw err;
 | 
			
		||||
				});
 | 
			
		||||
			} else if (message.groupId) {
 | 
			
		||||
				await this.messagingService.readGroupMessagingMessage(me.id, message.groupId, [message.id]).catch(err => {
 | 
			
		||||
					if (err.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage);
 | 
			
		||||
					throw err;
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { HybridTimelineChannelService } from './channels/hybrid-timeline.js';
 | 
			
		||||
import { LocalTimelineChannelService } from './channels/local-timeline.js';
 | 
			
		||||
import { HomeTimelineChannelService } from './channels/home-timeline.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -11,11 +12,8 @@ import { ServerStatsChannelService } from './channels/server-stats.js';
 | 
			
		|||
import { QueueStatsChannelService } from './channels/queue-stats.js';
 | 
			
		||||
import { UserListChannelService } from './channels/user-list.js';
 | 
			
		||||
import { AntennaChannelService } from './channels/antenna.js';
 | 
			
		||||
import { MessagingChannelService } from './channels/messaging.js';
 | 
			
		||||
import { MessagingIndexChannelService } from './channels/messaging-index.js';
 | 
			
		||||
import { DriveChannelService } from './channels/drive.js';
 | 
			
		||||
import { HashtagChannelService } from './channels/hashtag.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ChannelsService {
 | 
			
		||||
| 
						 | 
				
			
			@ -29,8 +27,6 @@ export class ChannelsService {
 | 
			
		|||
		private hashtagChannelService: HashtagChannelService,
 | 
			
		||||
		private antennaChannelService: AntennaChannelService,
 | 
			
		||||
		private channelChannelService: ChannelChannelService,
 | 
			
		||||
		private messagingChannelService: MessagingChannelService,
 | 
			
		||||
		private messagingIndexChannelService: MessagingIndexChannelService,
 | 
			
		||||
		private driveChannelService: DriveChannelService,
 | 
			
		||||
		private serverStatsChannelService: ServerStatsChannelService,
 | 
			
		||||
		private queueStatsChannelService: QueueStatsChannelService,
 | 
			
		||||
| 
						 | 
				
			
			@ -50,8 +46,6 @@ export class ChannelsService {
 | 
			
		|||
			case 'hashtag': return this.hashtagChannelService;
 | 
			
		||||
			case 'antenna': return this.antennaChannelService;
 | 
			
		||||
			case 'channel': return this.channelChannelService;
 | 
			
		||||
			case 'messaging': return this.messagingChannelService;
 | 
			
		||||
			case 'messagingIndex': return this.messagingIndexChannelService;
 | 
			
		||||
			case 'drive': return this.driveChannelService;
 | 
			
		||||
			case 'serverStats': return this.serverStatsChannelService;
 | 
			
		||||
			case 'queueStats': return this.queueStatsChannelService;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,32 +1,25 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { NotesRepository, UsersRepository } from '@/models/index.js';
 | 
			
		||||
import { isUserRelated } from '@/misc/is-user-related.js';
 | 
			
		||||
import type { User } from '@/models/entities/User.js';
 | 
			
		||||
import type { Packed } from '@/misc/schema.js';
 | 
			
		||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import Channel from '../channel.js';
 | 
			
		||||
import type { StreamMessages } from '../types.js';
 | 
			
		||||
 | 
			
		||||
class ChannelChannel extends Channel {
 | 
			
		||||
	public readonly chName = 'channel';
 | 
			
		||||
	public static shouldShare = false;
 | 
			
		||||
	public static requireCredential = false;
 | 
			
		||||
	private channelId: string;
 | 
			
		||||
	private typers: Record<User['id'], Date> = {};
 | 
			
		||||
	private emitTypersIntervalId: ReturnType<typeof setInterval>;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private noteEntityService: NoteEntityService,
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
 | 
			
		||||
		id: string,
 | 
			
		||||
		connection: Channel['connection'],
 | 
			
		||||
	) {
 | 
			
		||||
		super(id, connection);
 | 
			
		||||
		//this.onNote = this.onNote.bind(this);
 | 
			
		||||
		//this.emitTypers = this.emitTypers.bind(this);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
| 
						 | 
				
			
			@ -35,8 +28,6 @@ class ChannelChannel extends Channel {
 | 
			
		|||
 | 
			
		||||
		// Subscribe stream
 | 
			
		||||
		this.subscriber.on('notesStream', this.onNote);
 | 
			
		||||
		this.subscriber.on(`channelStream:${this.channelId}`, this.onEvent);
 | 
			
		||||
		this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
| 
						 | 
				
			
			@ -66,42 +57,10 @@ class ChannelChannel extends Channel {
 | 
			
		|||
		this.send('note', note);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private onEvent(data: StreamMessages['channel']['payload']) {
 | 
			
		||||
		if (data.type === 'typing') {
 | 
			
		||||
			const id = data.body;
 | 
			
		||||
			const begin = this.typers[id] == null;
 | 
			
		||||
			this.typers[id] = new Date();
 | 
			
		||||
			if (begin) {
 | 
			
		||||
				this.emitTypers();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async emitTypers() {
 | 
			
		||||
		const now = new Date();
 | 
			
		||||
 | 
			
		||||
		// Remove not typing users
 | 
			
		||||
		for (const [userId, date] of Object.entries(this.typers)) {
 | 
			
		||||
			if (now.getTime() - date.getTime() > 5000) delete this.typers[userId];
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { detail: false });
 | 
			
		||||
 | 
			
		||||
		this.send({
 | 
			
		||||
			type: 'typers',
 | 
			
		||||
			body: users,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public dispose() {
 | 
			
		||||
		// Unsubscribe events
 | 
			
		||||
		this.subscriber.off('notesStream', this.onNote);
 | 
			
		||||
		this.subscriber.off(`channelStream:${this.channelId}`, this.onEvent);
 | 
			
		||||
 | 
			
		||||
		clearInterval(this.emitTypersIntervalId);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +71,6 @@ export class ChannelChannelService {
 | 
			
		|||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private noteEntityService: NoteEntityService,
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -120,7 +78,6 @@ export class ChannelChannelService {
 | 
			
		|||
	public create(id: string, connection: Channel['connection']): ChannelChannel {
 | 
			
		||||
		return new ChannelChannel(
 | 
			
		||||
			this.noteEntityService,
 | 
			
		||||
			this.userEntityService,
 | 
			
		||||
			id,
 | 
			
		||||
			connection,
 | 
			
		||||
		);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,35 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import Channel from '../channel.js';
 | 
			
		||||
 | 
			
		||||
class MessagingIndexChannel extends Channel {
 | 
			
		||||
	public readonly chName = 'messagingIndex';
 | 
			
		||||
	public static shouldShare = true;
 | 
			
		||||
	public static requireCredential = true;
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async init(params: any) {
 | 
			
		||||
		// Subscribe messaging index stream
 | 
			
		||||
		this.subscriber.on(`messagingIndexStream:${this.user!.id}`, data => {
 | 
			
		||||
			this.send(data);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class MessagingIndexChannelService {
 | 
			
		||||
	public readonly shouldShare = MessagingIndexChannel.shouldShare;
 | 
			
		||||
	public readonly requireCredential = MessagingIndexChannel.requireCredential;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public create(id: string, connection: Channel['connection']): MessagingIndexChannel {
 | 
			
		||||
		return new MessagingIndexChannel(
 | 
			
		||||
			id,
 | 
			
		||||
			connection,
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,159 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { UserGroupJoiningsRepository, UsersRepository, MessagingMessagesRepository } from '@/models/index.js';
 | 
			
		||||
import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js';
 | 
			
		||||
import type { UserGroup } from '@/models/entities/UserGroup.js';
 | 
			
		||||
import { MessagingService } from '@/core/MessagingService.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import Channel from '../channel.js';
 | 
			
		||||
import type { StreamMessages } from '../types.js';
 | 
			
		||||
 | 
			
		||||
class MessagingChannel extends Channel {
 | 
			
		||||
	public readonly chName = 'messaging';
 | 
			
		||||
	public static shouldShare = false;
 | 
			
		||||
	public static requireCredential = true;
 | 
			
		||||
 | 
			
		||||
	private otherpartyId: string | null;
 | 
			
		||||
	private otherparty: User | null;
 | 
			
		||||
	private groupId: string | null;
 | 
			
		||||
	private subCh: `messagingStream:${User['id']}-${User['id']}` | `messagingStream:${UserGroup['id']}`;
 | 
			
		||||
	private typers: Record<User['id'], Date> = {};
 | 
			
		||||
	private emitTypersIntervalId: ReturnType<typeof setInterval>;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
		private userGroupJoiningsRepository: UserGroupJoiningsRepository,
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private messagingService: MessagingService,
 | 
			
		||||
 | 
			
		||||
		id: string,
 | 
			
		||||
		connection: Channel['connection'],
 | 
			
		||||
	) {
 | 
			
		||||
		super(id, connection);
 | 
			
		||||
		//this.onEvent = this.onEvent.bind(this);
 | 
			
		||||
		//this.onMessage = this.onMessage.bind(this);
 | 
			
		||||
		//this.emitTypers = this.emitTypers.bind(this);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async init(params: any) {
 | 
			
		||||
		this.otherpartyId = params.otherparty;
 | 
			
		||||
		this.otherparty = this.otherpartyId ? await this.usersRepository.findOneByOrFail({ id: this.otherpartyId }) : null;
 | 
			
		||||
		this.groupId = params.group;
 | 
			
		||||
 | 
			
		||||
		// Check joining
 | 
			
		||||
		if (this.groupId) {
 | 
			
		||||
			const joining = await this.userGroupJoiningsRepository.findOneBy({
 | 
			
		||||
				userId: this.user!.id,
 | 
			
		||||
				userGroupId: this.groupId,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (joining == null) {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
 | 
			
		||||
 | 
			
		||||
		this.subCh = this.otherpartyId
 | 
			
		||||
			? `messagingStream:${this.user!.id}-${this.otherpartyId}`
 | 
			
		||||
			: `messagingStream:${this.groupId}`;
 | 
			
		||||
 | 
			
		||||
		// Subscribe messaging stream
 | 
			
		||||
		this.subscriber.on(this.subCh, this.onEvent);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private onEvent(data: StreamMessages['messaging']['payload'] | StreamMessages['groupMessaging']['payload']) {
 | 
			
		||||
		if (data.type === 'typing') {
 | 
			
		||||
			const id = data.body;
 | 
			
		||||
			const begin = this.typers[id] == null;
 | 
			
		||||
			this.typers[id] = new Date();
 | 
			
		||||
			if (begin) {
 | 
			
		||||
				this.emitTypers();
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			this.send(data);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public onMessage(type: string, body: any) {
 | 
			
		||||
		switch (type) {
 | 
			
		||||
			case 'read':
 | 
			
		||||
				if (this.otherpartyId) {
 | 
			
		||||
					this.messagingService.readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]);
 | 
			
		||||
 | 
			
		||||
					// リモートユーザーからのメッセージだったら既読配信
 | 
			
		||||
					if (this.userEntityService.isLocalUser(this.user!) && this.userEntityService.isRemoteUser(this.otherparty!)) {
 | 
			
		||||
						this.messagingMessagesRepository.findOneBy({ id: body.id }).then(message => {
 | 
			
		||||
							if (message) this.messagingService.deliverReadActivity(this.user as LocalUser, this.otherparty as RemoteUser, message);
 | 
			
		||||
						});
 | 
			
		||||
					}
 | 
			
		||||
				} else if (this.groupId) {
 | 
			
		||||
					this.messagingService.readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]);
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async emitTypers() {
 | 
			
		||||
		const now = new Date();
 | 
			
		||||
 | 
			
		||||
		// Remove not typing users
 | 
			
		||||
		for (const [userId, date] of Object.entries(this.typers)) {
 | 
			
		||||
			if (now.getTime() - date.getTime() > 5000) delete this.typers[userId];
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { detail: false });
 | 
			
		||||
 | 
			
		||||
		this.send({
 | 
			
		||||
			type: 'typers',
 | 
			
		||||
			body: users,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public dispose() {
 | 
			
		||||
		this.subscriber.off(this.subCh, this.onEvent);
 | 
			
		||||
 | 
			
		||||
		clearInterval(this.emitTypersIntervalId);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class MessagingChannelService {
 | 
			
		||||
	public readonly shouldShare = MessagingChannel.shouldShare;
 | 
			
		||||
	public readonly requireCredential = MessagingChannel.requireCredential;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userGroupJoiningsRepository)
 | 
			
		||||
		private userGroupJoiningsRepository: UserGroupJoiningsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.messagingMessagesRepository)
 | 
			
		||||
		private messagingMessagesRepository: MessagingMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private messagingService: MessagingService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public create(id: string, connection: Channel['connection']): MessagingChannel {
 | 
			
		||||
		return new MessagingChannel(
 | 
			
		||||
			this.usersRepository,
 | 
			
		||||
			this.userGroupJoiningsRepository,
 | 
			
		||||
			this.messagingMessagesRepository,
 | 
			
		||||
			this.userEntityService,
 | 
			
		||||
			this.messagingService,
 | 
			
		||||
			id,
 | 
			
		||||
			connection,
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -147,12 +147,6 @@ export default class Connection {
 | 
			
		|||
			case 'disconnect': this.onChannelDisconnectRequested(body); break;
 | 
			
		||||
			case 'channel': this.onChannelMessageRequested(body); break;
 | 
			
		||||
			case 'ch': this.onChannelMessageRequested(body); break; // alias
 | 
			
		||||
 | 
			
		||||
			// 個々のチャンネルではなくルートレベルでこれらのメッセージを受け取る理由は、
 | 
			
		||||
			// クライアントの事情を考慮したとき、入力フォームはノートチャンネルやメッセージのメインコンポーネントとは別
 | 
			
		||||
			// なこともあるため、それらのコンポーネントがそれぞれ各チャンネルに接続するようにするのは面倒なため。
 | 
			
		||||
			case 'typingOnChannel': this.typingOnChannel(body.channel); break;
 | 
			
		||||
			case 'typingOnMessaging': this.typingOnMessaging(body); break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -325,24 +319,6 @@ export default class Connection {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private typingOnChannel(channel: ChannelModel['id']) {
 | 
			
		||||
		if (this.user) {
 | 
			
		||||
			this.globalEventService.publishChannelStream(channel, 'typing', this.user.id);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private typingOnMessaging(param: { partner?: User['id']; group?: UserGroup['id']; }) {
 | 
			
		||||
		if (this.user) {
 | 
			
		||||
			if (param.partner) {
 | 
			
		||||
				this.globalEventService.publishMessagingStream(param.partner, this.user.id, 'typing', this.user.id);
 | 
			
		||||
			} else if (param.group) {
 | 
			
		||||
				this.globalEventService.publishGroupMessagingStream(param.group, 'typing', this.user.id);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async updateFollowing() {
 | 
			
		||||
		const followings = await this.followingsRepository.find({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,6 @@ import type { Antenna } from '@/models/entities/Antenna.js';
 | 
			
		|||
import type { DriveFile } from '@/models/entities/DriveFile.js';
 | 
			
		||||
import type { DriveFolder } from '@/models/entities/DriveFolder.js';
 | 
			
		||||
import type { UserList } from '@/models/entities/UserList.js';
 | 
			
		||||
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
 | 
			
		||||
import type { UserGroup } from '@/models/entities/UserGroup.js';
 | 
			
		||||
import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
 | 
			
		||||
import type { Signin } from '@/models/entities/Signin.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -96,9 +95,6 @@ export interface MainStreamTypes {
 | 
			
		|||
	readAllUnreadMentions: undefined;
 | 
			
		||||
	unreadSpecifiedNote: Note['id'];
 | 
			
		||||
	readAllUnreadSpecifiedNotes: undefined;
 | 
			
		||||
	readAllMessagingMessages: undefined;
 | 
			
		||||
	messagingMessage: Packed<'MessagingMessage'>;
 | 
			
		||||
	unreadMessagingMessage: Packed<'MessagingMessage'>;
 | 
			
		||||
	readAllAntennas: undefined;
 | 
			
		||||
	unreadAntenna: Antenna;
 | 
			
		||||
	readAllAnnouncements: undefined;
 | 
			
		||||
| 
						 | 
				
			
			@ -153,10 +149,6 @@ type NoteStreamEventTypes = {
 | 
			
		|||
	};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface ChannelStreamTypes {
 | 
			
		||||
	typing: User['id'];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UserListStreamTypes {
 | 
			
		||||
	userAdded: Packed<'User'>;
 | 
			
		||||
	userRemoved: Packed<'User'>;
 | 
			
		||||
| 
						 | 
				
			
			@ -166,28 +158,6 @@ export interface AntennaStreamTypes {
 | 
			
		|||
	note: Note;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MessagingStreamTypes {
 | 
			
		||||
	read: MessagingMessage['id'][];
 | 
			
		||||
	typing: User['id'];
 | 
			
		||||
	message: Packed<'MessagingMessage'>;
 | 
			
		||||
	deleted: MessagingMessage['id'];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GroupMessagingStreamTypes {
 | 
			
		||||
	read: {
 | 
			
		||||
		ids: MessagingMessage['id'][];
 | 
			
		||||
		userId: User['id'];
 | 
			
		||||
	};
 | 
			
		||||
	typing: User['id'];
 | 
			
		||||
	message: Packed<'MessagingMessage'>;
 | 
			
		||||
	deleted: MessagingMessage['id'];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MessagingIndexStreamTypes {
 | 
			
		||||
	read: MessagingMessage['id'][];
 | 
			
		||||
	message: Packed<'MessagingMessage'>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AdminStreamTypes {
 | 
			
		||||
	newAbuseUserReport: {
 | 
			
		||||
		id: AbuseUserReport['id'];
 | 
			
		||||
| 
						 | 
				
			
			@ -242,10 +212,6 @@ export type StreamMessages = {
 | 
			
		|||
		name: `noteStream:${Note['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
 | 
			
		||||
	};
 | 
			
		||||
	channel: {
 | 
			
		||||
		name: `channelStream:${Channel['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<SerializedAll<ChannelStreamTypes>>;
 | 
			
		||||
	};
 | 
			
		||||
	userList: {
 | 
			
		||||
		name: `userListStream:${UserList['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<SerializedAll<UserListStreamTypes>>;
 | 
			
		||||
| 
						 | 
				
			
			@ -254,18 +220,6 @@ export type StreamMessages = {
 | 
			
		|||
		name: `antennaStream:${Antenna['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<SerializedAll<AntennaStreamTypes>>;
 | 
			
		||||
	};
 | 
			
		||||
	messaging: {
 | 
			
		||||
		name: `messagingStream:${User['id']}-${User['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<SerializedAll<MessagingStreamTypes>>;
 | 
			
		||||
	};
 | 
			
		||||
	groupMessaging: {
 | 
			
		||||
		name: `messagingStream:${UserGroup['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<SerializedAll<GroupMessagingStreamTypes>>;
 | 
			
		||||
	};
 | 
			
		||||
	messagingIndex: {
 | 
			
		||||
		name: `messagingIndexStream:${User['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<SerializedAll<MessagingIndexStreamTypes>>;
 | 
			
		||||
	};
 | 
			
		||||
	admin: {
 | 
			
		||||
		name: `adminStream:${User['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<SerializedAll<AdminStreamTypes>>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -778,63 +778,6 @@ describe('API: Endpoints', () => {
 | 
			
		|||
		}));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('messaging/messages/create', () => {
 | 
			
		||||
		test('メッセージを送信できる', async () => {
 | 
			
		||||
			const res = await api('/messaging/messages/create', {
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
				text: 'test'
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
			assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
 | 
			
		||||
			assert.strictEqual(res.body.text, 'test');
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		test('自分自身にはメッセージを送信できない', async () => {
 | 
			
		||||
			const res = await api('/messaging/messages/create', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				text: 'Yo'
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		test('存在しないユーザーにはメッセージを送信できない', async () => {
 | 
			
		||||
			const res = await api('/messaging/messages/create', {
 | 
			
		||||
				userId: '000000000000000000000000',
 | 
			
		||||
				text: 'test'
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		test('不正なユーザーIDで怒られる', async () => {
 | 
			
		||||
			const res = await api('/messaging/messages/create', {
 | 
			
		||||
				userId: 'foo',
 | 
			
		||||
				text: 'test'
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		test('テキストが無くて怒られる', async () => {
 | 
			
		||||
			const res = await api('/messaging/messages/create', {
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		test('文字数オーバーで怒られる', async () => {
 | 
			
		||||
			const res = await api('/messaging/messages/create', {
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
				text: '!'.repeat(1001)
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
		}));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('notes/replies', () => {
 | 
			
		||||
		test('自分に閲覧権限のない投稿は含まれない', async () => {
 | 
			
		||||
			const alicePost = await post(alice, {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -160,12 +160,6 @@ let hasNotSpecifiedMentions = $ref(false);
 | 
			
		|||
let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') || '[]'));
 | 
			
		||||
let imeText = $ref('');
 | 
			
		||||
 | 
			
		||||
const typing = throttle(3000, () => {
 | 
			
		||||
	if (props.channel) {
 | 
			
		||||
		stream.send('typingOnChannel', { channel: props.channel.id });
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const draftKey = $computed((): string => {
 | 
			
		||||
	let key = props.channel ? `channel:${props.channel.id}` : '';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -447,12 +441,10 @@ function clear() {
 | 
			
		|||
function onKeydown(ev: KeyboardEvent) {
 | 
			
		||||
	if ((ev.which === 10 || ev.which === 13) && (ev.ctrlKey || ev.metaKey) && canPost) post();
 | 
			
		||||
	if (ev.which === 27) emit('esc');
 | 
			
		||||
	typing();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onCompositionUpdate(ev: CompositionEvent) {
 | 
			
		||||
	imeText = ev.data;
 | 
			
		||||
	typing();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onCompositionEnd(ev: CompositionEvent) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -505,15 +505,6 @@ if ($i) {
 | 
			
		|||
		updateAccount({ hasUnreadSpecifiedNotes: false });
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	main.on('readAllMessagingMessages', () => {
 | 
			
		||||
		updateAccount({ hasUnreadMessagingMessage: false });
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	main.on('unreadMessagingMessage', () => {
 | 
			
		||||
		updateAccount({ hasUnreadMessagingMessage: true });
 | 
			
		||||
		sound.play('chatBg');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	main.on('readAllAntennas', () => {
 | 
			
		||||
		updateAccount({ hasUnreadAntenna: false });
 | 
			
		||||
	});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,13 +15,6 @@ export const navbarItemDef = reactive({
 | 
			
		|||
		indicated: computed(() => $i != null && $i.hasUnreadNotification),
 | 
			
		||||
		to: '/my/notifications',
 | 
			
		||||
	},
 | 
			
		||||
	messaging: {
 | 
			
		||||
		title: i18n.ts.messaging,
 | 
			
		||||
		icon: 'ti ti-messages',
 | 
			
		||||
		show: computed(() => $i != null),
 | 
			
		||||
		indicated: computed(() => $i != null && $i.hasUnreadMessagingMessage),
 | 
			
		||||
		to: '/my/messaging',
 | 
			
		||||
	},
 | 
			
		||||
	drive: {
 | 
			
		||||
		title: i18n.ts.drive,
 | 
			
		||||
		icon: 'ti ti-cloud',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,305 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
<MkStickyContainer>
 | 
			
		||||
	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 | 
			
		||||
	<MkSpacer :content-max="800">
 | 
			
		||||
		<div class="yweeujhr">
 | 
			
		||||
			<MkButton primary class="start" @click="start"><i class="ti ti-plus"></i> {{ $ts.startMessaging }}</MkButton>
 | 
			
		||||
 | 
			
		||||
			<div v-if="messages.length > 0" class="history">
 | 
			
		||||
				<MkA
 | 
			
		||||
					v-for="(message, i) in messages"
 | 
			
		||||
					:key="message.id"
 | 
			
		||||
					v-anim="i"
 | 
			
		||||
					class="message _panel"
 | 
			
		||||
					:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
 | 
			
		||||
					:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
 | 
			
		||||
					:data-index="i"
 | 
			
		||||
				>
 | 
			
		||||
					<div>
 | 
			
		||||
						<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" indicator link preview/>
 | 
			
		||||
						<header v-if="message.groupId">
 | 
			
		||||
							<span class="name">{{ message.group.name }}</span>
 | 
			
		||||
							<MkTime :time="message.createdAt" class="time"/>
 | 
			
		||||
						</header>
 | 
			
		||||
						<header v-else>
 | 
			
		||||
							<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
 | 
			
		||||
							<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
 | 
			
		||||
							<MkTime :time="message.createdAt" class="time"/>
 | 
			
		||||
						</header>
 | 
			
		||||
						<div class="body">
 | 
			
		||||
							<p class="text"><span v-if="isMe(message)" class="me">{{ $ts.you }}:</span>{{ message.text }}</p>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</MkA>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="!fetching && messages.length == 0" class="_fullinfo">
 | 
			
		||||
				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 | 
			
		||||
				<div>{{ $ts.noHistory }}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<MkLoading v-if="fetching"/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkStickyContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { defineAsyncComponent, defineComponent, inject, markRaw, onMounted, onUnmounted } from 'vue';
 | 
			
		||||
import * as Acct from 'misskey-js/built/acct';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import { acct } from '@/filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { stream } from '@/stream';
 | 
			
		||||
import { useRouter } from '@/router';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { definePageMetadata } from '@/scripts/page-metadata';
 | 
			
		||||
import { $i } from '@/account';
 | 
			
		||||
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
 | 
			
		||||
let fetching = $ref(true);
 | 
			
		||||
let moreFetching = $ref(false);
 | 
			
		||||
let messages = $ref([]);
 | 
			
		||||
let connection = $ref(null);
 | 
			
		||||
 | 
			
		||||
const getAcct = Acct.toString;
 | 
			
		||||
 | 
			
		||||
function isMe(message) {
 | 
			
		||||
	return message.userId === $i.id;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onMessage(message) {
 | 
			
		||||
	if (message.recipientId) {
 | 
			
		||||
		messages = messages.filter(m => !(
 | 
			
		||||
			(m.recipientId === message.recipientId && m.userId === message.userId) ||
 | 
			
		||||
			(m.recipientId === message.userId && m.userId === message.recipientId)));
 | 
			
		||||
 | 
			
		||||
		messages.unshift(message);
 | 
			
		||||
	} else if (message.groupId) {
 | 
			
		||||
		messages = messages.filter(m => m.groupId !== message.groupId);
 | 
			
		||||
		messages.unshift(message);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onRead(ids) {
 | 
			
		||||
	for (const id of ids) {
 | 
			
		||||
		const found = messages.find(m => m.id === id);
 | 
			
		||||
		if (found) {
 | 
			
		||||
			if (found.recipientId) {
 | 
			
		||||
				found.isRead = true;
 | 
			
		||||
			} else if (found.groupId) {
 | 
			
		||||
				found.reads.push($i.id);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function start(ev) {
 | 
			
		||||
	os.popupMenu([{
 | 
			
		||||
		text: i18n.ts.messagingWithUser,
 | 
			
		||||
		icon: 'ti ti-user',
 | 
			
		||||
		action: () => { startUser(); },
 | 
			
		||||
	}, {
 | 
			
		||||
		text: i18n.ts.messagingWithGroup,
 | 
			
		||||
		icon: 'ti ti-users',
 | 
			
		||||
		action: () => { startGroup(); },
 | 
			
		||||
	}], ev.currentTarget ?? ev.target);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function startUser() {
 | 
			
		||||
	os.selectUser().then(user => {
 | 
			
		||||
		router.push(`/my/messaging/${Acct.toString(user)}`);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function startGroup() {
 | 
			
		||||
	const groups1 = await os.api('users/groups/owned');
 | 
			
		||||
	const groups2 = await os.api('users/groups/joined');
 | 
			
		||||
	if (groups1.length === 0 && groups2.length === 0) {
 | 
			
		||||
		os.alert({
 | 
			
		||||
			type: 'warning',
 | 
			
		||||
			title: i18n.ts.youHaveNoGroups,
 | 
			
		||||
			text: i18n.ts.joinOrCreateGroup,
 | 
			
		||||
		});
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	const { canceled, result: group } = await os.select({
 | 
			
		||||
		title: i18n.ts.group,
 | 
			
		||||
		items: groups1.concat(groups2).map(group => ({
 | 
			
		||||
			value: group, text: group.name,
 | 
			
		||||
		})),
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
	router.push(`/my/messaging/group/${group.id}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	connection = markRaw(stream.useChannel('messagingIndex'));
 | 
			
		||||
 | 
			
		||||
	connection.on('message', onMessage);
 | 
			
		||||
	connection.on('read', onRead);
 | 
			
		||||
 | 
			
		||||
	os.api('messaging/history', { group: false }).then(userMessages => {
 | 
			
		||||
		os.api('messaging/history', { group: true }).then(groupMessages => {
 | 
			
		||||
			const _messages = userMessages.concat(groupMessages);
 | 
			
		||||
			_messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
 | 
			
		||||
			messages = _messages;
 | 
			
		||||
			fetching = false;
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
	if (connection) connection.dispose();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const headerActions = $computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = $computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePageMetadata({
 | 
			
		||||
	title: i18n.ts.messaging,
 | 
			
		||||
	icon: 'ti ti-messages',
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.yweeujhr {
 | 
			
		||||
 | 
			
		||||
	> .start {
 | 
			
		||||
		margin: 0 auto var(--margin) auto;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .history {
 | 
			
		||||
		> .message {
 | 
			
		||||
			display: block;
 | 
			
		||||
			text-decoration: none;
 | 
			
		||||
			margin-bottom: var(--margin);
 | 
			
		||||
 | 
			
		||||
			* {
 | 
			
		||||
				pointer-events: none;
 | 
			
		||||
				user-select: none;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:hover {
 | 
			
		||||
				.avatar {
 | 
			
		||||
					filter: saturate(200%);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:active {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.isRead,
 | 
			
		||||
			&.isMe {
 | 
			
		||||
				opacity: 0.8;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:not(.isMe):not(.isRead) {
 | 
			
		||||
				> div {
 | 
			
		||||
					background-image: url("/client-assets/unread.svg");
 | 
			
		||||
					background-repeat: no-repeat;
 | 
			
		||||
					background-position: 0 center;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:after {
 | 
			
		||||
				content: "";
 | 
			
		||||
				display: block;
 | 
			
		||||
				clear: both;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> div {
 | 
			
		||||
				padding: 20px 30px;
 | 
			
		||||
 | 
			
		||||
				&:after {
 | 
			
		||||
					content: "";
 | 
			
		||||
					display: block;
 | 
			
		||||
					clear: both;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> header {
 | 
			
		||||
					display: flex;
 | 
			
		||||
					align-items: center;
 | 
			
		||||
					margin-bottom: 2px;
 | 
			
		||||
					white-space: nowrap;
 | 
			
		||||
					overflow: hidden;
 | 
			
		||||
 | 
			
		||||
					> .name {
 | 
			
		||||
						margin: 0;
 | 
			
		||||
						padding: 0;
 | 
			
		||||
						overflow: hidden;
 | 
			
		||||
						text-overflow: ellipsis;
 | 
			
		||||
						font-size: 1em;
 | 
			
		||||
						font-weight: bold;
 | 
			
		||||
						transition: all 0.1s ease;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .username {
 | 
			
		||||
						margin: 0 8px;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .time {
 | 
			
		||||
						margin: 0 0 0 auto;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .avatar {
 | 
			
		||||
					float: left;
 | 
			
		||||
					width: 54px;
 | 
			
		||||
					height: 54px;
 | 
			
		||||
					margin: 0 16px 0 0;
 | 
			
		||||
					border-radius: 8px;
 | 
			
		||||
					transition: all 0.1s ease;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .body {
 | 
			
		||||
 | 
			
		||||
					> .text {
 | 
			
		||||
						display: block;
 | 
			
		||||
						margin: 0 0 0 0;
 | 
			
		||||
						padding: 0;
 | 
			
		||||
						overflow: hidden;
 | 
			
		||||
						overflow-wrap: break-word;
 | 
			
		||||
						font-size: 1.1em;
 | 
			
		||||
						color: var(--faceText);
 | 
			
		||||
 | 
			
		||||
						.me {
 | 
			
		||||
							opacity: 0.7;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .image {
 | 
			
		||||
						display: block;
 | 
			
		||||
						max-width: 100%;
 | 
			
		||||
						max-height: 512px;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 400px) {
 | 
			
		||||
	.yweeujhr {
 | 
			
		||||
		> .history {
 | 
			
		||||
			> .message {
 | 
			
		||||
				&:not(.isMe):not(.isRead) {
 | 
			
		||||
					> div {
 | 
			
		||||
						background-image: none;
 | 
			
		||||
						border-left: solid 4px #3aa2dc;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> div {
 | 
			
		||||
					padding: 16px;
 | 
			
		||||
					font-size: 0.9em;
 | 
			
		||||
 | 
			
		||||
					> .avatar {
 | 
			
		||||
						margin: 0 12px 0 0;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,366 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div
 | 
			
		||||
	:class="$style['root']"
 | 
			
		||||
	@dragover.stop="onDragover"
 | 
			
		||||
	@drop.stop="onDrop"
 | 
			
		||||
>
 | 
			
		||||
	<textarea
 | 
			
		||||
		:class="$style['textarea']"
 | 
			
		||||
		class="_acrylic"
 | 
			
		||||
		ref="textEl"
 | 
			
		||||
		v-model="text"
 | 
			
		||||
		:placeholder="i18n.ts.inputMessageHere"
 | 
			
		||||
		@keydown="onKeydown"
 | 
			
		||||
		@compositionupdate="onCompositionUpdate"
 | 
			
		||||
		@paste="onPaste"
 | 
			
		||||
	></textarea>
 | 
			
		||||
	<footer :class="$style['footer']">
 | 
			
		||||
		<div v-if="file" :class="$style['file']" @click="file = null">{{ file.name }}</div>
 | 
			
		||||
		<div :class="$style['buttons']">
 | 
			
		||||
			<button class="_button" :class="$style['button']" @click="chooseFile"><i class="ti ti-photo-plus"></i></button>
 | 
			
		||||
			<button class="_button" :class="$style['button']" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
 | 
			
		||||
			<button class="_button" :class="[$style['button'], $style['send']]" :disabled="!canSend || sending" :title="i18n.ts.send" @click="send">
 | 
			
		||||
				<template v-if="!sending"><i class="ti ti-send"></i></template><template v-if="sending"><MkLoading :em="true"/></template>
 | 
			
		||||
			</button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</footer>
 | 
			
		||||
	<input :class="$style['file-input']" ref="fileEl" type="file" @change="onChangeFile"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, watch } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import autosize from 'autosize';
 | 
			
		||||
//import insertTextAtCursor from 'insert-text-at-cursor';
 | 
			
		||||
import { throttle } from 'throttle-debounce';
 | 
			
		||||
import { formatTimeString } from '@/scripts/format-time-string';
 | 
			
		||||
import { selectFile } from '@/scripts/select-file';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { stream } from '@/stream';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
//import { Autocomplete } from '@/scripts/autocomplete';
 | 
			
		||||
import { uploadFile } from '@/scripts/upload';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	user?: Misskey.entities.UserDetailed | null;
 | 
			
		||||
	group?: Misskey.entities.UserGroup | null;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let textEl = $shallowRef<HTMLTextAreaElement>();
 | 
			
		||||
let fileEl = $shallowRef<HTMLInputElement>();
 | 
			
		||||
 | 
			
		||||
let text = $ref<string>('');
 | 
			
		||||
let file = $ref<Misskey.entities.DriveFile | null>(null);
 | 
			
		||||
let sending = $ref(false);
 | 
			
		||||
const typing = throttle(3000, () => {
 | 
			
		||||
	stream.send('typingOnMessaging', props.user ? { partner: props.user.id } : { group: props.group?.id });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
let draftKey = $computed(() => props.user ? 'user:' + props.user.id : 'group:' + props.group?.id);
 | 
			
		||||
let canSend = $computed(() => (text != null && text !== '') || file != null);
 | 
			
		||||
 | 
			
		||||
watch([$$(text), $$(file)], saveDraft);
 | 
			
		||||
 | 
			
		||||
async function onPaste(ev: ClipboardEvent) {
 | 
			
		||||
	if (!ev.clipboardData) return;
 | 
			
		||||
 | 
			
		||||
	const clipboardData = ev.clipboardData;
 | 
			
		||||
	const items = clipboardData.items;
 | 
			
		||||
 | 
			
		||||
	if (items.length === 1) {
 | 
			
		||||
		if (items[0].kind === 'file') {
 | 
			
		||||
			const pastedFile = items[0].getAsFile();
 | 
			
		||||
			if (!pastedFile) return;
 | 
			
		||||
			const lio = pastedFile.name.lastIndexOf('.');
 | 
			
		||||
			const ext = lio >= 0 ? pastedFile.name.slice(lio) : '';
 | 
			
		||||
			const formatted = formatTimeString(new Date(pastedFile.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, '1') + ext;
 | 
			
		||||
			if (formatted) upload(pastedFile, formatted);
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if (items[0].kind === 'file') {
 | 
			
		||||
			os.alert({
 | 
			
		||||
				type: 'error',
 | 
			
		||||
				text: i18n.ts.onlyOneFileCanBeAttached,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragover(ev: DragEvent) {
 | 
			
		||||
	if (!ev.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	const isFile = ev.dataTransfer.items[0].kind === 'file';
 | 
			
		||||
	const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
	if (isFile || isDriveFile) {
 | 
			
		||||
		ev.preventDefault();
 | 
			
		||||
		switch (ev.dataTransfer.effectAllowed) {
 | 
			
		||||
			case 'all':
 | 
			
		||||
			case 'uninitialized':
 | 
			
		||||
			case 'copy': 
 | 
			
		||||
			case 'copyLink': 
 | 
			
		||||
			case 'copyMove': 
 | 
			
		||||
				ev.dataTransfer.dropEffect = 'copy';
 | 
			
		||||
				break;
 | 
			
		||||
			case 'linkMove':
 | 
			
		||||
			case 'move':
 | 
			
		||||
				ev.dataTransfer.dropEffect = 'move';
 | 
			
		||||
				break;
 | 
			
		||||
			default:
 | 
			
		||||
				ev.dataTransfer.dropEffect = 'none';
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDrop(ev: DragEvent): void {
 | 
			
		||||
	if (!ev.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	// ファイルだったら
 | 
			
		||||
	if (ev.dataTransfer.files.length === 1) {
 | 
			
		||||
		ev.preventDefault();
 | 
			
		||||
		upload(ev.dataTransfer.files[0]);
 | 
			
		||||
		return;
 | 
			
		||||
	} else if (ev.dataTransfer.files.length > 1) {
 | 
			
		||||
		ev.preventDefault();
 | 
			
		||||
		os.alert({
 | 
			
		||||
			type: 'error',
 | 
			
		||||
			text: i18n.ts.onlyOneFileCanBeAttached,
 | 
			
		||||
		});
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのファイル
 | 
			
		||||
	const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
	if (driveFile != null && driveFile !== '') {
 | 
			
		||||
		file = JSON.parse(driveFile);
 | 
			
		||||
		ev.preventDefault();
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onKeydown(ev: KeyboardEvent) {
 | 
			
		||||
	typing();
 | 
			
		||||
	if ((ev.key === 'Enter') && (ev.ctrlKey || ev.metaKey) && canSend) {
 | 
			
		||||
		send();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onCompositionUpdate() {
 | 
			
		||||
	typing();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function chooseFile(ev: MouseEvent) {
 | 
			
		||||
	selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then(selectedFile => {
 | 
			
		||||
		file = selectedFile;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onChangeFile() {
 | 
			
		||||
	if (fileEl.files![0]) upload(fileEl.files[0]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function upload(fileToUpload: File, name?: string) {
 | 
			
		||||
	uploadFile(fileToUpload, defaultStore.state.uploadFolder, name).then(res => {
 | 
			
		||||
		file = res;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function send() {
 | 
			
		||||
	sending = true;
 | 
			
		||||
	os.api('messaging/messages/create', {
 | 
			
		||||
		userId: props.user ? props.user.id : undefined,
 | 
			
		||||
		groupId: props.group ? props.group.id : undefined,
 | 
			
		||||
		text: text ? text : undefined,
 | 
			
		||||
		fileId: file ? file.id : undefined,
 | 
			
		||||
	}).then(message => {
 | 
			
		||||
		clear();
 | 
			
		||||
	}).catch(err => {
 | 
			
		||||
		console.error(err);
 | 
			
		||||
	}).then(() => {
 | 
			
		||||
		sending = false;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function clear() {
 | 
			
		||||
	text = '';
 | 
			
		||||
	file = null;
 | 
			
		||||
	deleteDraft();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function saveDraft() {
 | 
			
		||||
	const drafts = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}');
 | 
			
		||||
 | 
			
		||||
	drafts[draftKey] = {
 | 
			
		||||
		updatedAt: new Date(),
 | 
			
		||||
		// eslint-disable-next-line id-denylist
 | 
			
		||||
		data: {
 | 
			
		||||
			text: text,
 | 
			
		||||
			file: file,
 | 
			
		||||
		},
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	miLocalStorage.setItem('message_drafts', JSON.stringify(drafts));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function deleteDraft() {
 | 
			
		||||
	const drafts = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}');
 | 
			
		||||
 | 
			
		||||
	delete drafts[draftKey];
 | 
			
		||||
 | 
			
		||||
	miLocalStorage.setItem('message_drafts', JSON.stringify(drafts));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function insertEmoji(ev: MouseEvent) {
 | 
			
		||||
	os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textEl);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	autosize(textEl);
 | 
			
		||||
 | 
			
		||||
	// TODO: detach when unmount
 | 
			
		||||
	// TODO
 | 
			
		||||
	//new Autocomplete(textEl, this, { model: 'text' });
 | 
			
		||||
 | 
			
		||||
	// 書きかけの投稿を復元
 | 
			
		||||
	const draft = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}')[draftKey];
 | 
			
		||||
	if (draft) {
 | 
			
		||||
		text = draft.data.text;
 | 
			
		||||
		file = draft.data.file;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	file,
 | 
			
		||||
	upload,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.textarea {
 | 
			
		||||
	cursor: auto;
 | 
			
		||||
	display: block;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	min-width: 100%;
 | 
			
		||||
	max-width: 100%;
 | 
			
		||||
	min-height: 80px;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 16px 16px 0 16px;
 | 
			
		||||
	resize: none;
 | 
			
		||||
	font-size: 1em;
 | 
			
		||||
	font-family: inherit;
 | 
			
		||||
	outline: none;
 | 
			
		||||
	border: none;
 | 
			
		||||
	border-radius: 0;
 | 
			
		||||
	box-shadow: none;
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
	color: var(--fg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.footer {
 | 
			
		||||
	position: sticky;
 | 
			
		||||
	bottom: 0;
 | 
			
		||||
	background: var(--panel);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.file {
 | 
			
		||||
	padding: 8px;
 | 
			
		||||
	color: var(--fg);
 | 
			
		||||
	background: transparent;
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
/*
 | 
			
		||||
.files {
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0 8px;
 | 
			
		||||
	list-style: none;
 | 
			
		||||
 | 
			
		||||
	&:after {
 | 
			
		||||
		content: '';
 | 
			
		||||
		display: block;
 | 
			
		||||
		clear: both;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> li {
 | 
			
		||||
		display: block;
 | 
			
		||||
		float: left;
 | 
			
		||||
		margin: 4px;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
		width: 64px;
 | 
			
		||||
		height: 64px;
 | 
			
		||||
		background-color: #eee;
 | 
			
		||||
		background-repeat: no-repeat;
 | 
			
		||||
		background-position: center center;
 | 
			
		||||
		background-size: cover;
 | 
			
		||||
		cursor: move;
 | 
			
		||||
 | 
			
		||||
		&:hover {
 | 
			
		||||
			> .remove {
 | 
			
		||||
				display: block;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.file-remove {
 | 
			
		||||
	display: none;
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	right: -6px;
 | 
			
		||||
	top: -6px;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	background: transparent;
 | 
			
		||||
	outline: none;
 | 
			
		||||
	border: none;
 | 
			
		||||
	border-radius: 0;
 | 
			
		||||
	box-shadow: none;
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
.buttons {
 | 
			
		||||
	display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button {
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 16px;
 | 
			
		||||
	font-size: 1em;
 | 
			
		||||
	font-weight: normal;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
	transition: color 0.1s ease;
 | 
			
		||||
 | 
			
		||||
	&:hover {
 | 
			
		||||
		color: var(--accent);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:active {
 | 
			
		||||
		color: var(--accentDarken);
 | 
			
		||||
		transition: color 0s ease;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
.send {
 | 
			
		||||
	margin-left: auto;
 | 
			
		||||
	color: var(--accent);
 | 
			
		||||
 | 
			
		||||
	&:hover {
 | 
			
		||||
		color: var(--accentLighten);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:active {
 | 
			
		||||
		color: var(--accentDarken);
 | 
			
		||||
		transition: color 0s ease;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.file-input {
 | 
			
		||||
	display: none;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,338 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="thvuemwp" :class="{ isMe }">
 | 
			
		||||
	<MkAvatar class="avatar" :user="message.user" indicator link preview/>
 | 
			
		||||
	<div class="content">
 | 
			
		||||
		<div class="balloon" :class="{ noText: message.text == null }">
 | 
			
		||||
			<button v-if="isMe" class="delete-button" :title="$ts.delete" @click="del">
 | 
			
		||||
				<img src="/client-assets/remove.png" alt="Delete"/>
 | 
			
		||||
			</button>
 | 
			
		||||
			<div v-if="!message.isDeleted" class="content">
 | 
			
		||||
				<Mfm v-if="message.text" ref="text" class="text" :text="message.text" :i="$i"/>
 | 
			
		||||
				<div v-if="message.file" class="file">
 | 
			
		||||
					<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
 | 
			
		||||
						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
 | 
			
		||||
						<p v-else>{{ message.file.name }}</p>
 | 
			
		||||
					</a>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-else class="content">
 | 
			
		||||
				<p class="is-deleted">{{ $ts.deleted }}</p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div></div>
 | 
			
		||||
		<MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/>
 | 
			
		||||
		<footer>
 | 
			
		||||
			<template v-if="isGroup">
 | 
			
		||||
				<span v-if="message.reads.length > 0" class="read">{{ $ts.messageRead }} {{ message.reads.length }}</span>
 | 
			
		||||
			</template>
 | 
			
		||||
			<template v-else>
 | 
			
		||||
				<span v-if="isMe && message.isRead" class="read">{{ $ts.messageRead }}</span>
 | 
			
		||||
			</template>
 | 
			
		||||
			<MkTime :time="message.createdAt"/>
 | 
			
		||||
			<template v-if="message.is_edited"><i class="ti ti-pencil"></i></template>
 | 
			
		||||
		</footer>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import * as mfm from 'mfm-js';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
 | 
			
		||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { $i } from '@/account';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	message: Misskey.entities.MessagingMessage;
 | 
			
		||||
	isGroup?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const isMe = $computed(() => props.message.userId === $i?.id);
 | 
			
		||||
const urls = $computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
 | 
			
		||||
 | 
			
		||||
function del(): void {
 | 
			
		||||
	os.api('messaging/messages/delete', {
 | 
			
		||||
		messageId: props.message.id,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.thvuemwp {
 | 
			
		||||
	$me-balloon-color: var(--accent);
 | 
			
		||||
 | 
			
		||||
	position: relative;
 | 
			
		||||
	background-color: transparent;
 | 
			
		||||
	display: flex;
 | 
			
		||||
 | 
			
		||||
	> .avatar {
 | 
			
		||||
		position: sticky;
 | 
			
		||||
		top: calc(var(--stickyTop, 0px) + 16px);
 | 
			
		||||
		display: block;
 | 
			
		||||
		width: 54px;
 | 
			
		||||
		height: 54px;
 | 
			
		||||
		transition: all 0.1s ease;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .content {
 | 
			
		||||
		min-width: 0;
 | 
			
		||||
 | 
			
		||||
		> .balloon {
 | 
			
		||||
			position: relative;
 | 
			
		||||
			display: inline-flex;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
			padding: 0;
 | 
			
		||||
			min-height: 38px;
 | 
			
		||||
			border-radius: 16px;
 | 
			
		||||
			max-width: 100%;
 | 
			
		||||
 | 
			
		||||
			&:before {
 | 
			
		||||
				content: "";
 | 
			
		||||
				pointer-events: none;
 | 
			
		||||
				display: block;
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				top: 12px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			& + * {
 | 
			
		||||
				clear: both;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:hover {
 | 
			
		||||
				> .delete-button {
 | 
			
		||||
					display: block;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .delete-button {
 | 
			
		||||
				display: none;
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				z-index: 1;
 | 
			
		||||
				top: -4px;
 | 
			
		||||
				right: -4px;
 | 
			
		||||
				margin: 0;
 | 
			
		||||
				padding: 0;
 | 
			
		||||
				cursor: pointer;
 | 
			
		||||
				outline: none;
 | 
			
		||||
				border: none;
 | 
			
		||||
				border-radius: 0;
 | 
			
		||||
				box-shadow: none;
 | 
			
		||||
				background: transparent;
 | 
			
		||||
 | 
			
		||||
				> img {
 | 
			
		||||
					vertical-align: bottom;
 | 
			
		||||
					width: 16px;
 | 
			
		||||
					height: 16px;
 | 
			
		||||
					cursor: pointer;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .content {
 | 
			
		||||
				max-width: 100%;
 | 
			
		||||
 | 
			
		||||
				> .is-deleted {
 | 
			
		||||
					display: block;
 | 
			
		||||
					margin: 0;
 | 
			
		||||
					padding: 0;
 | 
			
		||||
					overflow: hidden;
 | 
			
		||||
					overflow-wrap: break-word;
 | 
			
		||||
					font-size: 1em;
 | 
			
		||||
					color: rgba(#000, 0.5);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .text {
 | 
			
		||||
					display: block;
 | 
			
		||||
					margin: 0;
 | 
			
		||||
					padding: 12px 18px;
 | 
			
		||||
					overflow: hidden;
 | 
			
		||||
					overflow-wrap: break-word;
 | 
			
		||||
					word-break: break-word;
 | 
			
		||||
					font-size: 1em;
 | 
			
		||||
					color: rgba(#000, 0.8);
 | 
			
		||||
 | 
			
		||||
					& + .file {
 | 
			
		||||
						> a {
 | 
			
		||||
							border-radius: 0 0 16px 16px;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .file {
 | 
			
		||||
					> a {
 | 
			
		||||
						display: block;
 | 
			
		||||
						max-width: 100%;
 | 
			
		||||
						border-radius: 16px;
 | 
			
		||||
						overflow: hidden;
 | 
			
		||||
						text-decoration: none;
 | 
			
		||||
 | 
			
		||||
						&:hover {
 | 
			
		||||
							text-decoration: none;
 | 
			
		||||
 | 
			
		||||
							> p {
 | 
			
		||||
								background: #ccc;
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						> * {
 | 
			
		||||
							display: block;
 | 
			
		||||
							margin: 0;
 | 
			
		||||
							width: 100%;
 | 
			
		||||
							max-height: 512px;
 | 
			
		||||
							object-fit: contain;
 | 
			
		||||
							box-sizing: border-box;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						> p {
 | 
			
		||||
							padding: 30px;
 | 
			
		||||
							text-align: center;
 | 
			
		||||
							color: #555;
 | 
			
		||||
							background: #ddd;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> footer {
 | 
			
		||||
			display: block;
 | 
			
		||||
			margin: 2px 0 0 0;
 | 
			
		||||
			font-size: 0.65em;
 | 
			
		||||
 | 
			
		||||
			> .read {
 | 
			
		||||
				margin: 0 8px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> i {
 | 
			
		||||
				margin-left: 4px;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:not(.isMe) {
 | 
			
		||||
		padding-left: var(--margin);
 | 
			
		||||
 | 
			
		||||
		> .content {
 | 
			
		||||
			padding-left: 16px;
 | 
			
		||||
			padding-right: 32px;
 | 
			
		||||
 | 
			
		||||
			> .balloon {
 | 
			
		||||
				$color: var(--messageBg);
 | 
			
		||||
				background: $color;
 | 
			
		||||
 | 
			
		||||
				&.noText {
 | 
			
		||||
					background: transparent;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:not(.noText):before {
 | 
			
		||||
					left: -14px;
 | 
			
		||||
					border-top: solid 8px transparent;
 | 
			
		||||
					border-right: solid 8px $color;
 | 
			
		||||
					border-bottom: solid 8px transparent;
 | 
			
		||||
					border-left: solid 8px transparent;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .content {
 | 
			
		||||
					> .text {
 | 
			
		||||
						color: var(--fg);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> footer {
 | 
			
		||||
				text-align: left;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.isMe {
 | 
			
		||||
		flex-direction: row-reverse;
 | 
			
		||||
		padding-right: var(--margin);
 | 
			
		||||
		right: var(--margin); // 削除時にposition: absoluteになったときに使う
 | 
			
		||||
 | 
			
		||||
		> .content {
 | 
			
		||||
			padding-right: 16px;
 | 
			
		||||
			padding-left: 32px;
 | 
			
		||||
			text-align: right;
 | 
			
		||||
 | 
			
		||||
			> .balloon {
 | 
			
		||||
				background: $me-balloon-color;
 | 
			
		||||
				text-align: left;
 | 
			
		||||
 | 
			
		||||
				::selection {
 | 
			
		||||
					color: var(--accent);
 | 
			
		||||
					background-color: #fff;
 | 
			
		||||
				} 
 | 
			
		||||
 | 
			
		||||
				&.noText {
 | 
			
		||||
					background: transparent;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:not(.noText):before {
 | 
			
		||||
					right: -14px;
 | 
			
		||||
					left: auto;
 | 
			
		||||
					border-top: solid 8px transparent;
 | 
			
		||||
					border-right: solid 8px transparent;
 | 
			
		||||
					border-bottom: solid 8px transparent;
 | 
			
		||||
					border-left: solid 8px $me-balloon-color;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .content {
 | 
			
		||||
 | 
			
		||||
					> p.is-deleted {
 | 
			
		||||
						color: rgba(#fff, 0.5);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .text {
 | 
			
		||||
						&, ::v-deep(*) {
 | 
			
		||||
							color: var(--fgOnAccent) !important;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> footer {
 | 
			
		||||
				text-align: right;
 | 
			
		||||
 | 
			
		||||
				> .read {
 | 
			
		||||
					user-select: none;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 400px) {
 | 
			
		||||
	.thvuemwp {
 | 
			
		||||
		> .avatar {
 | 
			
		||||
			width: 48px;
 | 
			
		||||
			height: 48px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .content {
 | 
			
		||||
			> .balloon {
 | 
			
		||||
				> .content {
 | 
			
		||||
					> .text {
 | 
			
		||||
						font-size: 0.9em;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 500px) {
 | 
			
		||||
	.thvuemwp {
 | 
			
		||||
		> .content {
 | 
			
		||||
			> .balloon {
 | 
			
		||||
				> .content {
 | 
			
		||||
					> .text {
 | 
			
		||||
						padding: 8px 16px;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,415 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
<MkStickyContainer>
 | 
			
		||||
<template #header>
 | 
			
		||||
	<MkPageHeader />
 | 
			
		||||
</template>
 | 
			
		||||
<div
 | 
			
		||||
	ref="rootEl"
 | 
			
		||||
	:class="$style['root']"
 | 
			
		||||
	@dragover.prevent.stop="onDragover"
 | 
			
		||||
	@drop.prevent.stop="onDrop"
 | 
			
		||||
>
 | 
			
		||||
	<div :class="$style['body']">
 | 
			
		||||
		<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
 | 
			
		||||
			<template #empty>
 | 
			
		||||
				<div class="_fullinfo">
 | 
			
		||||
					<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 | 
			
		||||
					<div>{{ i18n.ts.noMessagesYet }}</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</template>
 | 
			
		||||
			<template #default="{ items: messages, fetching: pFetching }">
 | 
			
		||||
				<MkDateSeparatedList
 | 
			
		||||
					v-if="messages.length > 0"
 | 
			
		||||
					v-slot="{ item: message }"
 | 
			
		||||
					:class="{ [$style['messages']]: true, 'deny-move-transition': pFetching }"
 | 
			
		||||
					:items="messages"
 | 
			
		||||
					direction="up"
 | 
			
		||||
					reversed
 | 
			
		||||
				>
 | 
			
		||||
					<XMessage :key="message.id" :message="message" :is-group="group != null"/>
 | 
			
		||||
				</MkDateSeparatedList>
 | 
			
		||||
			</template>
 | 
			
		||||
		</MkPagination>
 | 
			
		||||
	</div>
 | 
			
		||||
	<footer :class="$style['footer']">
 | 
			
		||||
		<div v-if="typers.length > 0" :class="$style['typers']">
 | 
			
		||||
			<I18n :src="i18n.ts.typingUsers" text-tag="span">
 | 
			
		||||
				<template #users>
 | 
			
		||||
					<b v-for="typer in typers" :key="typer.id" :class="$style['user']">{{ typer.username }}</b>
 | 
			
		||||
				</template>
 | 
			
		||||
			</I18n>
 | 
			
		||||
			<MkEllipsis/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<Transition :name="animation ? 'fade' : ''">
 | 
			
		||||
			<div v-show="showIndicator" :class="$style['new-message']">
 | 
			
		||||
				<button class="_buttonPrimary" @click="onIndicatorClick" :class="$style['new-message-button']">
 | 
			
		||||
					<i class="fas ti-fw fa-arrow-circle-down" :class="$style['new-message-icon']"></i>{{ i18n.ts.newMessageExists }}
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</Transition>
 | 
			
		||||
		<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" :class="$style['form']"/>
 | 
			
		||||
	</footer>
 | 
			
		||||
</div>
 | 
			
		||||
</MkStickyContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import * as Acct from 'misskey-js/built/acct';
 | 
			
		||||
import XMessage from './messaging-room.message.vue';
 | 
			
		||||
import XForm from './messaging-room.form.vue';
 | 
			
		||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 | 
			
		||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
 | 
			
		||||
import { isBottomVisible, onScrollBottom, scrollToBottom } from '@/scripts/scroll';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { stream } from '@/stream';
 | 
			
		||||
import * as sound from '@/scripts/sound';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { $i } from '@/account';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { definePageMetadata } from '@/scripts/page-metadata';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	userAcct?: string;
 | 
			
		||||
	groupId?: string;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let rootEl = $shallowRef<HTMLDivElement>();
 | 
			
		||||
let formEl = $shallowRef<InstanceType<typeof XForm>>();
 | 
			
		||||
let pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>();
 | 
			
		||||
 | 
			
		||||
let fetching = $ref(true);
 | 
			
		||||
let user: Misskey.entities.UserDetailed | null = $ref(null);
 | 
			
		||||
let group: Misskey.entities.UserGroup | null = $ref(null);
 | 
			
		||||
let typers: Misskey.entities.User[] = $ref([]);
 | 
			
		||||
let connection: Misskey.ChannelConnection<Misskey.Channels['messaging']> | null = $ref(null);
 | 
			
		||||
let showIndicator = $ref(false);
 | 
			
		||||
const {
 | 
			
		||||
	animation,
 | 
			
		||||
} = defaultStore.reactiveState;
 | 
			
		||||
 | 
			
		||||
let pagination: Paging | null = $ref(null);
 | 
			
		||||
 | 
			
		||||
watch([() => props.userAcct, () => props.groupId], () => {
 | 
			
		||||
	if (connection) connection.dispose();
 | 
			
		||||
	fetch();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function fetch() {
 | 
			
		||||
	fetching = true;
 | 
			
		||||
 | 
			
		||||
	if (props.userAcct) {
 | 
			
		||||
		const acct = Acct.parse(props.userAcct);
 | 
			
		||||
		user = await os.api('users/show', { username: acct.username, host: acct.host || undefined });
 | 
			
		||||
		group = null;
 | 
			
		||||
		
 | 
			
		||||
		pagination = {
 | 
			
		||||
			endpoint: 'messaging/messages',
 | 
			
		||||
			limit: 20,
 | 
			
		||||
			params: {
 | 
			
		||||
				userId: user.id,
 | 
			
		||||
			},
 | 
			
		||||
			reversed: true,
 | 
			
		||||
			pageEl: $$(rootEl).value,
 | 
			
		||||
		};
 | 
			
		||||
		connection = stream.useChannel('messaging', {
 | 
			
		||||
			otherparty: user.id,
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		user = null;
 | 
			
		||||
		group = await os.api('users/groups/show', { groupId: props.groupId });
 | 
			
		||||
 | 
			
		||||
		pagination = {
 | 
			
		||||
			endpoint: 'messaging/messages',
 | 
			
		||||
			limit: 20,
 | 
			
		||||
			params: {
 | 
			
		||||
				groupId: group?.id,
 | 
			
		||||
			},
 | 
			
		||||
			reversed: true,
 | 
			
		||||
			pageEl: $$(rootEl).value,
 | 
			
		||||
		};
 | 
			
		||||
		connection = stream.useChannel('messaging', {
 | 
			
		||||
			group: group?.id,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	connection.on('message', onMessage);
 | 
			
		||||
	connection.on('read', onRead);
 | 
			
		||||
	connection.on('deleted', onDeleted);
 | 
			
		||||
	connection.on('typers', _typers => {
 | 
			
		||||
		typers = _typers.filter(u => u.id !== $i?.id);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	document.addEventListener('visibilitychange', onVisibilitychange);
 | 
			
		||||
 | 
			
		||||
	nextTick(() => {
 | 
			
		||||
		pagingComponent.inited.then(() => {
 | 
			
		||||
			thisScrollToBottom();
 | 
			
		||||
		});
 | 
			
		||||
		window.setTimeout(() => {
 | 
			
		||||
			fetching = false;
 | 
			
		||||
		}, 300);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragover(ev: DragEvent) {
 | 
			
		||||
	if (!ev.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	const isFile = ev.dataTransfer.items[0].kind === 'file';
 | 
			
		||||
	const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
 | 
			
		||||
	if (isFile || isDriveFile) {
 | 
			
		||||
		switch (ev.dataTransfer.effectAllowed) {
 | 
			
		||||
			case 'all':
 | 
			
		||||
			case 'uninitialized':
 | 
			
		||||
			case 'copy': 
 | 
			
		||||
			case 'copyLink': 
 | 
			
		||||
			case 'copyMove': 
 | 
			
		||||
				ev.dataTransfer.dropEffect = 'copy';
 | 
			
		||||
				break;
 | 
			
		||||
			case 'linkMove':
 | 
			
		||||
			case 'move':
 | 
			
		||||
				ev.dataTransfer.dropEffect = 'move';
 | 
			
		||||
				break;
 | 
			
		||||
			default:
 | 
			
		||||
				ev.dataTransfer.dropEffect = 'none';
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		ev.dataTransfer.dropEffect = 'none';
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDrop(ev: DragEvent): void {
 | 
			
		||||
	if (!ev.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	// ファイルだったら
 | 
			
		||||
	if (ev.dataTransfer.files.length === 1) {
 | 
			
		||||
		formEl.upload(ev.dataTransfer.files[0]);
 | 
			
		||||
		return;
 | 
			
		||||
	} else if (ev.dataTransfer.files.length > 1) {
 | 
			
		||||
		os.alert({
 | 
			
		||||
			type: 'error',
 | 
			
		||||
			text: i18n.ts.onlyOneFileCanBeAttached,
 | 
			
		||||
		});
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのファイル
 | 
			
		||||
	const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
	if (driveFile != null && driveFile !== '') {
 | 
			
		||||
		const file = JSON.parse(driveFile);
 | 
			
		||||
		formEl.file = file;
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onMessage(message) {
 | 
			
		||||
	sound.play('chat');
 | 
			
		||||
 | 
			
		||||
	const _isBottom = isBottomVisible(rootEl, 64);
 | 
			
		||||
 | 
			
		||||
	pagingComponent.prepend(message);
 | 
			
		||||
	if (message.userId !== $i?.id && !document.hidden) {
 | 
			
		||||
		connection?.send('read', {
 | 
			
		||||
			id: message.id,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (_isBottom) {
 | 
			
		||||
		// Scroll to bottom
 | 
			
		||||
		nextTick(() => {
 | 
			
		||||
			thisScrollToBottom();
 | 
			
		||||
		});
 | 
			
		||||
	} else if (message.userId !== $i?.id) {
 | 
			
		||||
		// Notify
 | 
			
		||||
		notifyNewMessage();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onRead(x) {
 | 
			
		||||
	if (user) {
 | 
			
		||||
		if (!Array.isArray(x)) x = [x];
 | 
			
		||||
		for (const id of x) {
 | 
			
		||||
			if (pagingComponent.items.some(y => y.id === id)) {
 | 
			
		||||
				const exist = pagingComponent.items.map(y => y.id).indexOf(id);
 | 
			
		||||
				pagingComponent.items[exist] = {
 | 
			
		||||
					...pagingComponent.items[exist],
 | 
			
		||||
					isRead: true,
 | 
			
		||||
				};
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else if (group) {
 | 
			
		||||
		for (const id of x.ids) {
 | 
			
		||||
			if (pagingComponent.items.some(y => y.id === id)) {
 | 
			
		||||
				const exist = pagingComponent.items.map(y => y.id).indexOf(id);
 | 
			
		||||
				pagingComponent.items[exist] = {
 | 
			
		||||
					...pagingComponent.items[exist],
 | 
			
		||||
					reads: [...pagingComponent.items[exist].reads, x.userId],
 | 
			
		||||
				};
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDeleted(id) {
 | 
			
		||||
	const msg = pagingComponent.items.find(m => m.id === id);
 | 
			
		||||
	if (msg) {
 | 
			
		||||
		pagingComponent.items = pagingComponent.items.filter(m => m.id !== msg.id);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function thisScrollToBottom() {
 | 
			
		||||
	scrollToBottom($$(rootEl).value, { behavior: 'smooth' });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onIndicatorClick() {
 | 
			
		||||
	showIndicator = false;
 | 
			
		||||
	thisScrollToBottom();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let scrollRemove: (() => void) | null = $ref(null);
 | 
			
		||||
 | 
			
		||||
function notifyNewMessage() {
 | 
			
		||||
	showIndicator = true;
 | 
			
		||||
 | 
			
		||||
	scrollRemove = onScrollBottom(rootEl, () => {
 | 
			
		||||
		showIndicator = false;
 | 
			
		||||
		scrollRemove = null;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onVisibilitychange() {
 | 
			
		||||
	if (document.hidden) return;
 | 
			
		||||
	for (const message of pagingComponent.items) {
 | 
			
		||||
		if (message.userId !== $i?.id && !message.isRead) {
 | 
			
		||||
			connection?.send('read', {
 | 
			
		||||
				id: message.id,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	fetch();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
	connection?.dispose();
 | 
			
		||||
	document.removeEventListener('visibilitychange', onVisibilitychange);
 | 
			
		||||
	if (scrollRemove) scrollRemove();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
definePageMetadata(computed(() => !fetching ? user ? {
 | 
			
		||||
	userName: user,
 | 
			
		||||
	avatar: user,
 | 
			
		||||
} : {
 | 
			
		||||
	title: group?.name,
 | 
			
		||||
	icon: 'ti ti-users',
 | 
			
		||||
} : null));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	display: content;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.body {
 | 
			
		||||
	min-height: 80%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.more {
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: 16px auto;
 | 
			
		||||
	padding: 0 12px;
 | 
			
		||||
	line-height: 24px;
 | 
			
		||||
	color: #fff;
 | 
			
		||||
	background: rgba(#000, 0.3);
 | 
			
		||||
	border-radius: 12px;
 | 
			
		||||
	&:hover {
 | 
			
		||||
		background: rgba(#000, 0.4);
 | 
			
		||||
	}
 | 
			
		||||
	&:active {
 | 
			
		||||
		background: rgba(#000, 0.5);
 | 
			
		||||
	}
 | 
			
		||||
	> i {
 | 
			
		||||
		margin-right: 4px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fetching {
 | 
			
		||||
	cursor: wait;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.messages {
 | 
			
		||||
	padding: 16px 0 0;
 | 
			
		||||
 | 
			
		||||
	> * {
 | 
			
		||||
		margin-bottom: 16px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.footer {
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	position: sticky;
 | 
			
		||||
	z-index: 2;
 | 
			
		||||
	padding-top: 8px;
 | 
			
		||||
	bottom: var(--minBottomSpacing);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.new-message {
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	padding-bottom: 8px;
 | 
			
		||||
	text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.new-message-button {
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0 12px;
 | 
			
		||||
	line-height: 32px;
 | 
			
		||||
	font-size: 12px;
 | 
			
		||||
	border-radius: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.new-message-icon {
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	margin-right: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.typers {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	bottom: 100%;
 | 
			
		||||
	padding: 0 8px 0 8px;
 | 
			
		||||
	font-size: 0.9em;
 | 
			
		||||
	color: var(--fgTransparentWeak);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.user + .user:before {
 | 
			
		||||
	content: ", ";
 | 
			
		||||
	font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.user:last-of-type:after {
 | 
			
		||||
	content: " ";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form {
 | 
			
		||||
	max-height: 12em;
 | 
			
		||||
	overflow-y: scroll;
 | 
			
		||||
	border-top: solid 0.5px var(--divider);
 | 
			
		||||
	border-bottom-left-radius: 0;
 | 
			
		||||
	border-bottom-right-radius: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fade-enter-active, .fade-leave-active {
 | 
			
		||||
	transition: opacity 0.1s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fade-enter-from, .fade-leave-to {
 | 
			
		||||
	transition: opacity 0.5s;
 | 
			
		||||
	opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -5,7 +5,6 @@
 | 
			
		|||
		<div class="_gaps_m">
 | 
			
		||||
			<FormLink @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</FormLink>
 | 
			
		||||
			<FormLink @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink>
 | 
			
		||||
			<FormLink @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
 | 
			
		||||
		</div>
 | 
			
		||||
	</FormSection>
 | 
			
		||||
	<FormSection>
 | 
			
		||||
| 
						 | 
				
			
			@ -47,10 +46,6 @@ async function readAllUnreadNotes() {
 | 
			
		|||
	await os.api('i/read-all-unread-notes');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function readAllMessagingMessages() {
 | 
			
		||||
	await os.api('i/read-all-messaging-messages');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function readAllNotifications() {
 | 
			
		||||
	await os.api('notifications/mark-all-as-read');
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -420,19 +420,6 @@ export const routes = [{
 | 
			
		|||
	path: '/my/achievements',
 | 
			
		||||
	component: page(() => import('./pages/achievements.vue')),
 | 
			
		||||
	loginRequired: true,
 | 
			
		||||
}, {
 | 
			
		||||
	name: 'messaging',
 | 
			
		||||
	path: '/my/messaging',
 | 
			
		||||
	component: page(() => import('./pages/messaging/index.vue')),
 | 
			
		||||
	loginRequired: true,
 | 
			
		||||
}, {
 | 
			
		||||
	path: '/my/messaging/:userAcct',
 | 
			
		||||
	component: page(() => import('./pages/messaging/messaging-room.vue')),
 | 
			
		||||
	loginRequired: true,
 | 
			
		||||
}, {
 | 
			
		||||
	path: '/my/messaging/group/:groupId',
 | 
			
		||||
	component: page(() => import('./pages/messaging/messaging-room.vue')),
 | 
			
		||||
	loginRequired: true,
 | 
			
		||||
}, {
 | 
			
		||||
	path: '/my/drive/folder/:folder',
 | 
			
		||||
	component: page(() => import('./pages/drive.vue')),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -236,23 +236,6 @@ async function composeNotification(data: pushNotificationDataMap[keyof pushNotif
 | 
			
		|||
				default:
 | 
			
		||||
					return null;
 | 
			
		||||
			}
 | 
			
		||||
		case 'unreadMessagingMessage':
 | 
			
		||||
			if (data.body.groupId === null) {
 | 
			
		||||
				return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), {
 | 
			
		||||
					icon: data.body.user.avatarUrl,
 | 
			
		||||
					badge: iconUrl('messages'),
 | 
			
		||||
					tag: `messaging:user:${data.body.userId}`,
 | 
			
		||||
					data,
 | 
			
		||||
					renotify: true,
 | 
			
		||||
				}];
 | 
			
		||||
			}
 | 
			
		||||
			return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group?.name ?? '' }), {
 | 
			
		||||
				icon: data.body.user.avatarUrl,
 | 
			
		||||
				badge: iconUrl('messages'),
 | 
			
		||||
				tag: `messaging:group:${data.body.groupId}`,
 | 
			
		||||
				data,
 | 
			
		||||
				renotify: true,
 | 
			
		||||
			}];
 | 
			
		||||
		case 'unreadAntennaNote':
 | 
			
		||||
			return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
 | 
			
		||||
				body: `${getUserName(data.body.note.user)}: ${data.body.note.text ?? ''}`,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,9 +15,9 @@ globalThis.addEventListener('activate', ev => {
 | 
			
		|||
			.then(cacheNames => Promise.all(
 | 
			
		||||
				cacheNames
 | 
			
		||||
					.filter((v) => v !== swLang.cacheName)
 | 
			
		||||
					.map(name => caches.delete(name))
 | 
			
		||||
					.map(name => caches.delete(name)),
 | 
			
		||||
			))
 | 
			
		||||
			.then(() => self.clients.claim())
 | 
			
		||||
			.then(() => self.clients.claim()),
 | 
			
		||||
	);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +34,7 @@ globalThis.addEventListener('fetch', ev => {
 | 
			
		|||
	if (!isHTMLRequest) return;
 | 
			
		||||
	ev.respondWith(
 | 
			
		||||
		fetch(ev.request)
 | 
			
		||||
		.catch(() => new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 }))
 | 
			
		||||
			.catch(() => new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 })),
 | 
			
		||||
	);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -42,14 +42,13 @@ globalThis.addEventListener('push', ev => {
 | 
			
		|||
	// クライアント取得
 | 
			
		||||
	ev.waitUntil(self.clients.matchAll({
 | 
			
		||||
		includeUncontrolled: true,
 | 
			
		||||
		type: 'window'
 | 
			
		||||
		type: 'window',
 | 
			
		||||
	}).then(async (clients: readonly WindowClient[]) => {
 | 
			
		||||
		const data: pushNotificationDataMap[keyof pushNotificationDataMap] = ev.data?.json();
 | 
			
		||||
 | 
			
		||||
		switch (data.type) {
 | 
			
		||||
			// case 'driveFileCreated':
 | 
			
		||||
			case 'notification':
 | 
			
		||||
			case 'unreadMessagingMessage':
 | 
			
		||||
			case 'unreadAntennaNote':
 | 
			
		||||
				// 1日以上経過している場合は無視
 | 
			
		||||
				if ((new Date()).getTime() - data.dateTime > 1000 * 60 * 60 * 24) break;
 | 
			
		||||
| 
						 | 
				
			
			@ -63,11 +62,6 @@ globalThis.addEventListener('push', ev => {
 | 
			
		|||
					if (n?.data?.type === 'notification') n.close();
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			case 'readAllMessagingMessages':
 | 
			
		||||
				for (const n of await self.registration.getNotifications()) {
 | 
			
		||||
					if (n?.data?.type === 'unreadMessagingMessage') n.close();
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			case 'readAllAntennas':
 | 
			
		||||
				for (const n of await self.registration.getNotifications()) {
 | 
			
		||||
					if (n?.data?.type === 'unreadAntennaNote') n.close();
 | 
			
		||||
| 
						 | 
				
			
			@ -75,25 +69,14 @@ globalThis.addEventListener('push', ev => {
 | 
			
		|||
				break;
 | 
			
		||||
			case 'readNotifications':
 | 
			
		||||
				for (const n of await self.registration.getNotifications()) {
 | 
			
		||||
					if (data.body?.notificationIds?.includes(n.data.body.id)) {
 | 
			
		||||
					if (data.body.notificationIds.includes(n.data.body.id)) {
 | 
			
		||||
						n.close();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			case 'readAllMessagingMessagesOfARoom':
 | 
			
		||||
				for (const n of await self.registration.getNotifications()) {
 | 
			
		||||
					if (n.data.type === 'unreadMessagingMessage'
 | 
			
		||||
						&& ('userId' in data.body
 | 
			
		||||
							? data.body.userId === n.data.body.userId
 | 
			
		||||
							: data.body.groupId === n.data.body.groupId)
 | 
			
		||||
						) {
 | 
			
		||||
							n.close();
 | 
			
		||||
						}
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			case 'readAntenna':
 | 
			
		||||
				for (const n of await self.registration.getNotifications()) {
 | 
			
		||||
					if (n?.data?.type === 'unreadAntennaNote' && data.body?.antennaId === n.data.body.antenna.id) {
 | 
			
		||||
					if (n?.data?.type === 'unreadAntennaNote' && data.body.antennaId === n.data.body.antenna.id) {
 | 
			
		||||
						n.close();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			@ -174,9 +157,6 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
 | 
			
		|||
						}
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			case 'unreadMessagingMessage':
 | 
			
		||||
				client = await swos.openChat(data.body, loginId);
 | 
			
		||||
				break;
 | 
			
		||||
			case 'unreadAntennaNote':
 | 
			
		||||
				client = await swos.openAntenna(data.body.antenna.id, loginId);
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -207,7 +187,7 @@ globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['me
 | 
			
		|||
				// Cache Storage全削除
 | 
			
		||||
				await caches.keys()
 | 
			
		||||
					.then(cacheNames => Promise.all(
 | 
			
		||||
						cacheNames.map(name => caches.delete(name))
 | 
			
		||||
						cacheNames.map(name => caches.delete(name)),
 | 
			
		||||
					));
 | 
			
		||||
				return; // TODO
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,15 +13,12 @@ export type SwMessage = {
 | 
			
		|||
// Defined also @/core/PushNotificationService.ts#L12
 | 
			
		||||
type pushNotificationDataSourceMap = {
 | 
			
		||||
	notification: Misskey.entities.Notification;
 | 
			
		||||
	unreadMessagingMessage: Misskey.entities.MessagingMessage;
 | 
			
		||||
	unreadAntennaNote: {
 | 
			
		||||
		antenna: { id: string, name: string };
 | 
			
		||||
		note: Misskey.entities.Note;
 | 
			
		||||
	};
 | 
			
		||||
	readNotifications: { notificationIds: string[] };
 | 
			
		||||
	readAllNotifications: undefined;
 | 
			
		||||
	readAllMessagingMessages: undefined;
 | 
			
		||||
	readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string };
 | 
			
		||||
	readAntenna: { antennaId: string };
 | 
			
		||||
	readAllAntennas: undefined;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue