@buerokratt-ria/common-gui-components 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.prettierrc ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "printWidth": 120
3
+ }
package/CHANGELOG.md CHANGED
@@ -1,7 +1,13 @@
1
1
  # Changelog
2
+
2
3
  All changes to this project will be documented in this file.
4
+
3
5
  ## Template [MajorVersion.MediterraneanVersion.MinorVersion] - DD-MM-YYYY
4
6
 
7
+ ## [0.0.9] - 01-04-2025
8
+
9
+ - Prevent end-users from spoofing URLs in messages
10
+
5
11
  ## [0.0.8] - 21-03-2025
6
12
 
7
13
  - Fixated markdown-to-jsx to version 7.7.3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buerokratt-ria/common-gui-components",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Common GUI components and pre defined templates.",
5
5
  "main": "index.ts",
6
6
  "author": "ExiRai",
@@ -1,270 +1,240 @@
1
- import React, { FC, useEffect, useMemo, useRef, useState } from 'react';
2
- import { format } from 'date-fns';
3
- import clsx from 'clsx';
4
- import {
5
- MdCheck,
6
- MdClose,
7
- MdOutlineCreate,
8
- MdOutlineCheck,
9
- } from 'react-icons/md';
10
- import { Message } from '../../types/message';
11
- import { CHAT_EVENTS, MessageStatus } from '../../types/chat';
12
- import Markdownify from './Markdownify';
13
- import { useTranslation } from 'react-i18next';
14
- import './Typing.scss';
15
- import { parseButtons, parseOptions } from '../../utils/parse-utils';
16
- import ButtonMessage from '../ButtonMessage';
17
- import OptionMessage from '../OptionMessage';
18
- import Track from '../Track';
19
- import Icon from '../Icon';
20
- import { HiOutlinePencil } from 'react-icons/hi';
21
- import Button from '../Button';
22
- import FormTextarea from '../FormElements/FormTextarea';
23
- import { apiDev } from '../../services';
24
- import { useToast } from '../../hooks';
25
- import { useMutation } from '@tanstack/react-query';
26
- import { AxiosError } from 'axios';
1
+ import React, { FC, useEffect, useMemo, useRef, useState } from "react";
2
+ import { format } from "date-fns";
3
+ import clsx from "clsx";
4
+ import { MdCheck, MdClose, MdOutlineCreate, MdOutlineCheck } from "react-icons/md";
5
+ import { Message } from "../../types/message";
6
+ import { CHAT_EVENTS, MessageStatus } from "../../types/chat";
7
+ import Markdownify from "./Markdownify";
8
+ import { useTranslation } from "react-i18next";
9
+ import "./Typing.scss";
10
+ import { parseButtons, parseOptions } from "../../utils/parse-utils";
11
+ import ButtonMessage from "../ButtonMessage";
12
+ import OptionMessage from "../OptionMessage";
13
+ import Track from "../Track";
14
+ import Icon from "../Icon";
15
+ import { HiOutlinePencil } from "react-icons/hi";
16
+ import Button from "../Button";
17
+ import FormTextarea from "../FormElements/FormTextarea";
18
+ import { apiDev } from "../../services";
19
+ import { useToast } from "../../hooks";
20
+ import { useMutation } from "@tanstack/react-query";
21
+ import { AxiosError } from "axios";
27
22
 
28
23
  type ChatMessageProps = {
29
- message: Message;
30
- onSelect: (message: Message) => void;
31
- selected: boolean;
32
- editableMessage?: boolean;
24
+ message: Message;
25
+ onSelect: (message: Message) => void;
26
+ selected: boolean;
27
+ editableMessage?: boolean;
33
28
  };
34
29
 
35
- const ChatMessage: FC<ChatMessageProps> = ({
36
- message,
37
- onSelect,
38
- selected,
39
- editableMessage,
40
- }) => {
41
- const { t } = useTranslation();
30
+ const ChatMessage: FC<ChatMessageProps> = ({ message, onSelect, selected, editableMessage }) => {
31
+ const { t } = useTranslation();
42
32
 
43
- const buttons = useMemo(() => parseButtons(message), [message.buttons]);
44
- const options = useMemo(() => parseOptions(message), [message.options]);
45
- const [isEditing, setIsEditing] = useState(false);
46
- const [content, setContent] = useState(message.content ?? '');
47
- const [inputContent, setInputContent] = useState(content);
48
- const [messageHeight, setMessageHeight] = useState(0);
49
- const messageRef = useRef<HTMLButtonElement>(null);
50
- const toast = useToast();
33
+ const buttons = useMemo(() => parseButtons(message), [message.buttons]);
34
+ const options = useMemo(() => parseOptions(message), [message.options]);
35
+ const [isEditing, setIsEditing] = useState(false);
36
+ const [content, setContent] = useState(message.content ?? "");
37
+ const [inputContent, setInputContent] = useState(content);
38
+ const [messageHeight, setMessageHeight] = useState(0);
39
+ const messageRef = useRef<HTMLButtonElement>(null);
40
+ const toast = useToast();
51
41
 
52
- useEffect(() => {
53
- setMessageHeight(messageRef?.current?.clientHeight ?? 0);
54
- });
42
+ useEffect(() => {
43
+ setMessageHeight(messageRef?.current?.clientHeight ?? 0);
44
+ });
55
45
 
56
- const approveMessage = useMutation({
57
- mutationFn: (data: { chatId: string; messageId: string }) => {
58
- return apiDev.post(`chats/messages/approve-validation`, {
59
- chatId: data.chatId,
60
- messageId: data.messageId,
61
- });
62
- },
63
- onSuccess: async () => {
64
- toast.open({
65
- type: 'success',
66
- title: t('global.notification'),
67
- message: t('chat.validations.messageApproved'),
68
- });
69
- return true;
70
- },
71
- onError: (error: AxiosError) => {
72
- toast.open({
73
- type: 'error',
74
- title: t('global.notificationError'),
75
- message: error.message,
76
- });
77
- return false;
78
- },
79
- });
46
+ const approveMessage = useMutation({
47
+ mutationFn: (data: { chatId: string; messageId: string }) => {
48
+ return apiDev.post(`chats/messages/approve-validation`, {
49
+ chatId: data.chatId,
50
+ messageId: data.messageId,
51
+ });
52
+ },
53
+ onSuccess: async () => {
54
+ toast.open({
55
+ type: "success",
56
+ title: t("global.notification"),
57
+ message: t("chat.validations.messageApproved"),
58
+ });
59
+ return true;
60
+ },
61
+ onError: (error: AxiosError) => {
62
+ toast.open({
63
+ type: "error",
64
+ title: t("global.notificationError"),
65
+ message: error.message,
66
+ });
67
+ return false;
68
+ },
69
+ });
80
70
 
81
- return (
82
- <div className={clsx('active-chat__messageContainer')}>
83
- <div
84
- className={clsx('active-chat__message', {
85
- 'active-chat__message--selected': selected,
86
- })}
87
- >
88
- {(message.event === CHAT_EVENTS.GREETING ||
89
- message.event === CHAT_EVENTS.WAITING_VALIDATION ||
90
- message.event === CHAT_EVENTS.APPROVED_VALIDATION ||
91
- !message.event) && (
92
- <>
93
- <button
94
- className={clsx('active-chat__message-text')}
95
- ref={messageRef}
96
- onClick={() => onSelect(message)}
97
- >
98
- <Track direction={isEditing ? 'vertical' : 'horizontal'}>
99
- {message.event === CHAT_EVENTS.WAITING_VALIDATION &&
100
- isEditing && (
101
- <FormTextarea
102
- name={''}
103
- label={''}
104
- minRows={1}
105
- maxRows={-1}
106
- maxLength={-1}
107
- style={{
108
- backgroundColor: 'transparent',
109
- border: 'none',
110
- width: '400px',
111
- }}
112
- defaultValue={content}
113
- onChange={(e) => {
114
- setInputContent(e.target.value);
115
- }}
116
- onClick={(e) => {
117
- e.stopPropagation();
118
- }}
119
- autoFocus
120
- />
121
- )}
122
- {!isEditing && <Markdownify message={content} />}
123
- {!message.content && options.length > 0 && 'ok'}
124
- {editableMessage && !isEditing && (
125
- <MdOutlineCreate className="active-chat__edit-icon" />
126
- )}
127
- {message.event === CHAT_EVENTS.WAITING_VALIDATION && (
128
- <button
129
- style={{
130
- color: 'white',
131
- alignSelf: 'end',
132
- paddingTop: '0.3rem',
133
- }}
134
- onClick={(event) => {
135
- event.stopPropagation();
136
- setMessageHeight(messageRef?.current?.clientHeight ?? 0);
137
- setIsEditing(true);
138
- }}
139
- >
140
- <Icon
141
- icon={<HiOutlinePencil fontSize={18} />}
142
- size="medium"
143
- />
144
- </button>
145
- )}
146
- </Track>
147
- </button>
148
- <Track
149
- direction="horizontal"
150
- style={{
151
- height: messageHeight,
152
- justifyContent: 'center',
153
- }}
154
- >
155
- <div>
156
- <time
157
- dateTime={message.created}
158
- className="active-chat__message-date"
159
- style={{ alignSelf: 'center' }}
160
- >
161
- {format(new Date(message.created), 'HH:mm:ss')}
162
- </time>
163
- </div>
164
- {message.event === CHAT_EVENTS.WAITING_VALIDATION &&
165
- isEditing && (
166
- <Track
167
- style={{
168
- position: 'absolute',
169
- bottom: 0,
170
- }}
171
- gap={2}
172
- >
173
- <Icon
174
- style={{ cursor: 'pointer' }}
175
- icon={
176
- <MdCheck
177
- fontSize={22}
178
- color="#308653"
179
- onClick={async () => {
180
- if (inputContent.length === 0) return;
181
- try {
182
- await apiDev.post('chats/messages/edit', {
183
- chatId: message.chatId,
184
- messageId: message.id ?? '',
185
- content: inputContent,
186
- });
187
- setIsEditing(false);
188
- setContent(inputContent);
189
- toast.open({
190
- type: 'success',
191
- title: t('global.notification'),
192
- message: t('chat.validations.messageChanged'),
193
- });
194
- } catch (_) {
195
- toast.open({
196
- type: 'error',
197
- title: t('global.notificationError'),
198
- message: t(
199
- 'chat.validations.messageChangeFailed'
200
- ),
201
- });
202
- }
203
- }}
204
- />
205
- }
206
- size="medium"
207
- />
208
- <Icon
209
- style={{ cursor: 'pointer' }}
210
- icon={
211
- <MdClose
212
- fontSize={22}
213
- color="#D73E3E"
214
- onClick={() => {
215
- setIsEditing(false);
216
- setInputContent(content ?? '');
217
- }}
218
- />
219
- }
220
- size="medium"
221
- />
222
- </Track>
223
- )}
224
- </Track>
225
- {selected && (
226
- <div className="active-chat__selection-icon">
227
- <MdOutlineCheck />
228
- </div>
229
- )}
230
- </>
71
+ return (
72
+ <div className={clsx("active-chat__messageContainer")}>
73
+ <div
74
+ className={clsx("active-chat__message", {
75
+ "active-chat__message--selected": selected,
76
+ })}
77
+ >
78
+ {(message.event === CHAT_EVENTS.GREETING ||
79
+ message.event === CHAT_EVENTS.WAITING_VALIDATION ||
80
+ message.event === CHAT_EVENTS.APPROVED_VALIDATION ||
81
+ !message.event) && (
82
+ <>
83
+ <button className={clsx("active-chat__message-text")} ref={messageRef} onClick={() => onSelect(message)}>
84
+ <Track direction={isEditing ? "vertical" : "horizontal"}>
85
+ {message.event === CHAT_EVENTS.WAITING_VALIDATION && isEditing && (
86
+ <FormTextarea
87
+ name={""}
88
+ label={""}
89
+ minRows={1}
90
+ maxRows={-1}
91
+ maxLength={-1}
92
+ style={{
93
+ backgroundColor: "transparent",
94
+ border: "none",
95
+ width: "400px",
96
+ }}
97
+ defaultValue={content}
98
+ onChange={(e) => {
99
+ setInputContent(e.target.value);
100
+ }}
101
+ onClick={(e) => {
102
+ e.stopPropagation();
103
+ }}
104
+ autoFocus
105
+ />
231
106
  )}
232
- </div>
233
- {message.event === CHAT_EVENTS.WAITING_VALIDATION && (
234
- <Button
235
- appearance="success"
107
+ {!isEditing && <Markdownify message={content} sanitizeLinks={message.authorRole === "end-user"} />}
108
+ {!message.content && options.length > 0 && "ok"}
109
+ {editableMessage && !isEditing && <MdOutlineCreate className="active-chat__edit-icon" />}
110
+ {message.event === CHAT_EVENTS.WAITING_VALIDATION && (
111
+ <button
236
112
  style={{
237
- borderRadius: '50px',
238
- marginTop: '5px',
239
- marginLeft: '-55px',
240
- paddingLeft: '40px',
241
- paddingRight: '40px',
242
- display: 'absolute',
243
- right: '10',
113
+ color: "white",
114
+ alignSelf: "end",
115
+ paddingTop: "0.3rem",
244
116
  }}
245
- onClick={() => {
246
- approveMessage.mutate({
247
- chatId: message.chatId,
248
- messageId: message.id ?? '',
249
- });
117
+ onClick={(event) => {
118
+ event.stopPropagation();
119
+ setMessageHeight(messageRef?.current?.clientHeight ?? 0);
120
+ setIsEditing(true);
250
121
  }}
122
+ >
123
+ <Icon icon={<HiOutlinePencil fontSize={18} />} size="medium" />
124
+ </button>
125
+ )}
126
+ </Track>
127
+ </button>
128
+ <Track
129
+ direction="horizontal"
130
+ style={{
131
+ height: messageHeight,
132
+ justifyContent: "center",
133
+ }}
134
+ >
135
+ <div>
136
+ <time dateTime={message.created} className="active-chat__message-date" style={{ alignSelf: "center" }}>
137
+ {format(new Date(message.created), "HH:mm:ss")}
138
+ </time>
139
+ </div>
140
+ {message.event === CHAT_EVENTS.WAITING_VALIDATION && isEditing && (
141
+ <Track
142
+ style={{
143
+ position: "absolute",
144
+ bottom: 0,
145
+ }}
146
+ gap={2}
251
147
  >
252
- {t('chat.validations.confirmAnswer')}
253
- </Button>
148
+ <Icon
149
+ style={{ cursor: "pointer" }}
150
+ icon={
151
+ <MdCheck
152
+ fontSize={22}
153
+ color="#308653"
154
+ onClick={async () => {
155
+ if (inputContent.length === 0) return;
156
+ try {
157
+ await apiDev.post("chats/messages/edit", {
158
+ chatId: message.chatId,
159
+ messageId: message.id ?? "",
160
+ content: inputContent,
161
+ });
162
+ setIsEditing(false);
163
+ setContent(inputContent);
164
+ toast.open({
165
+ type: "success",
166
+ title: t("global.notification"),
167
+ message: t("chat.validations.messageChanged"),
168
+ });
169
+ } catch (_) {
170
+ toast.open({
171
+ type: "error",
172
+ title: t("global.notificationError"),
173
+ message: t("chat.validations.messageChangeFailed"),
174
+ });
175
+ }
176
+ }}
177
+ />
178
+ }
179
+ size="medium"
180
+ />
181
+ <Icon
182
+ style={{ cursor: "pointer" }}
183
+ icon={
184
+ <MdClose
185
+ fontSize={22}
186
+ color="#D73E3E"
187
+ onClick={() => {
188
+ setIsEditing(false);
189
+ setInputContent(content ?? "");
190
+ }}
191
+ />
192
+ }
193
+ size="medium"
194
+ />
195
+ </Track>
196
+ )}
197
+ </Track>
198
+ {selected && (
199
+ <div className="active-chat__selection-icon">
200
+ <MdOutlineCheck />
201
+ </div>
254
202
  )}
255
- {buttons.length > 0 && <ButtonMessage buttons={buttons} />}
256
- {options.length > 0 && <OptionMessage options={options} />}
257
- {message.event === CHAT_EVENTS.READ ? (
258
- <span className="active-chat__message-status">
259
- {t('global.read')}
260
- <time dateTime={message.authorTimestamp}>
261
- {' '}
262
- {format(new Date(message.authorTimestamp), 'HH:mm:ss')}
263
- </time>
203
+ </>
204
+ )}
205
+ </div>
206
+ {message.event === CHAT_EVENTS.WAITING_VALIDATION && (
207
+ <Button
208
+ appearance="success"
209
+ style={{
210
+ borderRadius: "50px",
211
+ marginTop: "5px",
212
+ marginLeft: "-55px",
213
+ paddingLeft: "40px",
214
+ paddingRight: "40px",
215
+ display: "absolute",
216
+ right: "10",
217
+ }}
218
+ onClick={() => {
219
+ approveMessage.mutate({
220
+ chatId: message.chatId,
221
+ messageId: message.id ?? "",
222
+ });
223
+ }}
224
+ >
225
+ {t("chat.validations.confirmAnswer")}
226
+ </Button>
227
+ )}
228
+ {buttons.length > 0 && <ButtonMessage buttons={buttons} />}
229
+ {options.length > 0 && <OptionMessage options={options} />}
230
+ {message.event === CHAT_EVENTS.READ ? (
231
+ <span className="active-chat__message-status">
232
+ {t("global.read")}
233
+ <time dateTime={message.authorTimestamp}> {format(new Date(message.authorTimestamp), "HH:mm:ss")}</time>
264
234
  </span>
265
- ) : null}{' '}
266
- </div>
267
- );
235
+ ) : null}{" "}
236
+ </div>
237
+ );
268
238
  };
269
239
 
270
240
  export default ChatMessage;
@@ -1,49 +1,69 @@
1
- import React, { useState } from 'react';
2
- import Markdown from 'markdown-to-jsx';
3
- import './Chat.scss';
1
+ import React, { useState } from "react";
2
+ import Markdown from "markdown-to-jsx";
3
+ import "./Chat.scss";
4
4
 
5
5
  interface MarkdownifyProps {
6
- message: string | undefined;
6
+ message: string | undefined;
7
+ sanitizeLinks?: boolean;
7
8
  }
8
9
 
9
- const LinkPreview: React.FC<{ href: string; children: React.ReactNode }> = ({ href, children }) => {
10
- const [hasError, setHasError] = useState(false);
11
- const basicAuthPattern = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\/[^@]+@/;
10
+ const LinkPreview: React.FC<{
11
+ href: string;
12
+ children: React.ReactNode;
13
+ sanitizeLinks: boolean;
14
+ }> = ({ href, children, sanitizeLinks }) => {
15
+ const [hasError, setHasError] = useState(false);
16
+ const basicAuthPattern = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\/[^@]+@/;
12
17
 
13
- if (basicAuthPattern.test(href)) {
14
- return null;
15
- }
18
+ if (basicAuthPattern.test(href)) {
19
+ return null;
20
+ }
16
21
 
17
- return !hasError ? (
18
- <img
19
- src={href}
20
- alt={typeof children === "string" ? children : "Preview"}
21
- style={{ maxWidth: "100%", height: "auto", borderRadius: "20px" }}
22
- onError={() => setHasError(true)}
23
- />
24
- ) : (
25
- <a href={href} target="_blank" rel="noopener noreferrer">
26
- {children}
27
- </a>
22
+ if (sanitizeLinks) {
23
+ return (
24
+ <a href={href} target="_blank" rel="noopener noreferrer">
25
+ {href}
26
+ </a>
28
27
  );
28
+ }
29
+
30
+ return !hasError ? (
31
+ <img
32
+ src={href}
33
+ alt={typeof children === "string" ? children : "Preview"}
34
+ style={{ maxWidth: "100%", height: "auto", borderRadius: "20px" }}
35
+ onError={() => setHasError(true)}
36
+ />
37
+ ) : (
38
+ <a href={href} target="_blank" rel="noopener noreferrer">
39
+ {children}
40
+ </a>
41
+ );
29
42
  };
30
43
 
31
- const Markdownify: React.FC<MarkdownifyProps> = ({ message }) => (
32
- <div className={'reset'}>
33
- <Markdown
34
- options={{
35
- enforceAtxHeadings: true,
36
- overrides: {
37
- a: {
38
- component: LinkPreview,
39
- },
40
- },
41
- disableParsingRawHTML: true,
42
- }}
43
- >
44
- {message?.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) => { return String.fromCharCode(parseInt(hex, 16)); }).replace(/(?<=\n)\d+\.\s/g, "\n\n$&") ?? ""}
45
- </Markdown>
46
- </div>
44
+ const Markdownify: React.FC<MarkdownifyProps> = ({ message, sanitizeLinks = false }) => (
45
+ <div className={"reset"}>
46
+ <Markdown
47
+ options={{
48
+ enforceAtxHeadings: true,
49
+ overrides: {
50
+ a: {
51
+ component: LinkPreview,
52
+ props: {
53
+ sanitizeLinks,
54
+ },
55
+ },
56
+ },
57
+ disableParsingRawHTML: true,
58
+ }}
59
+ >
60
+ {message
61
+ ?.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) => {
62
+ return String.fromCharCode(parseInt(hex, 16));
63
+ })
64
+ .replace(/(?<=\n)\d+\.\s/g, "\n\n$&") ?? ""}
65
+ </Markdown>
66
+ </div>
47
67
  );
48
68
 
49
69
  export default Markdownify;
@@ -1,67 +1,64 @@
1
- import React, { FC, useMemo } from 'react';
2
- import { format } from 'date-fns';
3
- import { Message } from '../../types/message';
4
- import Markdownify from '../Chat/Markdownify';
5
- import { parseButtons, parseOptions } from '../../utils/parse-utils';
6
- import ButtonMessage from '../ButtonMessage';
7
- import OptionMessage from '../OptionMessage';
8
- import {useTranslation} from "react-i18next";
1
+ import React, { FC, useMemo } from "react";
2
+ import { format } from "date-fns";
3
+ import { Message } from "../../types/message";
4
+ import Markdownify from "../Chat/Markdownify";
5
+ import { parseButtons, parseOptions } from "../../utils/parse-utils";
6
+ import ButtonMessage from "../ButtonMessage";
7
+ import OptionMessage from "../OptionMessage";
8
+ import { useTranslation } from "react-i18next";
9
9
  import { ToastContextType } from "../../context";
10
10
 
11
11
  type ChatMessageProps = {
12
- message: Message;
13
- onMessageClick?: (message: Message) => void;
14
- toastContext: ToastContextType | null;
12
+ message: Message;
13
+ onMessageClick?: (message: Message) => void;
14
+ toastContext: ToastContextType | null;
15
15
  };
16
16
 
17
17
  const ChatMessage: FC<ChatMessageProps> = ({ message, onMessageClick, toastContext }) => {
18
- const buttons = useMemo(() => parseButtons(message), [message.buttons]);
19
- const options = useMemo(() => parseOptions(message), [message.options]);
20
- const { t } = useTranslation();
21
- const toast = toastContext;
18
+ const buttons = useMemo(() => parseButtons(message), [message.buttons]);
19
+ const options = useMemo(() => parseOptions(message), [message.options]);
20
+ const { t } = useTranslation();
21
+ const toast = toastContext;
22
22
 
23
- const handleContextMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
24
- event.preventDefault();
25
- const content = message.content ?? '';
26
- navigator.clipboard
27
- .writeText(content)
28
- .then(() => {
29
- toast?.open({
30
- type: 'success',
31
- title: t('global.notification'),
32
- message: t('toast.copied'),
33
- });
34
- })
35
- .catch((err) => {
36
- toast?.open({
37
- type: 'error',
38
- title: t('global.notification'),
39
- message: err?.message,
40
- });
41
- });
42
- };
23
+ const handleContextMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
24
+ event.preventDefault();
25
+ const content = message.content ?? "";
26
+ navigator.clipboard
27
+ .writeText(content)
28
+ .then(() => {
29
+ toast?.open({
30
+ type: "success",
31
+ title: t("global.notification"),
32
+ message: t("toast.copied"),
33
+ });
34
+ })
35
+ .catch((err) => {
36
+ toast?.open({
37
+ type: "error",
38
+ title: t("global.notification"),
39
+ message: err?.message,
40
+ });
41
+ });
42
+ };
43
43
 
44
- return (
45
- <>
46
- <div className="historical-chat__message">
47
- <button
48
- className="historical-chat__message-text"
49
- onClick={onMessageClick ? () => onMessageClick(message) : undefined}
50
- onContextMenu={handleContextMenu}
51
- >
52
- <Markdownify message={message.content ?? ''} />
53
- </button>
54
- <time
55
- dateTime={message.created}
56
- className="historical-chat__message-date"
57
- >
58
- {format(new Date(message.created ?? ''), 'HH:mm:ss')}
59
- </time>
60
- </div>
61
- {buttons.length > 0 && <ButtonMessage buttons={buttons} />}
62
- {options.length > 0 && <OptionMessage options={options} />}
63
- </>
64
- );
44
+ return (
45
+ <>
46
+ <div className="historical-chat__message">
47
+ <button
48
+ className="historical-chat__message-text"
49
+ onClick={onMessageClick ? () => onMessageClick(message) : undefined}
50
+ onContextMenu={handleContextMenu}
51
+ >
52
+ <Markdownify message={message.content ?? ""} sanitizeLinks={message.authorRole === "end-user"} />
53
+ </button>
54
+ <time dateTime={message.created} className="historical-chat__message-date">
55
+ {format(new Date(message.created ?? ""), "HH:mm:ss")}
56
+ </time>
57
+ </div>
58
+ {buttons.length > 0 && <ButtonMessage buttons={buttons} />}
59
+ {options.length > 0 && <OptionMessage options={options} />}
60
+ </>
61
+ );
65
62
  };
66
63
 
67
64
  export default ChatMessage;