Skip to content

Commit f5bd66c

Browse files
authored
Release Notes: UI updates for Mac + Windows, add “loadingError” state for Windows (#1672)
* chore: Add LoadingError state * chore: Add notification message for retry fetching release notes * chore: Add releasenotes to the windows build * feat: Add new retry button variant * chore: update copy * chore: Add platform to initSetup msg, platform-specific styles * chore: Add logging * chore: add to translations file and use in components * rm: inline.js * chore: Attempt adding windows platform * feat: Platform-specific styles applied * fix: update all buttons to follow platform styles * fix: Text * refactor: Update subheader text for tag expressing gratitude in translation file * chore: Add link to bottom of page * chore: Update what's new link * chore: Switch link underline to hover only * wip: button styles * chore: Add active state for button * fix: netlify cache attempt * chore: revert pkg.json change after netlify cache bust * rm: copy change in readme * chore: last fixes before review * feat: Add translations files * tests: adding loadingError state tests * fix: tests for loadingError * chore: Address comments * chore: update comment in button styles
1 parent a42a322 commit f5bd66c

32 files changed

+536
-77
lines changed

special-pages/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const support = {
4545
'release-notes': {
4646
integration: ['copy', 'build-js'],
4747
apple: ['copy', 'build-js'],
48+
windows: ['copy', 'build-js'],
4849
},
4950
/** @type {Partial<Record<ImportMeta['injectName'], string[]>>} */
5051
'special-error': {

special-pages/pages/release-notes/app/Components.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import {
1717
UpdateStatus,
1818
} from './components/ReleaseNotes';
1919
import { sampleData } from './sampleData.js';
20+
import { usePlatformName } from './settings.provider';
2021

2122
export function Components() {
2223
const { t } = useTypedTranslation();
2324
const { isDarkMode } = useEnv();
25+
const platform = usePlatformName();
2426
const todayInMilliseconds = Date.now();
2527
const yesterdayInMilliseconds = new Date(todayInMilliseconds - 24 * 60 * 60 * 1000).getTime();
2628

@@ -60,6 +62,7 @@ export function Components() {
6062

6163
<h2>Update Status</h2>
6264
<UpdateStatus status="loading" version="1.0.1" timestamp={yesterdayInMilliseconds} />
65+
<UpdateStatus status="loadingError" version="1.0.1" timestamp={yesterdayInMilliseconds} />
6366
<UpdateStatus status="loaded" version="1.0.1" timestamp={todayInMilliseconds} />
6467
<UpdateStatus status="updateReady" version="1.2.0" timestamp={todayInMilliseconds} />
6568
<UpdateStatus status="criticalUpdateReady" version="1.2.0" timestamp={todayInMilliseconds} />
@@ -70,13 +73,24 @@ export function Components() {
7073

7174
<h2>Update Buttons</h2>
7275
<div>
73-
<Button>{t('restartToUpdate')}</Button>
76+
<Button variant="accentBrand" size={platform === 'macos' ? 'lg' : 'md'}>
77+
{t('retryGettingReleaseNotes')}
78+
</Button>
7479
</div>
7580
<div>
76-
<Button>{t('updateBrowser')}</Button>
81+
<Button variant="accentBrand" size={platform === 'macos' ? 'lg' : 'md'}>
82+
{t('restartToUpdate')}
83+
</Button>
7784
</div>
7885
<div>
79-
<Button>{t('retryUpdate')}</Button>
86+
<Button variant="accentBrand" size={platform === 'macos' ? 'lg' : 'md'}>
87+
{t('updateBrowser')}
88+
</Button>
89+
</div>
90+
<div>
91+
<Button variant="accentBrand" size={platform === 'macos' ? 'lg' : 'md'}>
92+
{t('retryUpdate')}
93+
</Button>
8094
</div>
8195
<hr />
8296

@@ -113,6 +127,9 @@ export function Components() {
113127
<LoadingThen>
114128
<ReleaseNotes releaseData={sampleData.loaded} />
115129
</LoadingThen>
130+
<LoadingThen>
131+
<ReleaseNotes releaseData={sampleData.loadingError} />
132+
</LoadingThen>
116133
<LoadingThen>
117134
<ReleaseNotes releaseData={sampleData.updateDownloading} />
118135
</LoadingThen>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { h } from 'preact';
2+
3+
/**
4+
* @param {object} props
5+
* @param {string} props.className
6+
**/
7+
8+
export default function OpenIn16({ className }) {
9+
return (
10+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" className={className}>
11+
<path
12+
fill="currentColor"
13+
d="M7.361 1.013a.626.626 0 0 1 0 1.224l-.126.013H5A2.75 2.75 0 0 0 2.25 5v6A2.75 2.75 0 0 0 5 13.75h6A2.75 2.75 0 0 0 13.75 11V8.765a.625.625 0 0 1 1.25 0V11a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V5a4 4 0 0 1 4-4h2.235l.126.013Z"
14+
/>
15+
<path
16+
fill="currentColor"
17+
d="M12.875 1C14.049 1 15 1.951 15 3.125v2.25a.625.625 0 1 1-1.25 0v-2.24L9.067 7.817a.626.626 0 0 1-.884-.884l4.682-4.683h-2.24a.625.625 0 1 1 0-1.25h2.25Z"
18+
/>
19+
</svg>
20+
);
21+
}

special-pages/pages/release-notes/app/components/ReleaseNotes.js

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import { Fragment, h } from 'preact';
2-
import { useMessaging } from '../index';
32
import classNames from 'classnames';
3+
import { useMessaging } from '../index';
44
import { useTypedTranslation } from '../types';
5+
import { Button } from '../../../../shared/components/Button/Button';
6+
import { Card } from '../../../../shared/components/Card/Card';
57
// eslint-disable-next-line no-redeclare
68
import { Text } from '../../../../shared/components/Text/Text';
7-
import { Card } from '../../../../shared/components/Card/Card';
8-
import { Button } from '../../../../shared/components/Button/Button';
9+
import { usePlatformName } from '../settings.provider';
910
import { ContentPlaceholder } from './ContentPlaceholder';
11+
import OpenInIcon from './OpenIn16';
1012

1113
import styles from './ReleaseNotes.module.css';
1214

1315
/**
1416
* @typedef {import('../../types/release-notes.js').UpdateMessage} UpdateMessage
1517
* @typedef {import('../../types/release-notes.js').UpdateErrorState} UpdateErrorState
1618
* @typedef {import('../../types/release-notes.js').UpdateReadyState} UpdateReadyState
19+
* @typedef {import('../../types/release-notes.js').ReleaseNotesLoadingErrorState} ReleaseNotesLoadingErrorState
1720
* @typedef {import('../../types/release-notes.js').ReleaseNotesLoadedState} ReleaseNotesLoadedState
1821
* @typedef {import('../types.js').Notes} Notes
1922
*/
@@ -40,6 +43,7 @@ function StatusText({ status, version, progress = 0 }) {
4043
const statusTexts = {
4144
loaded: t('browserUpToDate'),
4245
loading: t('checkingForUpdate'),
46+
loadingError: t('loadingError'),
4347
updateReady: t('newVersionAvailable'),
4448
updateError: t('updateError'),
4549
criticalUpdateReady: t('criticallyOutOfDate'),
@@ -64,6 +68,7 @@ function StatusIcon({ status, className }) {
6468
const iconClasses = {
6569
loaded: styles.checkIcon,
6670
loading: styles.spinnerIcon,
71+
loadingError: styles.warningIcon,
6772
updateReady: styles.alertIcon,
6873
criticalUpdateReady: styles.warningIcon,
6974
updateError: styles.warningIcon,
@@ -214,7 +219,7 @@ export function CardContents({ releaseData }) {
214219
const { status } = releaseData;
215220
const isLoading = status === 'loading' || status === 'updateDownloading' || status === 'updatePreparing';
216221

217-
if (isLoading) {
222+
if (isLoading || status === 'loadingError') {
218223
return <ContentPlaceholder />;
219224
}
220225

@@ -251,24 +256,41 @@ export function CardContents({ releaseData }) {
251256

252257
/**
253258
* @param {object} props
254-
* @param {UpdateReadyState|UpdateErrorState} props.releaseData
259+
* @param {UpdateReadyState|UpdateErrorState|ReleaseNotesLoadingErrorState} props.releaseData
255260
*/
256261
export function UpdateButton({ releaseData }) {
257262
const { t } = useTypedTranslation();
258263
const { messages } = useMessaging();
264+
const platform = usePlatformName();
259265

260266
const { status } = releaseData;
261267
let button;
262268

269+
if (status === 'loadingError') {
270+
button = (
271+
<Button onClick={() => messages?.retryFetchReleaseNotes()} variant="accentBrand" size={platform === 'macos' ? 'lg' : 'md'}>
272+
{t('retryGettingReleaseNotes')}
273+
</Button>
274+
);
275+
}
276+
263277
if (status === 'updateError') {
264-
button = <Button onClick={() => messages?.retryUpdate()}>{t('retryUpdate')}</Button>;
278+
button = (
279+
<Button onClick={() => messages?.retryUpdate()} variant="accentBrand" size={platform === 'macos' ? 'lg' : 'md'}>
280+
{t('retryUpdate')}
281+
</Button>
282+
);
265283
}
266284

267285
if (status === 'updateReady' || status === 'criticalUpdateReady') {
268286
const { automaticUpdate } = releaseData;
269287
const buttonText = automaticUpdate ? t('restartToUpdate') : t('updateBrowser');
270288

271-
button = <Button onClick={() => messages?.browserRestart()}>{buttonText}</Button>;
289+
button = (
290+
<Button onClick={() => messages?.browserRestart()} variant="accentBrand" size={platform === 'macos' ? 'lg' : 'md'}>
291+
{buttonText}
292+
</Button>
293+
);
272294
}
273295

274296
if (!button) return null;
@@ -297,18 +319,26 @@ export function ReleaseNotes({ releaseData }) {
297319
}
298320
}
299321

300-
const shouldShowButton = status === 'updateReady' || status === 'criticalUpdateReady' || status === 'updateError';
322+
const shouldShowButton =
323+
status === 'updateReady' || status === 'criticalUpdateReady' || status === 'updateError' || status === 'loadingError';
301324

302325
return (
303326
<article className={styles.article}>
304327
<header className={styles.heading}>
328+
<p>{t('thankyou')}</p>
305329
<PageTitle title={t('browserReleaseNotes')} />
306330
<UpdateStatus status={status} timestamp={timestampInMilliseconds} version={currentVersion} progress={progress} />
307331
{shouldShowButton && <UpdateButton releaseData={releaseData} />}
308332
</header>
309-
<Card className={styles.card}>
310-
<CardContents releaseData={releaseData} />
311-
</Card>
333+
{status !== 'loadingError' && (
334+
<Card className={styles.card}>
335+
<CardContents releaseData={releaseData} />
336+
</Card>
337+
)}
338+
<a href="https://duckduckgo.com/updates" target="_blank" className={styles.updatesLink}>
339+
{t('whatsNewAtDuckDuckGoLink')}
340+
<OpenInIcon className={styles.linkIcon} />
341+
</a>
312342
</article>
313343
);
314344
}

special-pages/pages/release-notes/app/components/ReleaseNotes.module.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,22 @@
194194
font-weight: 600;
195195
line-height: calc(11 * var(--px-in-rem));
196196
}
197+
198+
.updatesLink {
199+
display: inline-flex;
200+
align-items: center;
201+
border-bottom: 1px solid transparent;
202+
width: fit-content;
203+
text-decoration: none;
204+
205+
&:hover {
206+
border-bottom-color: currentColor;
207+
}
208+
209+
.linkIcon {
210+
height: 1rem;
211+
width: 1rem;
212+
margin-left: var(--sp-1);
213+
vertical-align: middle;
214+
}
215+
}

special-pages/pages/release-notes/app/index.js

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,25 @@ import { callWithRetry } from '../../../shared/call-with-retry.js';
88
import enStrings from '../public/locales/en/release-notes.json';
99

1010
import '../../../shared/styles/global.css'; // global styles
11+
import { Settings } from './settings';
12+
import { SettingsProvider } from './settings.provider';
1113

1214
export const MessagingContext = createContext({
1315
messages: /** @type {import('../src/index.js').ReleaseNotesPage | null} */ (null),
1416
});
1517

1618
export const useMessaging = () => useContext(MessagingContext);
1719

20+
/**
21+
* @import { Environment } from "../../../shared/environment";
22+
* @param {Environment} environment
23+
* @param {Settings} settings
24+
*/
25+
function installGlobalSideEffects(environment, settings) {
26+
document.body.dataset.platformName = settings.platform.name;
27+
document.body.dataset.display = environment.display;
28+
}
29+
1830
export async function init(messages, baseEnvironment) {
1931
const result = await callWithRetry(() => messages.initialSetup());
2032

@@ -31,6 +43,15 @@ export async function init(messages, baseEnvironment) {
3143
.withTextLength(baseEnvironment.urlParams.get('textLength'))
3244
.withDisplay(baseEnvironment.urlParams.get('display'));
3345

46+
// create app-specific settings
47+
const settings = new Settings({})
48+
.withPlatformName(baseEnvironment.injectName)
49+
.withPlatformName(init.platform?.name)
50+
.withPlatformName(baseEnvironment.urlParams.get('platform'));
51+
52+
// install global side effects for platform-specific styles
53+
installGlobalSideEffects(environment, settings);
54+
3455
const strings =
3556
environment.locale === 'en'
3657
? enStrings
@@ -46,24 +67,33 @@ export async function init(messages, baseEnvironment) {
4667

4768
if (environment.display === 'app') {
4869
render(
49-
<EnvironmentProvider debugState={environment.debugState} injectName={environment.injectName} willThrow={environment.willThrow}>
50-
<TranslationProvider translationObject={strings} fallback={enStrings} textLength={environment.textLength}>
51-
<MessagingContext.Provider value={{ messages }}>
52-
<App />
53-
</MessagingContext.Provider>
54-
</TranslationProvider>
70+
<EnvironmentProvider
71+
debugState={environment.debugState}
72+
injectName={environment.injectName}
73+
willThrow={environment.willThrow}
74+
env={environment.env}
75+
>
76+
<SettingsProvider settings={settings}>
77+
<TranslationProvider translationObject={strings} fallback={enStrings} textLength={environment.textLength}>
78+
<MessagingContext.Provider value={{ messages }}>
79+
<App />
80+
</MessagingContext.Provider>
81+
</TranslationProvider>
82+
</SettingsProvider>
5583
</EnvironmentProvider>,
5684
root,
5785
);
5886
}
5987
if (environment.display === 'components') {
6088
render(
6189
<EnvironmentProvider debugState={environment.debugState} injectName={environment.injectName} willThrow={environment.willThrow}>
62-
<TranslationProvider translationObject={strings} fallback={enStrings} textLength={environment.textLength}>
63-
<MessagingContext.Provider value={{ messages }}>
64-
<Components />
65-
</MessagingContext.Provider>
66-
</TranslationProvider>
90+
<SettingsProvider settings={settings}>
91+
<TranslationProvider translationObject={strings} fallback={enStrings} textLength={environment.textLength}>
92+
<MessagingContext.Provider value={{ messages }}>
93+
<Components />
94+
</MessagingContext.Provider>
95+
</TranslationProvider>
96+
</SettingsProvider>
6797
</EnvironmentProvider>,
6898
root,
6999
);

special-pages/pages/release-notes/app/sampleData.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export const sampleData = {
1313
currentVersion: '1.0.1',
1414
lastUpdate: timestampInSeconds - 24 * 60 * 60,
1515
},
16+
loadingError: {
17+
status: 'loadingError',
18+
currentVersion: '1.0.1',
19+
lastUpdate: timestampInSeconds - 24 * 60 * 60,
20+
},
1621
loaded: {
1722
currentVersion: '1.0.1',
1823
latestVersion: '1.0.1',
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export class Settings {
2+
/**
3+
* @param {object} params
4+
* @param {{name: 'macos' | 'windows'}} [params.platform]
5+
*/
6+
constructor({ platform = { name: 'macos' } }) {
7+
this.platform = platform;
8+
}
9+
10+
withPlatformName(name) {
11+
/** @type {ImportMeta['platform'][]} */
12+
const valid = ['windows', 'macos'];
13+
14+
if (valid.includes(/** @type {any} */ (name))) {
15+
return new Settings({
16+
...this,
17+
platform: { name },
18+
});
19+
}
20+
return this;
21+
}
22+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { h, createContext } from 'preact';
2+
import { useContext } from 'preact/hooks';
3+
4+
const SettingsContext = createContext(/** @type {{settings: import("./settings.js").Settings}} */ ({}));
5+
6+
/**
7+
* @param {object} params
8+
* @param {import("./settings.js").Settings} params.settings
9+
* @param {import("preact").ComponentChild} params.children
10+
*/
11+
export function SettingsProvider({ settings, children }) {
12+
return <SettingsContext.Provider value={{ settings }}>{children}</SettingsContext.Provider>;
13+
}
14+
15+
export function usePlatformName() {
16+
return useContext(SettingsContext).settings.platform.name;
17+
}

0 commit comments

Comments
 (0)