Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
196 lines
6.2 KiB
TypeScript
196 lines
6.2 KiB
TypeScript
import type { IMessage } from '@rocket.chat/core-typings';
|
|
|
|
import { t } from '../../../../app/utils/lib/i18n';
|
|
import { closeUnclosedCodeBlock } from '../../../../lib/utils/closeUnclosedCodeBlock';
|
|
import { Messages, Rooms } from '../../../stores';
|
|
import { onClientBeforeSendMessage } from '../../onClientBeforeSendMessage';
|
|
import { dispatchToastMessage } from '../../toast';
|
|
import type { ChatAPI } from '../ChatAPI';
|
|
import { afterSendMessageCallback } from './afterSendMessageCallback';
|
|
import { processMessageEditing } from './processMessageEditing';
|
|
import { processMessageUploads } from './processMessageUploads';
|
|
import { processSetReaction } from './processSetReaction';
|
|
import { processSlashCommand } from './processSlashCommand';
|
|
import { processTooLongMessage } from './processTooLongMessage';
|
|
import { trim } from '../../../../lib/utils/stringUtils';
|
|
import { sdk } from '../../SDKClient';
|
|
import { onClientMessageReceived } from '../../onClientMessageReceived';
|
|
import { settings } from '../../settings';
|
|
import { getUserId, getUser } from '../../user';
|
|
import { upsertThreadMessageInCache } from '../../utils/threadMessageUtils';
|
|
|
|
const runOptimisticSendMessage = async (message: Partial<IMessage> & { rid: IMessage['rid']; msg: IMessage['msg'] }): Promise<void> => {
|
|
const uid = getUserId();
|
|
if (!uid || trim(message.msg) === '') {
|
|
return;
|
|
}
|
|
const messageAlreadyExists = message._id && Messages.state.get(message._id);
|
|
if (messageAlreadyExists) {
|
|
dispatchToastMessage({ type: 'error', message: t('Message_Already_Sent') });
|
|
return;
|
|
}
|
|
const user = getUser();
|
|
if (!user?.username) {
|
|
return;
|
|
}
|
|
|
|
const room = Rooms.state.get(message.rid);
|
|
if (room?.federated) {
|
|
return;
|
|
}
|
|
|
|
const optimistic: IMessage = {
|
|
...(message as IMessage),
|
|
ts: new Date(),
|
|
u: {
|
|
_id: uid,
|
|
username: user.username,
|
|
name: user.name || '',
|
|
},
|
|
temp: true,
|
|
...(settings.peek('Message_Read_Receipt_Enabled') ? { unread: true } : {}),
|
|
};
|
|
|
|
const processed = await onClientMessageReceived(optimistic);
|
|
Messages.state.store(processed);
|
|
|
|
if (processed.tmid) {
|
|
upsertThreadMessageInCache(processed, processed.rid, processed.tmid);
|
|
}
|
|
};
|
|
|
|
const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[], isSlashCommandAllowed?: boolean): Promise<void> => {
|
|
const mid = chat.currentEditingMessage.getMID();
|
|
|
|
if (await processSetReaction(chat, message)) {
|
|
return;
|
|
}
|
|
|
|
if (await processTooLongMessage(chat, message)) {
|
|
return;
|
|
}
|
|
|
|
if (isSlashCommandAllowed && (await processSlashCommand(chat, message))) {
|
|
return;
|
|
}
|
|
|
|
if (await processMessageUploads(chat, message)) {
|
|
chat.composer?.clear();
|
|
return;
|
|
}
|
|
|
|
message = (await onClientBeforeSendMessage({ ...message, isEditing: !!mid })) as IMessage & { isEditing?: boolean };
|
|
|
|
// e2e should be a client property only
|
|
delete message.e2e;
|
|
delete (message as IMessage & { isEditing?: boolean }).isEditing;
|
|
|
|
if (await processMessageEditing(chat, message, previewUrls)) {
|
|
return;
|
|
}
|
|
|
|
chat.composer?.clear();
|
|
await runOptimisticSendMessage(message);
|
|
|
|
await sdk.rest.post('/v1/chat.sendMessage', { message, previewUrls });
|
|
|
|
// Clear the optimistic `temp` flag only if the messages stream hasn't already
|
|
// replaced the record. Overwriting with the server response can clobber stream
|
|
// updates that arrive first — e.g. read-receipt-driven `unread: false`, async
|
|
// URL/quote attachments, or E2EE decrypt — leading to stale UI state.
|
|
Messages.state.update(
|
|
(record) => record._id === message._id && record.temp === true,
|
|
({ temp: _, ...record }) => record,
|
|
);
|
|
};
|
|
|
|
export const sendMessage = async (
|
|
chat: ChatAPI,
|
|
{
|
|
text,
|
|
tshow,
|
|
previewUrls,
|
|
isSlashCommandAllowed,
|
|
}: { text: string; tshow?: boolean; previewUrls?: string[]; isSlashCommandAllowed?: boolean; tmid?: IMessage['tmid'] },
|
|
): Promise<boolean> => {
|
|
if (!(await chat.data.isSubscribedToRoom())) {
|
|
try {
|
|
await chat.data.joinRoom();
|
|
} catch (error) {
|
|
dispatchToastMessage({ type: 'error', message: error });
|
|
return false;
|
|
}
|
|
}
|
|
|
|
chat.readStateManager.clearUnreadMark();
|
|
|
|
const uploadsStore = chat.composer?.uploads;
|
|
|
|
text = text.trim();
|
|
text = closeUnclosedCodeBlock(text);
|
|
const mid = chat.currentEditingMessage.getMID();
|
|
|
|
const hasFiles = uploadsStore && uploadsStore.get().length > 0;
|
|
if (!text && !mid && !hasFiles) {
|
|
// Nothing to do
|
|
return false;
|
|
}
|
|
|
|
if (text || hasFiles) {
|
|
const message = await chat.data.composeMessage(text, {
|
|
sendToChannel: tshow,
|
|
quotedMessages: chat.composer?.quotedMessages.get() ?? [],
|
|
originalMessage: mid ? await chat.data.findMessageByID(mid) : null,
|
|
});
|
|
|
|
// When editing an encrypted message with files, preserve the original attachments/files
|
|
// This ensures they're included in the re-encryption process
|
|
if (mid) {
|
|
const originalMessage = await chat.data.findMessageByID(mid);
|
|
|
|
if (originalMessage?.t === 'e2e' && originalMessage.attachments && originalMessage.attachments.length > 0) {
|
|
message.attachments = originalMessage.attachments;
|
|
message.file = originalMessage.file;
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Dismiss quoted messages optimistically — they are already baked into
|
|
// `message` by composeMessage above, so the composer preview must unmount
|
|
// regardless of whether the send request resolves. Keeping it coupled to a
|
|
// resolved request leaves the quote stuck in the composer when the REST call
|
|
// rejects even though the message was already broadcast over the stream.
|
|
chat.composer?.dismissAllQuotedMessages();
|
|
await process(chat, message, previewUrls, isSlashCommandAllowed);
|
|
await afterSendMessageCallback(message, message.rid);
|
|
} catch (error) {
|
|
dispatchToastMessage({ type: 'error', message: error });
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (mid) {
|
|
const originalMessage = await chat.data.findMessageByID(mid);
|
|
|
|
if (!originalMessage) {
|
|
dispatchToastMessage({ type: 'warning', message: t('Message_not_found') });
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
if (await chat.flows.processMessageEditing({ ...originalMessage, msg: '' }, previewUrls)) {
|
|
chat.currentEditingMessage.stop();
|
|
return false;
|
|
}
|
|
|
|
await chat.currentEditingMessage.reset();
|
|
await chat.flows.requestMessageDeletion(originalMessage);
|
|
return false;
|
|
} catch (error) {
|
|
dispatchToastMessage({ type: 'error', message: error });
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|