Editing and error correction (in progress)

This commit is contained in:
2026-09-30 12:43:19 +03:30
parent a3ed6187e9
commit 3c0ed3369e
5 changed files with 199 additions and 31 deletions
@@ -14,6 +14,7 @@ import {
AddConversationRoomMemberDto,
RemoveConversationRoomMemberDto,
UpdateConversationRoomMemberRoleDto,
ConversationTypingDto,
} from './dto';
import { ConversationsService } from './conversations.service';
@@ -43,6 +44,32 @@ export class ConversationsController {
return this.conversations.listRooms(user.userId, user.organizationId, user.role);
}
@Post('rooms/typing')
signalTyping(
@CurrentUser() user: { userId: string; organizationId: string; role: string },
@Body() dto: ConversationTypingDto,
) {
return this.conversations.signalTyping(
user.userId,
user.organizationId,
user.role,
dto.externalRoomId,
);
}
@Get('rooms/typing')
listTyping(
@CurrentUser() user: { userId: string; organizationId: string; role: string },
@Query('externalRoomId') externalRoomId: string,
) {
return this.conversations.listTyping(
user.userId,
user.organizationId,
user.role,
externalRoomId || '',
);
}
@Post('rooms/dm')
createDm(
@CurrentUser() user: { userId: string; organizationId: string; role: string },
@@ -26,12 +26,78 @@ import {
@Injectable()
export class ConversationsService {
/** presence: org|room|userId → epoch ms (بدون DB؛ کافی برای typing کوتاه‌عمر) */
private readonly typingPresence = new Map<string, number>();
private typingSweepAt = 0;
constructor(
private readonly prisma: PrismaService,
private readonly crypto: TahesabCryptoService,
private readonly chat: ChatExternalClient,
) {}
private typingKey(organizationId: string, roomId: string, userId: string) {
return `${organizationId}|${roomId}|${userId}`;
}
private sweepTypingPresence(now = Date.now()) {
if (now - this.typingSweepAt < 10_000) return;
this.typingSweepAt = now;
for (const [k, at] of this.typingPresence) {
if (now - at > 8000) this.typingPresence.delete(k);
}
}
async signalTyping(
userId: string,
organizationId: string,
role: string,
externalRoomId: string,
): Promise<{ ok: true }> {
await this.loadAccess(userId, organizationId, role);
const roomId = externalRoomId.trim();
if (!roomId) throw new BadRequestException('شناسه اتاق خالی است');
const now = Date.now();
this.typingPresence.set(this.typingKey(organizationId, roomId, userId), now);
this.sweepTypingPresence(now);
return { ok: true };
}
async listTyping(
userId: string,
organizationId: string,
role: string,
externalRoomId: string,
): Promise<Array<{ userId: string; fullName: string; chatUserId: string }>> {
await this.loadAccess(userId, organizationId, role);
const roomId = externalRoomId.trim();
if (!roomId) return [];
const now = Date.now();
this.sweepTypingPresence(now);
const prefix = `${organizationId}|${roomId}|`;
const ids: string[] = [];
for (const [k, at] of this.typingPresence) {
if (!k.startsWith(prefix)) continue;
if (now - at > 4000) {
this.typingPresence.delete(k);
continue;
}
const uid = k.slice(prefix.length);
if (!uid || uid === userId) continue;
ids.push(uid);
}
if (!ids.length) return [];
const users = await this.prisma.user.findMany({
where: { id: { in: ids } },
select: { id: true, fullName: true },
});
return users.map((u) => ({
userId: u.id,
fullName: u.fullName || 'کاربر',
chatUserId: buildChatUserId(organizationId, u.id),
}));
}
async getAccess(userId: string, organizationId: string, role: string): Promise<ConversationAccessDto> {
const access = await this.loadAccess(userId, organizationId, role);
return access;
+7
View File
@@ -162,3 +162,10 @@ export class UpdateConversationRoomMemberRoleDto {
@IsString()
role!: 'admin' | 'member';
}
export class ConversationTypingDto {
@IsString()
@MinLength(1)
@MaxLength(200)
externalRoomId!: string;
}