|
1 | | -import { useCallback, useEffect, useState } from 'react'; |
| 1 | +import { useCallback, useMemo, useState } from 'react'; |
2 | 2 |
|
3 | 3 | const STORAGE_KEY = 'banner-dismissal'; |
4 | 4 |
|
@@ -38,23 +38,14 @@ export const isBannerActive = ({ startDate, endDate }) => { |
38 | 38 | }; |
39 | 39 |
|
40 | 40 | /** |
41 | | - * Fetches the first active banner, preferring the global banner over |
| 41 | + * Finds the first active banner, preferring the global banner over |
42 | 42 | * the version-specific one. |
43 | 43 | * |
44 | | - * @param {string | undefined} remoteConfigUrl |
| 44 | + * @param {Record<string, BannerEntry> | undefined} websiteBanners |
45 | 45 | * @param {number | null} versionMajor |
46 | | - * @returns {Promise<ActiveBanner | null>} |
| 46 | + * @returns {ActiveBanner | null} |
47 | 47 | */ |
48 | | -export const loadBanner = async (remoteConfigUrl, versionMajor) => { |
49 | | - if (!remoteConfigUrl) { |
50 | | - return null; |
51 | | - } |
52 | | - |
53 | | - const response = await fetch(remoteConfigUrl); |
54 | | - |
55 | | - /** @type {{ websiteBanners?: Record<string, BannerEntry> }} */ |
56 | | - const { websiteBanners = {} } = await response.json(); |
57 | | - |
| 48 | +export const findBanner = (websiteBanners = {}, versionMajor) => { |
58 | 49 | const sections = |
59 | 50 | versionMajor == null ? ['index'] : ['index', `v${versionMajor}`]; |
60 | 51 |
|
@@ -86,42 +77,32 @@ export const saveBannerDismissal = banner => |
86 | 77 | localStorage.setItem(getStorageKey(banner.section), banner.text); |
87 | 78 |
|
88 | 79 | /** |
89 | | - * Loads, filters, and dismisses the announcement banner. |
| 80 | + * Selects, filters, and dismisses the announcement banner. |
90 | 81 | * |
91 | | - * @param {string | undefined} remoteConfigUrl |
| 82 | + * @param {Record<string, BannerEntry> | undefined} websiteBanners |
92 | 83 | * @param {number | null} versionMajor |
93 | 84 | * @returns {[ActiveBanner | null, () => void]} |
94 | 85 | */ |
95 | | -export default (remoteConfigUrl, versionMajor) => { |
96 | | - const [banner, setBanner] = useState( |
97 | | - /** @type {ActiveBanner | null} */ (null) |
98 | | - ); |
| 86 | +export default (websiteBanners, versionMajor) => { |
| 87 | + // Only re-renders the banner away; the dismissal itself lives in storage |
| 88 | + const [dismissed, setDismissed] = useState(false); |
99 | 89 |
|
100 | | - useEffect(() => { |
101 | | - let mounted = true; |
102 | | - |
103 | | - loadBanner(remoteConfigUrl, versionMajor) |
104 | | - .then(loaded => { |
105 | | - if (mounted) { |
106 | | - setBanner(loaded && !isBannerDismissed(loaded) ? loaded : null); |
107 | | - } |
108 | | - }) |
109 | | - .catch(() => {}); |
| 90 | + const found = useMemo( |
| 91 | + () => (websiteBanners ? findBanner(websiteBanners, versionMajor) : null), |
| 92 | + [websiteBanners, versionMajor] |
| 93 | + ); |
110 | 94 |
|
111 | | - return () => { |
112 | | - mounted = false; |
113 | | - }; |
114 | | - }, [remoteConfigUrl, versionMajor]); |
| 95 | + // `found` is only set client-side, once the remote config has loaded |
| 96 | + const banner = |
| 97 | + found && !dismissed && !isBannerDismissed(found) ? found : null; |
115 | 98 |
|
116 | 99 | const dismissBanner = useCallback(() => { |
117 | | - setBanner(current => { |
118 | | - if (current) { |
119 | | - saveBannerDismissal(current); |
120 | | - } |
121 | | - |
122 | | - return null; |
123 | | - }); |
124 | | - }, []); |
| 100 | + if (found) { |
| 101 | + saveBannerDismissal(found); |
| 102 | + } |
| 103 | + |
| 104 | + setDismissed(true); |
| 105 | + }, [found]); |
125 | 106 |
|
126 | 107 | return [banner, dismissBanner]; |
127 | 108 | }; |
0 commit comments