Add mentions, pin messages and conversations, and search within chats
This commit is contained in:
+128
-19
@@ -33,7 +33,49 @@ export type RoomLiveEvent = {
|
||||
fromSelf: boolean;
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
export type MessagePinEvent = {
|
||||
roomId: string;
|
||||
/** null = unpinned */
|
||||
messageId: string | null;
|
||||
preview: string;
|
||||
fromSelf: boolean;
|
||||
};
|
||||
|
||||
type RoomListener = (event: RoomLiveEvent) => void;
|
||||
const roomListeners = new Set<RoomListener>();
|
||||
|
||||
export function onRoomLiveEvent(listener: RoomListener) {
|
||||
roomListeners.add(listener);
|
||||
return () => roomListeners.delete(listener);
|
||||
}
|
||||
|
||||
function emitRoomLive(event: RoomLiveEvent) {
|
||||
for (const listener of roomListeners) {
|
||||
try {
|
||||
listener(event);
|
||||
} catch {
|
||||
// ignore listener errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PinListener = (event: MessagePinEvent) => void;
|
||||
const pinListeners = new Set<PinListener>();
|
||||
|
||||
export function onMessagePinEvent(listener: PinListener) {
|
||||
pinListeners.add(listener);
|
||||
return () => pinListeners.delete(listener);
|
||||
}
|
||||
|
||||
function emitMessagePin(event: MessagePinEvent) {
|
||||
for (const listener of pinListeners) {
|
||||
try {
|
||||
listener(event);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
const IMAGE_MAX_EDGE = 1600;
|
||||
const IMAGE_WEBP_QUALITY = 0.82;
|
||||
const MAX_UPLOAD_BYTES = 40 * 1024 * 1024;
|
||||
@@ -41,6 +83,9 @@ const MAX_UPLOAD_BYTES = 40 * 1024 * 1024;
|
||||
const HARD_DELETE_MARKER = '__MENTO_DEL__';
|
||||
/** Realtime hide command delivered as a normal text message (external WS delivers text reliably). */
|
||||
const HIDE_CMD_PREFIX = '__MENTO_HIDE__:';
|
||||
/** Realtime pin / unpin control messages. */
|
||||
const PIN_CMD_PREFIX = '__MENTO_PIN__:';
|
||||
const UNPIN_CMD = '__MENTO_UNPIN__';
|
||||
|
||||
function isDeletedContent(content: string): boolean {
|
||||
const t = content.trim();
|
||||
@@ -48,7 +93,9 @@ function isDeletedContent(content: string): boolean {
|
||||
/^(?:\[?\s*deleted\s*\]?|این پیام حذف شد|پیام حذف شد)$/i.test(t) ||
|
||||
t.includes('MENTO_DEL') ||
|
||||
t === HARD_DELETE_MARKER ||
|
||||
t.startsWith(HIDE_CMD_PREFIX)
|
||||
t.startsWith(HIDE_CMD_PREFIX) ||
|
||||
t.startsWith(PIN_CMD_PREFIX) ||
|
||||
t === UNPIN_CMD
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,6 +106,15 @@ function parseHideCommand(content: string): string | null {
|
||||
return id || null;
|
||||
}
|
||||
|
||||
/** null = not a pin cmd; '' = unpin; otherwise messageId */
|
||||
function parsePinCommand(content: string): string | null {
|
||||
const t = content.trim();
|
||||
if (t === UNPIN_CMD) return '';
|
||||
if (!t.startsWith(PIN_CMD_PREFIX)) return null;
|
||||
const id = t.slice(PIN_CMD_PREFIX.length).trim();
|
||||
return id || null;
|
||||
}
|
||||
|
||||
function purgeMessageById(id: string) {
|
||||
const key = String(id || '').trim();
|
||||
if (!key) return;
|
||||
@@ -159,23 +215,7 @@ const activeRoomId = ref('');
|
||||
const unreadTotal = ref(0);
|
||||
const pageOpen = ref(false);
|
||||
|
||||
type RoomListener = (event: RoomLiveEvent) => void;
|
||||
const roomListeners = new Set<RoomListener>();
|
||||
|
||||
export function onRoomLiveEvent(listener: RoomListener) {
|
||||
roomListeners.add(listener);
|
||||
return () => roomListeners.delete(listener);
|
||||
}
|
||||
|
||||
function emitRoomLive(event: RoomLiveEvent) {
|
||||
for (const listener of roomListeners) {
|
||||
try {
|
||||
listener(event);
|
||||
} catch {
|
||||
// ignore listener errors
|
||||
}
|
||||
}
|
||||
}
|
||||
const PAGE_SIZE = 200;
|
||||
|
||||
function messageTs(item: ChatMessage): number {
|
||||
const t = new Date(item.createdAt).getTime();
|
||||
@@ -734,6 +774,27 @@ function onSocketMessage(event: MessageEvent) {
|
||||
void persistHiddenOnServer(roomId, ids, true);
|
||||
return;
|
||||
}
|
||||
const pinTarget = parsePinCommand(purgeContent);
|
||||
if (pinTarget !== null) {
|
||||
const roomId = String(
|
||||
(parsed.room_id as string) ||
|
||||
((parsed.message as Record<string, unknown> | undefined)?.room_id as string) ||
|
||||
activeRoomId.value,
|
||||
);
|
||||
if (purgeId) rememberHidden(roomId, [purgeId]);
|
||||
if (pinTarget === '') {
|
||||
emitMessagePin({ roomId, messageId: null, preview: '', fromSelf: false });
|
||||
} else {
|
||||
const found = messages.value.find((m) => m.id === pinTarget);
|
||||
emitMessagePin({
|
||||
roomId,
|
||||
messageId: pinTarget,
|
||||
preview: found ? previewLabelForMessage(found) : '',
|
||||
fromSelf: false,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (purgeId && isDeletedContent(purgeContent)) {
|
||||
purgeMessageById(purgeId);
|
||||
return;
|
||||
@@ -1271,6 +1332,52 @@ export function useOrgChat() {
|
||||
}
|
||||
}
|
||||
|
||||
async function pinChatMessage(roomId: string, msg: ChatMessage) {
|
||||
const messageId = String(msg.id || '').trim();
|
||||
if (!roomId || !messageId || msg.optimistic) return;
|
||||
const preview = previewLabelForMessage(msg).slice(0, 240);
|
||||
await api.post('/conversations/messages/pin', {
|
||||
externalRoomId: roomId,
|
||||
messageId,
|
||||
preview,
|
||||
});
|
||||
emitMessagePin({ roomId, messageId, preview, fromSelf: true });
|
||||
await connectSocket();
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
const controlId = crypto.randomUUID();
|
||||
rememberHidden(roomId, [controlId]);
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
id: controlId,
|
||||
type: 'text',
|
||||
room_id: roomId,
|
||||
content: `${PIN_CMD_PREFIX}${messageId}`,
|
||||
}),
|
||||
);
|
||||
void persistHiddenOnServer(roomId, [controlId], true);
|
||||
}
|
||||
}
|
||||
|
||||
async function unpinChatMessage(roomId: string) {
|
||||
if (!roomId) return;
|
||||
await api.post('/conversations/messages/unpin', { externalRoomId: roomId });
|
||||
emitMessagePin({ roomId, messageId: null, preview: '', fromSelf: true });
|
||||
await connectSocket();
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
const controlId = crypto.randomUUID();
|
||||
rememberHidden(roomId, [controlId]);
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
id: controlId,
|
||||
type: 'text',
|
||||
room_id: roomId,
|
||||
content: UNPIN_CMD,
|
||||
}),
|
||||
);
|
||||
void persistHiddenOnServer(roomId, [controlId], true);
|
||||
}
|
||||
}
|
||||
|
||||
function clearActiveRoom() {
|
||||
stopHiddenPoll();
|
||||
activeRoomId.value = '';
|
||||
@@ -1298,6 +1405,8 @@ export function useOrgChat() {
|
||||
deleteMessage,
|
||||
deleteMessages,
|
||||
forwardMessages,
|
||||
pinChatMessage,
|
||||
unpinChatMessage,
|
||||
sendRead,
|
||||
ensureConnected,
|
||||
setConversationsPageOpen,
|
||||
|
||||
Reference in New Issue
Block a user