react-mention-input 1.1.15 → 1.1.16
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/demo.tsx +98 -0
- package/dist/MentionInput.css +8 -0
- package/dist/ShowMessageCard.css +52 -0
- package/dist/demo.d.ts +3 -0
- package/dist/demo.js +80 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +5 -0
- package/dist/src/MentionInput.d.ts +38 -0
- package/dist/src/MentionInput.js +376 -0
- package/dist/src/ShowMessageCard.d.ts +41 -0
- package/dist/src/ShowMessageCard.js +71 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/vite.config.d.ts +2 -0
- package/dist/vite.config.js +9 -0
- package/index.html +12 -0
- package/main.tsx +9 -0
- package/package.json +9 -5
- package/src/MentionInput.css +8 -0
- package/src/MentionInput.tsx +26 -4
- package/src/ShowMessageCard.css +52 -0
- package/src/ShowMessageCard.tsx +78 -29
- package/vite.config.ts +10 -0
package/demo.tsx
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { MentionInput, ShowMessageCard } from './src';
|
|
3
|
+
|
|
4
|
+
const Demo: React.FC = () => {
|
|
5
|
+
const [messages, setMessages] = useState<any[]>([
|
|
6
|
+
{
|
|
7
|
+
id: 1,
|
|
8
|
+
name: 'John Doe',
|
|
9
|
+
date: '2 hours ago',
|
|
10
|
+
comment: 'Updated the status to Draft. Need <span class="mention-highlight">@team-leads</span> review before proceeding. <span class="hashtag-highlight">#urgent</span> <span class="hashtag-highlight">#review</span>',
|
|
11
|
+
itemName: '26may_item001 (A)',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 2,
|
|
15
|
+
name: 'Mike Johnson',
|
|
16
|
+
date: '1 day ago',
|
|
17
|
+
comment: 'Revision A completed successfully. Ready for next phase. <span class="hashtag-highlight">#milestone</span>',
|
|
18
|
+
itemName: '26may_item001 (A)',
|
|
19
|
+
}
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
// Sample users for mentions
|
|
23
|
+
const users = [
|
|
24
|
+
{ id: 1, name: 'John Doe' },
|
|
25
|
+
{ id: 2, name: 'Jane Smith' },
|
|
26
|
+
{ id: 3, name: 'Mike Johnson' },
|
|
27
|
+
{ id: 4, name: 'Sarah Wilson' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const handleSendMessage = (messageData: {
|
|
31
|
+
messageText: string;
|
|
32
|
+
messageHTML: string;
|
|
33
|
+
userSelectListWithIds: { id: number; name: string }[];
|
|
34
|
+
userSelectListName: string[];
|
|
35
|
+
tags: string[];
|
|
36
|
+
images?: File[];
|
|
37
|
+
imageUrl?: string | null;
|
|
38
|
+
}) => {
|
|
39
|
+
console.log('Message Data:', messageData);
|
|
40
|
+
console.log('Extracted Tags:', messageData.tags);
|
|
41
|
+
|
|
42
|
+
// Create a new message for display
|
|
43
|
+
const newMessage = {
|
|
44
|
+
id: Date.now(),
|
|
45
|
+
name: 'You',
|
|
46
|
+
date: new Date().toLocaleTimeString(),
|
|
47
|
+
comment: messageData.messageHTML,
|
|
48
|
+
imageUrl: messageData.imageUrl,
|
|
49
|
+
tags: messageData.tags,
|
|
50
|
+
mentions: messageData.userSelectListName,
|
|
51
|
+
itemName: 'new_item001 (A)', // You can customize this or make it dynamic
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
setMessages([...messages, newMessage]);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div style={{ maxWidth: '600px', margin: '20px auto', padding: '20px' }}>
|
|
59
|
+
<h1>React Mention Input with Hashtag Support</h1>
|
|
60
|
+
|
|
61
|
+
<div style={{ marginBottom: '20px' }}>
|
|
62
|
+
<h3>Features:</h3>
|
|
63
|
+
<ul>
|
|
64
|
+
<li>Type <strong>@</strong> to mention users (e.g., @John Doe)</li>
|
|
65
|
+
<li>Type <strong>#</strong> to create hashtags (e.g., #urgent #review #milestone)</li>
|
|
66
|
+
<li>Links are automatically detected and highlighted</li>
|
|
67
|
+
<li>Hashtags are extracted and returned in the tags array</li>
|
|
68
|
+
</ul>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div style={{ marginBottom: '20px' }}>
|
|
72
|
+
<MentionInput
|
|
73
|
+
users={users}
|
|
74
|
+
placeholder="Type a message with @mentions and #hashtags..."
|
|
75
|
+
onSendMessage={handleSendMessage}
|
|
76
|
+
suggestionPosition="bottom"
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div>
|
|
81
|
+
<h3>Messages:</h3>
|
|
82
|
+
<ShowMessageCard data={messages} />
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f5f5f5', borderRadius: '8px' }}>
|
|
86
|
+
<h4>Example Usage:</h4>
|
|
87
|
+
<p>Try typing these examples:</p>
|
|
88
|
+
<ul>
|
|
89
|
+
<li>"Project update complete. Ready for @John Doe review #milestone #completed"</li>
|
|
90
|
+
<li>"Need urgent help with deployment @Jane Smith @Mike Johnson #urgent #deployment #help"</li>
|
|
91
|
+
<li>"Meeting scheduled for tomorrow #meeting #planning"</li>
|
|
92
|
+
</ul>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default Demo;
|
package/dist/MentionInput.css
CHANGED
|
@@ -193,6 +193,14 @@
|
|
|
193
193
|
font-weight: 500;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
.hashtag-highlight {
|
|
197
|
+
background-color: rgba(255, 165, 0, 0.1);
|
|
198
|
+
color: #FF8C00;
|
|
199
|
+
border-radius: 4px;
|
|
200
|
+
padding: 1px 4px;
|
|
201
|
+
font-weight: 500;
|
|
202
|
+
}
|
|
203
|
+
|
|
196
204
|
.link-highlight {
|
|
197
205
|
color: #2684FF;
|
|
198
206
|
text-decoration: none;
|
package/dist/ShowMessageCard.css
CHANGED
|
@@ -21,9 +21,25 @@
|
|
|
21
21
|
.message-card-header {
|
|
22
22
|
display: flex;
|
|
23
23
|
align-items: center;
|
|
24
|
+
justify-content: space-between;
|
|
24
25
|
margin-bottom: 8px; /* Space between header and body */
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
.message-card-header-left {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.message-card-item-name {
|
|
34
|
+
font-size: 14px;
|
|
35
|
+
color: #666;
|
|
36
|
+
font-weight: 500;
|
|
37
|
+
background-color: #f5f5f5;
|
|
38
|
+
padding: 4px 8px;
|
|
39
|
+
border-radius: 6px;
|
|
40
|
+
border: 1px solid #e0e0e0;
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
.message-card-img,
|
|
28
44
|
.message-card-initials {
|
|
29
45
|
width: 48px;
|
|
@@ -72,4 +88,40 @@
|
|
|
72
88
|
color: #007bff;
|
|
73
89
|
padding: 2px 4px;
|
|
74
90
|
border-radius: 4px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.hashtag-highlight {
|
|
94
|
+
background-color: rgba(255, 165, 0, 0.15);
|
|
95
|
+
color: #FF8C00;
|
|
96
|
+
padding: 2px 4px;
|
|
97
|
+
border-radius: 4px;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Tag chips styling */
|
|
102
|
+
.message-card-tags {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-wrap: wrap;
|
|
105
|
+
gap: 8px;
|
|
106
|
+
margin-top: 12px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.tag-chip {
|
|
110
|
+
padding: 6px 12px;
|
|
111
|
+
border-radius: 16px;
|
|
112
|
+
font-size: 12px;
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
border: none;
|
|
115
|
+
display: inline-block;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.hashtag-chip {
|
|
120
|
+
background-color: #D4A574;
|
|
121
|
+
color: #FFFFFF;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.mention-chip {
|
|
125
|
+
background-color: #2684FF;
|
|
126
|
+
color: #FFFFFF;
|
|
75
127
|
}
|
package/dist/demo.d.ts
ADDED
package/dist/demo.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
2
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
3
|
+
if (ar || !(i in from)) {
|
|
4
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
5
|
+
ar[i] = from[i];
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
9
|
+
};
|
|
10
|
+
import React, { useState } from 'react';
|
|
11
|
+
import { MentionInput, ShowMessageCard } from './src';
|
|
12
|
+
var Demo = function () {
|
|
13
|
+
var _a = useState([
|
|
14
|
+
{
|
|
15
|
+
id: 1,
|
|
16
|
+
name: 'John Doe',
|
|
17
|
+
date: '2 hours ago',
|
|
18
|
+
comment: 'Updated the status to Draft. Need <span class="mention-highlight">@team-leads</span> review before proceeding. <span class="hashtag-highlight">#urgent</span> <span class="hashtag-highlight">#review</span>',
|
|
19
|
+
itemName: '26may_item001 (A)',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 2,
|
|
23
|
+
name: 'Mike Johnson',
|
|
24
|
+
date: '1 day ago',
|
|
25
|
+
comment: 'Revision A completed successfully. Ready for next phase. <span class="hashtag-highlight">#milestone</span>',
|
|
26
|
+
itemName: '26may_item001 (A)',
|
|
27
|
+
}
|
|
28
|
+
]), messages = _a[0], setMessages = _a[1];
|
|
29
|
+
// Sample users for mentions
|
|
30
|
+
var users = [
|
|
31
|
+
{ id: 1, name: 'John Doe' },
|
|
32
|
+
{ id: 2, name: 'Jane Smith' },
|
|
33
|
+
{ id: 3, name: 'Mike Johnson' },
|
|
34
|
+
{ id: 4, name: 'Sarah Wilson' },
|
|
35
|
+
];
|
|
36
|
+
var handleSendMessage = function (messageData) {
|
|
37
|
+
console.log('Message Data:', messageData);
|
|
38
|
+
console.log('Extracted Tags:', messageData.tags);
|
|
39
|
+
// Create a new message for display
|
|
40
|
+
var newMessage = {
|
|
41
|
+
id: Date.now(),
|
|
42
|
+
name: 'You',
|
|
43
|
+
date: new Date().toLocaleTimeString(),
|
|
44
|
+
comment: messageData.messageHTML,
|
|
45
|
+
imageUrl: messageData.imageUrl,
|
|
46
|
+
tags: messageData.tags,
|
|
47
|
+
mentions: messageData.userSelectListName,
|
|
48
|
+
itemName: 'new_item001 (A)', // You can customize this or make it dynamic
|
|
49
|
+
};
|
|
50
|
+
setMessages(__spreadArray(__spreadArray([], messages, true), [newMessage], false));
|
|
51
|
+
};
|
|
52
|
+
return (React.createElement("div", { style: { maxWidth: '600px', margin: '20px auto', padding: '20px' } },
|
|
53
|
+
React.createElement("h1", null, "React Mention Input with Hashtag Support"),
|
|
54
|
+
React.createElement("div", { style: { marginBottom: '20px' } },
|
|
55
|
+
React.createElement("h3", null, "Features:"),
|
|
56
|
+
React.createElement("ul", null,
|
|
57
|
+
React.createElement("li", null,
|
|
58
|
+
"Type ",
|
|
59
|
+
React.createElement("strong", null, "@"),
|
|
60
|
+
" to mention users (e.g., @John Doe)"),
|
|
61
|
+
React.createElement("li", null,
|
|
62
|
+
"Type ",
|
|
63
|
+
React.createElement("strong", null, "#"),
|
|
64
|
+
" to create hashtags (e.g., #urgent #review #milestone)"),
|
|
65
|
+
React.createElement("li", null, "Links are automatically detected and highlighted"),
|
|
66
|
+
React.createElement("li", null, "Hashtags are extracted and returned in the tags array"))),
|
|
67
|
+
React.createElement("div", { style: { marginBottom: '20px' } },
|
|
68
|
+
React.createElement(MentionInput, { users: users, placeholder: "Type a message with @mentions and #hashtags...", onSendMessage: handleSendMessage, suggestionPosition: "bottom" })),
|
|
69
|
+
React.createElement("div", null,
|
|
70
|
+
React.createElement("h3", null, "Messages:"),
|
|
71
|
+
React.createElement(ShowMessageCard, { data: messages })),
|
|
72
|
+
React.createElement("div", { style: { marginTop: '20px', padding: '15px', backgroundColor: '#f5f5f5', borderRadius: '8px' } },
|
|
73
|
+
React.createElement("h4", null, "Example Usage:"),
|
|
74
|
+
React.createElement("p", null, "Try typing these examples:"),
|
|
75
|
+
React.createElement("ul", null,
|
|
76
|
+
React.createElement("li", null, "\"Project update complete. Ready for @John Doe review #milestone #completed\""),
|
|
77
|
+
React.createElement("li", null, "\"Need urgent help with deployment @Jane Smith @Mike Johnson #urgent #deployment #help\""),
|
|
78
|
+
React.createElement("li", null, "\"Meeting scheduled for tomorrow #meeting #planning\"")))));
|
|
79
|
+
};
|
|
80
|
+
export default Demo;
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import "./MentionInput.css";
|
|
3
|
+
interface User {
|
|
4
|
+
id: number;
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
interface MentionInputProps {
|
|
8
|
+
users: User[];
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
containerClassName?: string;
|
|
11
|
+
inputContainerClassName?: string;
|
|
12
|
+
inputClassName?: string;
|
|
13
|
+
sendBtnClassName?: string;
|
|
14
|
+
suggestionListClassName?: string;
|
|
15
|
+
suggestionItemClassName?: string;
|
|
16
|
+
attachedImageContainerClassName?: string;
|
|
17
|
+
attachedImageContainerStyle?: React.CSSProperties;
|
|
18
|
+
imgClassName?: string;
|
|
19
|
+
imgStyle?: React.CSSProperties;
|
|
20
|
+
sendButtonIcon?: ReactNode;
|
|
21
|
+
attachmentButtonIcon?: ReactNode;
|
|
22
|
+
onSendMessage?: (obj: {
|
|
23
|
+
messageText: string;
|
|
24
|
+
messageHTML: string;
|
|
25
|
+
userSelectListWithIds: {
|
|
26
|
+
id: number;
|
|
27
|
+
name: string;
|
|
28
|
+
}[];
|
|
29
|
+
userSelectListName: string[];
|
|
30
|
+
tags: string[];
|
|
31
|
+
images?: File[];
|
|
32
|
+
imageUrl?: string | null;
|
|
33
|
+
}) => void;
|
|
34
|
+
suggestionPosition?: 'top' | 'bottom' | 'left' | 'right';
|
|
35
|
+
onImageUpload?: (file: File) => Promise<string>;
|
|
36
|
+
}
|
|
37
|
+
declare const MentionInput: React.FC<MentionInputProps>;
|
|
38
|
+
export default MentionInput;
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
11
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
12
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
13
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
14
|
+
function step(op) {
|
|
15
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
16
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
17
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
18
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
19
|
+
switch (op[0]) {
|
|
20
|
+
case 0: case 1: t = op; break;
|
|
21
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
22
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
23
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
24
|
+
default:
|
|
25
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
26
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
27
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
28
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
29
|
+
if (t[2]) _.ops.pop();
|
|
30
|
+
_.trys.pop(); continue;
|
|
31
|
+
}
|
|
32
|
+
op = body.call(thisArg, _);
|
|
33
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
34
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
import React, { useState, useRef } from "react";
|
|
38
|
+
import ReactDOM from "react-dom";
|
|
39
|
+
import "./MentionInput.css";
|
|
40
|
+
var MentionInput = function (_a) {
|
|
41
|
+
var _b;
|
|
42
|
+
var users = _a.users, _c = _a.placeholder, placeholder = _c === void 0 ? "Type a message... (or drag & drop an image)" : _c, containerClassName = _a.containerClassName, inputContainerClassName = _a.inputContainerClassName, inputClassName = _a.inputClassName, sendBtnClassName = _a.sendBtnClassName, suggestionListClassName = _a.suggestionListClassName, suggestionItemClassName = _a.suggestionItemClassName, imgClassName = _a.imgClassName, imgStyle = _a.imgStyle, attachedImageContainerClassName = _a.attachedImageContainerClassName, attachedImageContainerStyle = _a.attachedImageContainerStyle, sendButtonIcon = _a.sendButtonIcon, attachmentButtonIcon = _a.attachmentButtonIcon, onSendMessage = _a.onSendMessage, _d = _a.suggestionPosition, suggestionPosition = _d === void 0 ? 'bottom' : _d, onImageUpload = _a.onImageUpload;
|
|
43
|
+
var _e = useState(""), inputValue = _e[0], setInputValue = _e[1]; // Plain text
|
|
44
|
+
var _f = useState([]), suggestions = _f[0], setSuggestions = _f[1];
|
|
45
|
+
var _g = useState(false), showSuggestions = _g[0], setShowSuggestions = _g[1];
|
|
46
|
+
var _h = useState(null), selectedImage = _h[0], setSelectedImage = _h[1];
|
|
47
|
+
var _j = useState(null), imageUrl = _j[0], setImageUrl = _j[1];
|
|
48
|
+
var _k = useState(false), isUploading = _k[0], setIsUploading = _k[1];
|
|
49
|
+
var _l = useState(false), isDraggingOver = _l[0], setIsDraggingOver = _l[1];
|
|
50
|
+
var inputRef = useRef(null);
|
|
51
|
+
var suggestionListRef = useRef(null);
|
|
52
|
+
var caretOffsetRef = useRef(0);
|
|
53
|
+
var userSelectListRef = useRef([]); // Only unique names
|
|
54
|
+
var userSelectListWithIdsRef = useRef([]); // Unique IDs with names
|
|
55
|
+
var tagsListRef = useRef([]); // Store hashtags
|
|
56
|
+
var fileInputRef = useRef(null);
|
|
57
|
+
var highlightMentionsAndLinks = function (text) {
|
|
58
|
+
// Regular expression for detecting links
|
|
59
|
+
var linkRegex = /(https?:\/\/[^\s]+)/g;
|
|
60
|
+
// Regular expression for detecting hashtags
|
|
61
|
+
var hashtagRegex = /#[\w]+/g;
|
|
62
|
+
// Highlight links
|
|
63
|
+
var highlightedText = text.replace(linkRegex, '<a href="$1" target="_blank" rel="noopener noreferrer" class="link-highlight">$1</a>');
|
|
64
|
+
// Highlight hashtags
|
|
65
|
+
highlightedText = highlightedText.replace(hashtagRegex, function (match) {
|
|
66
|
+
return "<span class=\"hashtag-highlight\">".concat(match, "</span>");
|
|
67
|
+
});
|
|
68
|
+
// Highlight mentions manually based on `userSelectListRef`
|
|
69
|
+
userSelectListRef === null || userSelectListRef === void 0 ? void 0 : userSelectListRef.current.forEach(function (userName) {
|
|
70
|
+
var mentionPattern = new RegExp("@".concat(userName, "(\\s|$)"), "g");
|
|
71
|
+
highlightedText = highlightedText.replace(mentionPattern, function (match) {
|
|
72
|
+
return "<span class=\"mention-highlight\">".concat(match.trim(), "</span> ");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
return highlightedText;
|
|
76
|
+
};
|
|
77
|
+
var restoreCaretPosition = function (node, caretOffset) {
|
|
78
|
+
var range = document.createRange();
|
|
79
|
+
var sel = window.getSelection();
|
|
80
|
+
var charCount = 0;
|
|
81
|
+
var findCaret = function (currentNode) {
|
|
82
|
+
var _a;
|
|
83
|
+
for (var _i = 0, _b = Array.from(currentNode.childNodes); _i < _b.length; _i++) {
|
|
84
|
+
var child = _b[_i];
|
|
85
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
86
|
+
var textLength = ((_a = child.textContent) === null || _a === void 0 ? void 0 : _a.length) || 0;
|
|
87
|
+
if (charCount + textLength >= caretOffset) {
|
|
88
|
+
range.setStart(child, caretOffset - charCount);
|
|
89
|
+
range.collapse(true);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
charCount += textLength;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
97
|
+
if (findCaret(child))
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
};
|
|
103
|
+
findCaret(node);
|
|
104
|
+
if (sel) {
|
|
105
|
+
sel.removeAllRanges();
|
|
106
|
+
sel.addRange(range);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var handleInputChange = function () {
|
|
110
|
+
if (!inputRef.current)
|
|
111
|
+
return;
|
|
112
|
+
// Store current selection before modifications
|
|
113
|
+
var selection = window.getSelection();
|
|
114
|
+
var range = selection === null || selection === void 0 ? void 0 : selection.getRangeAt(0);
|
|
115
|
+
var newCaretOffset = 0;
|
|
116
|
+
if (range && inputRef.current.contains(range.startContainer)) {
|
|
117
|
+
var preCaretRange = range.cloneRange();
|
|
118
|
+
preCaretRange.selectNodeContents(inputRef.current);
|
|
119
|
+
preCaretRange.setEnd(range.startContainer, range.startOffset);
|
|
120
|
+
newCaretOffset = preCaretRange.toString().length;
|
|
121
|
+
}
|
|
122
|
+
caretOffsetRef.current = newCaretOffset;
|
|
123
|
+
var plainText = inputRef.current.innerText;
|
|
124
|
+
setInputValue(plainText);
|
|
125
|
+
// Process for mention suggestions
|
|
126
|
+
var mentionMatch = plainText.slice(0, newCaretOffset).match(/@(\S*)$/);
|
|
127
|
+
if (mentionMatch) {
|
|
128
|
+
var query_1 = mentionMatch[1].toLowerCase();
|
|
129
|
+
var filteredUsers = query_1 === "" ? users : users.filter(function (user) {
|
|
130
|
+
return user.name.toLowerCase().includes(query_1);
|
|
131
|
+
});
|
|
132
|
+
setSuggestions(filteredUsers);
|
|
133
|
+
setShowSuggestions(filteredUsers.length > 0);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
setShowSuggestions(false);
|
|
137
|
+
}
|
|
138
|
+
// Extract and store hashtags
|
|
139
|
+
var hashtagMatches = plainText.match(/#[\w]+/g);
|
|
140
|
+
if (hashtagMatches) {
|
|
141
|
+
var uniqueTags = Array.from(new Set(hashtagMatches));
|
|
142
|
+
tagsListRef.current = uniqueTags;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
tagsListRef.current = [];
|
|
146
|
+
}
|
|
147
|
+
// Only apply highlighting if we have mentions, hashtags, or links to highlight
|
|
148
|
+
if (userSelectListRef.current.length > 0 || plainText.match(/(https?:\/\/[^\s]+)/g) || plainText.match(/#[\w]+/g)) {
|
|
149
|
+
var currentHTML = inputRef.current.innerHTML;
|
|
150
|
+
var htmlWithHighlights = highlightMentionsAndLinks(plainText);
|
|
151
|
+
// Only update if the highlighted HTML is different to avoid cursor jumping
|
|
152
|
+
if (currentHTML !== htmlWithHighlights) {
|
|
153
|
+
inputRef.current.innerHTML = htmlWithHighlights;
|
|
154
|
+
// Restore cursor position after changing innerHTML
|
|
155
|
+
restoreCaretPosition(inputRef.current, newCaretOffset);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var renderSuggestions = function () {
|
|
160
|
+
if (!showSuggestions || !inputRef.current)
|
|
161
|
+
return null;
|
|
162
|
+
var getInitials = function (name) {
|
|
163
|
+
var nameParts = name.split(" ");
|
|
164
|
+
var initials = nameParts
|
|
165
|
+
.map(function (part) { var _a; return ((_a = part[0]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || ""; })
|
|
166
|
+
.slice(0, 2)
|
|
167
|
+
.join("");
|
|
168
|
+
return initials;
|
|
169
|
+
};
|
|
170
|
+
var inputRect = inputRef.current.getBoundingClientRect();
|
|
171
|
+
var styles = {
|
|
172
|
+
position: 'absolute',
|
|
173
|
+
zIndex: 1000,
|
|
174
|
+
};
|
|
175
|
+
// Use suggestionPosition prop to adjust tooltip position
|
|
176
|
+
switch (suggestionPosition) {
|
|
177
|
+
case 'top':
|
|
178
|
+
styles.left = "".concat(inputRect.left, "px");
|
|
179
|
+
styles.top = "".concat(inputRect.top - 150, "px");
|
|
180
|
+
break;
|
|
181
|
+
case 'bottom':
|
|
182
|
+
styles.left = "".concat(inputRect.left, "px");
|
|
183
|
+
styles.top = "".concat(inputRect.bottom, "px");
|
|
184
|
+
break;
|
|
185
|
+
case 'left':
|
|
186
|
+
styles.left = "".concat(inputRect.left - 150, "px");
|
|
187
|
+
styles.top = "".concat(inputRect.top, "px");
|
|
188
|
+
break;
|
|
189
|
+
case 'right':
|
|
190
|
+
styles.left = "".concat(inputRect.right, "px");
|
|
191
|
+
styles.top = "".concat(inputRect.top, "px");
|
|
192
|
+
break;
|
|
193
|
+
default:
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
return ReactDOM.createPortal(React.createElement("div", { className: "suggestion-container ".concat(suggestionListClassName || ''), style: styles },
|
|
197
|
+
React.createElement("ul", { className: "suggestion-list", ref: suggestionListRef }, suggestions.map(function (user) { return (React.createElement("li", { key: user.id, onClick: function () { return handleSuggestionClick(user); }, className: "suggestion-item ".concat(suggestionItemClassName || ''), role: "option", tabIndex: 0, "aria-selected": "false" },
|
|
198
|
+
React.createElement("div", { className: "user-icon" }, getInitials(user === null || user === void 0 ? void 0 : user.name)),
|
|
199
|
+
React.createElement("span", { className: "user-name" }, user.name))); }))), window.document.body);
|
|
200
|
+
};
|
|
201
|
+
var handleSuggestionClick = function (user) {
|
|
202
|
+
if (!inputRef.current)
|
|
203
|
+
return;
|
|
204
|
+
var plainText = inputValue;
|
|
205
|
+
var caretOffset = caretOffsetRef.current;
|
|
206
|
+
var mentionMatch = plainText.slice(0, caretOffset).match(/@(\S*)$/);
|
|
207
|
+
if (!userSelectListRef.current.includes(user.name)) {
|
|
208
|
+
userSelectListRef.current.push(user.name);
|
|
209
|
+
}
|
|
210
|
+
// Check if the ID is already stored
|
|
211
|
+
var isIdExists = userSelectListWithIdsRef.current.some(function (item) { return item.id === user.id; });
|
|
212
|
+
if (!isIdExists) {
|
|
213
|
+
userSelectListWithIdsRef.current.push(user);
|
|
214
|
+
}
|
|
215
|
+
if (!mentionMatch)
|
|
216
|
+
return;
|
|
217
|
+
var mentionIndex = plainText.slice(0, caretOffset).lastIndexOf("@");
|
|
218
|
+
// Append space after the mention
|
|
219
|
+
var newValue = plainText.substring(0, mentionIndex + 1) + user.name + " " + plainText.substring(caretOffset);
|
|
220
|
+
setInputValue(newValue);
|
|
221
|
+
inputRef.current.innerText = newValue;
|
|
222
|
+
// Highlight mentions and links with
|
|
223
|
+
var htmlWithHighlights = highlightMentionsAndLinks(newValue);
|
|
224
|
+
// Set highlighted content
|
|
225
|
+
inputRef.current.innerHTML = htmlWithHighlights;
|
|
226
|
+
setShowSuggestions(false);
|
|
227
|
+
// Adjust caret position after adding the mention and space
|
|
228
|
+
var mentionEnd = mentionIndex + user.name.length + 1;
|
|
229
|
+
restoreCaretPosition(inputRef.current, mentionEnd + 1); // +1 for the space
|
|
230
|
+
};
|
|
231
|
+
var handleImageSelect = function (event) { return __awaiter(void 0, void 0, void 0, function () {
|
|
232
|
+
var files, file;
|
|
233
|
+
return __generator(this, function (_a) {
|
|
234
|
+
switch (_a.label) {
|
|
235
|
+
case 0:
|
|
236
|
+
files = Array.from(event.target.files || []);
|
|
237
|
+
if (!(files.length > 0)) return [3 /*break*/, 2];
|
|
238
|
+
file = files[0];
|
|
239
|
+
if (!file.type.startsWith('image/')) return [3 /*break*/, 2];
|
|
240
|
+
return [4 /*yield*/, uploadImage(file)];
|
|
241
|
+
case 1:
|
|
242
|
+
_a.sent();
|
|
243
|
+
_a.label = 2;
|
|
244
|
+
case 2: return [2 /*return*/];
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}); };
|
|
248
|
+
var handleDragOver = function (e) {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
e.stopPropagation();
|
|
251
|
+
// Only set dragging if files are being dragged
|
|
252
|
+
if (e.dataTransfer.types.includes('Files')) {
|
|
253
|
+
setIsDraggingOver(true);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
var handleDragLeave = function (e) {
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
e.stopPropagation();
|
|
259
|
+
// Check if we're leaving the container, not just moving between children
|
|
260
|
+
var rect = e.currentTarget.getBoundingClientRect();
|
|
261
|
+
var x = e.clientX;
|
|
262
|
+
var y = e.clientY;
|
|
263
|
+
if (x <= rect.left ||
|
|
264
|
+
x >= rect.right ||
|
|
265
|
+
y <= rect.top ||
|
|
266
|
+
y >= rect.bottom) {
|
|
267
|
+
setIsDraggingOver(false);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
var handleDrop = function (e) { return __awaiter(void 0, void 0, void 0, function () {
|
|
271
|
+
var files, imageFiles;
|
|
272
|
+
return __generator(this, function (_a) {
|
|
273
|
+
switch (_a.label) {
|
|
274
|
+
case 0:
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
e.stopPropagation();
|
|
277
|
+
setIsDraggingOver(false);
|
|
278
|
+
files = Array.from(e.dataTransfer.files);
|
|
279
|
+
if (!(files.length > 0)) return [3 /*break*/, 2];
|
|
280
|
+
imageFiles = files.filter(function (file) { return file.type.startsWith('image/'); });
|
|
281
|
+
if (!(imageFiles.length > 0)) return [3 /*break*/, 2];
|
|
282
|
+
return [4 /*yield*/, uploadImage(imageFiles[0])];
|
|
283
|
+
case 1:
|
|
284
|
+
_a.sent();
|
|
285
|
+
_a.label = 2;
|
|
286
|
+
case 2: return [2 /*return*/];
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}); };
|
|
290
|
+
var uploadImage = function (file) { return __awaiter(void 0, void 0, void 0, function () {
|
|
291
|
+
var url, error_1;
|
|
292
|
+
return __generator(this, function (_a) {
|
|
293
|
+
switch (_a.label) {
|
|
294
|
+
case 0:
|
|
295
|
+
if (!onImageUpload) {
|
|
296
|
+
// If no upload function provided, store the file directly
|
|
297
|
+
setSelectedImage(file);
|
|
298
|
+
setImageUrl(URL.createObjectURL(file));
|
|
299
|
+
return [2 /*return*/];
|
|
300
|
+
}
|
|
301
|
+
_a.label = 1;
|
|
302
|
+
case 1:
|
|
303
|
+
_a.trys.push([1, 3, 4, 5]);
|
|
304
|
+
setIsUploading(true);
|
|
305
|
+
return [4 /*yield*/, onImageUpload(file)];
|
|
306
|
+
case 2:
|
|
307
|
+
url = _a.sent();
|
|
308
|
+
setSelectedImage(file);
|
|
309
|
+
setImageUrl(url);
|
|
310
|
+
return [3 /*break*/, 5];
|
|
311
|
+
case 3:
|
|
312
|
+
error_1 = _a.sent();
|
|
313
|
+
console.error('Error uploading image:', error_1);
|
|
314
|
+
return [3 /*break*/, 5];
|
|
315
|
+
case 4:
|
|
316
|
+
setIsUploading(false);
|
|
317
|
+
return [7 /*endfinally*/];
|
|
318
|
+
case 5: return [2 /*return*/];
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}); };
|
|
322
|
+
var removeImage = function () {
|
|
323
|
+
setSelectedImage(null);
|
|
324
|
+
setImageUrl(null);
|
|
325
|
+
};
|
|
326
|
+
var handleSendMessage = function () {
|
|
327
|
+
if (inputRef.current) {
|
|
328
|
+
var messageText = inputRef.current.innerText.trim();
|
|
329
|
+
var messageHTML = inputRef.current.innerHTML.trim();
|
|
330
|
+
if ((messageText || selectedImage) && onSendMessage) {
|
|
331
|
+
onSendMessage({
|
|
332
|
+
messageText: messageText,
|
|
333
|
+
messageHTML: messageHTML,
|
|
334
|
+
userSelectListWithIds: userSelectListWithIdsRef.current,
|
|
335
|
+
userSelectListName: userSelectListRef.current,
|
|
336
|
+
tags: tagsListRef.current,
|
|
337
|
+
images: selectedImage ? [selectedImage] : [],
|
|
338
|
+
imageUrl: imageUrl
|
|
339
|
+
});
|
|
340
|
+
setInputValue("");
|
|
341
|
+
setShowSuggestions(false);
|
|
342
|
+
inputRef.current.innerText = "";
|
|
343
|
+
setSelectedImage(null);
|
|
344
|
+
setImageUrl(null);
|
|
345
|
+
userSelectListRef.current = [];
|
|
346
|
+
userSelectListWithIdsRef.current = [];
|
|
347
|
+
tagsListRef.current = [];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
var handleKeyDown = function (event) {
|
|
352
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
353
|
+
event.preventDefault(); // Prevent newline in content-editable
|
|
354
|
+
handleSendMessage(); // Trigger the same function as the Send button
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
return (React.createElement("div", { className: "mention-container ".concat(containerClassName || "") },
|
|
358
|
+
imageUrl && selectedImage && (React.createElement("div", { className: "image-preview-card ".concat(attachedImageContainerClassName || ""), style: attachedImageContainerStyle },
|
|
359
|
+
React.createElement("img", { src: imageUrl, alt: "Preview", className: imgClassName || "", style: imgStyle }),
|
|
360
|
+
React.createElement("button", { onClick: removeImage, className: "remove-image-btn", "aria-label": "Remove image" }, "\u00D7"))),
|
|
361
|
+
React.createElement("div", { className: "mention-input-container ".concat(inputContainerClassName || "", " ").concat(isDraggingOver ? 'dragging-over' : ''), onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDragEnd: function () { return setIsDraggingOver(false); }, onDrop: handleDrop },
|
|
362
|
+
isDraggingOver && (React.createElement("div", { className: "drag-overlay" },
|
|
363
|
+
React.createElement("div", { className: "drag-message" },
|
|
364
|
+
React.createElement("span", null, "Drop to upload")))),
|
|
365
|
+
React.createElement("button", { onClick: function () { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, className: "attachment-button", type: "button", "aria-label": "Attach image" },
|
|
366
|
+
React.createElement("span", { className: "attachment-icon" }, attachmentButtonIcon || "📷")),
|
|
367
|
+
React.createElement("div", { className: "mention-input-wrapper" },
|
|
368
|
+
(!inputValue || !inputRef.current || ((_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.innerText.trim()) === "") && (React.createElement("span", { className: "placeholder" }, placeholder)),
|
|
369
|
+
React.createElement("div", { ref: inputRef, contentEditable: true, suppressContentEditableWarning: true, className: "mention-input ".concat(inputClassName || ""), onInput: handleInputChange, onKeyDown: handleKeyDown, onFocus: function () { return document.execCommand('styleWithCSS', false, 'false'); } })),
|
|
370
|
+
React.createElement("button", { onClick: handleSendMessage, className: "send-button ".concat(sendBtnClassName || ""), "aria-label": "Send message" }, sendButtonIcon || "➤"),
|
|
371
|
+
React.createElement("input", { type: "file", ref: fileInputRef, accept: "image/*", onChange: handleImageSelect, style: { display: 'none' } }),
|
|
372
|
+
isUploading && (React.createElement("div", { className: "upload-loading" },
|
|
373
|
+
React.createElement("span", null, "Uploading...")))),
|
|
374
|
+
renderSuggestions()));
|
|
375
|
+
};
|
|
376
|
+
export default MentionInput;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import "./ShowMessageCard.css";
|
|
3
|
+
interface MessageCardProps {
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
}
|
|
6
|
+
interface ShowMessageCardProps {
|
|
7
|
+
data: MessageCardProps[];
|
|
8
|
+
nameKey?: string;
|
|
9
|
+
dateKey?: string;
|
|
10
|
+
commentKey?: string;
|
|
11
|
+
imgSrcKey?: string;
|
|
12
|
+
imageUrlKey?: string;
|
|
13
|
+
itemNameKey?: string;
|
|
14
|
+
containerClassName?: string;
|
|
15
|
+
containerStyle?: CSSProperties;
|
|
16
|
+
cardClassName?: string;
|
|
17
|
+
cardStyle?: CSSProperties;
|
|
18
|
+
headerClassName?: string;
|
|
19
|
+
headerStyle?: CSSProperties;
|
|
20
|
+
imgClassName?: string;
|
|
21
|
+
imgStyle?: CSSProperties;
|
|
22
|
+
infoClassName?: string;
|
|
23
|
+
infoStyle?: CSSProperties;
|
|
24
|
+
nameClassName?: string;
|
|
25
|
+
nameStyle?: CSSProperties;
|
|
26
|
+
dateClassName?: string;
|
|
27
|
+
dateStyle?: CSSProperties;
|
|
28
|
+
bodyClassName?: string;
|
|
29
|
+
bodyStyle?: CSSProperties;
|
|
30
|
+
commentClassName?: string;
|
|
31
|
+
commentStyle?: CSSProperties;
|
|
32
|
+
attachedImageClassName?: string;
|
|
33
|
+
attachedImageStyle?: CSSProperties;
|
|
34
|
+
attachedImageContainerClassName?: string;
|
|
35
|
+
attachedImageContainerStyle?: CSSProperties;
|
|
36
|
+
itemNameClassName?: string;
|
|
37
|
+
itemNameStyle?: CSSProperties;
|
|
38
|
+
renderItem?: (element: MessageCardProps) => ReactNode;
|
|
39
|
+
}
|
|
40
|
+
export declare const ShowMessageCard: React.FC<ShowMessageCardProps>;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
var __assign = (this && this.__assign) || function () {
|
|
2
|
+
__assign = Object.assign || function(t) {
|
|
3
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
4
|
+
s = arguments[i];
|
|
5
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
6
|
+
t[p] = s[p];
|
|
7
|
+
}
|
|
8
|
+
return t;
|
|
9
|
+
};
|
|
10
|
+
return __assign.apply(this, arguments);
|
|
11
|
+
};
|
|
12
|
+
import React, { useState } from "react";
|
|
13
|
+
import "./ShowMessageCard.css";
|
|
14
|
+
export var ShowMessageCard = function (_a) {
|
|
15
|
+
var data = _a.data, _b = _a.nameKey, nameKey = _b === void 0 ? "name" : _b, _c = _a.dateKey, dateKey = _c === void 0 ? "date" : _c, _d = _a.commentKey, commentKey = _d === void 0 ? "comment" : _d, _e = _a.imgSrcKey, imgSrcKey = _e === void 0 ? "imgSrc" : _e, _f = _a.imageUrlKey, imageUrlKey = _f === void 0 ? "imageUrl" : _f, // Default key for attached image
|
|
16
|
+
_g = _a.itemNameKey, // Default key for attached image
|
|
17
|
+
itemNameKey = _g === void 0 ? "name" : _g, // Default key for item identifier
|
|
18
|
+
containerClassName = _a.containerClassName, containerStyle = _a.containerStyle, cardClassName = _a.cardClassName, cardStyle = _a.cardStyle, headerClassName = _a.headerClassName, headerStyle = _a.headerStyle, imgClassName = _a.imgClassName, imgStyle = _a.imgStyle, infoClassName = _a.infoClassName, infoStyle = _a.infoStyle, nameClassName = _a.nameClassName, nameStyle = _a.nameStyle, dateClassName = _a.dateClassName, dateStyle = _a.dateStyle, bodyClassName = _a.bodyClassName, bodyStyle = _a.bodyStyle, commentClassName = _a.commentClassName, commentStyle = _a.commentStyle, attachedImageClassName = _a.attachedImageClassName, attachedImageStyle = _a.attachedImageStyle, attachedImageContainerClassName = _a.attachedImageContainerClassName, attachedImageContainerStyle = _a.attachedImageContainerStyle, itemNameClassName = _a.itemNameClassName, itemNameStyle = _a.itemNameStyle, renderItem = _a.renderItem;
|
|
19
|
+
// State to manage initials for images that fail to load
|
|
20
|
+
var _h = useState({}), initialsState = _h[0], setInitialsState = _h[1];
|
|
21
|
+
// Handle image load failure
|
|
22
|
+
var handleImageError = function (id) {
|
|
23
|
+
setInitialsState(function (prevState) {
|
|
24
|
+
var _a;
|
|
25
|
+
return (__assign(__assign({}, prevState), (_a = {}, _a[id] = true, _a)));
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
// Helper function to generate initials from the name
|
|
29
|
+
var getInitials = function (name) {
|
|
30
|
+
var nameParts = name.split(" ");
|
|
31
|
+
var initials = nameParts
|
|
32
|
+
.map(function (part) { var _a; return ((_a = part[0]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || ""; }) // Take the first letter of each part
|
|
33
|
+
.slice(0, 2) // Limit to 2 letters
|
|
34
|
+
.join("");
|
|
35
|
+
return initials;
|
|
36
|
+
};
|
|
37
|
+
// Helper function to extract hashtags and mentions from text
|
|
38
|
+
var extractTagsAndMentions = function (text) {
|
|
39
|
+
var plainText = text.replace(/<[^>]*>/g, ''); // Remove HTML tags to get plain text
|
|
40
|
+
var hashtags = plainText.match(/#[\w]+/g) || [];
|
|
41
|
+
var mentions = plainText.match(/@[\w\s-]+/g) || [];
|
|
42
|
+
return {
|
|
43
|
+
hashtags: Array.from(new Set(hashtags)), // Remove duplicates
|
|
44
|
+
mentions: Array.from(new Set(mentions.map(function (mention) { return mention.trim(); }))) // Remove duplicates and trim
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
return (React.createElement("div", { className: "message-card-container ".concat(containerClassName || ""), style: containerStyle }, data.map(function (item, index) {
|
|
48
|
+
if (renderItem !== undefined) {
|
|
49
|
+
// Use custom render function if provided
|
|
50
|
+
return (React.createElement(React.Fragment, { key: item.id || index }, renderItem(item)));
|
|
51
|
+
}
|
|
52
|
+
var showInitials = initialsState[item.id || index] || !item[imgSrcKey]; // Decide whether to show initials
|
|
53
|
+
// Extract tags and mentions from the comment
|
|
54
|
+
var _a = extractTagsAndMentions(item[commentKey] || ''), hashtags = _a.hashtags, mentions = _a.mentions;
|
|
55
|
+
return (React.createElement("div", { key: item.id || index, className: "message-card ".concat(cardClassName || ""), style: cardStyle },
|
|
56
|
+
React.createElement("div", { className: "message-card-header ".concat(headerClassName || ""), style: headerStyle },
|
|
57
|
+
React.createElement("div", { className: "message-card-header-left" },
|
|
58
|
+
showInitials ? (React.createElement("div", { className: "message-card-initials ".concat(imgClassName || ""), style: imgStyle }, getInitials(item[nameKey]))) : (React.createElement("img", { src: item[imgSrcKey], alt: item[nameKey], className: "message-card-img ".concat(imgClassName || ""), style: imgStyle, onError: function () { return handleImageError(item.id || index); } })),
|
|
59
|
+
React.createElement("div", { className: "message-card-info ".concat(infoClassName || ""), style: infoStyle },
|
|
60
|
+
React.createElement("h3", { className: "message-card-name ".concat(nameClassName || ""), style: nameStyle }, item[nameKey]),
|
|
61
|
+
React.createElement("p", { className: "message-card-date ".concat(dateClassName || ""), style: dateStyle }, item[dateKey]))),
|
|
62
|
+
item[itemNameKey] && (React.createElement("div", { className: "message-card-item-name ".concat(itemNameClassName || ""), style: itemNameStyle }, item[itemNameKey]))),
|
|
63
|
+
React.createElement("div", { className: "message-card-body ".concat(bodyClassName || ""), style: bodyStyle },
|
|
64
|
+
React.createElement("p", { className: "message-card-comment ".concat(commentClassName || ""), style: commentStyle, dangerouslySetInnerHTML: { __html: item[commentKey] } }),
|
|
65
|
+
(item === null || item === void 0 ? void 0 : item[imageUrlKey]) && (React.createElement("div", { className: "message-card-attached-image-container ".concat(attachedImageContainerClassName || ""), style: attachedImageContainerStyle },
|
|
66
|
+
React.createElement("img", { src: item[imageUrlKey], alt: "Attached", className: "message-card-attached-image ".concat(attachedImageClassName || ""), style: attachedImageStyle }))),
|
|
67
|
+
(hashtags.length > 0 || mentions.length > 0) && (React.createElement("div", { className: "message-card-tags" },
|
|
68
|
+
hashtags.map(function (tag, tagIndex) { return (React.createElement("span", { key: "hashtag-".concat(tagIndex), className: "tag-chip hashtag-chip" }, tag)); }),
|
|
69
|
+
mentions.map(function (mention, mentionIndex) { return (React.createElement("span", { key: "mention-".concat(mentionIndex), className: "tag-chip mention-chip" }, mention)); }))))));
|
|
70
|
+
})));
|
|
71
|
+
};
|
package/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>React Mention Input Demo</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/main.tsx
ADDED
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-mention-input",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.16",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
8
|
"build": "tsc && npm run copy-css",
|
|
9
|
-
"copy-css": "copyfiles -u 1 src/**/*.css dist"
|
|
9
|
+
"copy-css": "copyfiles -u 1 src/**/*.css dist",
|
|
10
|
+
"dev": "vite",
|
|
11
|
+
"demo": "vite"
|
|
10
12
|
},
|
|
11
13
|
"keywords": [
|
|
12
14
|
"mention",
|
|
@@ -27,13 +29,15 @@
|
|
|
27
29
|
"@types/node": "^22.9.0",
|
|
28
30
|
"@types/react": "^18.3.12",
|
|
29
31
|
"@types/react-dom": "^18.3.1",
|
|
32
|
+
"@vitejs/plugin-react": "^4.6.0",
|
|
30
33
|
"copyfiles": "^2.4.1",
|
|
31
34
|
"csstype": "^3.1.3",
|
|
32
35
|
"sass": "^1.81.0",
|
|
33
|
-
"typescript": "^5.6.3"
|
|
36
|
+
"typescript": "^5.6.3",
|
|
37
|
+
"vite": "^7.0.0"
|
|
34
38
|
},
|
|
35
39
|
"peerDependencies": {
|
|
36
|
-
"react": "^
|
|
37
|
-
"react-dom": "^
|
|
40
|
+
"react": "^18.3.1",
|
|
41
|
+
"react-dom": "^18.3.1"
|
|
38
42
|
}
|
|
39
43
|
}
|
package/src/MentionInput.css
CHANGED
|
@@ -193,6 +193,14 @@
|
|
|
193
193
|
font-weight: 500;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
.hashtag-highlight {
|
|
197
|
+
background-color: rgba(255, 165, 0, 0.1);
|
|
198
|
+
color: #FF8C00;
|
|
199
|
+
border-radius: 4px;
|
|
200
|
+
padding: 1px 4px;
|
|
201
|
+
font-weight: 500;
|
|
202
|
+
}
|
|
203
|
+
|
|
196
204
|
.link-highlight {
|
|
197
205
|
color: #2684FF;
|
|
198
206
|
text-decoration: none;
|
package/src/MentionInput.tsx
CHANGED
|
@@ -27,6 +27,7 @@ interface MentionInputProps {
|
|
|
27
27
|
messageHTML: string;
|
|
28
28
|
userSelectListWithIds: { id: number; name: string }[];
|
|
29
29
|
userSelectListName: string[];
|
|
30
|
+
tags: string[];
|
|
30
31
|
images?: File[];
|
|
31
32
|
imageUrl?: string | null;
|
|
32
33
|
}) => void;
|
|
@@ -68,17 +69,29 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
68
69
|
const caretOffsetRef = useRef<number>(0);
|
|
69
70
|
const userSelectListRef = useRef<string[]>([]); // Only unique names
|
|
70
71
|
const userSelectListWithIdsRef = useRef<{ id: number; name: string }[]>([]); // Unique IDs with names
|
|
72
|
+
const tagsListRef = useRef<string[]>([]); // Store hashtags
|
|
71
73
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
72
74
|
|
|
73
75
|
const highlightMentionsAndLinks = (text: string): string => {
|
|
74
76
|
// Regular expression for detecting links
|
|
75
77
|
const linkRegex = /(https?:\/\/[^\s]+)/g;
|
|
78
|
+
|
|
79
|
+
// Regular expression for detecting hashtags
|
|
80
|
+
const hashtagRegex = /#[\w]+/g;
|
|
76
81
|
|
|
77
82
|
// Highlight links
|
|
78
83
|
let highlightedText = text.replace(
|
|
79
84
|
linkRegex,
|
|
80
85
|
'<a href="$1" target="_blank" rel="noopener noreferrer" class="link-highlight">$1</a>'
|
|
81
86
|
);
|
|
87
|
+
|
|
88
|
+
// Highlight hashtags
|
|
89
|
+
highlightedText = highlightedText.replace(
|
|
90
|
+
hashtagRegex,
|
|
91
|
+
(match) => {
|
|
92
|
+
return `<span class="hashtag-highlight">${match}</span>`;
|
|
93
|
+
}
|
|
94
|
+
);
|
|
82
95
|
|
|
83
96
|
// Highlight mentions manually based on `userSelectListRef`
|
|
84
97
|
userSelectListRef?.current.forEach((userName) => {
|
|
@@ -160,8 +173,17 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
160
173
|
setShowSuggestions(false);
|
|
161
174
|
}
|
|
162
175
|
|
|
163
|
-
//
|
|
164
|
-
|
|
176
|
+
// Extract and store hashtags
|
|
177
|
+
const hashtagMatches = plainText.match(/#[\w]+/g);
|
|
178
|
+
if (hashtagMatches) {
|
|
179
|
+
const uniqueTags = Array.from(new Set(hashtagMatches));
|
|
180
|
+
tagsListRef.current = uniqueTags;
|
|
181
|
+
} else {
|
|
182
|
+
tagsListRef.current = [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Only apply highlighting if we have mentions, hashtags, or links to highlight
|
|
186
|
+
if (userSelectListRef.current.length > 0 || plainText.match(/(https?:\/\/[^\s]+)/g) || plainText.match(/#[\w]+/g)) {
|
|
165
187
|
const currentHTML = inputRef.current.innerHTML;
|
|
166
188
|
const htmlWithHighlights = highlightMentionsAndLinks(plainText);
|
|
167
189
|
|
|
@@ -373,6 +395,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
373
395
|
messageHTML,
|
|
374
396
|
userSelectListWithIds: userSelectListWithIdsRef.current,
|
|
375
397
|
userSelectListName: userSelectListRef.current,
|
|
398
|
+
tags: tagsListRef.current,
|
|
376
399
|
images: selectedImage ? [selectedImage] : [],
|
|
377
400
|
imageUrl: imageUrl
|
|
378
401
|
});
|
|
@@ -383,6 +406,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
383
406
|
setImageUrl(null);
|
|
384
407
|
userSelectListRef.current = [];
|
|
385
408
|
userSelectListWithIdsRef.current = [];
|
|
409
|
+
tagsListRef.current = [];
|
|
386
410
|
}
|
|
387
411
|
}
|
|
388
412
|
};
|
|
@@ -394,8 +418,6 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
394
418
|
}
|
|
395
419
|
};
|
|
396
420
|
|
|
397
|
-
console.log(inputValue, inputRef.current?.innerText.trim(), "inputValue====")
|
|
398
|
-
|
|
399
421
|
return (
|
|
400
422
|
<div className={`mention-container ${containerClassName || ""}`}>
|
|
401
423
|
{imageUrl && selectedImage && (
|
package/src/ShowMessageCard.css
CHANGED
|
@@ -21,9 +21,25 @@
|
|
|
21
21
|
.message-card-header {
|
|
22
22
|
display: flex;
|
|
23
23
|
align-items: center;
|
|
24
|
+
justify-content: space-between;
|
|
24
25
|
margin-bottom: 8px; /* Space between header and body */
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
.message-card-header-left {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.message-card-item-name {
|
|
34
|
+
font-size: 14px;
|
|
35
|
+
color: #666;
|
|
36
|
+
font-weight: 500;
|
|
37
|
+
background-color: #f5f5f5;
|
|
38
|
+
padding: 4px 8px;
|
|
39
|
+
border-radius: 6px;
|
|
40
|
+
border: 1px solid #e0e0e0;
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
.message-card-img,
|
|
28
44
|
.message-card-initials {
|
|
29
45
|
width: 48px;
|
|
@@ -72,4 +88,40 @@
|
|
|
72
88
|
color: #007bff;
|
|
73
89
|
padding: 2px 4px;
|
|
74
90
|
border-radius: 4px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.hashtag-highlight {
|
|
94
|
+
background-color: rgba(255, 165, 0, 0.15);
|
|
95
|
+
color: #FF8C00;
|
|
96
|
+
padding: 2px 4px;
|
|
97
|
+
border-radius: 4px;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Tag chips styling */
|
|
102
|
+
.message-card-tags {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-wrap: wrap;
|
|
105
|
+
gap: 8px;
|
|
106
|
+
margin-top: 12px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.tag-chip {
|
|
110
|
+
padding: 6px 12px;
|
|
111
|
+
border-radius: 16px;
|
|
112
|
+
font-size: 12px;
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
border: none;
|
|
115
|
+
display: inline-block;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.hashtag-chip {
|
|
120
|
+
background-color: #D4A574;
|
|
121
|
+
color: #FFFFFF;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.mention-chip {
|
|
125
|
+
background-color: #2684FF;
|
|
126
|
+
color: #FFFFFF;
|
|
75
127
|
}
|
package/src/ShowMessageCard.tsx
CHANGED
|
@@ -12,6 +12,7 @@ interface ShowMessageCardProps {
|
|
|
12
12
|
commentKey?: string; // Custom key for comment
|
|
13
13
|
imgSrcKey?: string; // Custom key for image source
|
|
14
14
|
imageUrlKey?: string; // Custom key for attached image URL
|
|
15
|
+
itemNameKey?: string; // Custom key for item identifier (top-right)
|
|
15
16
|
containerClassName?: string; // Class for the outermost container
|
|
16
17
|
containerStyle?: CSSProperties; // Style for the outermost container
|
|
17
18
|
cardClassName?: string; // Class for the card
|
|
@@ -34,6 +35,8 @@ interface ShowMessageCardProps {
|
|
|
34
35
|
attachedImageStyle?: CSSProperties; // Style for the attached image
|
|
35
36
|
attachedImageContainerClassName?: string; // Class for the attached image container
|
|
36
37
|
attachedImageContainerStyle?: CSSProperties; // Style for the attached image container
|
|
38
|
+
itemNameClassName?: string; // Class for the item name (top-right)
|
|
39
|
+
itemNameStyle?: CSSProperties; // Style for the item name (top-right)
|
|
37
40
|
renderItem?: (element: MessageCardProps) => ReactNode; // Custom render function
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -44,6 +47,7 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
44
47
|
commentKey = "comment",
|
|
45
48
|
imgSrcKey = "imgSrc",
|
|
46
49
|
imageUrlKey = "imageUrl", // Default key for attached image
|
|
50
|
+
itemNameKey = "name", // Default key for item identifier
|
|
47
51
|
containerClassName,
|
|
48
52
|
containerStyle,
|
|
49
53
|
cardClassName,
|
|
@@ -66,6 +70,8 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
66
70
|
attachedImageStyle,
|
|
67
71
|
attachedImageContainerClassName,
|
|
68
72
|
attachedImageContainerStyle,
|
|
73
|
+
itemNameClassName,
|
|
74
|
+
itemNameStyle,
|
|
69
75
|
renderItem, // Custom render function
|
|
70
76
|
}) => {
|
|
71
77
|
// State to manage initials for images that fail to load
|
|
@@ -91,6 +97,18 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
91
97
|
return initials;
|
|
92
98
|
};
|
|
93
99
|
|
|
100
|
+
// Helper function to extract hashtags and mentions from text
|
|
101
|
+
const extractTagsAndMentions = (text: string) => {
|
|
102
|
+
const plainText = text.replace(/<[^>]*>/g, ''); // Remove HTML tags to get plain text
|
|
103
|
+
const hashtags = plainText.match(/#[\w]+/g) || [];
|
|
104
|
+
const mentions = plainText.match(/@[\w\s-]+/g) || [];
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
hashtags: Array.from(new Set(hashtags)), // Remove duplicates
|
|
108
|
+
mentions: Array.from(new Set(mentions.map(mention => mention.trim()))) // Remove duplicates and trim
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
94
112
|
return (
|
|
95
113
|
<div
|
|
96
114
|
className={`message-card-container ${containerClassName || ""}`}
|
|
@@ -107,6 +125,9 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
107
125
|
}
|
|
108
126
|
|
|
109
127
|
const showInitials = initialsState[item.id || index] || !item[imgSrcKey]; // Decide whether to show initials
|
|
128
|
+
|
|
129
|
+
// Extract tags and mentions from the comment
|
|
130
|
+
const { hashtags, mentions } = extractTagsAndMentions(item[commentKey] || '');
|
|
110
131
|
|
|
111
132
|
return (
|
|
112
133
|
<div
|
|
@@ -118,39 +139,51 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
118
139
|
className={`message-card-header ${headerClassName || ""}`}
|
|
119
140
|
style={headerStyle}
|
|
120
141
|
>
|
|
121
|
-
|
|
142
|
+
<div className="message-card-header-left">
|
|
143
|
+
{showInitials ? (
|
|
144
|
+
<div
|
|
145
|
+
className={`message-card-initials ${imgClassName || ""}`}
|
|
146
|
+
style={imgStyle}
|
|
147
|
+
>
|
|
148
|
+
{getInitials(item[nameKey])}
|
|
149
|
+
</div>
|
|
150
|
+
) : (
|
|
151
|
+
<img
|
|
152
|
+
src={item[imgSrcKey]}
|
|
153
|
+
alt={item[nameKey]}
|
|
154
|
+
className={`message-card-img ${imgClassName || ""}`}
|
|
155
|
+
style={imgStyle}
|
|
156
|
+
onError={() => handleImageError(item.id || index)} // Pass card id or index
|
|
157
|
+
/>
|
|
158
|
+
)}
|
|
122
159
|
<div
|
|
123
|
-
className={`message-card-
|
|
124
|
-
style={
|
|
160
|
+
className={`message-card-info ${infoClassName || ""}`}
|
|
161
|
+
style={infoStyle}
|
|
125
162
|
>
|
|
126
|
-
|
|
163
|
+
<h3
|
|
164
|
+
className={`message-card-name ${nameClassName || ""}`}
|
|
165
|
+
style={nameStyle}
|
|
166
|
+
>
|
|
167
|
+
{item[nameKey]}
|
|
168
|
+
</h3>
|
|
169
|
+
<p
|
|
170
|
+
className={`message-card-date ${dateClassName || ""}`}
|
|
171
|
+
style={dateStyle}
|
|
172
|
+
>
|
|
173
|
+
{item[dateKey]}
|
|
174
|
+
</p>
|
|
127
175
|
</div>
|
|
128
|
-
) : (
|
|
129
|
-
<img
|
|
130
|
-
src={item[imgSrcKey]}
|
|
131
|
-
alt={item[nameKey]}
|
|
132
|
-
className={`message-card-img ${imgClassName || ""}`}
|
|
133
|
-
style={imgStyle}
|
|
134
|
-
onError={() => handleImageError(item.id || index)} // Pass card id or index
|
|
135
|
-
/>
|
|
136
|
-
)}
|
|
137
|
-
<div
|
|
138
|
-
className={`message-card-info ${infoClassName || ""}`}
|
|
139
|
-
style={infoStyle}
|
|
140
|
-
>
|
|
141
|
-
<h3
|
|
142
|
-
className={`message-card-name ${nameClassName || ""}`}
|
|
143
|
-
style={nameStyle}
|
|
144
|
-
>
|
|
145
|
-
{item[nameKey]}
|
|
146
|
-
</h3>
|
|
147
|
-
<p
|
|
148
|
-
className={`message-card-date ${dateClassName || ""}`}
|
|
149
|
-
style={dateStyle}
|
|
150
|
-
>
|
|
151
|
-
{item[dateKey]}
|
|
152
|
-
</p>
|
|
153
176
|
</div>
|
|
177
|
+
|
|
178
|
+
{/* Item identifier in top-right corner */}
|
|
179
|
+
{item[itemNameKey] && (
|
|
180
|
+
<div
|
|
181
|
+
className={`message-card-item-name ${itemNameClassName || ""}`}
|
|
182
|
+
style={itemNameStyle}
|
|
183
|
+
>
|
|
184
|
+
{item[itemNameKey]}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
154
187
|
</div>
|
|
155
188
|
<div
|
|
156
189
|
className={`message-card-body ${bodyClassName || ""}`}
|
|
@@ -176,6 +209,22 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
176
209
|
/>
|
|
177
210
|
</div>
|
|
178
211
|
)}
|
|
212
|
+
|
|
213
|
+
{/* Display hashtags and mentions as chips */}
|
|
214
|
+
{(hashtags.length > 0 || mentions.length > 0) && (
|
|
215
|
+
<div className="message-card-tags">
|
|
216
|
+
{hashtags.map((tag, tagIndex) => (
|
|
217
|
+
<span key={`hashtag-${tagIndex}`} className="tag-chip hashtag-chip">
|
|
218
|
+
{tag}
|
|
219
|
+
</span>
|
|
220
|
+
))}
|
|
221
|
+
{mentions.map((mention, mentionIndex) => (
|
|
222
|
+
<span key={`mention-${mentionIndex}`} className="tag-chip mention-chip">
|
|
223
|
+
{mention}
|
|
224
|
+
</span>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
179
228
|
</div>
|
|
180
229
|
</div>
|
|
181
230
|
);
|