Merge pull request #5660 from urbit/po/new-browser-api-toggles

grid: new browser api toggles/settings
This commit is contained in:
Patrick O'Sullivan 2022-03-29 18:28:34 -05:00 committed by GitHub
commit 618474dbae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 12034 additions and 35284 deletions

47079
pkg/grid/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
},
"dependencies": {
"@radix-ui/react-checkbox": "^0.1.5",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@radix-ui/react-dialog": "^0.0.20",
"@radix-ui/react-dropdown-menu": "^0.0.23",
"@radix-ui/react-icons": "^1.1.0",

View File

@ -2,6 +2,8 @@ import React, { useEffect } from 'react';
import Mousetrap from 'mousetrap';
import { BrowserRouter, Switch, Route, useHistory, useLocation } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
import FingerprintJS from '@fingerprintjs/fingerprintjs';
import { addNote } from '@urbit/api';
import { Grid } from './pages/Grid';
import useDocketState from './state/docket';
import { PermalinkRoutes } from './pages/PermalinkRoutes';
@ -10,23 +12,80 @@ import useContactState from './state/contact';
import api from './state/api';
import { useMedia } from './logic/useMedia';
import { useHarkStore } from './state/hark';
import { useSettingsState, useTheme } from './state/settings';
import { useLocalState } from './state/local';
import { useSettingsState, useBrowserSettings, useTheme } from './state/settings';
import { useBrowserId, useLocalState } from './state/local';
import { ErrorAlert } from './components/ErrorAlert';
import { useErrorHandler } from './logic/useErrorHandler';
const getNoteRedirect = (path: string) => {
if (path.startsWith('/desk/')) {
const [, , desk] = path.split('/');
return `/app/${desk}`;
return `/apps/${desk}`;
}
if (path.startsWith('/grid/')) {
// Handle links to grid features (preferences, etc)
const route = path
.split('/')
.filter((el) => el !== 'grid')
.join('/');
return route;
}
return '';
};
const getId = async () => {
const fpPromise = FingerprintJS.load();
const fp = await fpPromise;
const result = await fp.get();
return result.visitorId;
};
const AppRoutes = () => {
const { push } = useHistory();
const { search } = useLocation();
const handleError = useErrorHandler();
const browserId = useBrowserId();
const settings = useBrowserSettings();
const { loaded } = useSettingsState.getState();
useEffect(() => {
getId().then((value) => {
useLocalState.setState({ browserId: value });
});
}, [browserId]);
useEffect(() => {
// Check if user has previously stored settings for this browser.
// If not, send a notification prompting them to do so.
// Set both settings to false so that they're not bugged again.
if (browserId !== '' && loaded) {
const thisBrowserSettings = settings.filter((el: any) => el.browserId === browserId)[0];
if (!thisBrowserSettings) {
api.poke(
addNote(
{
path: '/browser-settings',
place: { desk: 'garden', path: '/desk/garden' }
},
{
title: [{ text: 'Browser Preferences' }],
time: Date.now(),
content: [
{ text: "You haven't set your preferences for this browser, set them now?" }
],
link: '/grid/leap/system-preferences/interface',
binned: '/desk/garden'
}
)
);
const newSettings = [{ browserId, protocolHandling: false, browserNotifications: false }];
useSettingsState
.getState()
.putEntry('browserSettings', 'settings', JSON.stringify(newSettings));
}
}
}, [browserId, loaded]);
useEffect(() => {
const query = new URLSearchParams(search);

View File

@ -3,10 +3,11 @@ import cn from 'classnames';
import { Link } from 'react-router-dom';
import { HarkLid, Vats, getVatPublisher } from '@urbit/api';
import { Button } from '../../components/Button';
import { useCurrentTheme, useProtocolHandling } from '../../state/local';
import { useBrowserId, useCurrentTheme } from '../../state/local';
import { getDarkColor } from '../../state/util';
import useKilnState from '../../state/kiln';
import {useHarkStore} from '../../state/hark';
import { useHarkStore } from '../../state/hark';
import { useProtocolHandling } from '../../state/settings';
const getCards = (vats: Vats, protocol: boolean): OnboardingCardProps[] => {
const cards = [
@ -52,7 +53,7 @@ const getCards = (vats: Vats, protocol: boolean): OnboardingCardProps[] => {
// color: '#82A6CA'
// }
];
if('registerProtocolHandler' in window.navigator && !protocol) {
if ('registerProtocolHandler' in window.navigator && !protocol) {
cards.push({
title: 'Open Urbit-Native Links',
body: 'Enable your Urbit to open links you find in the wild',
@ -64,9 +65,10 @@ const getCards = (vats: Vats, protocol: boolean): OnboardingCardProps[] => {
});
}
return cards.filter(card => {
return !Object.values(vats).find(vat => getVatPublisher(vat) == card.ship && vat?.arak?.rail?.desk === card.desk);
return cards.filter((card) => {
return !Object.values(vats).find(
(vat) => getVatPublisher(vat) == card.ship && vat?.arak?.rail?.desk === card.desk
);
});
};
@ -85,7 +87,7 @@ interface OnboardingCardProps {
const OnboardingCard = ({ title, button, href, body, color }: OnboardingCardProps) => (
<div
className="p-4 flex flex-col space-y-2 text-black bg-gray-100 justify-between rounded-xl"
className="flex flex-col justify-between p-4 text-black bg-gray-100 space-y-2 rounded-xl"
style={color ? { backgroundColor: color } : {}}
>
<div className="space-y-1">
@ -106,19 +108,22 @@ interface OnboardingNotificationProps {
export const OnboardingNotification = ({ unread = false, lid }: OnboardingNotificationProps) => {
const theme = useCurrentTheme();
const vats = useKilnState((s) => s.vats);
const protocolHandling = useProtocolHandling();
const browserId = useBrowserId();
const protocolHandling = useProtocolHandling(browserId);
const cards = getCards(vats, protocolHandling);
if(cards.length === 0 && !('time' in lid)) {
useHarkStore.getState().archiveNote({
path: '/',
place: {
path: '/onboard',
desk: window.desk
}
}, lid);
if (cards.length === 0 && !('time' in lid)) {
useHarkStore.getState().archiveNote(
{
path: '/',
place: {
path: '/onboard',
desk: window.desk
}
},
lid
);
return null;
}
return (

View File

@ -1,11 +1,37 @@
import React from 'react';
import { Setting } from '../../components/Setting';
import { useProtocolHandling, setLocalState } from '../../state/local';
import {
useBrowserNotifications,
useBrowserSettings,
useProtocolHandling,
useSettingsState
} from '../../state/settings';
import { useBrowserId } from '../../state/local';
export function InterfacePrefs() {
const protocolHandling = useProtocolHandling();
const settings = useBrowserSettings();
const browserId = useBrowserId();
const protocolHandling = useProtocolHandling(browserId);
const browserNotifications = useBrowserNotifications(browserId);
const secure = window.location.protocol === 'https:' || window.location.hostname === 'localhost';
const linkHandlingAllowed = secure && 'registerProtocolHandler' in window.navigator;
const setProtocolHandling = (setting: boolean) => {
const newSettings = [{ browserId, protocolHandling: setting, browserNotifications }];
if (!settings.includes(newSettings)) {
useSettingsState
.getState()
.putEntry('browserSettings', 'settings', JSON.stringify(newSettings));
}
};
const setBrowserNotifications = (setting: boolean) => {
const newSettings = [{ browserId, browserNotifications: setting, protocolHandling }];
if (!settings.includes(newSettings)) {
useSettingsState
.getState()
.putEntry('browserSettings', 'settings', JSON.stringify(newSettings));
}
};
const toggleProtoHandling = async () => {
if (!protocolHandling && window?.navigator?.registerProtocolHandler) {
try {
@ -14,42 +40,64 @@ export function InterfacePrefs() {
'/apps/grid/perma?ext=%s',
'Urbit Links'
);
setLocalState((draft) => {
draft.protocolHandling = true;
});
setProtocolHandling(true);
} catch (e) {
console.error(e);
}
} else if (protocolHandling && window.navigator?.unregisterProtocolHandler) {
try {
window.navigator.unregisterProtocolHandler('web+urbitgraph', '/apps/grid/perma?ext=%s');
setLocalState((draft) => {
draft.protocolHandling = false;
});
setProtocolHandling(false);
} catch (e) {
console.error(e);
}
}
};
const toggleNotifications = async () => {
if (!browserNotifications) {
Notification.requestPermission();
setBrowserNotifications(true);
} else {
setBrowserNotifications(false);
}
};
return (
<>
<h2 className="h3 mb-7">Interface Settings</h2>
<Setting
on={protocolHandling}
toggle={toggleProtoHandling}
name="Handle Urbit links"
disabled={!linkHandlingAllowed}
>
<p>
Automatically open urbit links with this urbit
{!linkHandlingAllowed && (
<>
, <strong className="text-orange-500">requires HTTPS</strong>
</>
)}
</p>
</Setting>
<div className="space-y-3">
<Setting
on={protocolHandling}
toggle={toggleProtoHandling}
name="Handle Urbit links"
disabled={!linkHandlingAllowed}
>
<p>
Automatically open urbit links when using this browser.
{!linkHandlingAllowed && (
<>
, <strong className="text-orange-500">requires HTTPS</strong>
</>
)}
</p>
</Setting>
<Setting
on={browserNotifications}
toggle={toggleNotifications}
name="Show desktop notifications"
disabled={!secure}
>
<p>
Show desktop notifications in this browser.
{!secure && (
<>
, <strong className="text-orange-500">requires HTTPS</strong>
</>
)}
</p>
</Setting>
</div>
</>
);
}

View File

@ -9,9 +9,6 @@ const selDnd = (s: SettingsState) => s.display.doNotDisturb;
async function toggleDnd() {
const state = useSettingsState.getState();
const curr = selDnd(state);
if (curr) {
Notification.requestPermission();
}
await state.putEntry('display', 'doNotDisturb', !curr);
}

View File

@ -4,7 +4,7 @@ import produce from 'immer';
import { clearStorageMigration, createStorageKey, storageVersion } from './util';
interface LocalState {
protocolHandling: boolean;
browserId: string;
currentTheme: 'light' | 'dark';
set: (f: (s: LocalState) => void) => void;
}
@ -14,7 +14,7 @@ export const useLocalState = create<LocalState>(
(set, get) => ({
set: (f) => set(produce(get(), f)),
currentTheme: 'light',
protocolHandling: false
browserId: ''
}),
{
name: createStorageKey('local'),
@ -24,9 +24,9 @@ export const useLocalState = create<LocalState>(
)
);
const selProtocolHandling = (s: LocalState) => s.protocolHandling;
export function useProtocolHandling() {
return useLocalState(selProtocolHandling);
const selBrowserId = (s: LocalState) => s.browserId;
export function useBrowserId() {
return useLocalState(selBrowserId);
}
const selCurrentTheme = (s: LocalState) => s.currentTheme;

View File

@ -25,6 +25,9 @@ interface BaseSettingsState {
order: string[];
};
loaded: boolean;
browserSettings: {
settings: string;
};
putEntry: (bucket: string, key: string, value: Value) => Promise<void>;
fetchAll: () => Promise<void>;
[ref: string]: unknown;
@ -79,6 +82,9 @@ export const useSettingsState = createState<BaseSettingsState>(
tiles: {
order: []
},
browserSettings: {
settings: ''
},
loaded: false,
putEntry: async (bucket, key, val) => {
const poke = doPutEntry(window.desk, bucket, key, val);
@ -110,3 +116,24 @@ const selTheme = (s: SettingsState) => s.display.theme;
export function useTheme() {
return useSettingsState(selTheme);
}
const selBrowserSettings = (s: SettingsState) => s.browserSettings.settings;
export function useBrowserSettings() {
const settings = useSettingsState(selBrowserSettings);
console.log({ settings });
return settings !== '' ? JSON.parse(settings) : [];
}
export function useProtocolHandling(browserId: string) {
const settings = useBrowserSettings();
const { protocolHandling = false } =
settings.filter((el: any) => el.browserId === browserId)[0] ?? false;
return protocolHandling;
}
export function useBrowserNotifications(browserId: string) {
const settings = useBrowserSettings();
const { browserNotifications = false } =
settings.filter((el: any) => el.browserId === browserId)[0] ?? false;
return browserNotifications;
}