Adding the typing... indicator, loading older messages, and managing members/admins after creation.
This commit is contained in:
@@ -9,6 +9,7 @@ import type {
|
||||
ConversationPeerDto,
|
||||
ConversationPinnedMessageDto,
|
||||
ConversationRoomDto,
|
||||
ConversationRoomMemberDto,
|
||||
ConversationSessionDto,
|
||||
ConversationSettingsDto,
|
||||
PlatformConversationStatsDto,
|
||||
@@ -195,18 +196,32 @@ export class ConversationsService {
|
||||
select: { role: true },
|
||||
});
|
||||
const effectiveRole = String(membership?.role ?? role);
|
||||
const orgAdmin = effectiveRole === 'owner' || effectiveRole === 'admin';
|
||||
const roomAdminIds = await this.userAdminRoomIds(organizationId, userId);
|
||||
|
||||
try {
|
||||
const external = await this.chat.listUserRooms(creds.host, creds.apiKey, chatUserId);
|
||||
return external.map((row) => {
|
||||
const dto = this.toRoomDto(row, trackedByExternal, organizationId, userId, effectiveRole);
|
||||
return { ...dto, pinned: pinnedIds.has(dto.externalRoomId) };
|
||||
const isRoomAdmin =
|
||||
roomAdminIds.has(dto.externalRoomId) ||
|
||||
trackedByExternal.get(dto.externalRoomId)?.createdByUserId === userId;
|
||||
return {
|
||||
...dto,
|
||||
pinned: pinnedIds.has(dto.externalRoomId),
|
||||
canPost:
|
||||
dto.kind !== 'channel' ? true : Boolean(isRoomAdmin || orgAdmin),
|
||||
canManageMembers:
|
||||
dto.kind === 'dm' ? false : Boolean(isRoomAdmin || orgAdmin),
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
// اگر سرویس چت در دسترس نبود، حداقل roomهای ثبتشده در CRM را نشان بده
|
||||
if (tracked.length) {
|
||||
return tracked.map((t) => {
|
||||
const kind = this.normalizeKind(t.kind);
|
||||
const isRoomAdmin =
|
||||
roomAdminIds.has(t.externalRoomId) || t.createdByUserId === userId;
|
||||
return {
|
||||
id: t.id,
|
||||
externalRoomId: t.externalRoomId,
|
||||
@@ -226,7 +241,8 @@ export class ConversationsService {
|
||||
: null
|
||||
: null,
|
||||
pinned: pinnedIds.has(t.externalRoomId),
|
||||
canPost: this.canPostInRoom(kind, t.createdByUserId, userId, effectiveRole),
|
||||
canPost: kind !== 'channel' ? true : Boolean(isRoomAdmin || orgAdmin),
|
||||
canManageMembers: kind === 'dm' ? false : Boolean(isRoomAdmin || orgAdmin),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -355,6 +371,8 @@ export class ConversationsService {
|
||||
},
|
||||
});
|
||||
|
||||
await this.seedRoomMembers(organizationId, row.externalRoomId, userId, uniqueIds);
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
externalRoomId: row.externalRoomId,
|
||||
@@ -365,6 +383,7 @@ export class ConversationsService {
|
||||
lastMessageAt: null,
|
||||
peerUserId: null,
|
||||
canPost: true,
|
||||
canManageMembers: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -417,6 +436,8 @@ export class ConversationsService {
|
||||
},
|
||||
});
|
||||
|
||||
await this.seedRoomMembers(organizationId, row.externalRoomId, userId, uniqueIds);
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
externalRoomId: row.externalRoomId,
|
||||
@@ -427,6 +448,7 @@ export class ConversationsService {
|
||||
lastMessageAt: null,
|
||||
peerUserId: null,
|
||||
canPost: true,
|
||||
canManageMembers: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -602,6 +624,240 @@ export class ConversationsService {
|
||||
return new Set(rows.map((r) => r.externalRoomId));
|
||||
}
|
||||
|
||||
private async userAdminRoomIds(organizationId: string, userId: string): Promise<Set<string>> {
|
||||
const rows = await this.prisma.conversationRoomMember.findMany({
|
||||
where: { organizationId, userId, role: 'admin' },
|
||||
select: { externalRoomId: true },
|
||||
take: 1000,
|
||||
});
|
||||
return new Set(rows.map((r) => r.externalRoomId));
|
||||
}
|
||||
|
||||
private async seedRoomMembers(
|
||||
organizationId: string,
|
||||
externalRoomId: string,
|
||||
adminUserId: string,
|
||||
memberUserIds: string[],
|
||||
) {
|
||||
const rows = [
|
||||
{
|
||||
organizationId,
|
||||
externalRoomId,
|
||||
userId: adminUserId,
|
||||
role: 'admin',
|
||||
},
|
||||
...memberUserIds.map((id) => ({
|
||||
organizationId,
|
||||
externalRoomId,
|
||||
userId: id,
|
||||
role: 'member',
|
||||
})),
|
||||
];
|
||||
await this.prisma.conversationRoomMember.createMany({
|
||||
data: rows,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async assertCanManageRoom(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
externalRoomId: string,
|
||||
) {
|
||||
await this.loadAccess(userId, organizationId, role);
|
||||
const room = await this.prisma.conversationRoom.findFirst({
|
||||
where: { organizationId, externalRoomId },
|
||||
});
|
||||
if (!room) throw new NotFoundException('گفتگو پیدا نشد');
|
||||
if (room.kind === 'dm') {
|
||||
throw new BadRequestException('مدیریت اعضا برای گفتگوی خصوصی نیست');
|
||||
}
|
||||
const orgAdmin = role === 'owner' || role === 'admin';
|
||||
if (orgAdmin || room.createdByUserId === userId) return room;
|
||||
const mine = await this.prisma.conversationRoomMember.findFirst({
|
||||
where: { organizationId, externalRoomId, userId, role: 'admin' },
|
||||
});
|
||||
if (!mine) throw new ForbiddenException('اجازه مدیریت اعضای این گفتگو را ندارید');
|
||||
return room;
|
||||
}
|
||||
|
||||
async listRoomMembers(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
externalRoomId: string,
|
||||
): Promise<ConversationRoomMemberDto[]> {
|
||||
await this.loadAccess(userId, organizationId, role);
|
||||
const roomId = externalRoomId.trim();
|
||||
if (!roomId) throw new BadRequestException('شناسه اتاق خالی است');
|
||||
|
||||
const room = await this.prisma.conversationRoom.findFirst({
|
||||
where: { organizationId, externalRoomId: roomId },
|
||||
});
|
||||
if (!room) throw new NotFoundException('گفتگو پیدا نشد');
|
||||
|
||||
let rows = await this.prisma.conversationRoomMember.findMany({
|
||||
where: { organizationId, externalRoomId: roomId },
|
||||
});
|
||||
|
||||
// اتاقهای قدیمی بدون ردیف عضو — حداقل سازنده را ادمین نشان بده
|
||||
if (!rows.length && room.createdByUserId) {
|
||||
await this.seedRoomMembers(organizationId, roomId, room.createdByUserId, []);
|
||||
rows = await this.prisma.conversationRoomMember.findMany({
|
||||
where: { organizationId, externalRoomId: roomId },
|
||||
});
|
||||
}
|
||||
|
||||
const userIds = rows.map((r) => r.userId);
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, fullName: true, avatarStorageKey: true },
|
||||
});
|
||||
const byId = new Map(users.map((u) => [u.id, u]));
|
||||
|
||||
return rows
|
||||
.map((r) => {
|
||||
const u = byId.get(r.userId);
|
||||
return {
|
||||
userId: r.userId,
|
||||
fullName: u?.fullName || 'کاربر',
|
||||
role: (r.role === 'admin' ? 'admin' : 'member') as 'admin' | 'member',
|
||||
hasAvatar: Boolean(u?.avatarStorageKey),
|
||||
isMe: r.userId === userId,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.role !== b.role) return a.role === 'admin' ? -1 : 1;
|
||||
return a.fullName.localeCompare(b.fullName, 'fa');
|
||||
});
|
||||
}
|
||||
|
||||
async addRoomMember(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
externalRoomId: string,
|
||||
memberUserId: string,
|
||||
memberRole: 'admin' | 'member' = 'member',
|
||||
): Promise<ConversationRoomMemberDto[]> {
|
||||
const room = await this.assertCanManageRoom(userId, organizationId, role, externalRoomId.trim());
|
||||
const targetId = memberUserId.trim();
|
||||
if (!targetId || targetId === userId) {
|
||||
// اجازه افزودن خود لازم نیست
|
||||
}
|
||||
if (!targetId) throw new BadRequestException('شناسه عضو خالی است');
|
||||
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
where: {
|
||||
organizationId,
|
||||
userId: targetId,
|
||||
isActive: true,
|
||||
user: { isActive: true },
|
||||
},
|
||||
});
|
||||
if (!membership) throw new NotFoundException('همکار در این سازمان پیدا نشد');
|
||||
|
||||
await this.prisma.conversationRoomMember.upsert({
|
||||
where: {
|
||||
organizationId_externalRoomId_userId: {
|
||||
organizationId,
|
||||
externalRoomId: room.externalRoomId,
|
||||
userId: targetId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
organizationId,
|
||||
externalRoomId: room.externalRoomId,
|
||||
userId: targetId,
|
||||
role: memberRole === 'admin' ? 'admin' : 'member',
|
||||
},
|
||||
update: {
|
||||
role: memberRole === 'admin' ? 'admin' : 'member',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const creds = await this.getCredentials(organizationId);
|
||||
await this.chat.addRoomMembers(creds.host, creds.apiKey, room.externalRoomId, [
|
||||
buildChatUserId(organizationId, targetId),
|
||||
]);
|
||||
} catch {
|
||||
// CRM بهروز شده؛ همگامسازی خارجی best-effort است
|
||||
}
|
||||
|
||||
return this.listRoomMembers(userId, organizationId, role, room.externalRoomId);
|
||||
}
|
||||
|
||||
async removeRoomMember(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
externalRoomId: string,
|
||||
memberUserId: string,
|
||||
): Promise<ConversationRoomMemberDto[]> {
|
||||
const room = await this.assertCanManageRoom(userId, organizationId, role, externalRoomId.trim());
|
||||
const targetId = memberUserId.trim();
|
||||
if (!targetId) throw new BadRequestException('شناسه عضو خالی است');
|
||||
if (targetId === room.createdByUserId) {
|
||||
throw new BadRequestException('نمیتوان سازنده گفتگو را حذف کرد');
|
||||
}
|
||||
if (targetId === userId) {
|
||||
throw new BadRequestException('نمیتوانید خودتان را حذف کنید');
|
||||
}
|
||||
|
||||
await this.prisma.conversationRoomMember.deleteMany({
|
||||
where: {
|
||||
organizationId,
|
||||
externalRoomId: room.externalRoomId,
|
||||
userId: targetId,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const creds = await this.getCredentials(organizationId);
|
||||
await this.chat.removeRoomMembers(creds.host, creds.apiKey, room.externalRoomId, [
|
||||
buildChatUserId(organizationId, targetId),
|
||||
]);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
|
||||
return this.listRoomMembers(userId, organizationId, role, room.externalRoomId);
|
||||
}
|
||||
|
||||
async updateRoomMemberRole(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
externalRoomId: string,
|
||||
memberUserId: string,
|
||||
memberRole: 'admin' | 'member',
|
||||
): Promise<ConversationRoomMemberDto[]> {
|
||||
const room = await this.assertCanManageRoom(userId, organizationId, role, externalRoomId.trim());
|
||||
const targetId = memberUserId.trim();
|
||||
if (!targetId) throw new BadRequestException('شناسه عضو خالی است');
|
||||
if (targetId === room.createdByUserId && memberRole !== 'admin') {
|
||||
throw new BadRequestException('نقش سازنده گفتگو را نمیتوان از ادمین برداشت');
|
||||
}
|
||||
|
||||
const existing = await this.prisma.conversationRoomMember.findFirst({
|
||||
where: {
|
||||
organizationId,
|
||||
externalRoomId: room.externalRoomId,
|
||||
userId: targetId,
|
||||
},
|
||||
});
|
||||
if (!existing) throw new NotFoundException('عضو پیدا نشد');
|
||||
|
||||
await this.prisma.conversationRoomMember.update({
|
||||
where: { id: existing.id },
|
||||
data: { role: memberRole === 'admin' ? 'admin' : 'member' },
|
||||
});
|
||||
|
||||
return this.listRoomMembers(userId, organizationId, role, room.externalRoomId);
|
||||
}
|
||||
|
||||
async setRoomPinned(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
|
||||
Reference in New Issue
Block a user