react-mention-input 1.1.29 → 1.1.30

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.
@@ -18,7 +18,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
18
18
  }
19
19
  return to.concat(ar || Array.prototype.slice.call(from));
20
20
  };
21
- import React, { useState } from "react";
21
+ import React, { useState, memo } from "react";
22
22
  import "./ShowMessageCard.css";
23
23
  import { useProtectedImage } from "./useProtectedImage";
24
24
  export var ShowMessageCard = function (_a) {
@@ -50,8 +50,8 @@ export var ShowMessageCard = function (_a) {
50
50
  .join("");
51
51
  return initials;
52
52
  };
53
- // Component to render protected images
54
- var ProtectedImage = function (_a) {
53
+ // Component to render protected images - memoized to prevent recreation on every render
54
+ var ProtectedImage = memo(function (_a) {
55
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
56
  var displayUrl = useProtectedImage({
57
57
  url: url,
@@ -66,7 +66,7 @@ export var ShowMessageCard = function (_a) {
66
66
  return (React.createElement("div", { className: containerClassName, style: containerStyle }, imgElement));
67
67
  }
68
68
  return imgElement;
69
- };
69
+ });
70
70
  // Helper function to extract hashtags and mentions from text
71
71
  var extractTagsAndMentions = function (text) {
72
72
  // First extract from HTML with spans
@@ -7,6 +7,7 @@ interface UseProtectedImageOptions {
7
7
  * Custom hook to handle protected image URLs that require authentication tokens in headers.
8
8
  * For protected URLs, it fetches the image with auth headers and converts it to a blob URL.
9
9
  * For non-protected URLs, it returns the URL as-is.
10
+ * Uses a module-level cache to prevent blinking on re-renders.
10
11
  */
11
12
  export declare const useProtectedImage: ({ url, isProtected, getAuthHeaders, }: UseProtectedImageOptions) => string | null;
12
13
  export {};
@@ -46,40 +46,49 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
46
46
  }
47
47
  };
48
48
  import { useState, useEffect, useRef } from 'react';
49
+ // Module-level cache to persist blob URLs across component re-renders
50
+ var blobUrlCache = new Map();
51
+ var fetchingUrls = new Set();
49
52
  /**
50
53
  * Custom hook to handle protected image URLs that require authentication tokens in headers.
51
54
  * For protected URLs, it fetches the image with auth headers and converts it to a blob URL.
52
55
  * For non-protected URLs, it returns the URL as-is.
56
+ * Uses a module-level cache to prevent blinking on re-renders.
53
57
  */
54
58
  export var useProtectedImage = function (_a) {
55
59
  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
- var blobUrlForUrlRef = useRef(null); // Track which URL the blob URL corresponds to
60
+ var _b = useState(function () {
61
+ // Initialize from cache if available
62
+ return url ? blobUrlCache.get(url) || null : null;
63
+ }), blobUrl = _b[0], setBlobUrl = _b[1];
59
64
  var previousUrlRef = useRef(null);
60
65
  var isProtectedRef = useRef(isProtected);
61
66
  var getAuthHeadersRef = useRef(getAuthHeaders);
67
+ var mountedRef = useRef(true);
62
68
  // Update refs when props change (but don't trigger re-fetch)
63
69
  useEffect(function () {
64
70
  isProtectedRef.current = isProtected;
65
71
  getAuthHeadersRef.current = getAuthHeaders;
66
72
  }, [isProtected, getAuthHeaders]);
67
73
  useEffect(function () {
68
- // If URL hasn't changed, don't do anything
74
+ mountedRef.current = true;
75
+ return function () {
76
+ mountedRef.current = false;
77
+ };
78
+ }, []);
79
+ useEffect(function () {
80
+ // Always check cache first and restore if needed (synchronous)
81
+ if (url && blobUrlCache.has(url) && !blobUrl) {
82
+ var cachedBlobUrl = blobUrlCache.get(url);
83
+ setBlobUrl(cachedBlobUrl);
84
+ }
85
+ // If URL hasn't changed, keep using cached blob URL
69
86
  if (url === previousUrlRef.current) {
70
87
  return;
71
88
  }
72
- // Store previous URL and blob URL for later cleanup
73
89
  var oldUrl = previousUrlRef.current;
74
- var oldBlobUrl = blobUrlRef.current;
75
90
  previousUrlRef.current = url;
76
91
  if (!url) {
77
- // If URL is null, clean up old blob URL and clear state
78
- if (oldBlobUrl) {
79
- URL.revokeObjectURL(oldBlobUrl);
80
- blobUrlRef.current = null;
81
- blobUrlForUrlRef.current = null;
82
- }
83
92
  setBlobUrl(null);
84
93
  return;
85
94
  }
@@ -87,23 +96,35 @@ export var useProtectedImage = function (_a) {
87
96
  var shouldUseAuth = typeof isProtectedRef.current === 'boolean'
88
97
  ? isProtectedRef.current
89
98
  : isProtectedRef.current ? isProtectedRef.current(url) : false;
90
- // If not protected, clean up old blob URL and use original URL
99
+ // If not protected, use original URL
91
100
  if (!shouldUseAuth || !getAuthHeadersRef.current) {
92
- if (oldBlobUrl) {
93
- URL.revokeObjectURL(oldBlobUrl);
94
- blobUrlRef.current = null;
95
- blobUrlForUrlRef.current = null;
101
+ // Clear any cached blob URL for this URL
102
+ if (blobUrlCache.has(url)) {
103
+ var cachedBlobUrl = blobUrlCache.get(url);
104
+ URL.revokeObjectURL(cachedBlobUrl);
105
+ blobUrlCache.delete(url);
96
106
  }
97
107
  setBlobUrl(null);
98
108
  return;
99
109
  }
110
+ // Check cache first - if we have a cached blob URL, use it immediately
111
+ if (blobUrlCache.has(url)) {
112
+ var cachedBlobUrl = blobUrlCache.get(url);
113
+ setBlobUrl(cachedBlobUrl);
114
+ return;
115
+ }
116
+ // If already fetching this URL, don't fetch again
117
+ if (fetchingUrls.has(url)) {
118
+ return;
119
+ }
100
120
  // Fetch protected image with auth headers
121
+ fetchingUrls.add(url);
101
122
  var fetchProtectedImage = function () { return __awaiter(void 0, void 0, void 0, function () {
102
123
  var headers, response, blob, newBlobUrl, error_1;
103
124
  return __generator(this, function (_a) {
104
125
  switch (_a.label) {
105
126
  case 0:
106
- _a.trys.push([0, 4, , 5]);
127
+ _a.trys.push([0, 4, 5, 6]);
107
128
  return [4 /*yield*/, Promise.resolve(getAuthHeadersRef.current())];
108
129
  case 1:
109
130
  headers = _a.sent();
@@ -120,44 +141,36 @@ export var useProtectedImage = function (_a) {
120
141
  case 3:
121
142
  blob = _a.sent();
122
143
  newBlobUrl = URL.createObjectURL(blob);
123
- // Only update if URL hasn't changed during fetch
124
- if (previousUrlRef.current === url) {
125
- // Revoke old blob URL now that new one is ready
126
- if (oldBlobUrl && oldUrl !== url) {
127
- URL.revokeObjectURL(oldBlobUrl);
128
- }
129
- blobUrlRef.current = newBlobUrl;
130
- blobUrlForUrlRef.current = url;
144
+ // Only update if URL hasn't changed during fetch and component is still mounted
145
+ if (previousUrlRef.current === url && mountedRef.current) {
146
+ // Cache the blob URL
147
+ blobUrlCache.set(url, newBlobUrl);
131
148
  setBlobUrl(newBlobUrl);
132
149
  }
133
150
  else {
134
- // URL changed during fetch, revoke this blob URL
151
+ // URL changed during fetch or component unmounted, revoke this blob URL
135
152
  URL.revokeObjectURL(newBlobUrl);
136
153
  }
137
- return [3 /*break*/, 5];
154
+ return [3 /*break*/, 6];
138
155
  case 4:
139
156
  error_1 = _a.sent();
140
157
  console.error('Error fetching protected image:', error_1);
141
- // On error, only clear if URL hasn't changed
142
- if (previousUrlRef.current === url) {
158
+ // On error, only clear if URL hasn't changed and component is mounted
159
+ if (previousUrlRef.current === url && mountedRef.current) {
143
160
  setBlobUrl(null);
144
161
  }
145
- return [3 /*break*/, 5];
146
- case 5: return [2 /*return*/];
162
+ return [3 /*break*/, 6];
163
+ case 5:
164
+ fetchingUrls.delete(url);
165
+ return [7 /*endfinally*/];
166
+ case 6: return [2 /*return*/];
147
167
  }
148
168
  });
149
169
  }); };
150
170
  fetchProtectedImage();
151
- // Cleanup function - revoke blob URL on unmount
152
- return function () {
153
- // On unmount, clean up the current blob URL
154
- if (blobUrlRef.current) {
155
- URL.revokeObjectURL(blobUrlRef.current);
156
- blobUrlRef.current = null;
157
- blobUrlForUrlRef.current = null;
158
- }
159
- };
160
- }, [url]); // Only depend on url, not isProtected or getAuthHeaders
171
+ }, [url]); // Only depend on url
172
+ // Cleanup: Don't revoke blob URLs on unmount - keep them in cache for reuse
173
+ // They will be cleaned up when the URL changes or when the cache is cleared
161
174
  // Return blob URL if available, otherwise return original URL (or null)
162
175
  if (!url) {
163
176
  return null;
@@ -165,5 +178,10 @@ export var useProtectedImage = function (_a) {
165
178
  var isUrlProtected = typeof isProtectedRef.current === 'boolean'
166
179
  ? isProtectedRef.current
167
180
  : isProtectedRef.current ? isProtectedRef.current(url) : false;
168
- return blobUrl || (!isUrlProtected ? url : null);
181
+ // For protected URLs, return cached blob URL or current blobUrl state
182
+ // For non-protected URLs, return the original URL
183
+ if (isUrlProtected) {
184
+ return blobUrl || blobUrlCache.get(url) || null;
185
+ }
186
+ return url;
169
187
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-mention-input",
3
- "version": "1.1.29",
3
+ "version": "1.1.30",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
@@ -1,4 +1,4 @@
1
- import React, { CSSProperties, useState, ReactNode } from "react";
1
+ import React, { CSSProperties, useState, ReactNode, useMemo, memo } from "react";
2
2
  import "./ShowMessageCard.css";
3
3
  import { useProtectedImage } from "./useProtectedImage";
4
4
 
@@ -112,8 +112,8 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
112
112
  return initials;
113
113
  };
114
114
 
115
- // Component to render protected images
116
- const ProtectedImage: React.FC<{
115
+ // Component to render protected images - memoized to prevent recreation on every render
116
+ const ProtectedImage = memo<{
117
117
  url: string;
118
118
  alt: string;
119
119
  className?: string;
@@ -122,7 +122,7 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
122
122
  containerStyle?: CSSProperties;
123
123
  onError?: () => void;
124
124
  renderInContainer?: boolean;
125
- }> = ({ url, alt, className, style, containerClassName, containerStyle, onError, renderInContainer = true }) => {
125
+ }>(({ url, alt, className, style, containerClassName, containerStyle, onError, renderInContainer = true }) => {
126
126
  const displayUrl = useProtectedImage({
127
127
  url,
128
128
  isProtected: isProtectedUrl,
@@ -152,7 +152,7 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
152
152
  }
153
153
 
154
154
  return imgElement;
155
- };
155
+ });
156
156
 
157
157
  // Helper function to extract hashtags and mentions from text
158
158
  const extractTagsAndMentions = (text: string) => {
@@ -6,22 +6,29 @@ interface UseProtectedImageOptions {
6
6
  getAuthHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
7
7
  }
8
8
 
9
+ // Module-level cache to persist blob URLs across component re-renders
10
+ const blobUrlCache = new Map<string, string>();
11
+ const fetchingUrls = new Set<string>();
12
+
9
13
  /**
10
14
  * Custom hook to handle protected image URLs that require authentication tokens in headers.
11
15
  * For protected URLs, it fetches the image with auth headers and converts it to a blob URL.
12
16
  * For non-protected URLs, it returns the URL as-is.
17
+ * Uses a module-level cache to prevent blinking on re-renders.
13
18
  */
14
19
  export const useProtectedImage = ({
15
20
  url,
16
21
  isProtected,
17
22
  getAuthHeaders,
18
23
  }: UseProtectedImageOptions): string | null => {
19
- const [blobUrl, setBlobUrl] = useState<string | null>(null);
20
- const blobUrlRef = useRef<string | null>(null);
21
- const blobUrlForUrlRef = useRef<string | null>(null); // Track which URL the blob URL corresponds to
24
+ const [blobUrl, setBlobUrl] = useState<string | null>(() => {
25
+ // Initialize from cache if available
26
+ return url ? blobUrlCache.get(url) || null : null;
27
+ });
22
28
  const previousUrlRef = useRef<string | null>(null);
23
29
  const isProtectedRef = useRef(isProtected);
24
30
  const getAuthHeadersRef = useRef(getAuthHeaders);
31
+ const mountedRef = useRef(true);
25
32
 
26
33
  // Update refs when props change (but don't trigger re-fetch)
27
34
  useEffect(() => {
@@ -30,23 +37,28 @@ export const useProtectedImage = ({
30
37
  }, [isProtected, getAuthHeaders]);
31
38
 
32
39
  useEffect(() => {
33
- // If URL hasn't changed, don't do anything
40
+ mountedRef.current = true;
41
+ return () => {
42
+ mountedRef.current = false;
43
+ };
44
+ }, []);
45
+
46
+ useEffect(() => {
47
+ // Always check cache first and restore if needed (synchronous)
48
+ if (url && blobUrlCache.has(url) && !blobUrl) {
49
+ const cachedBlobUrl = blobUrlCache.get(url)!;
50
+ setBlobUrl(cachedBlobUrl);
51
+ }
52
+
53
+ // If URL hasn't changed, keep using cached blob URL
34
54
  if (url === previousUrlRef.current) {
35
55
  return;
36
56
  }
37
57
 
38
- // Store previous URL and blob URL for later cleanup
39
58
  const oldUrl = previousUrlRef.current;
40
- const oldBlobUrl = blobUrlRef.current;
41
59
  previousUrlRef.current = url;
42
60
 
43
61
  if (!url) {
44
- // If URL is null, clean up old blob URL and clear state
45
- if (oldBlobUrl) {
46
- URL.revokeObjectURL(oldBlobUrl);
47
- blobUrlRef.current = null;
48
- blobUrlForUrlRef.current = null;
49
- }
50
62
  setBlobUrl(null);
51
63
  return;
52
64
  }
@@ -56,18 +68,32 @@ export const useProtectedImage = ({
56
68
  ? isProtectedRef.current
57
69
  : isProtectedRef.current ? isProtectedRef.current(url) : false;
58
70
 
59
- // If not protected, clean up old blob URL and use original URL
71
+ // If not protected, use original URL
60
72
  if (!shouldUseAuth || !getAuthHeadersRef.current) {
61
- if (oldBlobUrl) {
62
- URL.revokeObjectURL(oldBlobUrl);
63
- blobUrlRef.current = null;
64
- blobUrlForUrlRef.current = null;
73
+ // Clear any cached blob URL for this URL
74
+ if (blobUrlCache.has(url)) {
75
+ const cachedBlobUrl = blobUrlCache.get(url)!;
76
+ URL.revokeObjectURL(cachedBlobUrl);
77
+ blobUrlCache.delete(url);
65
78
  }
66
79
  setBlobUrl(null);
67
80
  return;
68
81
  }
69
82
 
83
+ // Check cache first - if we have a cached blob URL, use it immediately
84
+ if (blobUrlCache.has(url)) {
85
+ const cachedBlobUrl = blobUrlCache.get(url)!;
86
+ setBlobUrl(cachedBlobUrl);
87
+ return;
88
+ }
89
+
90
+ // If already fetching this URL, don't fetch again
91
+ if (fetchingUrls.has(url)) {
92
+ return;
93
+ }
94
+
70
95
  // Fetch protected image with auth headers
96
+ fetchingUrls.add(url);
71
97
  const fetchProtectedImage = async () => {
72
98
  try {
73
99
  const headers = await Promise.resolve(getAuthHeadersRef.current!());
@@ -85,40 +111,31 @@ export const useProtectedImage = ({
85
111
  const blob = await response.blob();
86
112
  const newBlobUrl = URL.createObjectURL(blob);
87
113
 
88
- // Only update if URL hasn't changed during fetch
89
- if (previousUrlRef.current === url) {
90
- // Revoke old blob URL now that new one is ready
91
- if (oldBlobUrl && oldUrl !== url) {
92
- URL.revokeObjectURL(oldBlobUrl);
93
- }
94
- blobUrlRef.current = newBlobUrl;
95
- blobUrlForUrlRef.current = url;
114
+ // Only update if URL hasn't changed during fetch and component is still mounted
115
+ if (previousUrlRef.current === url && mountedRef.current) {
116
+ // Cache the blob URL
117
+ blobUrlCache.set(url, newBlobUrl);
96
118
  setBlobUrl(newBlobUrl);
97
119
  } else {
98
- // URL changed during fetch, revoke this blob URL
120
+ // URL changed during fetch or component unmounted, revoke this blob URL
99
121
  URL.revokeObjectURL(newBlobUrl);
100
122
  }
101
123
  } catch (error) {
102
124
  console.error('Error fetching protected image:', error);
103
- // On error, only clear if URL hasn't changed
104
- if (previousUrlRef.current === url) {
125
+ // On error, only clear if URL hasn't changed and component is mounted
126
+ if (previousUrlRef.current === url && mountedRef.current) {
105
127
  setBlobUrl(null);
106
128
  }
129
+ } finally {
130
+ fetchingUrls.delete(url);
107
131
  }
108
132
  };
109
133
 
110
134
  fetchProtectedImage();
135
+ }, [url]); // Only depend on url
111
136
 
112
- // Cleanup function - revoke blob URL on unmount
113
- return () => {
114
- // On unmount, clean up the current blob URL
115
- if (blobUrlRef.current) {
116
- URL.revokeObjectURL(blobUrlRef.current);
117
- blobUrlRef.current = null;
118
- blobUrlForUrlRef.current = null;
119
- }
120
- };
121
- }, [url]); // Only depend on url, not isProtected or getAuthHeaders
137
+ // Cleanup: Don't revoke blob URLs on unmount - keep them in cache for reuse
138
+ // They will be cleaned up when the URL changes or when the cache is cleared
122
139
 
123
140
  // Return blob URL if available, otherwise return original URL (or null)
124
141
  if (!url) {
@@ -128,6 +145,13 @@ export const useProtectedImage = ({
128
145
  const isUrlProtected = typeof isProtectedRef.current === 'boolean'
129
146
  ? isProtectedRef.current
130
147
  : isProtectedRef.current ? isProtectedRef.current(url) : false;
131
- return blobUrl || (!isUrlProtected ? url : null);
148
+
149
+ // For protected URLs, return cached blob URL or current blobUrl state
150
+ // For non-protected URLs, return the original URL
151
+ if (isUrlProtected) {
152
+ return blobUrl || blobUrlCache.get(url) || null;
153
+ }
154
+
155
+ return url;
132
156
  };
133
157