Skip to content

Commit 2145e03

Browse files
authored
ntp: favorites ship review (#1432)
* ntp: favorites ship review * slightly larger hover * don't allow animations on macos when it's a userImage background * fixed logic * linting
1 parent ad9bc97 commit 2145e03

20 files changed

+648
-418
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import styles from './CompanyIcon.module.css';
2+
import { DDG_STATS_OTHER_COMPANY_IDENTIFIER } from '../privacy-stats/constants.js';
3+
import { h } from 'preact';
4+
import { useState } from 'preact/hooks';
5+
6+
const mappings = {
7+
'google-analytics-google': 'google-analytics',
8+
};
9+
10+
const states = /** @type {const} */ ({
11+
loading: 'loading',
12+
loaded: 'loaded',
13+
loadingFallback: 'loadingFallback',
14+
loadedFallback: 'loadedFallback',
15+
errored: 'errored',
16+
});
17+
18+
/**
19+
* @typedef {states[keyof states]} State
20+
*/
21+
22+
/**
23+
* @param {object} props
24+
* @param {string} props.displayName
25+
*/
26+
export function CompanyIcon({ displayName }) {
27+
const icon = displayName.toLowerCase().split('.')[0];
28+
const cleaned = icon.replace(/[^a-z ]/g, '').replace(/ /g, '-');
29+
const id = cleaned in mappings ? mappings[cleaned] : cleaned;
30+
const firstChar = id[0];
31+
const [state, setState] = useState(/** @type {State} */ (states.loading));
32+
33+
const src =
34+
state === 'loading' || state === 'loaded'
35+
? `./company-icons/${id}.svg`
36+
: state === 'loadingFallback' || state === 'loadedFallback'
37+
? `./company-icons/${firstChar}.svg`
38+
: null;
39+
40+
if (src === null || icon === DDG_STATS_OTHER_COMPANY_IDENTIFIER) {
41+
return (
42+
<span className={styles.icon}>
43+
<Other />
44+
</span>
45+
);
46+
}
47+
48+
return (
49+
<span className={styles.icon}>
50+
<img
51+
src={src}
52+
alt={''}
53+
class={styles.companyImgIcon}
54+
data-loaded={state === states.loaded || state === states.loadedFallback}
55+
onLoad={() => setState((prev) => (prev === states.loading ? states.loaded : states.loadedFallback))}
56+
onError={() => {
57+
setState((prev) => {
58+
if (prev === states.loading) return states.loadingFallback;
59+
return states.errored;
60+
});
61+
}}
62+
/>
63+
</span>
64+
);
65+
}
66+
67+
function Other() {
68+
return (
69+
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
70+
<path
71+
fill-rule="evenodd"
72+
clip-rule="evenodd"
73+
d="M1 16C1 7.71573 7.71573 1 16 1C24.2843 1 31 7.71573 31 16C31 16.0648 30.9996 16.1295 30.9988 16.1941C30.9996 16.2126 31 16.2313 31 16.25C31 16.284 30.9986 16.3177 30.996 16.3511C30.8094 24.4732 24.1669 31 16 31C7.83308 31 1.19057 24.4732 1.00403 16.3511C1.00136 16.3177 1 16.284 1 16.25C1 16.2313 1.00041 16.2126 1.00123 16.1941C1.00041 16.1295 1 16.0648 1 16ZM3.58907 17.5C4.12835 22.0093 7.06824 25.781 11.0941 27.5006C10.8572 27.0971 10.6399 26.674 10.4426 26.24C9.37903 23.9001 8.69388 20.8489 8.53532 17.5H3.58907ZM8.51564 15H3.53942C3.91376 10.2707 6.92031 6.28219 11.0941 4.49944C10.8572 4.90292 10.6399 5.326 10.4426 5.76003C9.32633 8.21588 8.62691 11.4552 8.51564 15ZM11.0383 17.5C11.1951 20.5456 11.8216 23.2322 12.7185 25.2055C13.8114 27.6098 15.0657 28.5 16 28.5C16.9343 28.5 18.1886 27.6098 19.2815 25.2055C20.1784 23.2322 20.8049 20.5456 20.9617 17.5H11.0383ZM20.983 15H11.017C11.1277 11.7487 11.7728 8.87511 12.7185 6.79454C13.8114 4.39021 15.0657 3.5 16 3.5C16.9343 3.5 18.1886 4.39021 19.2815 6.79454C20.2272 8.87511 20.8723 11.7487 20.983 15ZM23.4647 17.5C23.3061 20.8489 22.621 23.9001 21.5574 26.24C21.3601 26.674 21.1428 27.0971 20.9059 27.5006C24.9318 25.781 27.8717 22.0093 28.4109 17.5H23.4647ZM28.4606 15H23.4844C23.3731 11.4552 22.6737 8.21588 21.5574 5.76003C21.3601 5.326 21.1428 4.90291 20.9059 4.49944C25.0797 6.28219 28.0862 10.2707 28.4606 15Z"
74+
fill="currentColor"
75+
/>
76+
</svg>
77+
);
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.icon {
2+
display: block;
3+
width: 1rem;
4+
height: 1rem;
5+
border-radius: 50%;
6+
flex-shrink: 0;
7+
8+
img, svg {
9+
display: block;
10+
font-size: 0;
11+
width: 1rem;
12+
height: 1rem;
13+
}
14+
15+
&:has([data-errored=true]) {
16+
outline: 1px solid var(--ntp-surface-border-color);
17+
[data-theme=dark] & {
18+
outline-color: var(--color-white-at-9);
19+
}
20+
}
21+
}
22+
23+
.companyImgIcon {
24+
opacity: 0;
25+
}
26+
27+
.companyImgIcon[data-loaded=true] {
28+
opacity: 1;
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { h } from 'preact';
2+
import { useState } from 'preact/hooks';
3+
import { DDG_DEFAULT_ICON_SIZE, DDG_FALLBACK_ICON, DDG_FALLBACK_ICON_DARK } from '../favorites/constants.js';
4+
import styles from '../favorites/components/Tile.module.css';
5+
import { urlToColor } from '../favorites/getColorForString.js';
6+
import cn from 'classnames';
7+
8+
/**
9+
* @typedef {'loading_favicon_src'
10+
* | 'did_load_favicon_src'
11+
* | 'loading_fallback_img'
12+
* | 'did_load_fallback_img'
13+
* | 'using_fallback_text'
14+
* | 'fallback_img_failed'
15+
* } ImgState
16+
*/
17+
const states = /** @type {Record<ImgState, ImgState>} */ ({
18+
loading_favicon_src: 'loading_favicon_src',
19+
did_load_favicon_src: 'did_load_favicon_src',
20+
21+
loading_fallback_img: 'loading_fallback_img',
22+
did_load_fallback_img: 'did_load_fallback_img',
23+
fallback_img_failed: 'fallback_img_failed',
24+
25+
using_fallback_text: 'using_fallback_text',
26+
});
27+
28+
/**
29+
*
30+
* Loads and displays an image for a given webpage.
31+
*
32+
* @param {Object} props - The props for the image loader.
33+
* @param {string|null|undefined} props.faviconSrc - The URL of the favicon image to load.
34+
* @param {number} props.faviconMax - The maximum size this icon be displayed as
35+
* @param {string} props.title - The title associated with the image.
36+
* @param {'light' | 'dark'} props.theme - the currently applied theme
37+
* @param {'favorite-tile' | 'history-favicon'} props.displayKind
38+
* @param {string|null} props.etldPlusOne - The relevant domain section of the url
39+
*/
40+
export function ImageWithState({ faviconSrc, faviconMax, title, etldPlusOne, theme, displayKind }) {
41+
const size = Math.min(faviconMax, DDG_DEFAULT_ICON_SIZE);
42+
const sizeClass = displayKind === 'favorite-tile' ? styles.faviconLarge : styles.faviconSmall;
43+
44+
// try to use the defined image source
45+
// prettier-ignore
46+
const imgsrc = faviconSrc
47+
? faviconSrc + '?preferredSize=' + size
48+
: null;
49+
50+
// prettier-ignore
51+
const initialState = (() => {
52+
/**
53+
* If the favicon has `src`, always prefer it
54+
*/
55+
if (imgsrc) return states.loading_favicon_src;
56+
/**
57+
* Failing that, use fallback text if possible
58+
*/
59+
if (etldPlusOne) return states.using_fallback_text;
60+
/**
61+
* If we get here, we have no favicon src, and no chance of using fallback text
62+
*/
63+
return states.loading_fallback_img;
64+
})();
65+
66+
const [state, setState] = useState(/** @type {ImgState} */ (initialState));
67+
68+
switch (state) {
69+
/**
70+
* These are the happy paths, where we are loading the favicon source and it does not 404
71+
*/
72+
case states.loading_favicon_src:
73+
case states.did_load_favicon_src: {
74+
if (!imgsrc) {
75+
console.warn('unreachable - must have imgsrc here');
76+
return null;
77+
}
78+
return (
79+
<img
80+
src={imgsrc}
81+
class={cn(styles.favicon, sizeClass)}
82+
alt=""
83+
data-state={state}
84+
onLoad={() => setState(states.did_load_favicon_src)}
85+
onError={() => {
86+
if (etldPlusOne) {
87+
setState(states.using_fallback_text);
88+
} else {
89+
setState(states.loading_fallback_img);
90+
}
91+
}}
92+
/>
93+
);
94+
}
95+
/**
96+
* A fallback can be applied when the `etldPlusOne` is there. For example,
97+
* if `etldPlusOne = 'example.com'`, we can display `Ex` and use the domain name
98+
* to select a background color.
99+
*/
100+
case states.using_fallback_text: {
101+
if (!etldPlusOne) {
102+
console.warn('unreachable - must have etld+1 here');
103+
return null;
104+
}
105+
/** @type {Record<string, string>|undefined} */
106+
let style;
107+
const fallbackColor = urlToColor(etldPlusOne);
108+
if (fallbackColor) {
109+
style = { background: fallbackColor };
110+
}
111+
const chars = etldPlusOne.slice(0, 2);
112+
return (
113+
<div class={cn(styles.favicon, sizeClass, styles.faviconText)} style={style} data-state={state}>
114+
<span>{chars[0]}</span>
115+
<span>{chars[1]}</span>
116+
</div>
117+
);
118+
}
119+
/**
120+
* If we get here, we couldn't load the favicon source OR the fallback text
121+
* So, we default to a globe icon
122+
*/
123+
case states.loading_fallback_img:
124+
case states.did_load_fallback_img: {
125+
return (
126+
<img
127+
src={theme === 'light' ? DDG_FALLBACK_ICON : DDG_FALLBACK_ICON_DARK}
128+
class={cn(styles.favicon, sizeClass)}
129+
alt=""
130+
data-state={state}
131+
onLoad={() => setState(states.did_load_fallback_img)}
132+
onError={() => setState(states.fallback_img_failed)}
133+
/>
134+
);
135+
}
136+
default:
137+
return null;
138+
}
139+
}

special-pages/pages/new-tab/app/favorites/components/Favorites.examples.js

-4
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,6 @@ export const favoritesExamples = {
5151
<FavoritesConsumer />
5252
</MockFavoritesProvider>
5353
<br />
54-
<MockFavoritesProvider data={favorites.two}>
55-
<FavoritesConsumer />
56-
</MockFavoritesProvider>
57-
<br />
5854
<MockFavoritesProvider data={favorites.single}>
5955
<FavoritesConsumer />
6056
</MockFavoritesProvider>

0 commit comments

Comments
 (0)