Summary: This prefixes APIs of `flipper-plugin`, that are used by Flipper, but should not be used by plugins directly, with `_`. Also added tests to make sure we are always intentional when extending the exposed APIs Reviewed By: passy Differential Revision: D24991700 fbshipit-source-id: ed3700efa188fca7f5a14d5c68250598cf011e42
168 lines
4.3 KiB
TypeScript
168 lines
4.3 KiB
TypeScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
*/
|
|
|
|
import React, {createContext, useCallback, useContext} from 'react';
|
|
import {Badge, Tooltip, Typography, Button} from 'antd';
|
|
import styled from '@emotion/styled';
|
|
import {keyframes} from 'emotion';
|
|
import reactElementToJSXString from 'react-element-to-jsx-string';
|
|
import {SandyPluginContext} from '../plugin/PluginContext';
|
|
import {createState, useValue} from '../state/atom';
|
|
import {SandyDevicePluginInstance} from '../plugin/DevicePlugin';
|
|
import {Layout} from './Layout';
|
|
import {BulbTwoTone} from '@ant-design/icons';
|
|
import {createHash} from 'crypto';
|
|
import type {TooltipPlacement} from 'antd/lib/tooltip';
|
|
import {SandyPluginInstance} from '../plugin/Plugin';
|
|
import {theme} from './theme';
|
|
|
|
const {Text} = Typography;
|
|
|
|
type NuxManager = ReturnType<typeof createNuxManager>;
|
|
|
|
const storageKey = `FLIPPER_NUX_STATE`;
|
|
|
|
export function getNuxKey(
|
|
elem: React.ReactNode,
|
|
currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance,
|
|
) {
|
|
return `${currentPlugin?.definition.id ?? 'flipper'}:${createHash('sha256')
|
|
.update(reactElementToJSXString(elem))
|
|
.digest('base64')}`;
|
|
}
|
|
|
|
export function createNuxManager() {
|
|
const ticker = createState(0);
|
|
|
|
let readMap: Record<string, boolean> = JSON.parse(
|
|
window.localStorage.getItem(storageKey) || '{}',
|
|
);
|
|
|
|
function save() {
|
|
// trigger all Nux Elements to re-compute state
|
|
ticker.set(ticker.get() + 1);
|
|
window.localStorage.setItem(storageKey, JSON.stringify(readMap, null, 2));
|
|
}
|
|
|
|
return {
|
|
markRead(
|
|
elem: React.ReactNode,
|
|
currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance,
|
|
): void {
|
|
readMap[getNuxKey(elem, currentPlugin)] = true;
|
|
save();
|
|
},
|
|
isRead(
|
|
elem: React.ReactNode,
|
|
currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance,
|
|
): boolean {
|
|
return !!readMap[getNuxKey(elem, currentPlugin)];
|
|
},
|
|
resetHints(): void {
|
|
readMap = {};
|
|
save();
|
|
},
|
|
ticker,
|
|
};
|
|
}
|
|
|
|
const stubManager: NuxManager = {
|
|
markRead() {},
|
|
isRead() {
|
|
return true;
|
|
},
|
|
resetHints() {},
|
|
ticker: createState(0),
|
|
};
|
|
|
|
export const NuxManagerContext = createContext<NuxManager>(stubManager);
|
|
|
|
/**
|
|
* Creates a New-User-eXperience element; a lightbulb that will show the user new features
|
|
*/
|
|
export function NUX({
|
|
children,
|
|
title,
|
|
placement,
|
|
}: {
|
|
children: React.ReactNode;
|
|
title: string;
|
|
placement?: TooltipPlacement;
|
|
}) {
|
|
const manager = useContext(NuxManagerContext);
|
|
const pluginInstance = useContext(SandyPluginContext);
|
|
// changing the ticker will force `isRead` to be recomputed
|
|
const _tick = useValue(manager.ticker);
|
|
const isRead = manager.isRead(title, pluginInstance);
|
|
const dismiss = useCallback(() => {
|
|
manager.markRead(title, pluginInstance);
|
|
}, [title, manager, pluginInstance]);
|
|
|
|
return (
|
|
<UnanimatedBadge
|
|
count={
|
|
isRead ? (
|
|
0
|
|
) : (
|
|
<Tooltip
|
|
placement={placement}
|
|
color={theme.backgroundWash}
|
|
title={
|
|
<Layout.Container
|
|
center
|
|
gap
|
|
pad
|
|
style={{color: theme.textColorPrimary}}>
|
|
<BulbTwoTone style={{fontSize: 24}} />
|
|
<Text>{title}</Text>
|
|
<Button size="small" type="default" onClick={dismiss}>
|
|
Dismiss
|
|
</Button>
|
|
</Layout.Container>
|
|
}>
|
|
<Pulse />
|
|
</Tooltip>
|
|
)
|
|
}>
|
|
{children}
|
|
</UnanimatedBadge>
|
|
);
|
|
}
|
|
|
|
// We force visibility of the badge to invisible if count has dropped,
|
|
// otherwise ANT will await animation end, which looks really awkard, see D24918536
|
|
const UnanimatedBadge = styled(Badge)(({count}) => ({
|
|
'.ant-scroll-number-custom-component': {
|
|
visibility: count === 0 ? 'hidden' : undefined,
|
|
},
|
|
}));
|
|
|
|
const pulse = keyframes({
|
|
'0%': {
|
|
opacity: 0.2,
|
|
},
|
|
'100%': {
|
|
opacity: 1,
|
|
},
|
|
});
|
|
|
|
const Pulse = styled.div({
|
|
cursor: 'pointer',
|
|
background: theme.warningColor,
|
|
animation: `${pulse} 2s infinite alternate`,
|
|
borderRadius: 20,
|
|
height: 12,
|
|
width: 12,
|
|
':hover': {
|
|
opacity: `1 !important`,
|
|
background: theme.errorColor,
|
|
animationPlayState: 'paused',
|
|
},
|
|
});
|