Add mentions, pin messages and conversations, and search within chats
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { RequireOrgModule } from '../billing/org-module.decorator';
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
CreateConversationDmDto,
|
||||
CreateConversationGroupDto,
|
||||
HideConversationMessagesDto,
|
||||
PinConversationMessageDto,
|
||||
PinConversationRoomDto,
|
||||
UnpinConversationMessageDto,
|
||||
} from './dto';
|
||||
import { ConversationsService } from './conversations.service';
|
||||
|
||||
@@ -63,6 +66,20 @@ export class ConversationsController {
|
||||
);
|
||||
}
|
||||
|
||||
@Post('rooms/pin')
|
||||
pinRoom(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@Body() dto: PinConversationRoomDto,
|
||||
) {
|
||||
return this.conversations.setRoomPinned(
|
||||
user.userId,
|
||||
user.organizationId,
|
||||
user.role,
|
||||
dto.externalRoomId,
|
||||
dto.pinned,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('messages/hide')
|
||||
hideMessages(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@@ -90,4 +107,58 @@ export class ConversationsController {
|
||||
externalRoomId || '',
|
||||
);
|
||||
}
|
||||
|
||||
@Get('messages/pin')
|
||||
getPinnedMessage(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@Query('externalRoomId') externalRoomId: string,
|
||||
) {
|
||||
return this.conversations.getPinnedMessage(
|
||||
user.userId,
|
||||
user.organizationId,
|
||||
user.role,
|
||||
externalRoomId || '',
|
||||
);
|
||||
}
|
||||
|
||||
@Post('messages/pin')
|
||||
pinMessage(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@Body() dto: PinConversationMessageDto,
|
||||
) {
|
||||
return this.conversations.pinMessage(
|
||||
user.userId,
|
||||
user.organizationId,
|
||||
user.role,
|
||||
dto.externalRoomId,
|
||||
dto.messageId,
|
||||
dto.preview,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('messages/unpin')
|
||||
unpinMessage(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@Body() dto: UnpinConversationMessageDto,
|
||||
) {
|
||||
return this.conversations.unpinMessage(
|
||||
user.userId,
|
||||
user.organizationId,
|
||||
user.role,
|
||||
dto.externalRoomId,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('messages/pin')
|
||||
unpinMessageDelete(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@Query('externalRoomId') externalRoomId: string,
|
||||
) {
|
||||
return this.conversations.unpinMessage(
|
||||
user.userId,
|
||||
user.organizationId,
|
||||
user.role,
|
||||
externalRoomId || '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import type {
|
||||
ConversationAccessDto,
|
||||
ConversationPeerDto,
|
||||
ConversationPinnedMessageDto,
|
||||
ConversationRoomDto,
|
||||
ConversationSessionDto,
|
||||
ConversationSettingsDto,
|
||||
@@ -188,19 +189,21 @@ export class ConversationsService {
|
||||
where: { organizationId },
|
||||
});
|
||||
const trackedByExternal = new Map(tracked.map((t) => [t.externalRoomId, t]));
|
||||
const pinnedIds = await this.userPinnedRoomIds(organizationId, userId);
|
||||
|
||||
try {
|
||||
const external = await this.chat.listUserRooms(creds.host, creds.apiKey, chatUserId);
|
||||
return external.map((row) =>
|
||||
this.toRoomDto(row, trackedByExternal, organizationId, userId),
|
||||
);
|
||||
return external.map((row) => {
|
||||
const dto = this.toRoomDto(row, trackedByExternal, organizationId, userId);
|
||||
return { ...dto, pinned: pinnedIds.has(dto.externalRoomId) };
|
||||
});
|
||||
} catch (e) {
|
||||
// اگر سرویس چت در دسترس نبود، حداقل roomهای ثبتشده در CRM را نشان بده
|
||||
if (tracked.length) {
|
||||
return tracked.map((t) => ({
|
||||
id: t.id,
|
||||
externalRoomId: t.externalRoomId,
|
||||
kind: t.kind === 'group' ? 'group' : 'dm',
|
||||
kind: (t.kind === 'group' ? 'group' : 'dm') as 'dm' | 'group',
|
||||
name: t.name || (t.kind === 'group' ? 'گروه' : 'گفتگو'),
|
||||
unreadCount: 0,
|
||||
lastMessagePreview: null,
|
||||
@@ -213,6 +216,7 @@ export class ConversationsService {
|
||||
? t.dmUserMinId
|
||||
: null
|
||||
: null,
|
||||
pinned: pinnedIds.has(t.externalRoomId),
|
||||
}));
|
||||
}
|
||||
throw e;
|
||||
@@ -443,7 +447,7 @@ export class ConversationsService {
|
||||
let preview = lastMsg
|
||||
? String(lastMsg.content ?? lastMsg.text ?? lastMsg.body ?? '').trim()
|
||||
: '';
|
||||
if (/^__MENTO_HIDE__:/i.test(preview) || /MENTO_DEL/i.test(preview)) {
|
||||
if (/^__MENTO_HIDE__:/i.test(preview) || /MENTO_DEL/i.test(preview) || /^__MENTO_PIN__:/i.test(preview) || /^__MENTO_UNPIN__$/i.test(preview)) {
|
||||
preview = '';
|
||||
}
|
||||
if (lastType === 'image' || lastType === 'photo' || lastType === 'img') {
|
||||
@@ -491,9 +495,134 @@ export class ConversationsService {
|
||||
lastMessagePreview: preview || null,
|
||||
lastMessageAt: lastAtRaw ? String(lastAtRaw) : null,
|
||||
peerUserId,
|
||||
pinned: false,
|
||||
};
|
||||
}
|
||||
|
||||
private async userPinnedRoomIds(organizationId: string, userId: string): Promise<Set<string>> {
|
||||
const rows = await this.prisma.conversationRoomUserPin.findMany({
|
||||
where: { organizationId, userId },
|
||||
select: { externalRoomId: true },
|
||||
take: 500,
|
||||
});
|
||||
return new Set(rows.map((r) => r.externalRoomId));
|
||||
}
|
||||
|
||||
async setRoomPinned(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
externalRoomId: string,
|
||||
pinned: boolean,
|
||||
): Promise<{ externalRoomId: string; pinned: boolean }> {
|
||||
await this.loadAccess(userId, organizationId, role);
|
||||
const roomId = externalRoomId.trim();
|
||||
if (!roomId) throw new BadRequestException('شناسه اتاق خالی است');
|
||||
|
||||
if (pinned) {
|
||||
await this.prisma.conversationRoomUserPin.upsert({
|
||||
where: {
|
||||
organizationId_userId_externalRoomId: {
|
||||
organizationId,
|
||||
userId,
|
||||
externalRoomId: roomId,
|
||||
},
|
||||
},
|
||||
create: { organizationId, userId, externalRoomId: roomId },
|
||||
update: {},
|
||||
});
|
||||
} else {
|
||||
await this.prisma.conversationRoomUserPin.deleteMany({
|
||||
where: { organizationId, userId, externalRoomId: roomId },
|
||||
});
|
||||
}
|
||||
return { externalRoomId: roomId, pinned };
|
||||
}
|
||||
|
||||
async getPinnedMessage(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
externalRoomId: string,
|
||||
): Promise<ConversationPinnedMessageDto | null> {
|
||||
await this.loadAccess(userId, organizationId, role);
|
||||
const roomId = externalRoomId.trim();
|
||||
if (!roomId) throw new BadRequestException('شناسه اتاق خالی است');
|
||||
|
||||
const row = await this.prisma.conversationPinnedMessage.findUnique({
|
||||
where: {
|
||||
organizationId_externalRoomId: { organizationId, externalRoomId: roomId },
|
||||
},
|
||||
});
|
||||
if (!row) return null;
|
||||
return {
|
||||
messageId: row.messageId,
|
||||
preview: row.preview,
|
||||
pinnedByUserId: row.pinnedByUserId,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async pinMessage(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
externalRoomId: string,
|
||||
messageId: string,
|
||||
preview?: string,
|
||||
): Promise<ConversationPinnedMessageDto> {
|
||||
await this.loadAccess(userId, organizationId, role);
|
||||
const roomId = externalRoomId.trim();
|
||||
const msgId = messageId.trim();
|
||||
if (!roomId) throw new BadRequestException('شناسه اتاق خالی است');
|
||||
if (!msgId) throw new BadRequestException('شناسه پیام خالی است');
|
||||
|
||||
const cleanPreview = String(preview || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 240);
|
||||
|
||||
const row = await this.prisma.conversationPinnedMessage.upsert({
|
||||
where: {
|
||||
organizationId_externalRoomId: { organizationId, externalRoomId: roomId },
|
||||
},
|
||||
create: {
|
||||
organizationId,
|
||||
externalRoomId: roomId,
|
||||
messageId: msgId,
|
||||
preview: cleanPreview,
|
||||
pinnedByUserId: userId,
|
||||
},
|
||||
update: {
|
||||
messageId: msgId,
|
||||
preview: cleanPreview,
|
||||
pinnedByUserId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
messageId: row.messageId,
|
||||
preview: row.preview,
|
||||
pinnedByUserId: row.pinnedByUserId,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async unpinMessage(
|
||||
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('شناسه اتاق خالی است');
|
||||
await this.prisma.conversationPinnedMessage.deleteMany({
|
||||
where: { organizationId, externalRoomId: roomId },
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async hideMessages(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
|
||||
@@ -68,3 +68,37 @@ export class HideConversationMessagesDto {
|
||||
@IsBoolean()
|
||||
forEveryone?: boolean;
|
||||
}
|
||||
|
||||
export class PinConversationMessageDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(200)
|
||||
externalRoomId!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(200)
|
||||
messageId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(240)
|
||||
preview?: string;
|
||||
}
|
||||
|
||||
export class UnpinConversationMessageDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(200)
|
||||
externalRoomId!: string;
|
||||
}
|
||||
|
||||
export class PinConversationRoomDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(200)
|
||||
externalRoomId!: string;
|
||||
|
||||
@IsBoolean()
|
||||
pinned!: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user