Add channel to conversation
This commit is contained in:
@@ -4,6 +4,7 @@ import { sanitizePermissions } from '../org-roles/role-defaults';
|
||||
export type ConversationAccess = {
|
||||
canUse: boolean;
|
||||
canCreateGroup: boolean;
|
||||
canCreateChannel: boolean;
|
||||
};
|
||||
|
||||
export function resolveConversationAccess(
|
||||
@@ -11,11 +12,14 @@ export function resolveConversationAccess(
|
||||
orgRolePermissions: unknown,
|
||||
): ConversationAccess {
|
||||
if (membershipRole === 'owner' || membershipRole === 'admin') {
|
||||
return { canUse: true, canCreateGroup: true };
|
||||
return { canUse: true, canCreateGroup: true, canCreateChannel: true };
|
||||
}
|
||||
const permissions = sanitizePermissions(orgRolePermissions) as RolePermissionKey[];
|
||||
const canUse = permissions.includes('conversations');
|
||||
const canCreateGroup = permissions.includes('conversations_groups');
|
||||
return {
|
||||
canUse: permissions.includes('conversations'),
|
||||
canCreateGroup: permissions.includes('conversations_groups'),
|
||||
canUse,
|
||||
canCreateGroup,
|
||||
canCreateChannel: canCreateGroup,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { OrgModuleGuard } from '../billing/org-module.guard';
|
||||
import {
|
||||
CreateConversationDmDto,
|
||||
CreateConversationGroupDto,
|
||||
CreateConversationChannelDto,
|
||||
HideConversationMessagesDto,
|
||||
PinConversationMessageDto,
|
||||
PinConversationRoomDto,
|
||||
@@ -66,6 +67,20 @@ export class ConversationsController {
|
||||
);
|
||||
}
|
||||
|
||||
@Post('rooms/channel')
|
||||
createChannel(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
@Body() dto: CreateConversationChannelDto,
|
||||
) {
|
||||
return this.conversations.createChannel(
|
||||
user.userId,
|
||||
user.organizationId,
|
||||
user.role,
|
||||
dto.name,
|
||||
dto.memberUserIds,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('rooms/pin')
|
||||
pinRoom(
|
||||
@CurrentUser() user: { userId: string; organizationId: string; role: string },
|
||||
|
||||
@@ -190,34 +190,45 @@ export class ConversationsService {
|
||||
});
|
||||
const trackedByExternal = new Map(tracked.map((t) => [t.externalRoomId, t]));
|
||||
const pinnedIds = await this.userPinnedRoomIds(organizationId, userId);
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
where: { userId, organizationId, isActive: true },
|
||||
select: { role: true },
|
||||
});
|
||||
const effectiveRole = String(membership?.role ?? role);
|
||||
|
||||
try {
|
||||
const external = await this.chat.listUserRooms(creds.host, creds.apiKey, chatUserId);
|
||||
return external.map((row) => {
|
||||
const dto = this.toRoomDto(row, trackedByExternal, organizationId, userId);
|
||||
const dto = this.toRoomDto(row, trackedByExternal, organizationId, userId, effectiveRole);
|
||||
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') as 'dm' | 'group',
|
||||
name: t.name || (t.kind === 'group' ? 'گروه' : 'گفتگو'),
|
||||
unreadCount: 0,
|
||||
lastMessagePreview: null,
|
||||
lastMessageAt: null,
|
||||
peerUserId:
|
||||
t.kind === 'dm'
|
||||
? t.dmUserMinId === userId
|
||||
? t.dmUserMaxId
|
||||
: t.dmUserMaxId === userId
|
||||
? t.dmUserMinId
|
||||
: null
|
||||
: null,
|
||||
pinned: pinnedIds.has(t.externalRoomId),
|
||||
}));
|
||||
return tracked.map((t) => {
|
||||
const kind = this.normalizeKind(t.kind);
|
||||
return {
|
||||
id: t.id,
|
||||
externalRoomId: t.externalRoomId,
|
||||
kind,
|
||||
name:
|
||||
t.name ||
|
||||
(kind === 'channel' ? 'کانال' : kind === 'group' ? 'گروه' : 'گفتگو'),
|
||||
unreadCount: 0,
|
||||
lastMessagePreview: null,
|
||||
lastMessageAt: null,
|
||||
peerUserId:
|
||||
kind === 'dm'
|
||||
? t.dmUserMinId === userId
|
||||
? t.dmUserMaxId
|
||||
: t.dmUserMaxId === userId
|
||||
? t.dmUserMinId
|
||||
: null
|
||||
: null,
|
||||
pinned: pinnedIds.has(t.externalRoomId),
|
||||
canPost: this.canPostInRoom(kind, t.createdByUserId, userId, effectiveRole),
|
||||
};
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@@ -353,6 +364,69 @@ export class ConversationsService {
|
||||
lastMessagePreview: null,
|
||||
lastMessageAt: null,
|
||||
peerUserId: null,
|
||||
canPost: true,
|
||||
};
|
||||
}
|
||||
|
||||
async createChannel(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
role: string,
|
||||
name: string,
|
||||
memberUserIds: string[],
|
||||
): Promise<ConversationRoomDto> {
|
||||
const access = await this.loadAccess(userId, organizationId, role);
|
||||
if (!access.canCreateChannel) {
|
||||
throw new ForbiddenException('اجازه ساخت کانال را ندارید');
|
||||
}
|
||||
|
||||
const uniqueIds = [...new Set(memberUserIds.filter((id) => id && id !== userId))];
|
||||
const memberships = await this.prisma.membership.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
userId: { in: uniqueIds },
|
||||
isActive: true,
|
||||
user: { isActive: true },
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
if (memberships.length !== uniqueIds.length) {
|
||||
throw new BadRequestException('برخی اعضای کانال در این سازمان فعال نیستند');
|
||||
}
|
||||
|
||||
const creds = await this.getCredentials(organizationId);
|
||||
const members = [
|
||||
buildChatUserId(organizationId, userId),
|
||||
...uniqueIds.map((id) => buildChatUserId(organizationId, id)),
|
||||
];
|
||||
const trimmedName = name.trim();
|
||||
// سرویس خارجی معمولاً CHANNEL ندارد؛ بهصورت GROUP ساخته و در CRM بهعنوان کانال یکطرفه نگه داشته میشود
|
||||
const created = await this.chat.createRoom(creds.host, creds.apiKey, {
|
||||
name: trimmedName,
|
||||
type: 'GROUP',
|
||||
members,
|
||||
});
|
||||
|
||||
const row = await this.prisma.conversationRoom.create({
|
||||
data: {
|
||||
organizationId,
|
||||
externalRoomId: created.room_id,
|
||||
kind: 'channel',
|
||||
name: trimmedName,
|
||||
createdByUserId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
externalRoomId: row.externalRoomId,
|
||||
kind: 'channel',
|
||||
name: trimmedName,
|
||||
unreadCount: 0,
|
||||
lastMessagePreview: null,
|
||||
lastMessageAt: null,
|
||||
peerUserId: null,
|
||||
canPost: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -422,6 +496,23 @@ export class ConversationsService {
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeKind(raw: string | null | undefined): 'dm' | 'group' | 'channel' {
|
||||
if (raw === 'channel') return 'channel';
|
||||
if (raw === 'dm') return 'dm';
|
||||
return 'group';
|
||||
}
|
||||
|
||||
private canPostInRoom(
|
||||
kind: 'dm' | 'group' | 'channel',
|
||||
createdByUserId: string | null | undefined,
|
||||
currentUserId: string,
|
||||
role: string,
|
||||
): boolean {
|
||||
if (kind !== 'channel') return true;
|
||||
if (createdByUserId && createdByUserId === currentUserId) return true;
|
||||
return role === 'owner' || role === 'admin';
|
||||
}
|
||||
|
||||
private toRoomDto(
|
||||
row: ExternalRoomRow,
|
||||
trackedByExternal: Map<
|
||||
@@ -431,14 +522,16 @@ export class ConversationsService {
|
||||
name: string | null;
|
||||
dmUserMinId: string | null;
|
||||
dmUserMaxId: string | null;
|
||||
createdByUserId: string | null;
|
||||
}
|
||||
>,
|
||||
organizationId: string,
|
||||
currentUserId: string,
|
||||
role = '',
|
||||
): ConversationRoomDto {
|
||||
const externalRoomId = String(row.id ?? row.room_id ?? '').trim();
|
||||
const tracked = trackedByExternal.get(externalRoomId);
|
||||
const kind = tracked?.kind === 'group' ? 'group' : tracked?.kind === 'dm' ? 'dm' : 'group';
|
||||
const kind = this.normalizeKind(tracked?.kind);
|
||||
|
||||
const lastMsg = (row.last_message ?? row.lastMessage) as Record<string, unknown> | undefined;
|
||||
const lastType = String(lastMsg?.type_content ?? lastMsg?.type ?? 'text')
|
||||
@@ -478,7 +571,7 @@ export class ConversationsService {
|
||||
|
||||
let name =
|
||||
String(tracked?.name ?? row.name ?? row.title ?? '').trim() ||
|
||||
(kind === 'group' ? 'گروه' : 'گفتگو');
|
||||
(kind === 'channel' ? 'کانال' : kind === 'group' ? 'گروه' : 'گفتگو');
|
||||
|
||||
let peerUserId: string | null = null;
|
||||
if (tracked?.kind === 'dm' && tracked.dmUserMinId && tracked.dmUserMaxId) {
|
||||
@@ -489,13 +582,14 @@ export class ConversationsService {
|
||||
return {
|
||||
id: externalRoomId,
|
||||
externalRoomId,
|
||||
kind: kind as 'dm' | 'group',
|
||||
kind,
|
||||
name,
|
||||
unreadCount: Number.isFinite(unread) ? Math.max(0, unread) : 0,
|
||||
lastMessagePreview: preview || null,
|
||||
lastMessageAt: lastAtRaw ? String(lastAtRaw) : null,
|
||||
peerUserId,
|
||||
pinned: false,
|
||||
canPost: this.canPostInRoom(kind, tracked?.createdByUserId, currentUserId, role),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,19 @@ export class CreateConversationGroupDto {
|
||||
memberUserIds!: string[];
|
||||
}
|
||||
|
||||
export class CreateConversationChannelDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(120)
|
||||
name!: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ArrayMaxSize(100)
|
||||
@IsString({ each: true })
|
||||
memberUserIds!: string[];
|
||||
}
|
||||
|
||||
export class HideConversationMessagesDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
|
||||
@@ -40,8 +40,11 @@ const activeRoom = ref<ConversationRoomDto | null>(null);
|
||||
const draft = ref('');
|
||||
const showNewDm = ref(false);
|
||||
const showNewGroup = ref(false);
|
||||
const showNewChannel = ref(false);
|
||||
const groupName = ref('');
|
||||
const groupMemberIds = ref<string[]>([]);
|
||||
const channelName = ref('');
|
||||
const channelMemberIds = ref<string[]>([]);
|
||||
const dmPeerId = ref('');
|
||||
const chatUserId = ref('');
|
||||
const pendingAttaches = ref<PendingAttach[]>([]);
|
||||
@@ -96,7 +99,7 @@ const selectedIds = ref<string[]>([]);
|
||||
const showForward = ref(false);
|
||||
const forwardBusy = ref(false);
|
||||
const roomListQuery = ref('');
|
||||
const roomListTab = ref<'all' | 'dm' | 'group'>('all');
|
||||
const roomListTab = ref<'all' | 'dm' | 'group' | 'channel'>('all');
|
||||
const pinnedMessage = ref<ConversationPinnedMessageDto | null>(null);
|
||||
const chatSearchOpen = ref(false);
|
||||
const chatSearchQuery = ref('');
|
||||
@@ -105,6 +108,15 @@ const mentionState = ref<{ start: number; end: number; query: string } | null>(n
|
||||
const mentionIndex = ref(0);
|
||||
|
||||
const canCreateGroup = computed(() => access.value?.canCreateGroup ?? false);
|
||||
const canCreateChannel = computed(
|
||||
() => access.value?.canCreateChannel ?? access.value?.canCreateGroup ?? false,
|
||||
);
|
||||
const canPostInActive = computed(() => {
|
||||
const room = activeRoom.value;
|
||||
if (!room) return false;
|
||||
if (room.kind !== 'channel') return true;
|
||||
return room.canPost !== false;
|
||||
});
|
||||
|
||||
async function scrollToBottom() {
|
||||
await nextTick();
|
||||
@@ -129,6 +141,7 @@ const filteredRooms = computed(() => {
|
||||
return sortedRooms.value.filter((room) => {
|
||||
if (roomListTab.value === 'dm' && room.kind !== 'dm') return false;
|
||||
if (roomListTab.value === 'group' && room.kind !== 'group') return false;
|
||||
if (roomListTab.value === 'channel' && room.kind !== 'channel') return false;
|
||||
if (!q) return true;
|
||||
const title = roomTitle(room).toLowerCase();
|
||||
const preview = (room.lastMessagePreview || '').toLowerCase();
|
||||
@@ -164,11 +177,17 @@ const activeSearchHitId = computed(() => {
|
||||
});
|
||||
|
||||
function roomTitle(room: ConversationRoomDto) {
|
||||
if (room.kind === 'group') return room.name;
|
||||
if (room.kind === 'group' || room.kind === 'channel') return room.name;
|
||||
const peer = peers.value.find((p) => p.id === room.peerUserId);
|
||||
return peer?.fullName ?? room.name;
|
||||
}
|
||||
|
||||
function roomKindLabel(kind: ConversationRoomDto['kind']) {
|
||||
if (kind === 'channel') return 'کانال';
|
||||
if (kind === 'group') return 'گروه';
|
||||
return 'خصوصی';
|
||||
}
|
||||
|
||||
type TextSeg = { text: string; mention?: boolean; hit?: boolean };
|
||||
|
||||
function messageTextSegments(content: string, highlightQuery = ''): TextSeg[] {
|
||||
@@ -941,6 +960,10 @@ async function openRoom(room: ConversationRoomDto) {
|
||||
|
||||
async function submitMessage() {
|
||||
if (!activeRoom.value) return;
|
||||
if (!canPostInActive.value) {
|
||||
error.value = 'فقط مدیران کانال میتوانند پیام بفرستند.';
|
||||
return;
|
||||
}
|
||||
if (sending.value || recording.value) return;
|
||||
const text = draft.value.trim();
|
||||
const attaches = [...pendingAttaches.value];
|
||||
@@ -1034,6 +1057,27 @@ async function startGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
async function startChannel() {
|
||||
if (!channelName.value.trim() || !channelMemberIds.value.length) return;
|
||||
try {
|
||||
const { data } = await api.post<ConversationRoomDto>('/conversations/rooms/channel', {
|
||||
name: channelName.value.trim(),
|
||||
memberUserIds: channelMemberIds.value,
|
||||
});
|
||||
rooms.value = [{ ...data, canPost: true }, ...rooms.value];
|
||||
showNewChannel.value = false;
|
||||
channelName.value = '';
|
||||
channelMemberIds.value = [];
|
||||
await openRoom({ ...data, canPost: true });
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e) && e.response?.status === 403) {
|
||||
error.value = 'اجازه ساخت کانال را ندارید.';
|
||||
} else {
|
||||
error.value = 'ساخت کانال ناموفق بود.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleGroupMember(id: string) {
|
||||
if (groupMemberIds.value.includes(id)) {
|
||||
groupMemberIds.value = groupMemberIds.value.filter((x) => x !== id);
|
||||
@@ -1042,6 +1086,14 @@ function toggleGroupMember(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleChannelMember(id: string) {
|
||||
if (channelMemberIds.value.includes(id)) {
|
||||
channelMemberIds.value = channelMemberIds.value.filter((x) => x !== id);
|
||||
} else {
|
||||
channelMemberIds.value = [...channelMemberIds.value, id];
|
||||
}
|
||||
}
|
||||
|
||||
watch(activeRoom, () => {
|
||||
draft.value = '';
|
||||
clearPendingAttaches();
|
||||
@@ -1134,6 +1186,14 @@ onUnmounted(() => {
|
||||
>
|
||||
گروه
|
||||
</button>
|
||||
<button
|
||||
v-if="canCreateChannel"
|
||||
type="button"
|
||||
class="fab ghost"
|
||||
@click="showNewChannel = true"
|
||||
>
|
||||
کانال
|
||||
</button>
|
||||
</div>
|
||||
<div class="rail-filters">
|
||||
<label class="rail-search">
|
||||
@@ -1173,6 +1233,15 @@ onUnmounted(() => {
|
||||
>
|
||||
گروه
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="roomListTab === 'channel'"
|
||||
:class="{ active: roomListTab === 'channel' }"
|
||||
@click="roomListTab = 'channel'"
|
||||
>
|
||||
کانال
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="room-list">
|
||||
@@ -1295,7 +1364,9 @@ onUnmounted(() => {
|
||||
</span>
|
||||
<div class="stage-titles">
|
||||
<h2>{{ roomTitle(activeRoom) }}</h2>
|
||||
<span class="kind-tag">{{ activeRoom.kind === 'group' ? 'گروه' : 'خصوصی' }}</span>
|
||||
<span class="kind-tag" :data-kind="activeRoom.kind">{{
|
||||
roomKindLabel(activeRoom.kind)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="stage-tools">
|
||||
<button type="button" class="select-act" title="جستجو" @click="openChatSearch">
|
||||
@@ -1470,7 +1541,11 @@ onUnmounted(() => {
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form class="composer" @submit.prevent="submitMessage">
|
||||
<div v-if="!canPostInActive" class="channel-readonly">
|
||||
<UiIcon name="megaphone" :size="18" />
|
||||
<span>فقط مدیران کانال میتوانند پیام بفرستند</span>
|
||||
</div>
|
||||
<form v-else class="composer" @submit.prevent="submitMessage">
|
||||
<div v-if="mentionState && mentionCandidates.length" class="mention-sheet">
|
||||
<button
|
||||
v-for="(peer, idx) in mentionCandidates"
|
||||
@@ -1699,6 +1774,29 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog v-if="showNewChannel" class="modal" open @click.self="showNewChannel = false">
|
||||
<div class="modal-card wide">
|
||||
<h3>کانال جدید</h3>
|
||||
<p class="hint">کانال یکطرفه است؛ فقط شما و مدیران سازمان میتوانند پیام بفرستند.</p>
|
||||
<input v-model="channelName" type="text" placeholder="نام کانال" />
|
||||
<p class="hint">اعضا (مشاهدهکنندگان)</p>
|
||||
<div class="member-grid">
|
||||
<label v-for="p in peers" :key="p.id" class="member-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="channelMemberIds.includes(p.id)"
|
||||
@change="toggleChannelMember(p.id)"
|
||||
/>
|
||||
{{ p.fullName }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" @click="showNewChannel = false">انصراف</button>
|
||||
<button type="button" class="primary" @click="startChannel">ساخت کانال</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog v-if="showForward" class="modal" open @click.self="showForward = false">
|
||||
<div class="modal-card">
|
||||
<h3>ارسال برای دیگری</h3>
|
||||
@@ -1826,7 +1924,8 @@ onUnmounted(() => {
|
||||
}
|
||||
.rail-actions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
gap: 0.35rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.rail-filters {
|
||||
display: flex;
|
||||
@@ -1859,8 +1958,8 @@ onUnmounted(() => {
|
||||
}
|
||||
.rail-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.25rem;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.2rem;
|
||||
padding: 0.2rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
@@ -1869,9 +1968,9 @@ onUnmounted(() => {
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 9px;
|
||||
padding: 0.35rem 0.25rem;
|
||||
padding: 0.35rem 0.15rem;
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
@@ -2231,6 +2330,33 @@ onUnmounted(() => {
|
||||
color: #6d28d9;
|
||||
width: fit-content;
|
||||
}
|
||||
.kind-tag[data-kind='dm'] {
|
||||
background: rgba(13, 148, 136, 0.12);
|
||||
color: #0f766e;
|
||||
}
|
||||
.kind-tag[data-kind='group'] {
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
color: #4338ca;
|
||||
}
|
||||
.kind-tag[data-kind='channel'] {
|
||||
background: rgba(245, 158, 11, 0.16);
|
||||
color: #b45309;
|
||||
font-weight: 700;
|
||||
}
|
||||
.channel-readonly {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.85rem 1rem calc(0.85rem + env(safe-area-inset-bottom, 0px));
|
||||
border-top: 1px solid var(--line);
|
||||
background: #fff7ed;
|
||||
color: #9a3412;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.msg-stream {
|
||||
flex: 1;
|
||||
list-style: none;
|
||||
|
||||
@@ -3946,6 +3946,8 @@ export interface UpdateConversationSettingsRequest {
|
||||
export interface ConversationAccessDto {
|
||||
canUse: boolean;
|
||||
canCreateGroup: boolean;
|
||||
/** ساخت کانال — فعلاً همان مجوز ساخت گروه */
|
||||
canCreateChannel: boolean;
|
||||
}
|
||||
|
||||
export interface ConversationSessionDto {
|
||||
@@ -3966,7 +3968,7 @@ export interface ConversationPeerDto {
|
||||
export interface ConversationRoomDto {
|
||||
id: string;
|
||||
externalRoomId: string;
|
||||
kind: "dm" | "group";
|
||||
kind: "dm" | "group" | "channel";
|
||||
name: string;
|
||||
unreadCount: number;
|
||||
lastMessagePreview: string | null;
|
||||
@@ -3974,6 +3976,8 @@ export interface ConversationRoomDto {
|
||||
peerUserId: string | null;
|
||||
/** سنجاق در لیست گفتگو — فقط برای کاربر جاری */
|
||||
pinned?: boolean;
|
||||
/** در کانال: آیا کاربر جاری میتواند پیام بفرستد */
|
||||
canPost?: boolean;
|
||||
}
|
||||
|
||||
export interface ConversationPinnedMessageDto {
|
||||
|
||||
Reference in New Issue
Block a user