-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* ntp: favorites ship review * slightly larger hover * don't allow animations on macos when it's a userImage background * fixed logic * linting
- Loading branch information
1 parent
ad9bc97
commit 2145e03
Showing
20 changed files
with
648 additions
and
418 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} | ||
|
||
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> | ||
); | ||
} |
29 changes: 29 additions & 0 deletions
29
special-pages/pages/new-tab/app/components/CompanyIcon.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
139
special-pages/pages/new-tab/app/components/ImageWithState.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.