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
146 lines
5.7 KiB
TypeScript
146 lines
5.7 KiB
TypeScript
import type { IMessage, ITranslatedMessage, MessageAttachment } from '@rocket.chat/core-typings';
|
|
import {
|
|
isFileAttachment,
|
|
isE2EEMessage,
|
|
isQuoteAttachment,
|
|
isTranslatedAttachment,
|
|
isTranslatedMessage,
|
|
isEncryptedMessageAttachment,
|
|
} from '@rocket.chat/core-typings';
|
|
import type { Options, Root } from '@rocket.chat/message-parser';
|
|
import { parse } from '@rocket.chat/message-parser';
|
|
|
|
import { getMarkdownParserLimit } from './getMarkdownParserLimit';
|
|
import { toPlainTextRoot } from './toPlainTextRoot';
|
|
import type { AutoTranslateOptions } from '../views/room/MessageList/hooks/useAutoTranslate';
|
|
import { isParsedMessage } from '../views/room/MessageList/lib/isParsedMessage';
|
|
|
|
type WithRequiredProperty<Type, Key extends keyof Type> = Omit<Type, Key> & {
|
|
[Property in Key]-?: Type[Property];
|
|
};
|
|
|
|
export type MessageWithMdEnforced<TMessage extends IMessage & Partial<ITranslatedMessage> = IMessage & Partial<ITranslatedMessage>> =
|
|
WithRequiredProperty<TMessage, 'md'> & {
|
|
/** The exact source text `md` was parsed from (translation-aware), so its `fallback` offsets can be sliced. */
|
|
mdSource?: string;
|
|
};
|
|
/**
|
|
* Removes null values for known properties values.
|
|
* Adds a property `md` to the message with the parsed message if is not provided.
|
|
* if has `attachments` property, but attachment is missing `md` property, it will be added.
|
|
* if translation is enabled and message contains `translations` property, it will be replaced by the parsed message.
|
|
* @param message The message to be parsed.
|
|
* @param parseOptions The options to be used in the parser.
|
|
* @param autoTranslateOptions The auto translate options to be used in the parser.
|
|
* @returns message normalized.
|
|
*/
|
|
export const parseMessageTextToAstMarkdown = <
|
|
TMessage extends IMessage & Partial<ITranslatedMessage> = IMessage & Partial<ITranslatedMessage>,
|
|
>(
|
|
message: TMessage,
|
|
parseOptions: Options,
|
|
autoTranslateOptions: AutoTranslateOptions,
|
|
): MessageWithMdEnforced => {
|
|
const msg = removePossibleNullMessageValues(message);
|
|
const { showAutoTranslate, autoTranslateLanguage } = autoTranslateOptions;
|
|
const translations = autoTranslateLanguage && isTranslatedMessage(msg) && msg.translations;
|
|
const translated = showAutoTranslate(message);
|
|
|
|
const text = (translated && translations && translations[autoTranslateLanguage]) || msg.msg;
|
|
|
|
return {
|
|
...msg,
|
|
md: isE2EEMessage(message) || translated ? textToMessageToken(text, parseOptions) : (msg.md ?? textToMessageToken(text, parseOptions)),
|
|
// `text` is the exact string `md` was parsed from (translation/E2EE-aware, and equal to
|
|
// `msg.msg` otherwise), so block `fallback` offsets slice against the right source.
|
|
mdSource: text,
|
|
...(msg.attachments && {
|
|
attachments: parseMessageAttachments(msg.attachments, parseOptions, { autoTranslateLanguage, translated }),
|
|
}),
|
|
};
|
|
};
|
|
|
|
export const parseMessageAttachment = <T extends MessageAttachment>(
|
|
attachment: T,
|
|
parseOptions: Options,
|
|
autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean },
|
|
): T => {
|
|
const { translated, autoTranslateLanguage } = autoTranslateOptions;
|
|
if (!attachment.text && !attachment.description) {
|
|
return attachment;
|
|
}
|
|
|
|
if (isQuoteAttachment(attachment) && attachment.attachments) {
|
|
attachment.attachments = parseMessageAttachments(attachment.attachments, parseOptions, autoTranslateOptions);
|
|
}
|
|
|
|
const text =
|
|
(isTranslatedAttachment(attachment) && autoTranslateLanguage && attachment?.translations?.[autoTranslateLanguage]) ||
|
|
attachment.text ||
|
|
attachment.description ||
|
|
'';
|
|
|
|
if (isFileAttachment(attachment) && attachment.description) {
|
|
attachment.descriptionMd =
|
|
translated || isEncryptedMessageAttachment(attachment)
|
|
? textToMessageToken(text, parseOptions)
|
|
: (attachment.descriptionMd ?? textToMessageToken(text, parseOptions));
|
|
}
|
|
|
|
return {
|
|
...attachment,
|
|
md: translated ? textToMessageToken(text, parseOptions) : (attachment.md ?? textToMessageToken(text, parseOptions)),
|
|
};
|
|
};
|
|
|
|
export const parseMessageAttachments = <T extends MessageAttachment>(
|
|
attachments: T[],
|
|
parseOptions: Options,
|
|
autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean },
|
|
): T[] => attachments.map((attachment) => parseMessageAttachment(attachment, parseOptions, autoTranslateOptions));
|
|
|
|
const isNotNullOrUndefined = (value: unknown): boolean => value !== null && value !== undefined;
|
|
|
|
// In a previous version of the app, some values were being set to null.
|
|
// This is a workaround to remove those null values.
|
|
// A migration script should be created to remove this code.
|
|
export const removePossibleNullMessageValues = <TMessage extends IMessage = IMessage>({
|
|
editedBy,
|
|
editedAt,
|
|
emoji,
|
|
avatar,
|
|
alias,
|
|
customFields,
|
|
groupable,
|
|
attachments,
|
|
reactions,
|
|
...message
|
|
}: any): TMessage => ({
|
|
...message,
|
|
...(isNotNullOrUndefined(editedBy) && { editedBy }),
|
|
...(isNotNullOrUndefined(editedAt) && { editedAt }),
|
|
...(isNotNullOrUndefined(emoji) && { emoji }),
|
|
...(isNotNullOrUndefined(avatar) && { avatar }),
|
|
...(isNotNullOrUndefined(alias) && { alias }),
|
|
...(isNotNullOrUndefined(customFields) && { customFields }),
|
|
...(isNotNullOrUndefined(groupable) && { groupable }),
|
|
...(isNotNullOrUndefined(attachments) && { attachments }),
|
|
...(isNotNullOrUndefined(reactions) && { reactions }),
|
|
});
|
|
|
|
const textToMessageToken = (textOrRoot: string | Root, parseOptions: Options): Root => {
|
|
if (!textOrRoot) {
|
|
return [];
|
|
}
|
|
|
|
if (isParsedMessage(textOrRoot)) {
|
|
return textOrRoot;
|
|
}
|
|
|
|
const parsedMessage = textOrRoot.length > getMarkdownParserLimit() ? toPlainTextRoot(textOrRoot) : parse(textOrRoot, parseOptions);
|
|
|
|
const parsedMessageCleaned = parsedMessage[0].type !== 'LINE_BREAK' ? parsedMessage : (parsedMessage.slice(1) as Root);
|
|
|
|
return parsedMessageCleaned;
|
|
};
|