Adding the typing... indicator, loading older messages, and managing members/admins after creation.
This commit is contained in:
+154
-25
@@ -214,8 +214,47 @@ const messages = shallowRef<ChatMessage[]>([]);
|
||||
const activeRoomId = ref('');
|
||||
const unreadTotal = ref(0);
|
||||
const pageOpen = ref(false);
|
||||
const hasMoreMessages = ref(false);
|
||||
const loadingOlderMessages = ref(false);
|
||||
/** تعداد خام پیامهای گرفتهشده از API (برای offset) */
|
||||
let fetchedRawCount = 0;
|
||||
const typingMap = ref<Record<string, Array<{ userId: string; at: number }>>>({});
|
||||
let typingSweepTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const PAGE_SIZE = 200;
|
||||
function startTypingSweep() {
|
||||
if (typingSweepTimer) return;
|
||||
typingSweepTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
let changed = false;
|
||||
const next: Record<string, Array<{ userId: string; at: number }>> = {};
|
||||
for (const [roomId, list] of Object.entries(typingMap.value)) {
|
||||
const kept = list.filter((t) => now - t.at < 3500);
|
||||
if (kept.length) next[roomId] = kept;
|
||||
if (kept.length !== list.length) changed = true;
|
||||
}
|
||||
if (changed) typingMap.value = next;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function applyTypingEvent(roomId: string, userId: string) {
|
||||
const rid = String(roomId || '').trim();
|
||||
const uid = String(userId || '').trim();
|
||||
if (!rid || !uid) 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() });
|
||||
typingMap.value = { ...typingMap.value, [rid]: list };
|
||||
}
|
||||
|
||||
function clearTypingForRoom(roomId: string) {
|
||||
if (!typingMap.value[roomId]) return;
|
||||
const next = { ...typingMap.value };
|
||||
delete next[roomId];
|
||||
typingMap.value = next;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 40;
|
||||
|
||||
function messageTs(item: ChatMessage): number {
|
||||
const t = new Date(item.createdAt).getTime();
|
||||
@@ -732,7 +771,24 @@ function onSocketMessage(event: MessageEvent) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
const type = String(parsed.type ?? parsed.event ?? '').toLowerCase();
|
||||
if (type === 'pong' || type === 'ping' || type === 'typing' || type === 'read') return;
|
||||
if (type === 'pong' || type === 'ping' || type === 'read') return;
|
||||
if (type === 'typing') {
|
||||
const roomId = String(
|
||||
parsed.room_id ??
|
||||
parsed.roomId ??
|
||||
((parsed.message as Record<string, unknown> | undefined)?.room_id as string) ??
|
||||
activeRoomId.value,
|
||||
);
|
||||
const userId = String(
|
||||
parsed.user_id ??
|
||||
parsed.sender_id ??
|
||||
parsed.from_user_id ??
|
||||
parsed.userId ??
|
||||
'',
|
||||
);
|
||||
applyTypingEvent(roomId, userId);
|
||||
return;
|
||||
}
|
||||
if (type === 'seen') {
|
||||
applySeenPayload(parsed);
|
||||
return;
|
||||
@@ -959,9 +1015,39 @@ export function useOrgChat() {
|
||||
pageOpen.value = open;
|
||||
}
|
||||
|
||||
function normalizeMessageList(
|
||||
list: Record<string, unknown>[],
|
||||
roomId: string,
|
||||
hiddenSet: Set<string>,
|
||||
myId: string,
|
||||
): ChatMessage[] {
|
||||
for (const item of list) {
|
||||
const rawContent = String(item.content ?? item.text ?? item.body ?? '').trim();
|
||||
const target = parseHideCommand(rawContent);
|
||||
if (target) hiddenSet.add(target);
|
||||
}
|
||||
rememberHidden(roomId, [...hiddenSet]);
|
||||
|
||||
const normalized: ChatMessage[] = [];
|
||||
for (const item of list) {
|
||||
const m = normalizeIncoming(item, roomId);
|
||||
if (!m || hiddenSet.has(m.id)) continue;
|
||||
normalized.push(
|
||||
m.senderId === myId
|
||||
? { ...m, delivery: (m.readAt ? 'read' : 'sent') as ChatDelivery }
|
||||
: m,
|
||||
);
|
||||
}
|
||||
return sortAsc(normalized);
|
||||
}
|
||||
|
||||
async function loadMessages(roomId: string) {
|
||||
activeRoomId.value = roomId;
|
||||
loadingMessages.value = true;
|
||||
loadingOlderMessages.value = false;
|
||||
fetchedRawCount = 0;
|
||||
hasMoreMessages.value = false;
|
||||
clearTypingForRoom(roomId);
|
||||
startHiddenPoll(roomId);
|
||||
try {
|
||||
const session = await ensureSession();
|
||||
@@ -981,35 +1067,72 @@ export function useOrgChat() {
|
||||
if (!res.ok) throw new Error('messages failed');
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : body?.data ?? [];
|
||||
|
||||
// Collect hide-command targets from history so we filter even without CRM row yet
|
||||
for (const item of list as Record<string, unknown>[]) {
|
||||
const rawContent = String(
|
||||
item.content ?? item.text ?? item.body ?? '',
|
||||
).trim();
|
||||
const target = parseHideCommand(rawContent);
|
||||
if (target) hiddenSet.add(target);
|
||||
}
|
||||
rememberHidden(roomId, [...hiddenSet]);
|
||||
|
||||
const myId = session.chatUserId;
|
||||
const normalized: ChatMessage[] = [];
|
||||
for (const item of list as Record<string, unknown>[]) {
|
||||
const m = normalizeIncoming(item, roomId);
|
||||
if (!m || hiddenSet.has(m.id)) continue;
|
||||
normalized.push(
|
||||
m.senderId === myId
|
||||
? { ...m, delivery: (m.readAt ? 'read' : 'sent') as ChatDelivery }
|
||||
: m,
|
||||
);
|
||||
}
|
||||
messages.value = sortAsc(normalized);
|
||||
const rows = list as Record<string, unknown>[];
|
||||
fetchedRawCount = rows.length;
|
||||
hasMoreMessages.value = rows.length >= PAGE_SIZE;
|
||||
messages.value = normalizeMessageList(rows, roomId, hiddenSet, session.chatUserId);
|
||||
sendRead(roomId);
|
||||
} finally {
|
||||
loadingMessages.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOlderMessages(roomId: string): Promise<boolean> {
|
||||
if (!roomId || loadingOlderMessages.value || loadingMessages.value) return false;
|
||||
if (!hasMoreMessages.value) return false;
|
||||
loadingOlderMessages.value = true;
|
||||
try {
|
||||
const session = await ensureSession();
|
||||
const hiddenIds = await fetchHiddenFromServer(roomId);
|
||||
const hiddenSet = new Set(hiddenIds);
|
||||
const params = new URLSearchParams({
|
||||
api_key: new URL(session.wsUrl).searchParams.get('api_key') ?? '',
|
||||
room_id: roomId,
|
||||
user_id: session.chatUserId,
|
||||
limit: String(PAGE_SIZE),
|
||||
offset: String(fetchedRawCount),
|
||||
});
|
||||
const url = `${session.messagesBaseUrl}/api/messages?${params.toString()}`;
|
||||
const res = await fetch(url, { headers: { Accept: 'application/json' }, cache: 'no-store' });
|
||||
if (!res.ok) throw new Error('older messages failed');
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : body?.data ?? [];
|
||||
const rows = list as Record<string, unknown>[];
|
||||
fetchedRawCount += rows.length;
|
||||
hasMoreMessages.value = rows.length >= PAGE_SIZE;
|
||||
if (!rows.length) return false;
|
||||
const older = normalizeMessageList(rows, roomId, hiddenSet, session.chatUserId);
|
||||
const existingIds = new Set(messages.value.map((m) => m.id));
|
||||
const fresh = older.filter((m) => !existingIds.has(m.id));
|
||||
messages.value = sortAsc([...fresh, ...messages.value]);
|
||||
return true;
|
||||
} finally {
|
||||
loadingOlderMessages.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
let lastTypingSentAt = 0;
|
||||
async function sendTyping(roomId: string) {
|
||||
const rid = String(roomId || '').trim();
|
||||
if (!rid) return;
|
||||
const now = Date.now();
|
||||
if (now - lastTypingSentAt < 1800) return;
|
||||
lastTypingSentAt = now;
|
||||
try {
|
||||
await connectSocket();
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) return;
|
||||
socket.send(JSON.stringify({ type: 'typing', room_id: rid }));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function typingUserIdsForRoom(roomId: string): string[] {
|
||||
const list = typingMap.value[roomId] || [];
|
||||
const now = Date.now();
|
||||
return list.filter((t) => now - t.at < 3500).map((t) => t.userId);
|
||||
}
|
||||
|
||||
async function sendText(roomId: string, text: string, reply: string | null = null) {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return;
|
||||
@@ -1398,7 +1521,13 @@ export function useOrgChat() {
|
||||
messages: computed(() => messages.value),
|
||||
unreadTotal: computed(() => unreadTotal.value),
|
||||
loadingMessages,
|
||||
hasMoreMessages: computed(() => hasMoreMessages.value),
|
||||
loadingOlderMessages: computed(() => loadingOlderMessages.value),
|
||||
typingMap: computed(() => typingMap.value),
|
||||
loadMessages,
|
||||
loadOlderMessages,
|
||||
sendTyping,
|
||||
typingUserIdsForRoom,
|
||||
sendText,
|
||||
sendFile,
|
||||
editMessage,
|
||||
|
||||
Reference in New Issue
Block a user