@buerokratt-ria/common-gui-components 0.0.8 → 0.0.10
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
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
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.10] - 14-04-2025
|
|
8
|
+
|
|
9
|
+
- Changed dialog visibility
|
|
10
|
+
|
|
11
|
+
## [0.0.9] - 01-04-2025
|
|
12
|
+
|
|
13
|
+
- Prevent end-users from spoofing URLs in messages
|
|
14
|
+
|
|
5
15
|
## [0.0.8] - 21-03-2025
|
|
6
16
|
|
|
7
17
|
- Fixated markdown-to-jsx to version 7.7.3
|
package/package.json
CHANGED
|
@@ -1,270 +1,240 @@
|
|
|
1
|
-
import React, { FC, useEffect, useMemo, useRef, useState } from
|
|
2
|
-
import { format } from
|
|
3
|
-
import clsx from
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
message: Message;
|
|
25
|
+
onSelect: (message: Message) => void;
|
|
26
|
+
selected: boolean;
|
|
27
|
+
editableMessage?: boolean;
|
|
33
28
|
};
|
|
34
29
|
|
|
35
|
-
const ChatMessage: FC<ChatMessageProps> = ({
|
|
36
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setMessageHeight(messageRef?.current?.clientHeight ?? 0);
|
|
44
|
+
});
|
|
55
45
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
<
|
|
235
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
235
|
+
) : null}{" "}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
268
238
|
};
|
|
269
239
|
|
|
270
240
|
export default ChatMessage;
|
|
@@ -1,49 +1,69 @@
|
|
|
1
|
-
import React, { useState } from
|
|
2
|
-
import Markdown from
|
|
3
|
-
import
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import Markdown from "markdown-to-jsx";
|
|
3
|
+
import "./Chat.scss";
|
|
4
4
|
|
|
5
5
|
interface MarkdownifyProps {
|
|
6
|
-
|
|
6
|
+
message: string | undefined;
|
|
7
|
+
sanitizeLinks?: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
const LinkPreview: React.FC<{
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
if (basicAuthPattern.test(href)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
2
|
-
import { format } from
|
|
3
|
-
import { Message } from
|
|
4
|
-
import Markdownify from
|
|
5
|
-
import { parseButtons, parseOptions } from
|
|
6
|
-
import ButtonMessage from
|
|
7
|
-
import OptionMessage from
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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;
|