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:
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user