From 39e2eb24a616dfd08abd9c6951846a9bac26a3bc Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sun, 4 Oct 2026 12:12:05 +0330 Subject: [PATCH] Keep desktop modals open on outside click and allow larger group member lists. Co-authored-by: Cursor --- .../conversations/conversations.service.ts | 48 ++++++++-- apps/web/src/components/ui/UiModal.vue | 34 +++++-- apps/web/src/views/ConversationsView.vue | 96 ++++++++++++++++--- 3 files changed, 148 insertions(+), 30 deletions(-) diff --git a/apps/api/src/conversations/conversations.service.ts b/apps/api/src/conversations/conversations.service.ts index caa1e17..b39c923 100644 --- a/apps/api/src/conversations/conversations.service.ts +++ b/apps/api/src/conversations/conversations.service.ts @@ -40,6 +40,34 @@ export class ConversationsService { return `${organizationId}|${roomId}|${userId}`; } + /** + * سرویس خارجی چت اغلب در create بیش از ~۶ عضو را رد می‌کند. + * ابتدا با سقف کوچک می‌سازیم و بقیه را با add_members اضافه می‌کنیم. + */ + private async createExternalRoomChunked( + host: string, + apiKey: string, + name: string, + type: string, + members: string[], + ): Promise<{ room_id: string }> { + const CREATE_CAP = 6; + const ADD_CHUNK = 6; + const unique = [...new Set(members.filter(Boolean))]; + const head = unique.slice(0, CREATE_CAP); + const rest = unique.slice(CREATE_CAP); + const created = await this.chat.createRoom(host, apiKey, { + name, + type, + members: head.length ? head : unique.slice(0, 1), + }); + for (let i = 0; i < rest.length; i += ADD_CHUNK) { + const chunk = rest.slice(i, i + ADD_CHUNK); + await this.chat.addRoomMembers(host, apiKey, created.room_id, chunk); + } + return created; + } + private sweepTypingPresence(now = Date.now()) { if (now - this.typingSweepAt < 10_000) return; this.typingSweepAt = now; @@ -483,11 +511,13 @@ export class ConversationsService { ...uniqueIds.map((id) => buildChatUserId(organizationId, id)), ]; const trimmedName = name.trim(); - const created = await this.chat.createRoom(creds.host, creds.apiKey, { - name: trimmedName, - type: 'GROUP', + const created = await this.createExternalRoomChunked( + creds.host, + creds.apiKey, + trimmedName, + 'GROUP', members, - }); + ); const row = await this.prisma.conversationRoom.create({ data: { @@ -548,11 +578,13 @@ export class ConversationsService { ]; const trimmedName = name.trim(); // سرویس خارجی معمولاً CHANNEL ندارد؛ به‌صورت GROUP ساخته و در CRM به‌عنوان کانال یک‌طرفه نگه داشته می‌شود - const created = await this.chat.createRoom(creds.host, creds.apiKey, { - name: trimmedName, - type: 'GROUP', + const created = await this.createExternalRoomChunked( + creds.host, + creds.apiKey, + trimmedName, + 'GROUP', members, - }); + ); const row = await this.prisma.conversationRoom.create({ data: { diff --git a/apps/web/src/components/ui/UiModal.vue b/apps/web/src/components/ui/UiModal.vue index 658665e..0c0e9ee 100644 --- a/apps/web/src/components/ui/UiModal.vue +++ b/apps/web/src/components/ui/UiModal.vue @@ -2,12 +2,23 @@ import { onUnmounted, watch } from 'vue'; import UiIcon from '@/components/UiIcon.vue'; -const props = defineProps<{ - open: boolean; - title: string; - /** برای مودال روی مودال (مثل انتخاب مخاطب داخل فرم) */ - nest?: boolean; -}>(); +const props = withDefaults( + defineProps<{ + open: boolean; + title: string; + /** برای مودال روی مودال (مثل انتخاب مخاطب داخل فرم) */ + nest?: boolean; + /** + * بستن با کلیک روی پس‌زمینه. + * پیش‌فرض: فقط موبایل (تمام‌صفحه). در دسکتاپ عمداً بسته نمی‌شود. + */ + closeOnBackdrop?: boolean | 'mobile'; + }>(), + { + nest: false, + closeOnBackdrop: 'mobile', + }, +); const emit = defineEmits<{ close: []; @@ -24,8 +35,17 @@ onUnmounted(() => { document.body.style.overflow = ''; }); +function isMobileViewport() { + return typeof window !== 'undefined' && window.matchMedia('(max-width: 900px)').matches; +} + function onBackdrop(event: MouseEvent) { - if (event.target === event.currentTarget) emit('close'); + if (event.target !== event.currentTarget) return; + const mode = props.closeOnBackdrop; + if (mode === false) return; + if (mode === true || (mode === 'mobile' && isMobileViewport())) { + emit('close'); + } } diff --git a/apps/web/src/views/ConversationsView.vue b/apps/web/src/views/ConversationsView.vue index 88a11e6..d3272a5 100644 --- a/apps/web/src/views/ConversationsView.vue +++ b/apps/web/src/views/ConversationsView.vue @@ -1575,8 +1575,17 @@ async function startGroup() { groupMemberIds.value = []; await openRoom(data); } catch (e) { - if (axios.isAxiosError(e) && e.response?.status === 403) { - error.value = 'اجازه ساخت گروه را ندارید.'; + if (axios.isAxiosError(e)) { + const msg = (e.response?.data as { message?: string | string[] })?.message; + if (e.response?.status === 403) { + error.value = 'اجازه ساخت گروه را ندارید.'; + } else if (Array.isArray(msg)) { + error.value = msg.join('، '); + } else if (typeof msg === 'string' && msg.trim()) { + error.value = msg; + } else { + error.value = 'ساخت گروه ناموفق بود.'; + } } else { error.value = 'ساخت گروه ناموفق بود.'; } @@ -1620,6 +1629,13 @@ function toggleChannelMember(id: string) { } } +/** در دسکتاپ با کلیک بیرون مودال بسته نشود (انتخاب اعضا قطع نشود) */ +function closeModalOnBackdrop(close: () => void) { + if (typeof window !== 'undefined' && window.matchMedia('(max-width: 900px)').matches) { + close(); + } +} + function openNewDm() { peerPickerQuery.value = ''; dmPeerId.value = ''; @@ -2478,7 +2494,12 @@ onUnmounted(() => { - + - -