react-mention-input 1.1.26 → 1.1.27
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/dist/MentionInput.d.ts +2 -0
- package/dist/MentionInput.js +10 -3
- package/dist/ShowMessageCard.d.ts +2 -0
- package/dist/ShowMessageCard.js +22 -4
- package/dist/useProtectedImage.d.ts +12 -0
- package/dist/useProtectedImage.js +129 -0
- package/package.json +1 -1
- package/src/MentionInput.tsx +14 -2
- package/src/ShowMessageCard.tsx +58 -13
- package/src/useProtectedImage.ts +91 -0
package/dist/MentionInput.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ interface MentionInputProps {
|
|
|
32
32
|
}) => void;
|
|
33
33
|
suggestionPosition?: 'top' | 'bottom' | 'left' | 'right';
|
|
34
34
|
onImageUpload?: (file: File) => Promise<string>;
|
|
35
|
+
getAuthHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
36
|
+
isProtectedUrl?: boolean | ((url: string) => boolean);
|
|
35
37
|
}
|
|
36
38
|
declare const MentionInput: React.FC<MentionInputProps>;
|
|
37
39
|
export default MentionInput;
|
package/dist/MentionInput.js
CHANGED
|
@@ -46,9 +46,10 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
|
46
46
|
import React, { useState, useRef, useEffect } from "react";
|
|
47
47
|
import ReactDOM from "react-dom";
|
|
48
48
|
import "./MentionInput.css";
|
|
49
|
+
import { useProtectedImage } from "./useProtectedImage";
|
|
49
50
|
var MentionInput = function (_a) {
|
|
50
51
|
var _b;
|
|
51
|
-
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 ? 'top' : _d, onImageUpload = _a.onImageUpload;
|
|
52
|
+
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 ? 'top' : _d, onImageUpload = _a.onImageUpload, getAuthHeaders = _a.getAuthHeaders, isProtectedUrl = _a.isProtectedUrl;
|
|
52
53
|
var _e = useState(""), inputValue = _e[0], setInputValue = _e[1]; // Plain text
|
|
53
54
|
var _f = useState([]), suggestions = _f[0], setSuggestions = _f[1];
|
|
54
55
|
var _g = useState(false), showSuggestions = _g[0], setShowSuggestions = _g[1];
|
|
@@ -56,6 +57,12 @@ var MentionInput = function (_a) {
|
|
|
56
57
|
var _j = useState(null), imageUrl = _j[0], setImageUrl = _j[1];
|
|
57
58
|
var _k = useState(false), isUploading = _k[0], setIsUploading = _k[1];
|
|
58
59
|
var _l = useState(false), isDraggingOver = _l[0], setIsDraggingOver = _l[1];
|
|
60
|
+
// Use protected image hook to handle authenticated image URLs
|
|
61
|
+
var displayImageUrl = useProtectedImage({
|
|
62
|
+
url: imageUrl,
|
|
63
|
+
isProtected: isProtectedUrl,
|
|
64
|
+
getAuthHeaders: getAuthHeaders,
|
|
65
|
+
});
|
|
59
66
|
var inputRef = useRef(null);
|
|
60
67
|
var suggestionListRef = useRef(null);
|
|
61
68
|
var caretOffsetRef = useRef(0);
|
|
@@ -444,8 +451,8 @@ var MentionInput = function (_a) {
|
|
|
444
451
|
}
|
|
445
452
|
};
|
|
446
453
|
return (React.createElement("div", { className: "mention-container ".concat(containerClassName || "") },
|
|
447
|
-
|
|
448
|
-
React.createElement("img", { src:
|
|
454
|
+
displayImageUrl && selectedImage && (React.createElement("div", { className: "image-preview-card ".concat(attachedImageContainerClassName || ""), style: attachedImageContainerStyle },
|
|
455
|
+
React.createElement("img", { src: displayImageUrl, alt: "Preview", className: imgClassName || "", style: imgStyle }),
|
|
449
456
|
React.createElement("button", { onClick: removeImage, className: "remove-image-btn", "aria-label": "Remove image" }, "\u00D7"))),
|
|
450
457
|
React.createElement("div", { className: "mention-input-container ".concat(inputContainerClassName || "", " ").concat(isDraggingOver ? 'dragging-over' : ''), onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDragEnd: function () { return setIsDraggingOver(false); }, onDrop: handleDrop },
|
|
451
458
|
isDraggingOver && (React.createElement("div", { className: "drag-overlay" },
|
|
@@ -46,6 +46,8 @@ interface ShowMessageCardProps {
|
|
|
46
46
|
item: MessageCardProps;
|
|
47
47
|
}) => ReactNode;
|
|
48
48
|
renderItem?: (element: MessageCardProps) => ReactNode;
|
|
49
|
+
getAuthHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
50
|
+
isProtectedUrl?: boolean | ((url: string) => boolean);
|
|
49
51
|
}
|
|
50
52
|
export declare const ShowMessageCard: React.FC<ShowMessageCardProps>;
|
|
51
53
|
export {};
|
package/dist/ShowMessageCard.js
CHANGED
|
@@ -20,6 +20,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
|
20
20
|
};
|
|
21
21
|
import React, { useState } from "react";
|
|
22
22
|
import "./ShowMessageCard.css";
|
|
23
|
+
import { useProtectedImage } from "./useProtectedImage";
|
|
23
24
|
export var ShowMessageCard = function (_a) {
|
|
24
25
|
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
|
|
25
26
|
_g = _a.objectNameKey, // Default key for attached image
|
|
@@ -29,7 +30,8 @@ export var ShowMessageCard = function (_a) {
|
|
|
29
30
|
_j = _a.objectTypeIconKey, // Default key for revision
|
|
30
31
|
objectTypeIconKey = _j === void 0 ? "object_type_icon" : _j, // Default key for object type icon
|
|
31
32
|
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, objectNameClassName = _a.objectNameClassName, objectNameStyle = _a.objectNameStyle, revisionClassName = _a.revisionClassName, revisionStyle = _a.revisionStyle, objectChipRender = _a.objectChipRender, // Custom render function for object chip
|
|
32
|
-
renderItem = _a.renderItem
|
|
33
|
+
renderItem = _a.renderItem, // Custom render function
|
|
34
|
+
getAuthHeaders = _a.getAuthHeaders, isProtectedUrl = _a.isProtectedUrl;
|
|
33
35
|
// State to manage initials for images that fail to load
|
|
34
36
|
var _k = useState({}), initialsState = _k[0], setInitialsState = _k[1];
|
|
35
37
|
// Handle image load failure
|
|
@@ -48,6 +50,23 @@ export var ShowMessageCard = function (_a) {
|
|
|
48
50
|
.join("");
|
|
49
51
|
return initials;
|
|
50
52
|
};
|
|
53
|
+
// Component to render protected images
|
|
54
|
+
var ProtectedImage = function (_a) {
|
|
55
|
+
var url = _a.url, alt = _a.alt, className = _a.className, style = _a.style, containerClassName = _a.containerClassName, containerStyle = _a.containerStyle, onError = _a.onError, _b = _a.renderInContainer, renderInContainer = _b === void 0 ? true : _b;
|
|
56
|
+
var displayUrl = useProtectedImage({
|
|
57
|
+
url: url,
|
|
58
|
+
isProtected: isProtectedUrl,
|
|
59
|
+
getAuthHeaders: getAuthHeaders,
|
|
60
|
+
});
|
|
61
|
+
if (!displayUrl) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
var imgElement = (React.createElement("img", { src: displayUrl, alt: alt, className: className, style: style, onError: onError }));
|
|
65
|
+
if (renderInContainer && containerClassName) {
|
|
66
|
+
return (React.createElement("div", { className: containerClassName, style: containerStyle }, imgElement));
|
|
67
|
+
}
|
|
68
|
+
return imgElement;
|
|
69
|
+
};
|
|
51
70
|
// Helper function to extract hashtags and mentions from text
|
|
52
71
|
var extractTagsAndMentions = function (text) {
|
|
53
72
|
// First extract from HTML with spans
|
|
@@ -82,15 +101,14 @@ export var ShowMessageCard = function (_a) {
|
|
|
82
101
|
return (React.createElement("div", { key: item.id || index, className: "message-card-wrapper" },
|
|
83
102
|
React.createElement("div", { className: "message-card-header ".concat(headerClassName || ""), style: headerStyle },
|
|
84
103
|
React.createElement("div", { className: "message-card-header-left" },
|
|
85
|
-
showInitials ? (React.createElement("div", { className: "message-card-initials ".concat(imgClassName || ""), style: imgStyle }, getInitials(item[nameKey]))) : (React.createElement(
|
|
104
|
+
showInitials ? (React.createElement("div", { className: "message-card-initials ".concat(imgClassName || ""), style: imgStyle }, getInitials(item[nameKey]))) : (React.createElement(ProtectedImage, { url: item[imgSrcKey], alt: item[nameKey], className: "message-card-img ".concat(imgClassName || ""), style: imgStyle, onError: function () { return handleImageError(item.id || index); }, renderInContainer: false })),
|
|
86
105
|
React.createElement("div", { className: "message-card-info ".concat(infoClassName || ""), style: infoStyle },
|
|
87
106
|
React.createElement("h3", { className: "message-card-name ".concat(nameClassName || ""), style: nameStyle }, item[nameKey]),
|
|
88
107
|
React.createElement("p", { className: "message-card-date ".concat(dateClassName || ""), style: dateStyle }, item[dateKey])))),
|
|
89
108
|
React.createElement("div", { className: "message-card ".concat(cardClassName || ""), style: cardStyle },
|
|
90
109
|
React.createElement("div", { className: "message-card-body ".concat(bodyClassName || ""), style: bodyStyle },
|
|
91
110
|
React.createElement("p", { className: "message-card-comment ".concat(commentClassName || ""), style: commentStyle, dangerouslySetInnerHTML: { __html: item[commentKey] } }),
|
|
92
|
-
(item === null || item === void 0 ? void 0 : item[imageUrlKey]) && (React.createElement("
|
|
93
|
-
React.createElement("img", { src: item[imageUrlKey], alt: "Attached", className: "message-card-attached-image ".concat(attachedImageClassName || ""), style: attachedImageStyle }))),
|
|
111
|
+
(item === null || item === void 0 ? void 0 : item[imageUrlKey]) && (React.createElement(ProtectedImage, { url: item[imageUrlKey], alt: "Attached", className: "message-card-attached-image ".concat(attachedImageClassName || ""), style: attachedImageStyle, containerClassName: "message-card-attached-image-container ".concat(attachedImageContainerClassName || ""), containerStyle: attachedImageContainerStyle })),
|
|
94
112
|
item[objectNameKey] && (React.createElement("div", { className: "message-card-related" },
|
|
95
113
|
React.createElement("span", { className: "related-label" }, "Related: "),
|
|
96
114
|
(item[objectNameKey] || item[revisionKey] || item[objectTypeIconKey]) && (React.createElement(React.Fragment, null, objectChipRender ? (objectChipRender({
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface UseProtectedImageOptions {
|
|
2
|
+
url: string | null;
|
|
3
|
+
isProtected?: boolean | ((url: string) => boolean);
|
|
4
|
+
getAuthHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Custom hook to handle protected image URLs that require authentication tokens in headers.
|
|
8
|
+
* For protected URLs, it fetches the image with auth headers and converts it to a blob URL.
|
|
9
|
+
* For non-protected URLs, it returns the URL as-is.
|
|
10
|
+
*/
|
|
11
|
+
export declare const useProtectedImage: ({ url, isProtected, getAuthHeaders, }: UseProtectedImageOptions) => string | null;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
13
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
14
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
15
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
16
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
17
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
18
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
22
|
+
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);
|
|
23
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
24
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
25
|
+
function step(op) {
|
|
26
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
27
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
28
|
+
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;
|
|
29
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
30
|
+
switch (op[0]) {
|
|
31
|
+
case 0: case 1: t = op; break;
|
|
32
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
33
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
34
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
35
|
+
default:
|
|
36
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
37
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
38
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
39
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
40
|
+
if (t[2]) _.ops.pop();
|
|
41
|
+
_.trys.pop(); continue;
|
|
42
|
+
}
|
|
43
|
+
op = body.call(thisArg, _);
|
|
44
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
45
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
import { useState, useEffect, useRef } from 'react';
|
|
49
|
+
/**
|
|
50
|
+
* Custom hook to handle protected image URLs that require authentication tokens in headers.
|
|
51
|
+
* For protected URLs, it fetches the image with auth headers and converts it to a blob URL.
|
|
52
|
+
* For non-protected URLs, it returns the URL as-is.
|
|
53
|
+
*/
|
|
54
|
+
export var useProtectedImage = function (_a) {
|
|
55
|
+
var url = _a.url, isProtected = _a.isProtected, getAuthHeaders = _a.getAuthHeaders;
|
|
56
|
+
var _b = useState(null), blobUrl = _b[0], setBlobUrl = _b[1];
|
|
57
|
+
var blobUrlRef = useRef(null);
|
|
58
|
+
useEffect(function () {
|
|
59
|
+
// Cleanup previous blob URL
|
|
60
|
+
if (blobUrlRef.current) {
|
|
61
|
+
URL.revokeObjectURL(blobUrlRef.current);
|
|
62
|
+
blobUrlRef.current = null;
|
|
63
|
+
}
|
|
64
|
+
setBlobUrl(null);
|
|
65
|
+
if (!url) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Check if URL is protected
|
|
69
|
+
var shouldUseAuth = typeof isProtected === 'boolean'
|
|
70
|
+
? isProtected
|
|
71
|
+
: isProtected ? isProtected(url) : false;
|
|
72
|
+
// If not protected, don't fetch - will use original URL
|
|
73
|
+
if (!shouldUseAuth || !getAuthHeaders) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Fetch protected image with auth headers
|
|
77
|
+
var fetchProtectedImage = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
78
|
+
var headers, response, blob, newBlobUrl, error_1;
|
|
79
|
+
return __generator(this, function (_a) {
|
|
80
|
+
switch (_a.label) {
|
|
81
|
+
case 0:
|
|
82
|
+
_a.trys.push([0, 4, , 5]);
|
|
83
|
+
return [4 /*yield*/, Promise.resolve(getAuthHeaders())];
|
|
84
|
+
case 1:
|
|
85
|
+
headers = _a.sent();
|
|
86
|
+
return [4 /*yield*/, fetch(url, {
|
|
87
|
+
method: 'GET',
|
|
88
|
+
headers: __assign({}, headers),
|
|
89
|
+
})];
|
|
90
|
+
case 2:
|
|
91
|
+
response = _a.sent();
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error("Failed to fetch protected image: ".concat(response.statusText));
|
|
94
|
+
}
|
|
95
|
+
return [4 /*yield*/, response.blob()];
|
|
96
|
+
case 3:
|
|
97
|
+
blob = _a.sent();
|
|
98
|
+
newBlobUrl = URL.createObjectURL(blob);
|
|
99
|
+
blobUrlRef.current = newBlobUrl;
|
|
100
|
+
setBlobUrl(newBlobUrl);
|
|
101
|
+
return [3 /*break*/, 5];
|
|
102
|
+
case 4:
|
|
103
|
+
error_1 = _a.sent();
|
|
104
|
+
console.error('Error fetching protected image:', error_1);
|
|
105
|
+
// On error, fallback to original URL (might show broken image, but won't crash)
|
|
106
|
+
setBlobUrl(null);
|
|
107
|
+
return [3 /*break*/, 5];
|
|
108
|
+
case 5: return [2 /*return*/];
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}); };
|
|
112
|
+
fetchProtectedImage();
|
|
113
|
+
// Cleanup function
|
|
114
|
+
return function () {
|
|
115
|
+
if (blobUrlRef.current) {
|
|
116
|
+
URL.revokeObjectURL(blobUrlRef.current);
|
|
117
|
+
blobUrlRef.current = null;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}, [url, isProtected, getAuthHeaders]);
|
|
121
|
+
// Return blob URL if available, otherwise return original URL (or null)
|
|
122
|
+
if (!url) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
var isUrlProtected = typeof isProtected === 'boolean'
|
|
126
|
+
? isProtected
|
|
127
|
+
: isProtected ? isProtected(url) : false;
|
|
128
|
+
return blobUrl || (!isUrlProtected ? url : null);
|
|
129
|
+
};
|
package/package.json
CHANGED
package/src/MentionInput.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useRef, ReactNode, useEffect } from "react";
|
|
2
2
|
import ReactDOM from "react-dom";
|
|
3
3
|
import "./MentionInput.css";
|
|
4
|
+
import { useProtectedImage } from "./useProtectedImage";
|
|
4
5
|
|
|
5
6
|
interface User {
|
|
6
7
|
id: number;
|
|
@@ -32,6 +33,8 @@ interface MentionInputProps {
|
|
|
32
33
|
}) => void;
|
|
33
34
|
suggestionPosition?: 'top' | 'bottom' | 'left' | 'right'; // New prop for tooltip position
|
|
34
35
|
onImageUpload?: (file: File) => Promise<string>; // New prop for image upload
|
|
36
|
+
getAuthHeaders?: () => Record<string, string> | Promise<Record<string, string>>; // Function to get auth headers for protected images
|
|
37
|
+
isProtectedUrl?: boolean | ((url: string) => boolean); // Boolean or function to determine if a URL is protected and requires auth
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
const MentionInput: React.FC<MentionInputProps> = ({
|
|
@@ -52,6 +55,8 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
52
55
|
onSendMessage,
|
|
53
56
|
suggestionPosition = 'top',
|
|
54
57
|
onImageUpload,
|
|
58
|
+
getAuthHeaders,
|
|
59
|
+
isProtectedUrl,
|
|
55
60
|
}) => {
|
|
56
61
|
const [inputValue, setInputValue] = useState<string>(""); // Plain text
|
|
57
62
|
const [suggestions, setSuggestions] = useState<User[]>([]);
|
|
@@ -61,6 +66,13 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
61
66
|
const [isUploading, setIsUploading] = useState<boolean>(false);
|
|
62
67
|
const [isDraggingOver, setIsDraggingOver] = useState<boolean>(false);
|
|
63
68
|
|
|
69
|
+
// Use protected image hook to handle authenticated image URLs
|
|
70
|
+
const displayImageUrl = useProtectedImage({
|
|
71
|
+
url: imageUrl,
|
|
72
|
+
isProtected: isProtectedUrl,
|
|
73
|
+
getAuthHeaders,
|
|
74
|
+
});
|
|
75
|
+
|
|
64
76
|
|
|
65
77
|
|
|
66
78
|
const inputRef = useRef<HTMLDivElement>(null);
|
|
@@ -532,10 +544,10 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
532
544
|
|
|
533
545
|
return (
|
|
534
546
|
<div className={`mention-container ${containerClassName || ""}`}>
|
|
535
|
-
{
|
|
547
|
+
{displayImageUrl && selectedImage && (
|
|
536
548
|
<div className={`image-preview-card ${attachedImageContainerClassName || ""}`} style={attachedImageContainerStyle}>
|
|
537
549
|
<img
|
|
538
|
-
src={
|
|
550
|
+
src={displayImageUrl}
|
|
539
551
|
alt="Preview"
|
|
540
552
|
className={imgClassName || ""}
|
|
541
553
|
style={imgStyle}
|
package/src/ShowMessageCard.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { CSSProperties, useState, ReactNode } from "react";
|
|
2
2
|
import "./ShowMessageCard.css";
|
|
3
|
+
import { useProtectedImage } from "./useProtectedImage";
|
|
3
4
|
|
|
4
5
|
interface MessageCardProps {
|
|
5
6
|
[key: string]: any; // Support dynamic keys
|
|
@@ -43,6 +44,8 @@ interface ShowMessageCardProps {
|
|
|
43
44
|
revisionStyle?: CSSProperties; // Style for the revision (top-right)
|
|
44
45
|
objectChipRender?: (objectData: { objectName?: string; revision?: string; objectTypeIcon?: string; item: MessageCardProps }) => ReactNode; // Custom render function for object chip
|
|
45
46
|
renderItem?: (element: MessageCardProps) => ReactNode; // Custom render function
|
|
47
|
+
getAuthHeaders?: () => Record<string, string> | Promise<Record<string, string>>; // Function to get auth headers for protected images
|
|
48
|
+
isProtectedUrl?: boolean | ((url: string) => boolean); // Boolean or function to determine if a URL is protected and requires auth
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
@@ -83,6 +86,8 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
83
86
|
revisionStyle,
|
|
84
87
|
objectChipRender, // Custom render function for object chip
|
|
85
88
|
renderItem, // Custom render function
|
|
89
|
+
getAuthHeaders,
|
|
90
|
+
isProtectedUrl,
|
|
86
91
|
}) => {
|
|
87
92
|
// State to manage initials for images that fail to load
|
|
88
93
|
const [initialsState, setInitialsState] = useState<{ [key: string]: boolean }>(
|
|
@@ -107,6 +112,48 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
107
112
|
return initials;
|
|
108
113
|
};
|
|
109
114
|
|
|
115
|
+
// Component to render protected images
|
|
116
|
+
const ProtectedImage: React.FC<{
|
|
117
|
+
url: string;
|
|
118
|
+
alt: string;
|
|
119
|
+
className?: string;
|
|
120
|
+
style?: CSSProperties;
|
|
121
|
+
containerClassName?: string;
|
|
122
|
+
containerStyle?: CSSProperties;
|
|
123
|
+
onError?: () => void;
|
|
124
|
+
renderInContainer?: boolean;
|
|
125
|
+
}> = ({ url, alt, className, style, containerClassName, containerStyle, onError, renderInContainer = true }) => {
|
|
126
|
+
const displayUrl = useProtectedImage({
|
|
127
|
+
url,
|
|
128
|
+
isProtected: isProtectedUrl,
|
|
129
|
+
getAuthHeaders,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!displayUrl) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const imgElement = (
|
|
137
|
+
<img
|
|
138
|
+
src={displayUrl}
|
|
139
|
+
alt={alt}
|
|
140
|
+
className={className}
|
|
141
|
+
style={style}
|
|
142
|
+
onError={onError}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (renderInContainer && containerClassName) {
|
|
147
|
+
return (
|
|
148
|
+
<div className={containerClassName} style={containerStyle}>
|
|
149
|
+
{imgElement}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return imgElement;
|
|
155
|
+
};
|
|
156
|
+
|
|
110
157
|
// Helper function to extract hashtags and mentions from text
|
|
111
158
|
const extractTagsAndMentions = (text: string) => {
|
|
112
159
|
// First extract from HTML with spans
|
|
@@ -171,12 +218,13 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
171
218
|
{getInitials(item[nameKey])}
|
|
172
219
|
</div>
|
|
173
220
|
) : (
|
|
174
|
-
<
|
|
175
|
-
|
|
221
|
+
<ProtectedImage
|
|
222
|
+
url={item[imgSrcKey]}
|
|
176
223
|
alt={item[nameKey]}
|
|
177
224
|
className={`message-card-img ${imgClassName || ""}`}
|
|
178
225
|
style={imgStyle}
|
|
179
226
|
onError={() => handleImageError(item.id || index)}
|
|
227
|
+
renderInContainer={false}
|
|
180
228
|
/>
|
|
181
229
|
)}
|
|
182
230
|
<div
|
|
@@ -216,17 +264,14 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
216
264
|
|
|
217
265
|
{/* Display attached image if available */}
|
|
218
266
|
{item?.[imageUrlKey] && (
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
style={attachedImageStyle}
|
|
228
|
-
/>
|
|
229
|
-
</div>
|
|
267
|
+
<ProtectedImage
|
|
268
|
+
url={item[imageUrlKey]}
|
|
269
|
+
alt="Attached"
|
|
270
|
+
className={`message-card-attached-image ${attachedImageClassName || ""}`}
|
|
271
|
+
style={attachedImageStyle}
|
|
272
|
+
containerClassName={`message-card-attached-image-container ${attachedImageContainerClassName || ""}`}
|
|
273
|
+
containerStyle={attachedImageContainerStyle}
|
|
274
|
+
/>
|
|
230
275
|
)}
|
|
231
276
|
|
|
232
277
|
{/* Display related object at bottom-left */}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
interface UseProtectedImageOptions {
|
|
4
|
+
url: string | null;
|
|
5
|
+
isProtected?: boolean | ((url: string) => boolean);
|
|
6
|
+
getAuthHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Custom hook to handle protected image URLs that require authentication tokens in headers.
|
|
11
|
+
* For protected URLs, it fetches the image with auth headers and converts it to a blob URL.
|
|
12
|
+
* For non-protected URLs, it returns the URL as-is.
|
|
13
|
+
*/
|
|
14
|
+
export const useProtectedImage = ({
|
|
15
|
+
url,
|
|
16
|
+
isProtected,
|
|
17
|
+
getAuthHeaders,
|
|
18
|
+
}: UseProtectedImageOptions): string | null => {
|
|
19
|
+
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
|
20
|
+
const blobUrlRef = useRef<string | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
// Cleanup previous blob URL
|
|
24
|
+
if (blobUrlRef.current) {
|
|
25
|
+
URL.revokeObjectURL(blobUrlRef.current);
|
|
26
|
+
blobUrlRef.current = null;
|
|
27
|
+
}
|
|
28
|
+
setBlobUrl(null);
|
|
29
|
+
|
|
30
|
+
if (!url) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if URL is protected
|
|
35
|
+
const shouldUseAuth = typeof isProtected === 'boolean'
|
|
36
|
+
? isProtected
|
|
37
|
+
: isProtected ? isProtected(url) : false;
|
|
38
|
+
|
|
39
|
+
// If not protected, don't fetch - will use original URL
|
|
40
|
+
if (!shouldUseAuth || !getAuthHeaders) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fetch protected image with auth headers
|
|
45
|
+
const fetchProtectedImage = async () => {
|
|
46
|
+
try {
|
|
47
|
+
const headers = await Promise.resolve(getAuthHeaders());
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
method: 'GET',
|
|
50
|
+
headers: {
|
|
51
|
+
...headers,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(`Failed to fetch protected image: ${response.statusText}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const blob = await response.blob();
|
|
60
|
+
const newBlobUrl = URL.createObjectURL(blob);
|
|
61
|
+
blobUrlRef.current = newBlobUrl;
|
|
62
|
+
setBlobUrl(newBlobUrl);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Error fetching protected image:', error);
|
|
65
|
+
// On error, fallback to original URL (might show broken image, but won't crash)
|
|
66
|
+
setBlobUrl(null);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
fetchProtectedImage();
|
|
71
|
+
|
|
72
|
+
// Cleanup function
|
|
73
|
+
return () => {
|
|
74
|
+
if (blobUrlRef.current) {
|
|
75
|
+
URL.revokeObjectURL(blobUrlRef.current);
|
|
76
|
+
blobUrlRef.current = null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}, [url, isProtected, getAuthHeaders]);
|
|
80
|
+
|
|
81
|
+
// Return blob URL if available, otherwise return original URL (or null)
|
|
82
|
+
if (!url) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const isUrlProtected = typeof isProtected === 'boolean'
|
|
87
|
+
? isProtected
|
|
88
|
+
: isProtected ? isProtected(url) : false;
|
|
89
|
+
return blobUrl || (!isUrlProtected ? url : null);
|
|
90
|
+
};
|
|
91
|
+
|