Editing and error correction (in progress)
This commit is contained in:
@@ -221,17 +221,20 @@ const hasMoreMessages = ref(false);
|
||||
const loadingOlderMessages = ref(false);
|
||||
/** تعداد خام پیامهای گرفتهشده از API (برای offset) */
|
||||
let fetchedRawCount = 0;
|
||||
const typingMap = ref<Record<string, Array<{ userId: string; at: number }>>>({});
|
||||
const typingMap = ref<
|
||||
Record<string, Array<{ userId: string; name?: string; at: number }>>
|
||||
>({});
|
||||
let typingSweepTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let typingPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function startTypingSweep() {
|
||||
if (typingSweepTimer) return;
|
||||
typingSweepTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
let changed = false;
|
||||
const next: Record<string, Array<{ userId: string; at: number }>> = {};
|
||||
const next: Record<string, Array<{ userId: string; name?: string; at: number }>> = {};
|
||||
for (const [roomId, list] of Object.entries(typingMap.value)) {
|
||||
const kept = list.filter((t) => now - t.at < 3500);
|
||||
const kept = list.filter((t) => now - t.at < 4000);
|
||||
if (kept.length) next[roomId] = kept;
|
||||
if (kept.length !== list.length) changed = true;
|
||||
}
|
||||
@@ -239,17 +242,51 @@ function startTypingSweep() {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function applyTypingEvent(roomId: string, userId: string) {
|
||||
function applyTypingEvent(roomId: string, userId: string, name?: string) {
|
||||
const rid = String(roomId || activeRoomId.value || '').trim();
|
||||
const uid = String(userId || '').trim() || '__someone__';
|
||||
if (!rid) return;
|
||||
if (sessionCache?.chatUserId && uid === sessionCache.chatUserId) return;
|
||||
startTypingSweep();
|
||||
const list = [...(typingMap.value[rid] || [])].filter((t) => t.userId !== uid);
|
||||
list.push({ userId: uid, at: Date.now() });
|
||||
list.push({ userId: uid, name: name || undefined, at: Date.now() });
|
||||
typingMap.value = { ...typingMap.value, [rid]: list };
|
||||
}
|
||||
|
||||
function applyTypingFromServer(
|
||||
roomId: string,
|
||||
typers: Array<{ userId: string; fullName: string; chatUserId: string }>,
|
||||
) {
|
||||
const rid = String(roomId || '').trim();
|
||||
if (!rid) return;
|
||||
const now = Date.now();
|
||||
const list = typers
|
||||
.map((t) => ({
|
||||
userId: t.chatUserId || t.userId,
|
||||
name: t.fullName || undefined,
|
||||
at: now,
|
||||
}))
|
||||
.filter((t) => t.userId && t.userId !== sessionCache?.chatUserId);
|
||||
const prev = typingMap.value[rid] || [];
|
||||
// نگه داشتن رویدادهای خیلی تازه از WS اگر سرور هنوز خالی است
|
||||
const merged = new Map<string, { userId: string; name?: string; at: number }>();
|
||||
for (const t of prev) {
|
||||
if (now - t.at < 4000) merged.set(t.userId, t);
|
||||
}
|
||||
for (const t of list) merged.set(t.userId, t);
|
||||
const nextList = [...merged.values()];
|
||||
if (!nextList.length) {
|
||||
if (prev.length) {
|
||||
const next = { ...typingMap.value };
|
||||
delete next[rid];
|
||||
typingMap.value = next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
startTypingSweep();
|
||||
typingMap.value = { ...typingMap.value, [rid]: nextList };
|
||||
}
|
||||
|
||||
function parseTypingCommand(content: string): string | null {
|
||||
const t = content.trim();
|
||||
if (!t.startsWith(TYPING_CMD_PREFIX)) return null;
|
||||
@@ -274,6 +311,34 @@ function clearTypingUser(roomId: string, userId: string) {
|
||||
typingMap.value = next;
|
||||
}
|
||||
|
||||
function stopTypingPoll() {
|
||||
if (typingPollTimer) {
|
||||
clearInterval(typingPollTimer);
|
||||
typingPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startTypingPoll(roomId: string) {
|
||||
stopTypingPoll();
|
||||
const rid = String(roomId || '').trim();
|
||||
if (!rid) return;
|
||||
const tick = async () => {
|
||||
if (activeRoomId.value !== rid) return;
|
||||
try {
|
||||
const { data } = await api.get<
|
||||
Array<{ userId: string; fullName: string; chatUserId: string }>
|
||||
>('/conversations/rooms/typing', { params: { externalRoomId: rid } });
|
||||
applyTypingFromServer(rid, Array.isArray(data) ? data : []);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
void tick();
|
||||
typingPollTimer = setInterval(() => {
|
||||
void tick();
|
||||
}, 1400);
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 40;
|
||||
|
||||
function messageTs(item: ChatMessage): number {
|
||||
@@ -1101,6 +1166,7 @@ export function useOrgChat() {
|
||||
hasMoreMessages.value = false;
|
||||
clearTypingForRoom(roomId);
|
||||
startHiddenPoll(roomId);
|
||||
startTypingPoll(roomId);
|
||||
try {
|
||||
const session = await ensureSession();
|
||||
await connectSocket();
|
||||
@@ -1168,33 +1234,28 @@ export function useOrgChat() {
|
||||
const rid = String(roomId || '').trim();
|
||||
if (!rid) return;
|
||||
const now = Date.now();
|
||||
if (now - lastTypingSentAt < 1200) return;
|
||||
if (now - lastTypingSentAt < 1100) return;
|
||||
lastTypingSentAt = now;
|
||||
// مسیر اصلی: CRM presence (سرویس خارجی typing را relay نمیکند)
|
||||
try {
|
||||
await api.post('/conversations/rooms/typing', { externalRoomId: rid });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
// best-effort native WS typing
|
||||
try {
|
||||
await connectSocket();
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) return;
|
||||
const session = await ensureSession();
|
||||
// Native typing event (if server relays it)
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: 'typing',
|
||||
room_id: rid,
|
||||
user_id: session.chatUserId,
|
||||
sender_id: session.chatUserId,
|
||||
}),
|
||||
);
|
||||
// Fallback control text — external chat reliably delivers text; filtered from UI
|
||||
const controlId = crypto.randomUUID();
|
||||
rememberHidden(rid, [controlId]);
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
id: controlId,
|
||||
type: 'text',
|
||||
room_id: rid,
|
||||
content: `${TYPING_CMD_PREFIX}${session.chatUserId}`,
|
||||
}),
|
||||
);
|
||||
void persistHiddenOnServer(rid, [controlId], true);
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
const session = await ensureSession();
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: 'typing',
|
||||
room_id: rid,
|
||||
user_id: session.chatUserId,
|
||||
sender_id: session.chatUserId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -1576,6 +1637,7 @@ export function useOrgChat() {
|
||||
|
||||
function clearActiveRoom() {
|
||||
stopHiddenPoll();
|
||||
stopTypingPoll();
|
||||
activeRoomId.value = '';
|
||||
messages.value = [];
|
||||
}
|
||||
@@ -1584,6 +1646,7 @@ export function useOrgChat() {
|
||||
wantSocket = false;
|
||||
sessionCache = null;
|
||||
stopHiddenPoll();
|
||||
stopTypingPoll();
|
||||
disconnectSocket();
|
||||
messages.value = [];
|
||||
activeRoomId.value = '';
|
||||
|
||||
@@ -170,13 +170,18 @@ const typingLabel = computed(() => {
|
||||
const roomId = activeRoom.value?.externalRoomId;
|
||||
if (!roomId) return '';
|
||||
const list = (typingMap.value?.[roomId] || []).filter(
|
||||
(t) => Date.now() - t.at < 3500,
|
||||
(t) => Date.now() - t.at < 4000,
|
||||
);
|
||||
if (!list.length) return '';
|
||||
const names = list
|
||||
.map((t) => {
|
||||
if (t.name) return t.name;
|
||||
if (t.userId === '__someone__') return '';
|
||||
return peers.value.find((p) => p.chatUserId === t.userId)?.fullName || '';
|
||||
return (
|
||||
peers.value.find((p) => p.chatUserId === t.userId)?.fullName ||
|
||||
peers.value.find((p) => p.id === t.userId)?.fullName ||
|
||||
''
|
||||
);
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (!names.length) return 'در حال نوشتن…';
|
||||
|
||||
Reference in New Issue
Block a user