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.
@@ -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 ? '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
- imageUrl && selectedImage && (React.createElement("div", { className: "image-preview-card ".concat(attachedImageContainerClassName || ""), style: attachedImageContainerStyle },
448
- 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 }),
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 {};
@@ -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.26",
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> = ({
@@ -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
- {imageUrl && selectedImage && (
547
+ {displayImageUrl && selectedImage && (
536
548
  <div className={`image-preview-card ${attachedImageContainerClassName || ""}`} style={attachedImageContainerStyle}>
537
549
  <img
538
- src={imageUrl}
550
+ src={displayImageUrl}
539
551
  alt="Preview"
540
552
  className={imgClassName || ""}
541
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
+