Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ntp: favorites ship review #1432

Merged
merged 5 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions special-pages/pages/new-tab/app/components/CompanyIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import styles from './CompanyIcon.module.css';
import { DDG_STATS_OTHER_COMPANY_IDENTIFIER } from '../privacy-stats/constants.js';
import { h } from 'preact';
import { useState } from 'preact/hooks';

const mappings = {
'google-analytics-google': 'google-analytics',
};

const states = /** @type {const} */ ({
loading: 'loading',
loaded: 'loaded',
loadingFallback: 'loadingFallback',
loadedFallback: 'loadedFallback',
errored: 'errored',
});

/**
* @typedef {states[keyof states]} State
*/

/**
* @param {object} props
* @param {string} props.displayName
*/
export function CompanyIcon({ displayName }) {
const icon = displayName.toLowerCase().split('.')[0];
const cleaned = icon.replace(/[^a-z ]/g, '').replace(/ /g, '-');
const id = cleaned in mappings ? mappings[cleaned] : cleaned;
const firstChar = id[0];
const [state, setState] = useState(/** @type {State} */ (states.loading));

const src =
state === 'loading' || state === 'loaded'
? `./company-icons/${id}.svg`
: state === 'loadingFallback' || state === 'loadedFallback'
? `./company-icons/${firstChar}.svg`
: null;

if (src === null || icon === DDG_STATS_OTHER_COMPANY_IDENTIFIER) {
return (
<span className={styles.icon}>
<Other />
</span>
);
}

return (
<span className={styles.icon}>
<img
src={src}
alt={''}
class={styles.companyImgIcon}
data-loaded={state === states.loaded || state === states.loadedFallback}
onLoad={() => setState((prev) => (prev === states.loading ? states.loaded : states.loadedFallback))}
onError={() => {
setState((prev) => {
if (prev === states.loading) return states.loadingFallback;
return states.errored;
});
}}
/>
</span>
);
}
shakyShane marked this conversation as resolved.
Show resolved Hide resolved

function Other() {
return (
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
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"
fill="currentColor"
/>
</svg>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.icon {
display: block;
width: 1rem;
height: 1rem;
border-radius: 50%;
flex-shrink: 0;

img, svg {
display: block;
font-size: 0;
width: 1rem;
height: 1rem;
}

&:has([data-errored=true]) {
outline: 1px solid var(--ntp-surface-border-color);
[data-theme=dark] & {
outline-color: var(--color-white-at-9);
}
}
}

.companyImgIcon {
opacity: 0;
}

.companyImgIcon[data-loaded=true] {
opacity: 1;
}
139 changes: 139 additions & 0 deletions special-pages/pages/new-tab/app/components/ImageWithState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { h } from 'preact';
import { useState } from 'preact/hooks';
import { DDG_DEFAULT_ICON_SIZE, DDG_FALLBACK_ICON, DDG_FALLBACK_ICON_DARK } from '../favorites/constants.js';
import styles from '../favorites/components/Tile.module.css';
import { urlToColor } from '../favorites/getColorForString.js';
import cn from 'classnames';

/**
* @typedef {'loading_favicon_src'
* | 'did_load_favicon_src'
* | 'loading_fallback_img'
* | 'did_load_fallback_img'
* | 'using_fallback_text'
* | 'fallback_img_failed'
* } ImgState
*/
const states = /** @type {Record<ImgState, ImgState>} */ ({
loading_favicon_src: 'loading_favicon_src',
did_load_favicon_src: 'did_load_favicon_src',

loading_fallback_img: 'loading_fallback_img',
did_load_fallback_img: 'did_load_fallback_img',
fallback_img_failed: 'fallback_img_failed',

using_fallback_text: 'using_fallback_text',
});

/**
*
* Loads and displays an image for a given webpage.
*
* @param {Object} props - The props for the image loader.
* @param {string|null|undefined} props.faviconSrc - The URL of the favicon image to load.
* @param {number} props.faviconMax - The maximum size this icon be displayed as
* @param {string} props.title - The title associated with the image.
* @param {'light' | 'dark'} props.theme - the currently applied theme
* @param {'favorite-tile' | 'history-favicon'} props.displayKind
* @param {string|null} props.etldPlusOne - The relevant domain section of the url
*/
export function ImageWithState({ faviconSrc, faviconMax, title, etldPlusOne, theme, displayKind }) {
const size = Math.min(faviconMax, DDG_DEFAULT_ICON_SIZE);
const sizeClass = displayKind === 'favorite-tile' ? styles.faviconLarge : styles.faviconSmall;

// try to use the defined image source
// prettier-ignore
const imgsrc = faviconSrc
? faviconSrc + '?preferredSize=' + size
: null;

// prettier-ignore
const initialState = (() => {
/**
* If the favicon has `src`, always prefer it
*/
if (imgsrc) return states.loading_favicon_src;
/**
* Failing that, use fallback text if possible
*/
if (etldPlusOne) return states.using_fallback_text;
/**
* If we get here, we have no favicon src, and no chance of using fallback text
*/
return states.loading_fallback_img;
})();

const [state, setState] = useState(/** @type {ImgState} */ (initialState));

switch (state) {
/**
* These are the happy paths, where we are loading the favicon source and it does not 404
*/
case states.loading_favicon_src:
case states.did_load_favicon_src: {
if (!imgsrc) {
console.warn('unreachable - must have imgsrc here');
return null;
}
return (
<img
src={imgsrc}
class={cn(styles.favicon, sizeClass)}
alt=""
data-state={state}
onLoad={() => setState(states.did_load_favicon_src)}
onError={() => {
if (etldPlusOne) {
setState(states.using_fallback_text);
} else {
setState(states.loading_fallback_img);
}
}}
/>
);
}
/**
* A fallback can be applied when the `etldPlusOne` is there. For example,
* if `etldPlusOne = 'example.com'`, we can display `Ex` and use the domain name
* to select a background color.
*/
case states.using_fallback_text: {
if (!etldPlusOne) {
console.warn('unreachable - must have etld+1 here');
return null;
}
/** @type {Record<string, string>|undefined} */
let style;
const fallbackColor = urlToColor(etldPlusOne);
if (fallbackColor) {
style = { background: fallbackColor };
}
const chars = etldPlusOne.slice(0, 2);
return (
<div class={cn(styles.favicon, sizeClass, styles.faviconText)} style={style} data-state={state}>
<span>{chars[0]}</span>
<span>{chars[1]}</span>
</div>
);
}
/**
* If we get here, we couldn't load the favicon source OR the fallback text
* So, we default to a globe icon
*/
case states.loading_fallback_img:
case states.did_load_fallback_img: {
return (
<img
src={theme === 'light' ? DDG_FALLBACK_ICON : DDG_FALLBACK_ICON_DARK}
class={cn(styles.favicon, sizeClass)}
alt=""
data-state={state}
onLoad={() => setState(states.did_load_fallback_img)}
onError={() => setState(states.fallback_img_failed)}
/>
);
}
default:
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ export const favoritesExamples = {
<FavoritesConsumer />
</MockFavoritesProvider>
<br />
<MockFavoritesProvider data={favorites.two}>
<FavoritesConsumer />
</MockFavoritesProvider>
<br />
<MockFavoritesProvider data={favorites.single}>
<FavoritesConsumer />
</MockFavoritesProvider>
Expand Down
Loading
Loading