react-mention-input 1.1.25 → 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.
@@ -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;
@@ -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 ? 'bottom' : _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);
@@ -229,6 +236,7 @@ var MentionInput = function (_a) {
229
236
  }
230
237
  };
231
238
  var renderSuggestions = function () {
239
+ var _a, _b, _c, _d, _e, _f;
232
240
  if (!showSuggestions || !inputRef.current)
233
241
  return null;
234
242
  var getInitials = function (name) {
@@ -240,27 +248,32 @@ var MentionInput = function (_a) {
240
248
  return initials;
241
249
  };
242
250
  var inputRect = inputRef.current.getBoundingClientRect();
251
+ var scrollLeft = (_c = (_b = (_a = window.scrollX) !== null && _a !== void 0 ? _a : window.pageXOffset) !== null && _b !== void 0 ? _b : document.documentElement.scrollLeft) !== null && _c !== void 0 ? _c : 0;
252
+ var scrollTop = (_f = (_e = (_d = window.scrollY) !== null && _d !== void 0 ? _d : window.pageYOffset) !== null && _e !== void 0 ? _e : document.documentElement.scrollTop) !== null && _f !== void 0 ? _f : 0;
243
253
  var styles = {
244
254
  position: 'absolute',
245
255
  zIndex: 1000,
256
+ minWidth: inputRect.width,
246
257
  };
247
258
  // Use suggestionPosition prop to adjust tooltip position
248
259
  switch (suggestionPosition) {
249
260
  case 'top':
250
- styles.left = "".concat(inputRect.left, "px");
251
- styles.top = "".concat(inputRect.top - 150, "px");
261
+ styles.left = "".concat(inputRect.left + scrollLeft, "px");
262
+ styles.top = "".concat(inputRect.top + scrollTop - 8, "px");
263
+ styles.transform = 'translateY(-100%)';
252
264
  break;
253
265
  case 'bottom':
254
- styles.left = "".concat(inputRect.left, "px");
255
- styles.top = "".concat(inputRect.bottom, "px");
266
+ styles.left = "".concat(inputRect.left + scrollLeft, "px");
267
+ styles.top = "".concat(inputRect.bottom + scrollTop + 8, "px");
256
268
  break;
257
269
  case 'left':
258
- styles.left = "".concat(inputRect.left - 150, "px");
259
- styles.top = "".concat(inputRect.top, "px");
270
+ styles.left = "".concat(inputRect.left + scrollLeft - 8, "px");
271
+ styles.top = "".concat(inputRect.top + scrollTop, "px");
272
+ styles.transform = "".concat(styles.transform ? "".concat(styles.transform, " ") : '', "translateX(-100%)");
260
273
  break;
261
274
  case 'right':
262
- styles.left = "".concat(inputRect.right, "px");
263
- styles.top = "".concat(inputRect.top, "px");
275
+ styles.left = "".concat(inputRect.right + scrollLeft + 8, "px");
276
+ styles.top = "".concat(inputRect.top + scrollTop, "px");
264
277
  break;
265
278
  default:
266
279
  break;
@@ -438,8 +451,8 @@ var MentionInput = function (_a) {
438
451
  }
439
452
  };
440
453
  return (React.createElement("div", { className: "mention-container ".concat(containerClassName || "") },
441
- imageUrl && selectedImage && (React.createElement("div", { className: "image-preview-card ".concat(attachedImageContainerClassName || ""), style: attachedImageContainerStyle },
442
- React.createElement("img", { src: imageUrl, alt: "Preview", className: imgClassName || "", style: imgStyle }),
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 }),
443
456
  React.createElement("button", { onClick: removeImage, className: "remove-image-btn", "aria-label": "Remove image" }, "\u00D7"))),
444
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 },
445
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 {};
@@ -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("img", { src: item[imgSrcKey], alt: item[nameKey], className: "message-card-img ".concat(imgClassName || ""), style: imgStyle, onError: function () { return handleImageError(item.id || index); } })),
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("div", { className: "message-card-attached-image-container ".concat(attachedImageContainerClassName || ""), style: attachedImageContainerStyle },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-mention-input",
3
- "version": "1.1.25",
3
+ "version": "1.1.27",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
@@ -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> = ({
@@ -50,8 +53,10 @@ const MentionInput: React.FC<MentionInputProps> = ({
50
53
  sendButtonIcon,
51
54
  attachmentButtonIcon,
52
55
  onSendMessage,
53
- suggestionPosition = 'bottom',
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);
@@ -302,28 +314,35 @@ const MentionInput: React.FC<MentionInputProps> = ({
302
314
  };
303
315
 
304
316
  const inputRect = inputRef.current.getBoundingClientRect();
317
+ const scrollLeft =
318
+ window.scrollX ?? window.pageXOffset ?? document.documentElement.scrollLeft ?? 0;
319
+ const scrollTop =
320
+ window.scrollY ?? window.pageYOffset ?? document.documentElement.scrollTop ?? 0;
305
321
  const styles: React.CSSProperties = {
306
322
  position: 'absolute',
307
323
  zIndex: 1000,
324
+ minWidth: inputRect.width,
308
325
  };
309
326
 
310
327
  // Use suggestionPosition prop to adjust tooltip position
311
328
  switch (suggestionPosition) {
312
329
  case 'top':
313
- styles.left = `${inputRect.left}px`;
314
- styles.top = `${inputRect.top - 150}px`;
330
+ styles.left = `${inputRect.left + scrollLeft}px`;
331
+ styles.top = `${inputRect.top + scrollTop - 8}px`;
332
+ styles.transform = 'translateY(-100%)';
315
333
  break;
316
334
  case 'bottom':
317
- styles.left = `${inputRect.left}px`;
318
- styles.top = `${inputRect.bottom}px`;
335
+ styles.left = `${inputRect.left + scrollLeft}px`;
336
+ styles.top = `${inputRect.bottom + scrollTop + 8}px`;
319
337
  break;
320
338
  case 'left':
321
- styles.left = `${inputRect.left - 150}px`;
322
- styles.top = `${inputRect.top}px`;
339
+ styles.left = `${inputRect.left + scrollLeft - 8}px`;
340
+ styles.top = `${inputRect.top + scrollTop}px`;
341
+ styles.transform = `${styles.transform ? `${styles.transform} ` : ''}translateX(-100%)`;
323
342
  break;
324
343
  case 'right':
325
- styles.left = `${inputRect.right}px`;
326
- styles.top = `${inputRect.top}px`;
344
+ styles.left = `${inputRect.right + scrollLeft + 8}px`;
345
+ styles.top = `${inputRect.top + scrollTop}px`;
327
346
  break;
328
347
  default:
329
348
  break;
@@ -525,10 +544,10 @@ const MentionInput: React.FC<MentionInputProps> = ({
525
544
 
526
545
  return (
527
546
  <div className={`mention-container ${containerClassName || ""}`}>
528
- {imageUrl && selectedImage && (
547
+ {displayImageUrl && selectedImage && (
529
548
  <div className={`image-preview-card ${attachedImageContainerClassName || ""}`} style={attachedImageContainerStyle}>
530
549
  <img
531
- src={imageUrl}
550
+ src={displayImageUrl}
532
551
  alt="Preview"
533
552
  className={imgClassName || ""}
534
553
  style={imgStyle}
@@ -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
- <img
175
- src={item[imgSrcKey]}
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
- <div
220
- className={`message-card-attached-image-container ${attachedImageContainerClassName || ""}`}
221
- style={attachedImageContainerStyle}
222
- >
223
- <img
224
- src={item[imageUrlKey]}
225
- alt="Attached"
226
- className={`message-card-attached-image ${attachedImageClassName || ""}`}
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
+