diff --git a/DSL/Resql/users/POST/get-chat-by-id.sql b/DSL/Resql/users/POST/get-chat-by-id.sql
index a7c3daaee..2784931bf 100644
--- a/DSL/Resql/users/POST/get-chat-by-id.sql
+++ b/DSL/Resql/users/POST/get-chat-by-id.sql
@@ -12,6 +12,15 @@ csa_title_config AS (
AND id IN (SELECT max(id) FROM configuration WHERE key = 'is_csa_title_visible' GROUP BY key)
AND NOT deleted
),
+chat_history_comments AS (
+ SELECT
+ comment,
+ chat_id,
+ created,
+ author_display_name
+ FROM chat_history_comments
+ where id in (SELECT MAX(id) AS maxId FROM chat_history_comments GROUP BY chat_id)
+),
chatById AS(
SELECT
base_id,
@@ -72,8 +81,12 @@ SELECT c.base_id AS id,
ELSE ''
END) AS csa_title,
m.content AS last_message,
- m.updated AS last_message_timestamp
+ m.updated AS last_message_timestamp,
+ s.comment,
+ s.created as comment_added_date,
+ s.author_display_name as comment_author
FROM chatById AS c
JOIN message AS m ON c.base_id = m.chat_base_id
+LEFT JOIN chat_history_comments AS s ON s.chat_id = m.chat_base_id
ORDER BY m.updated DESC
LIMIT 1;
diff --git a/GUI/package-lock.json b/GUI/package-lock.json
index d0e72656d..52e002dac 100644
--- a/GUI/package-lock.json
+++ b/GUI/package-lock.json
@@ -8,7 +8,7 @@
"name": "byk-training-module-gui",
"version": "0.0.0",
"dependencies": {
- "@buerokratt-ria/common-gui-components": "^0.0.42",
+ "@buerokratt-ria/common-gui-components": "^0.0.43",
"@buerokratt-ria/header": "^0.1.47",
"@buerokratt-ria/menu": "^0.2.10",
"@buerokratt-ria/styles": "^0.0.1",
@@ -1821,9 +1821,9 @@
}
},
"node_modules/@buerokratt-ria/common-gui-components": {
- "version": "0.0.42",
- "resolved": "https://registry.npmjs.org/@buerokratt-ria/common-gui-components/-/common-gui-components-0.0.42.tgz",
- "integrity": "sha512-FOPZBhsbIw3Ii1wi21WzgyAWmaOi6X2/OFE4+bP46tRipe8bsuxK8ZSJ1V5tyWXh6HCCoU2ykXTpdEl9e546hQ==",
+ "version": "0.0.43",
+ "resolved": "https://registry.npmjs.org/@buerokratt-ria/common-gui-components/-/common-gui-components-0.0.43.tgz",
+ "integrity": "sha512-xyJyfHo02VARBA7B09vmDxcVZP9a9DFEhm5IADSF5/TX4j8YkzbhEKA4dw1MNhICnnrNrWRo6XAdHDQe84hU2Q==",
"peerDependencies": {
"@buerokratt-ria/header": "^0.1.20",
"@fontsource/roboto": "^4.5.8",
diff --git a/GUI/package.json b/GUI/package.json
index fff2df425..68d7ba0be 100644
--- a/GUI/package.json
+++ b/GUI/package.json
@@ -14,7 +14,7 @@
"@buerokratt-ria/header": "^0.1.47",
"@buerokratt-ria/menu": "^0.2.10",
"@buerokratt-ria/styles": "^0.0.1",
- "@buerokratt-ria/common-gui-components": "^0.0.42",
+ "@buerokratt-ria/common-gui-components": "^0.0.43",
"@fontsource/roboto": "^4.5.8",
"@formkit/auto-animate": "^1.0.0-beta.5",
"@radix-ui/react-accessible-icon": "^1.0.1",
diff --git a/GUI/src/components/HistoricalChat/Markdownify.tsx b/GUI/src/components/HistoricalChat/Markdownify.tsx
index 9e23aaf3c..e55aa56c3 100644
--- a/GUI/src/components/HistoricalChat/Markdownify.tsx
+++ b/GUI/src/components/HistoricalChat/Markdownify.tsx
@@ -7,25 +7,57 @@ interface MarkdownifyProps {
message: string | undefined;
}
-const LinkPreview: React.FC<{ href: string; children: React.ReactNode }> = ({ href, children }) => {
+const isValidImageUrl = (s: string): boolean => {
+ try {
+ const u = new URL(s);
+ if (!/^(https?|data):$/i.test(u.protocol)) return false;
+ if (s.startsWith('data:image/')) {
+ return /^data:image\/(png|jpe?g|gif|webp|svg\+xml|bmp);(base64,|charset=utf-8;)/i.test(s);
+ }
+ const path = u.pathname.toLowerCase();
+ const search = u.search.toLowerCase();
+ if (/\.(png|jpe?g|gif|webp|svg|ico|bmp|tiff?|avif|heic|heif|apng)([?#]|$)/i.test(path)) {
+ return true;
+ }
+ if (/\/(images?|img|photos?|pictures?|media|uploads?|thumb|avatar)\//i.test(path)) {
+ return true;
+ }
+ return /[?&](format|type|image|img|photo)=(png|jpe?g|gif|webp|svg|ico)/i.test(search);
+ } catch {
+ return false;
+ }
+};
+
+const LinkPreview: React.FC<{
+ href: string;
+ children: React.ReactNode;
+}> = ({ href, children }) => {
const [hasError, setHasError] = useState(false);
const basicAuthPattern = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\/[^@]+@/;
if (basicAuthPattern.test(href)) {
return null;
}
-
- return !hasError ? (
+
+ if (!isValidImageUrl(href)) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return hasError ? (
+
+ {children}
+
+ ) : (
setHasError(true)}
/>
- ) : (
-
- {children}
-
);
};
@@ -41,8 +73,14 @@ function formatMessage(message?: string): string {
.replaceAll(/\\?\$v\w*/g, '')
.replaceAll(/\\?\$g\w*/g, '');
- return filteredMessage
- .replaceAll(/([0-9A-Fa-f]+);/g, (_, hex: string) => String.fromCharCode(parseInt(hex, 16)))
+ const dataImagePattern = /((?:^|\s))(data:image\/[a-z0-9+]+;[^)\s]+)/gi;
+ const finalMessage = filteredMessage.replaceAll(
+ dataImagePattern,
+ (_, prefix, dataUrl) => `${prefix}[image](${dataUrl})`
+ );
+
+ return finalMessage
+ .replaceAll(/([0-9A-F]+);/gi, (_, hex: string) => String.fromCharCode(parseInt(hex, 16)))
.replaceAll('&', '&')
.replaceAll('>', '>')
.replaceAll('<', '<')
@@ -50,7 +88,7 @@ function formatMessage(message?: string): string {
.replaceAll(''', "'")
.replaceAll(''', "'")
.replaceAll(/(^|\n)(\d{4})\.\s/g, (match, prefix, year) => {
- const remainingText = filteredMessage.substring(filteredMessage.indexOf(match) + match.length);
+ const remainingText = finalMessage.substring(finalMessage.indexOf(match) + match.length);
const sentenceEnd = remainingText.indexOf('\n\n');
if (sentenceEnd !== -1) {
const currentSentence = remainingText.substring(0, sentenceEnd);
@@ -60,7 +98,7 @@ function formatMessage(message?: string): string {
}
return `${prefix}${year}\\. `;
})
- .replaceAll(/(?<=\n)\d+\.\s/g, hasSpecialFormat(filteredMessage) ? '\n\n$&' : '$&')
+ .replaceAll(/(?<=\n)\d+\.\s/g, hasSpecialFormat(finalMessage) ? '\n\n$&' : '$&')
.replaceAll(/^(\s+)/g, (match) => match.replaceAll(' ', ' '));
}