diff --git a/desktop/flipper-plugin/src/ui/NUX.tsx b/desktop/flipper-plugin/src/ui/NUX.tsx index 6a48f8329..5010293a5 100644 --- a/desktop/flipper-plugin/src/ui/NUX.tsx +++ b/desktop/flipper-plugin/src/ui/NUX.tsx @@ -7,7 +7,13 @@ * @format */ -import React, {createContext, useCallback, useContext} from 'react'; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; import {Badge, Tooltip, Typography, Button} from 'antd'; import styled from '@emotion/styled'; import {keyframes} from '@emotion/css'; @@ -17,11 +23,11 @@ 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'; import {Tracked} from './Tracked'; +import {sha256} from '../utils/sha256'; const {Text} = Typography; @@ -29,13 +35,12 @@ type NuxManager = ReturnType; const storageKey = `FLIPPER_NUX_STATE`; -export function getNuxKey( +export async function getNuxKey( elem: React.ReactNode, currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance, -) { - return `${currentPlugin?.definition.id ?? 'flipper'}:${createHash('sha256') - .update(reactElementToJSXString(elem)) - .digest('base64')}`; +): Promise { + const hash = await sha256(reactElementToJSXString(elem)); + return `${currentPlugin?.definition.id ?? 'flipper'}:${hash}`; } export function createNuxManager() { @@ -52,18 +57,18 @@ export function createNuxManager() { } return { - markRead( + async markRead( elem: React.ReactNode, currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance, - ): void { - readMap[getNuxKey(elem, currentPlugin)] = true; + ): Promise { + readMap[await getNuxKey(elem, currentPlugin)] = true; save(); }, - isRead( + async isRead( elem: React.ReactNode, currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance, - ): boolean { - return !!readMap[getNuxKey(elem, currentPlugin)]; + ): Promise { + return !!readMap[await getNuxKey(elem, currentPlugin)]; }, resetHints(): void { readMap = {}; @@ -74,8 +79,8 @@ export function createNuxManager() { } const stubManager: NuxManager = { - markRead() {}, - isRead() { + async markRead() {}, + async isRead() { return true; }, resetHints() {}, @@ -100,7 +105,18 @@ export function NUX({ const pluginInstance = useContext(SandyPluginContext); // changing the ticker will force `isRead` to be recomputed const _tick = useValue(manager.ticker); - const isRead = manager.isRead(title, pluginInstance); + // start with Read = true until proven otherwise, to avoid Nux glitches + const [isRead, setIsRead] = useState(true); + + useEffect(() => { + manager + .isRead(title, pluginInstance) + .then(setIsRead) + .catch((e) => { + console.warn('Failed to read NUX status', e); + }); + }, [manager, title, pluginInstance, _tick]); + const dismiss = useCallback(() => { manager.markRead(title, pluginInstance); }, [title, manager, pluginInstance]); diff --git a/desktop/flipper-plugin/src/ui/__tests__/NUX.node.tsx b/desktop/flipper-plugin/src/ui/__tests__/NUX.node.tsx index f646fc9bb..811664263 100644 --- a/desktop/flipper-plugin/src/ui/__tests__/NUX.node.tsx +++ b/desktop/flipper-plugin/src/ui/__tests__/NUX.node.tsx @@ -11,25 +11,23 @@ import {TestUtils} from '../../'; import React from 'react'; import {getNuxKey} from '../NUX'; -test('nuxkey computation', () => { - expect(getNuxKey('test')).toMatchInlineSnapshot( - `"flipper:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="`, - ); - expect(getNuxKey('test')).toMatchInlineSnapshot( - `"flipper:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="`, - ); - expect(getNuxKey('test2')).toMatchInlineSnapshot( - `"flipper:YDA64iuZiGG847KPM+7BvnWKITyGyTwHbb6fVYwRx1I="`, - ); - expect(getNuxKey(
bla
)).toMatchInlineSnapshot( - `"flipper:myN0Mqqzs3fPwYDKGEQVG9XD9togJNWYJiy1VNQOf18="`, - ); - expect(getNuxKey(
bla2
)).toMatchInlineSnapshot( - `"flipper:B6kICeYCJMWeUThs5TWCLuiwCqzr5cWn67xXA4ET0bU="`, - ); +test('nuxkey computation', async () => { + // Not a very good test, as our hashing api's are not available in Node... + expect(await getNuxKey('test')).toMatchInlineSnapshot(`"flipper:test"`); + expect(await getNuxKey('test2')).toMatchInlineSnapshot(`"flipper:test2"`); + expect(await getNuxKey(
bla
)).toMatchInlineSnapshot(` + "flipper:
+ bla +
" + `); + expect(await getNuxKey(
bla2
)).toMatchInlineSnapshot(` + "flipper:
+ bla2 +
" + `); }); -test('nuxkey computation with plugin', () => { +test('nuxkey computation with plugin', async () => { const res = TestUtils.startPlugin({ Component() { return null; @@ -40,8 +38,6 @@ test('nuxkey computation with plugin', () => { }); expect( - getNuxKey('test', (res as any)._backingInstance), - ).toMatchInlineSnapshot( - `"TestPlugin:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="`, - ); + await getNuxKey('test', (res as any)._backingInstance), + ).toMatchInlineSnapshot(`"TestPlugin:test"`); }); diff --git a/desktop/flipper-plugin/src/utils/sha256.tsx b/desktop/flipper-plugin/src/utils/sha256.tsx new file mode 100644 index 000000000..81413e7c2 --- /dev/null +++ b/desktop/flipper-plugin/src/utils/sha256.tsx @@ -0,0 +1,23 @@ +/** + * 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 + */ + +export function sha256(message: string): Promise { + if (process.env.NODE_ENV === 'test') { + return Promise.resolve(message.substr(0, 100)); + } + // From https://stackoverflow.com/a/48161723/1983583 + const msgBuffer = new TextEncoder().encode(message); + return crypto.subtle.digest('SHA-256', msgBuffer).then((hashBuffer) => { + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + return hashHex; + }); +}