Edit text and links, mute and unmute chat
This commit is contained in:
@@ -15,6 +15,8 @@ import {
|
||||
RemoveConversationRoomMemberDto,
|
||||
UpdateConversationRoomMemberRoleDto,
|
||||
ConversationTypingDto,
|
||||
MuteConversationRoomDto,
|
||||
ToggleMessageReactionDto,
|
||||
} from './dto';
|
||||
import { ConversationsService } from './conversations.service';
|
||||
|
||||
@@ -186,6 +188,49 @@ export class ConversationsController {
|
||||
);
|
||||
}
|
||||
|
||||
@Post('rooms/mute')
|
||||
muteRoom(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@Body() dto: MuteConversationRoomDto,
|
||||
) {
|
||||
return this.conversations.setRoomMuted(
|
||||
user.userId,
|
||||
user.organizationId,
|
||||
user.role,
|
||||
dto.externalRoomId,
|
||||
dto.muted,
|
||||
dto.allowMentions !== false,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('messages/reactions')
|
||||
listReactions(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@Query('externalRoomId') externalRoomId: string,
|
||||
) {
|
||||
return this.conversations.listMessageReactions(
|
||||
user.userId,
|
||||
user.organizationId,
|
||||
user.role,
|
||||
externalRoomId || '',
|
||||
);
|
||||
}
|
||||
|
||||
@Post('messages/reactions')
|
||||
toggleReaction(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@Body() dto: ToggleMessageReactionDto,
|
||||
) {
|
||||
return this.conversations.toggleMessageReaction(
|
||||
user.userId,
|
||||
user.organizationId,
|
||||
user.role,
|
||||
dto.externalRoomId,
|
||||
dto.messageId,
|
||||
dto.emoji,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('messages/hide')
|
||||
hideMessages(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
|
||||
@@ -312,6 +312,7 @@ export class ConversationsService {
|
||||
});
|
||||
const trackedByExternal = new Map(tracked.map((t) => [t.externalRoomId, t]));
|
||||
const pinnedIds = await this.userPinnedRoomIds(organizationId, userId);
|
||||
const muteMap = await this.userMuteMap(organizationId, userId);
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
where: { userId, organizationId, isActive: true },
|
||||
select: { role: true },
|
||||
@@ -327,9 +328,12 @@ export class ConversationsService {
|
||||
const isRoomAdmin =
|
||||
roomAdminIds.has(dto.externalRoomId) ||
|
||||
trackedByExternal.get(dto.externalRoomId)?.createdByUserId === userId;
|
||||
const mute = muteMap.get(dto.externalRoomId);
|
||||
return {
|
||||
...dto,
|
||||
pinned: pinnedIds.has(dto.externalRoomId),
|
||||
muted: Boolean(mute),
|
||||
muteAllowMentions: mute ? mute.allowMentions : true,
|
||||
canPost:
|
||||
dto.kind !== 'channel' ? true : Boolean(isRoomAdmin || orgAdmin),
|
||||
canManageMembers:
|
||||
@@ -343,6 +347,7 @@ export class ConversationsService {
|
||||
const kind = this.normalizeKind(t.kind);
|
||||
const isRoomAdmin =
|
||||
roomAdminIds.has(t.externalRoomId) || t.createdByUserId === userId;
|
||||
const mute = muteMap.get(t.externalRoomId);
|
||||
return {
|
||||
id: t.id,
|
||||
externalRoomId: t.externalRoomId,
|
||||
@@ -362,6 +367,8 @@ export class ConversationsService {
|
||||
: null
|
||||
: null,
|
||||
pinned: pinnedIds.has(t.externalRoomId),
|
||||
muted: Boolean(mute),
|
||||
muteAllowMentions: mute ? mute.allowMentions : true,
|
||||
canPost: kind !== 'channel' ? true : Boolean(isRoomAdmin || orgAdmin),
|
||||
canManageMembers: kind === 'dm' ? false : Boolean(isRoomAdmin || orgAdmin),
|
||||
};
|
||||
@@ -683,7 +690,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) || /^__MENTO_PIN__:/i.test(preview) || /^__MENTO_UNPIN__$/i.test(preview) || /^__MENTO_TYPING__:/i.test(preview)) {
|
||||
if (/^__MENTO_HIDE__:/i.test(preview) || /MENTO_DEL/i.test(preview) || /^__MENTO_PIN__:/i.test(preview) || /^__MENTO_UNPIN__$/i.test(preview) || /^__MENTO_TYPING__:/i.test(preview) || /^__MENTO_REACT__:/i.test(preview)) {
|
||||
preview = '';
|
||||
}
|
||||
if (lastType === 'image' || lastType === 'photo' || lastType === 'img') {
|
||||
@@ -745,6 +752,18 @@ export class ConversationsService {
|
||||
return new Set(rows.map((r) => r.externalRoomId));
|
||||
}
|
||||
|
||||
private async userMuteMap(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
): Promise<Map<string, { allowMentions: boolean }>> {
|
||||
const rows = await this.prisma.conversationRoomUserMute.findMany({
|
||||
where: { organizationId, userId },
|
||||
select: { externalRoomId: true, allowMentions: true },
|
||||
take: 2000,
|
||||
});
|
||||
return new Map(rows.map((r) => [r.externalRoomId, { allowMentions: r.allowMentions }]));
|
||||
}
|
||||
|
||||
private async userAdminRoomIds(organizationId: string, userId: string): Promise<Set<string>> {
|
||||
const rows = await this.prisma.conversationRoomMember.findMany({
|
||||
where: { organizationId, userId, role: 'admin' },
|
||||
@@ -1048,6 +1067,129 @@ export class ConversationsService {
|
||||
return { externalRoomId: roomId, pinned };
|
||||
}
|
||||
|
||||
async setRoomMuted(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
externalRoomId: string,
|
||||
muted: boolean,
|
||||
allowMentions = true,
|
||||
): Promise<{ externalRoomId: string; muted: boolean; muteAllowMentions: boolean }> {
|
||||
await this.loadAccess(userId, organizationId, role);
|
||||
const roomId = externalRoomId.trim();
|
||||
if (!roomId) throw new BadRequestException('شناسه اتاق خالی است');
|
||||
|
||||
if (muted) {
|
||||
await this.prisma.conversationRoomUserMute.upsert({
|
||||
where: {
|
||||
organizationId_userId_externalRoomId: {
|
||||
organizationId,
|
||||
userId,
|
||||
externalRoomId: roomId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
organizationId,
|
||||
userId,
|
||||
externalRoomId: roomId,
|
||||
allowMentions: allowMentions !== false,
|
||||
},
|
||||
update: { allowMentions: allowMentions !== false },
|
||||
});
|
||||
} else {
|
||||
await this.prisma.conversationRoomUserMute.deleteMany({
|
||||
where: { organizationId, userId, externalRoomId: roomId },
|
||||
});
|
||||
}
|
||||
return {
|
||||
externalRoomId: roomId,
|
||||
muted,
|
||||
muteAllowMentions: muted ? allowMentions !== false : true,
|
||||
};
|
||||
}
|
||||
|
||||
async listMessageReactions(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
externalRoomId: string,
|
||||
): Promise<
|
||||
Array<{ messageId: string; emoji: string; count: number; me: boolean }>
|
||||
> {
|
||||
await this.loadAccess(userId, organizationId, role);
|
||||
const roomId = externalRoomId.trim();
|
||||
if (!roomId) return [];
|
||||
const rows = await this.prisma.conversationMessageReaction.findMany({
|
||||
where: { organizationId, externalRoomId: roomId },
|
||||
select: { messageId: true, emoji: true, userId: true },
|
||||
take: 5000,
|
||||
});
|
||||
const map = new Map<string, { messageId: string; emoji: string; count: number; me: boolean }>();
|
||||
for (const r of rows) {
|
||||
const key = `${r.messageId}\0${r.emoji}`;
|
||||
const cur = map.get(key) || {
|
||||
messageId: r.messageId,
|
||||
emoji: r.emoji,
|
||||
count: 0,
|
||||
me: false,
|
||||
};
|
||||
cur.count += 1;
|
||||
if (r.userId === userId) cur.me = true;
|
||||
map.set(key, cur);
|
||||
}
|
||||
return [...map.values()];
|
||||
}
|
||||
|
||||
async toggleMessageReaction(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
externalRoomId: string,
|
||||
messageId: string,
|
||||
emoji: string,
|
||||
): Promise<{
|
||||
added: boolean;
|
||||
reactions: Array<{ messageId: string; emoji: string; count: number; me: boolean }>;
|
||||
}> {
|
||||
await this.loadAccess(userId, organizationId, role);
|
||||
const roomId = externalRoomId.trim();
|
||||
const mid = messageId.trim();
|
||||
const em = emoji.trim().slice(0, 16);
|
||||
if (!roomId || !mid || !em) throw new BadRequestException('پارامتر واکنش ناقص است');
|
||||
|
||||
const existing = await this.prisma.conversationMessageReaction.findFirst({
|
||||
where: {
|
||||
organizationId,
|
||||
externalRoomId: roomId,
|
||||
messageId: mid,
|
||||
userId,
|
||||
emoji: em,
|
||||
},
|
||||
});
|
||||
let added = false;
|
||||
if (existing) {
|
||||
await this.prisma.conversationMessageReaction.delete({ where: { id: existing.id } });
|
||||
} else {
|
||||
await this.prisma.conversationMessageReaction.create({
|
||||
data: {
|
||||
organizationId,
|
||||
externalRoomId: roomId,
|
||||
messageId: mid,
|
||||
userId,
|
||||
emoji: em,
|
||||
},
|
||||
});
|
||||
added = true;
|
||||
}
|
||||
const reactions = await this.listMessageReactions(
|
||||
userId,
|
||||
organizationId,
|
||||
role,
|
||||
roomId,
|
||||
);
|
||||
return { added, reactions };
|
||||
}
|
||||
|
||||
async getPinnedMessage(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
|
||||
@@ -169,3 +169,34 @@ export class ConversationTypingDto {
|
||||
@MaxLength(200)
|
||||
externalRoomId!: string;
|
||||
}
|
||||
|
||||
export class MuteConversationRoomDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(200)
|
||||
externalRoomId!: string;
|
||||
|
||||
@IsBoolean()
|
||||
muted!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
allowMentions?: boolean;
|
||||
}
|
||||
|
||||
export class ToggleMessageReactionDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(200)
|
||||
externalRoomId!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(200)
|
||||
messageId!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(16)
|
||||
emoji!: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user