Keep desktop modals open on outside click and allow larger group member lists.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-04 12:12:05 +03:30
co-authored by Cursor
parent 8b37525f2f
commit 39e2eb24a6
3 changed files with 148 additions and 30 deletions
@@ -40,6 +40,34 @@ export class ConversationsService {
return `${organizationId}|${roomId}|${userId}`; 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()) { private sweepTypingPresence(now = Date.now()) {
if (now - this.typingSweepAt < 10_000) return; if (now - this.typingSweepAt < 10_000) return;
this.typingSweepAt = now; this.typingSweepAt = now;
@@ -483,11 +511,13 @@ export class ConversationsService {
...uniqueIds.map((id) => buildChatUserId(organizationId, id)), ...uniqueIds.map((id) => buildChatUserId(organizationId, id)),
]; ];
const trimmedName = name.trim(); const trimmedName = name.trim();
const created = await this.chat.createRoom(creds.host, creds.apiKey, { const created = await this.createExternalRoomChunked(
name: trimmedName, creds.host,
type: 'GROUP', creds.apiKey,
trimmedName,
'GROUP',
members, members,
}); );
const row = await this.prisma.conversationRoom.create({ const row = await this.prisma.conversationRoom.create({
data: { data: {
@@ -548,11 +578,13 @@ export class ConversationsService {
]; ];
const trimmedName = name.trim(); const trimmedName = name.trim();
// سرویس خارجی معمولاً CHANNEL ندارد؛ به‌صورت GROUP ساخته و در CRM به‌عنوان کانال یک‌طرفه نگه داشته می‌شود // سرویس خارجی معمولاً CHANNEL ندارد؛ به‌صورت GROUP ساخته و در CRM به‌عنوان کانال یک‌طرفه نگه داشته می‌شود
const created = await this.chat.createRoom(creds.host, creds.apiKey, { const created = await this.createExternalRoomChunked(
name: trimmedName, creds.host,
type: 'GROUP', creds.apiKey,
trimmedName,
'GROUP',
members, members,
}); );
const row = await this.prisma.conversationRoom.create({ const row = await this.prisma.conversationRoom.create({
data: { data: {
+27 -7
View File
@@ -2,12 +2,23 @@
import { onUnmounted, watch } from 'vue'; import { onUnmounted, watch } from 'vue';
import UiIcon from '@/components/UiIcon.vue'; import UiIcon from '@/components/UiIcon.vue';
const props = defineProps<{ const props = withDefaults(
open: boolean; defineProps<{
title: string; open: boolean;
/** برای مودال روی مودال (مثل انتخاب مخاطب داخل فرم) */ title: string;
nest?: boolean; /** برای مودال روی مودال (مثل انتخاب مخاطب داخل فرم) */
}>(); nest?: boolean;
/**
* بستن با کلیک روی پس‌زمینه.
* پیش‌فرض: فقط موبایل (تمام‌صفحه). در دسکتاپ عمداً بسته نمی‌شود.
*/
closeOnBackdrop?: boolean | 'mobile';
}>(),
{
nest: false,
closeOnBackdrop: 'mobile',
},
);
const emit = defineEmits<{ const emit = defineEmits<{
close: []; close: [];
@@ -24,8 +35,17 @@ onUnmounted(() => {
document.body.style.overflow = ''; document.body.style.overflow = '';
}); });
function isMobileViewport() {
return typeof window !== 'undefined' && window.matchMedia('(max-width: 900px)').matches;
}
function onBackdrop(event: MouseEvent) { 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');
}
} }
</script> </script>
+81 -15
View File
@@ -1575,8 +1575,17 @@ async function startGroup() {
groupMemberIds.value = []; groupMemberIds.value = [];
await openRoom(data); await openRoom(data);
} catch (e) { } catch (e) {
if (axios.isAxiosError(e) && e.response?.status === 403) { if (axios.isAxiosError(e)) {
error.value = 'اجازه ساخت گروه را ندارید.'; 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 { } else {
error.value = 'ساخت گروه ناموفق بود.'; 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() { function openNewDm() {
peerPickerQuery.value = ''; peerPickerQuery.value = '';
dmPeerId.value = ''; dmPeerId.value = '';
@@ -2478,7 +2494,12 @@ onUnmounted(() => {
</section> </section>
</div> </div>
<dialog v-if="showNewDm" class="modal" open @click.self="showNewDm = false"> <dialog
v-if="showNewDm"
class="modal"
open
@click.self="closeModalOnBackdrop(() => (showNewDm = false))"
>
<div class="modal-card wide"> <div class="modal-card wide">
<h3>گفتگوی جدید</h3> <h3>گفتگوی جدید</h3>
<p class="hint">همکار موردنظر را انتخاب کنید</p> <p class="hint">همکار موردنظر را انتخاب کنید</p>
@@ -2518,8 +2539,13 @@ onUnmounted(() => {
</div> </div>
</dialog> </dialog>
<dialog v-if="showNewGroup" class="modal" open @click.self="showNewGroup = false"> <dialog
<div class="modal-card wide"> v-if="showNewGroup"
class="modal"
open
@click.self="closeModalOnBackdrop(() => (showNewGroup = false))"
>
<div class="modal-card wide member-picker-modal">
<h3>گروه جدید</h3> <h3>گروه جدید</h3>
<input v-model="groupName" type="text" placeholder="نام گروه" /> <input v-model="groupName" type="text" placeholder="نام گروه" />
<div class="picker-head"> <div class="picker-head">
@@ -2585,8 +2611,13 @@ onUnmounted(() => {
</div> </div>
</dialog> </dialog>
<dialog v-if="showNewChannel" class="modal" open @click.self="showNewChannel = false"> <dialog
<div class="modal-card wide"> v-if="showNewChannel"
class="modal"
open
@click.self="closeModalOnBackdrop(() => (showNewChannel = false))"
>
<div class="modal-card wide member-picker-modal">
<h3>کانال جدید</h3> <h3>کانال جدید</h3>
<p class="hint channel-hint"> <p class="hint channel-hint">
کانال یک‌طرفه است؛ فقط شما و مدیران سازمان می‌توانند پیام بفرستند. کانال یک‌طرفه است؛ فقط شما و مدیران سازمان می‌توانند پیام بفرستند.
@@ -2659,7 +2690,7 @@ onUnmounted(() => {
v-if="showActivityFromMsg" v-if="showActivityFromMsg"
class="modal" class="modal"
open open
@click.self="closeActivityFromMsg" @click.self="closeModalOnBackdrop(closeActivityFromMsg)"
> >
<div class="modal-card activity-sheet"> <div class="modal-card activity-sheet">
<template v-if="!activityCreatedId"> <template v-if="!activityCreatedId">
@@ -2706,7 +2737,12 @@ onUnmounted(() => {
</div> </div>
</dialog> </dialog>
<dialog v-if="showCrmLink" class="modal" open @click.self="showCrmLink = false"> <dialog
v-if="showCrmLink"
class="modal"
open
@click.self="closeModalOnBackdrop(() => (showCrmLink = false))"
>
<div class="modal-card wide"> <div class="modal-card wide">
<h3>لینک CRM</h3> <h3>لینک CRM</h3>
<p class="hint">شخص، معامله یا تیکت را انتخاب کنید تا در پیام درج شود.</p> <p class="hint">شخص، معامله یا تیکت را انتخاب کنید تا در پیام درج شود.</p>
@@ -2765,7 +2801,12 @@ onUnmounted(() => {
</div> </div>
</dialog> </dialog>
<dialog v-if="showAiAssist" class="modal" open @click.self="showAiAssist = false"> <dialog
v-if="showAiAssist"
class="modal"
open
@click.self="closeModalOnBackdrop(() => (showAiAssist = false))"
>
<div class="modal-card wide ai-assist-sheet"> <div class="modal-card wide ai-assist-sheet">
<h3>{{ brand.askLabelFa }}</h3> <h3>{{ brand.askLabelFa }}</h3>
<p class="hint">با توجه به CRM و چند پیام اخیر این گفتگو پاسخ می‌دهد؛ می‌توانید پاسخ را در پیش‌نویس درج کنید.</p> <p class="hint">با توجه به CRM و چند پیام اخیر این گفتگو پاسخ می‌دهد؛ می‌توانید پاسخ را در پیش‌نویس درج کنید.</p>
@@ -2781,8 +2822,13 @@ onUnmounted(() => {
</div> </div>
</dialog> </dialog>
<dialog v-if="showMembers" class="modal" open @click.self="showMembers = false"> <dialog
<div class="modal-card wide members-sheet"> v-if="showMembers"
class="modal"
open
@click.self="closeModalOnBackdrop(() => (showMembers = false))"
>
<div class="modal-card wide members-sheet member-picker-modal">
<h3>اعضای {{ activeRoom ? roomTitle(activeRoom) : 'گفتگو' }}</h3> <h3>اعضای {{ activeRoom ? roomTitle(activeRoom) : 'گفتگو' }}</h3>
<p v-if="activeRoom?.kind === 'channel'" class="hint channel-hint"> <p v-if="activeRoom?.kind === 'channel'" class="hint channel-hint">
ادمین‌ها می‌توانند در کانال پیام بفرستند؛ بقیه فقط مشاهده می‌کنند. ادمین‌ها می‌توانند در کانال پیام بفرستند؛ بقیه فقط مشاهده می‌کنند.
@@ -2839,7 +2885,12 @@ onUnmounted(() => {
</div> </div>
</dialog> </dialog>
<dialog v-if="showForward" class="modal" open @click.self="showForward = false"> <dialog
v-if="showForward"
class="modal"
open
@click.self="closeModalOnBackdrop(() => (showForward = false))"
>
<div class="modal-card"> <div class="modal-card">
<h3>ارسال برای دیگری</h3> <h3>ارسال برای دیگری</h3>
<p class="hint">{{ toFaDigits(selectedCount) }} پیام انتخاب شده</p> <p class="hint">{{ toFaDigits(selectedCount) }} پیام انتخاب شده</p>
@@ -4212,9 +4263,22 @@ onUnmounted(() => {
} }
.modal-card.wide { .modal-card.wide {
width: min(440px, 100%); width: min(440px, 100%);
max-height: min(88vh, 640px); max-height: min(88vh, 720px);
overflow: auto; overflow: auto;
} }
.modal-card.member-picker-modal {
display: flex;
flex-direction: column;
gap: 0.75rem;
overflow: hidden;
min-height: min(70vh, 560px);
}
.modal-card.member-picker-modal > .peer-list,
.modal-card.member-picker-modal > .members-list {
flex: 1 1 auto;
min-height: 220px;
max-height: none;
}
.modal-card select, .modal-card select,
.modal-card input[type='text'], .modal-card input[type='text'],
.modal-card input[type='search'], .modal-card input[type='search'],
@@ -4302,10 +4366,12 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.3rem; gap: 0.3rem;
max-height: 240px; max-height: min(48vh, 360px);
overflow: auto; overflow: auto;
overscroll-behavior: contain;
padding: 0.15rem; padding: 0.15rem;
margin: 0 -0.15rem; margin: 0 -0.15rem;
-webkit-overflow-scrolling: touch;
} }
.peer-row { .peer-row {
display: grid; display: grid;