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

Hack days, VPN widget #1499

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ const url = typeof window !== 'undefined' ? new URL(window.location.href) : new
* @typedef {import('../../../types/new-tab.js').ActivityOnDataPatchSubscription['params']} PatchParams
*/

function clone(v) {
if (typeof structuredClone !== 'undefined') return structuredClone(v);
return JSON.parse(JSON.stringify(v));
}

export function activityMockTransport() {
/** @type {import('../../../types/new-tab.ts').ActivityData} */
let dataset = structuredClone(activityMocks.few);
let dataset = clone(activityMocks.few);

if (url.searchParams.has('activity')) {
const key = url.searchParams.get('activity');
if (key && key in activityMocks) {
dataset = structuredClone(activityMocks[key]);
dataset = clone(activityMocks[key]);
} else if (key?.match(/^\d+$/)) {
dataset = getJsonSync(parseInt(key));
}
Expand Down
14 changes: 14 additions & 0 deletions special-pages/pages/new-tab/app/components/Icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,17 @@ export function BackChevron() {
</svg>
);
}

export function Globe() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16L7.99654 16C3.57985 15.9981 0 12.4171 0 8ZM1.57645 7C1.91127 4.83163 3.31776 3.01707 5.23782 2.11439C5.16056 2.26035 5.08714 2.41139 5.01752 2.56668C4.48089 3.76379 4.12931 5.30652 4.02931 7H1.57645ZM1.51895 8.5C1.70056 10.8874 3.17191 12.9144 5.23782 13.8856C5.16057 13.7396 5.08714 13.5886 5.01752 13.4333C4.42851 12.1194 4.06244 10.389 4.00728 8.5H1.51895ZM5.50797 8.5C5.56288 10.2161 5.89758 11.7295 6.38629 12.8197C6.6545 13.418 6.95441 13.8562 7.24966 14.1348C7.54091 14.4096 7.79309 14.5 8 14.5H8.00174C8.20833 14.4995 8.4599 14.4088 8.75034 14.1348C9.04559 13.8562 9.34551 13.418 9.61372 12.8197C10.1024 11.7295 10.4371 10.2161 10.492 8.5H5.50797ZM11.9927 8.5C11.9376 10.389 11.5715 12.1194 10.9825 13.4333C10.9129 13.5886 10.8394 13.7396 10.7622 13.8856C12.8281 12.9144 14.2994 10.8874 14.4811 8.5H11.9927ZM14.4236 7H11.9707C11.8707 5.30652 11.5191 3.76379 10.9825 2.56668C10.9129 2.41139 10.8394 2.26035 10.7622 2.11439C12.6822 3.01707 14.0887 4.83163 14.4236 7ZM10.4678 7H5.53216C5.63017 5.49043 5.94562 4.16329 6.38629 3.18027C6.6545 2.58195 6.95441 2.14382 7.24966 1.86525C7.54091 1.59044 7.79309 1.5 8 1.5C8.20691 1.5 8.45909 1.59044 8.75034 1.86525C9.04559 2.14382 9.34551 2.58195 9.61372 3.18027C10.0544 4.16329 10.3698 5.49043 10.4678 7Z"
fill="currentColor"
fill-opacity="0.84"
/>
</svg>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export class VisibilityRowData {
* @param {object} params
* @param {string} params.id - a unique id
* @param {string} params.title - the title as it should appear in the menu
* @param {'shield' | 'star'} params.icon - known icon name, maps to an SVG
* @param {'shield' | 'star' | 'globe'} params.icon - known icon name, maps to an SVG
* @param {(id: string) => void} params.toggle - toggle function for this item
* @param {number} params.index - position in the menu
* @param {WidgetVisibility} params.visibility - known icon name, maps to an SVG
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { h } from 'preact';
import cn from 'classnames';
import { useId, useContext } from 'preact/hooks';

import { DuckFoot, Shield } from '../../components/Icons.js';
import { DuckFoot, Globe, Shield } from '../../components/Icons.js';
import styles from './VisibilityMenu.module.css';
import { useTypedTranslation } from '../../types.js';
import { Switch } from '../../../../../shared/components/Switch/Switch.js';
Expand Down Expand Up @@ -53,6 +53,7 @@ export function VisibilityMenu({ rows }) {
<span className={styles.svg}>
{row.icon === 'shield' && <DuckFoot />}
{row.icon === 'star' && <Shield />}
{row.icon === 'globe' && <Globe />}
</span>
<span>{row.title ?? row.id}</span>
</label>
Expand All @@ -79,6 +80,7 @@ export function EmbeddedVisibilityMenu({ rows }) {
<span className={styles.svg}>
{row.icon === 'shield' && <DuckFoot />}
{row.icon === 'star' && <Shield />}
{row.icon === 'globe' && <Globe />}
</span>
<span>{row.title ?? row.id}</span>
<Switch
Expand Down
10 changes: 10 additions & 0 deletions special-pages/pages/new-tab/app/entry-points/vpn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { h } from 'preact';
import { Centered } from '../components/Layout.js';
import { VpnCustomized } from '../vpn/components/Vpn.js';
export function factory() {
return (
<Centered data-entry-point="vpn">
<VpnCustomized />
</Centered>
);
}
5 changes: 5 additions & 0 deletions special-pages/pages/new-tab/app/mock-transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { variants as nextSteps } from './next-steps/nextsteps.data.js';
import { customizerData, customizerMockTransport } from './customizer/mocks.js';
import { freemiumPIRDataExamples } from './freemium-pir-banner/mocks/freemiumPIRBanner.data.js';
import { activityMockTransport } from './activity/mocks/activity.mock-transport.js';
import { vpnMockTransport } from './vpn/mocks/vpn.mock-transport.js';

/**
* @typedef {import('../types/new-tab').Favorite} Favorite
Expand Down Expand Up @@ -105,6 +106,7 @@ export function mockTransport() {
const transports = {
customizer: customizerMockTransport(),
activity: activityMockTransport(),
vpn: vpnMockTransport(),
};

return new TestTransportConfig({
Expand Down Expand Up @@ -515,6 +517,9 @@ export function mockTransport() {
widgetConfigFromStorage.push({ id: 'activity', visibility: 'visible' });
}

widgetsFromStorage.push({ id: 'vpn' });
widgetConfigFromStorage.push({ id: 'vpn', visibility: 'visible' });

/** @type {import('../types/new-tab').NewTabPageSettings} */
const settings = {};
if (url.searchParams.get('customizerDrawer') === 'enabled') {
Expand Down
1 change: 1 addition & 0 deletions special-pages/pages/new-tab/app/new-tab.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ children:
- ./favorites/favorites.md
- ./next-steps/next-steps.md
- ./customizer/customizer.md
- ./vpn/vpn.md
---

## Requests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
padding-left: 6px;
grid-column-gap: 10px;
}

&.vpn {
grid-template-columns: 1.5rem auto max-content 2rem;
grid-template-rows: auto;
grid-template-areas: 'icon title inline-controls expander';
align-items: center;
}
}

.headingIcon {
Expand Down Expand Up @@ -115,6 +122,56 @@
font-size: var(--title-2-font-size);
font-weight: var(--title-2-font-weight);
line-height: var(--title-2-line-height);

small {
display: block;
font-size: var(--body-font-size);
font-weight: var(--body-font-weight);
line-height: var(--body-line-height);
color: var(--ntp-text-muted)
}
}

.inlineControls {
grid-area: inline-controls;
display: flex;
}

.inlineBtn {
display: flex;
height: 31px;
padding: 0px var(--Windows-Medium, 12px);
justify-content: center;
align-items: center;
gap: 8px;
border-radius: var(--Windows-Button-Medium, 6px);
background: var(--Windows-Control-Accent-Background-Rest, #3969EF);
border: 0;
color: white;
}

.inlineSwitch {
display: flex;
align-items: center;
color: var(--ntp-text-muted);
&[data-state="connected"] svg {
color: #21C000;
}
&[data-state="disconnected"] svg {
color: #F9BE1A;
}
&[data-pending="connecting"] svg {
color: #21C000;
}
&[data-pending="disconnecting"] svg {
color: #F9BE1A;
}
label {
margin-left: 12px;
}
svg {
margin-right: 8px;
}
}

.widgetExpander {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useTypedTranslationWith } from '../../types.js';
import styles from './PrivacyStats.module.css';
import { ShowHideButton } from '../../components/ShowHideButton.jsx';
import cn from 'classnames';
import { h } from 'preact';
import { Switch } from '../../../../../shared/components/Switch/Switch.js';
import { useVpnApi } from '../../vpn/VpnProvider.js';
import { usePlatformName } from '../../settings.provider.js';
import { useContext } from 'preact/hooks';
import { CustomizerThemesContext } from '../../customizer/CustomizerProvider.js';

/**
* @import vpnStrings from "../../vpn/strings.json"
* @param {object} props
* @param {import('../../../types/new-tab.js').VPNWidgetData} props.data
* @param {import('../../../types/new-tab.js').Expansion} props.expansion
* @param {boolean} props.canExpand
* @param {() => void} props.onToggle
* @param {import('preact').ComponentProps<'button'>} [props.buttonAttrs]
*/
export function VpnHeading({ data, expansion, canExpand, onToggle, buttonAttrs = {} }) {
const { t } = useTypedTranslationWith(/** @type {vpnStrings} */ ({}));
const platformName = usePlatformName();
const { main } = useContext(CustomizerThemesContext);
const { disconnect, connect, tryForFree } = useVpnApi();
const title = (() => {
switch (data.state) {
case 'connected':
return t('vpn_connectedTitle');
case 'disconnected':
return t('vpn_disconnectedTitle');
case 'unsubscribed':
return t('vpn_disabled');
}
})();

let checked = false;
if (data.state === 'connected' && data.pending !== 'disconnecting') {
checked = true;
}
if (data.state === 'disconnected' && data.pending === 'connecting') {
checked = true;
}
return (
<div className={cn(styles.heading, styles.activityVariant, styles.vpn)} data-testid={'VpnHeading'}>
<span className={styles.headingIcon}>
<img src="./icons/vpn.svg" alt="" />
</span>
<h2 className={styles.title}>
{title}
{data.state === 'unsubscribed' && <small>{t('vpn_disabledSubtitle')}</small>}
</h2>
<div class={styles.inlineControls}>
{data.state === 'unsubscribed' && (
<button class={styles.inlineBtn} onClick={tryForFree}>
{t('vpn_tryButton')}
</button>
)}
{data.state !== 'unsubscribed' && (
<div class={styles.inlineSwitch} data-state={data.state} data-pending={data.pending}>
<ConnectedDot />
{data.state === 'connected' && data.pending === 'none' && t('vpn_connectedLabel')}
{data.state === 'disconnected' && data.pending === 'none' && t('vpn_disconnectedLabel')}
{data.state === 'connected' && data.pending === 'disconnecting' && t('vpn_disconnectingLabel')}
{data.state === 'disconnected' && data.pending === 'connecting' && t('vpn_connectingLabel')}
<Switch
pending={data.pending !== 'none'}
ariaLabel={''}
checked={checked}
onChecked={connect}
onUnchecked={disconnect}
platformName={platformName}
theme={main.value}
/>
</div>
)}
</div>
{canExpand && (
<span className={styles.widgetExpander}>
<ShowHideButton
buttonAttrs={{
...buttonAttrs,
'aria-expanded': expansion === 'expanded',
'aria-pressed': expansion === 'expanded',
}}
onClick={onToggle}
text={expansion === 'expanded' ? t('vpn_hideLabel') : t('vpn_toggleLabel')}
shape="round"
/>
</span>
)}
</div>
);
}

/**
* @param {number} from - unix timestamp
* @param {Intl.DateTimeFormat} formatter
* @returns {string}
*/
function calculateConnectedDisplay(from, formatter) {

Check failure on line 101 in special-pages/pages/new-tab/app/privacy-stats/components/VpnHeading.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

'calculateConnectedDisplay' is defined but never used
if (!Number.isFinite(from) || from < 0) {
throw new Error('Invalid timestamp');
}

const now = Date.now();
const duration = Math.max(0, Math.floor((now - from) / 1000));

// Create a Date object with the duration in milliseconds
const date = new Date(duration * 1000);

return formatter.format(date);
}

/**
* @param {object} props
* @param {number} props.connectedSince - unix timestamp representing when the connection started
*/
function ConnectedText({ connectedSince }) {

Check failure on line 119 in special-pages/pages/new-tab/app/privacy-stats/components/VpnHeading.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

'ConnectedText' is defined but never used
return <div class={styles.connectionStatus}>Connected</div>;
}

function ConnectedDot() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8" fill="none">
<circle cx="4" cy="4" r="4" fill="currentColor" />
</svg>
);
}
7 changes: 6 additions & 1 deletion special-pages/pages/new-tab/app/styles/ntp-theme.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
:root {
--default-light-bg: var(--color-gray-0);
--default-dark-bg: var(--color-gray-85);
--ntp-gap: 2rem;
--ntp-gap: calc(32 * var(--px-in-rem));
--ntp-drawer-width: calc(224 * var(--px-in-rem));
--ntp-drawer-scroll-width: 12px;
--ntp-combined-width: calc(var(--ntp-drawer-width) + var(--ntp-drawer-scroll-width));
Expand Down Expand Up @@ -80,4 +80,9 @@
--title-3-em-font-size: 16px;
--title-3-em-font-weight: 600;
--title-3-em-line-height: 20px;

/* label small */
--small-label-font-size: 12px;
--small-label-font-weight: 400;
--small-label-line-height: 14px;
}
Loading
Loading