Show recent changes automatically at startup

Summary:
This shows a changelog as popup at startup, but only if it wasn't shown before, and only if there are new items in the changelog.

The full changelog can still be accessed through the menu

Changelog: From this release onward we will show important update messages through this dialog.

Reviewed By: passy

Differential Revision: D20492594

fbshipit-source-id: 4663979c8781b468430b9f8b628c4f506578b461
This commit is contained in:
Michel Weststrate
2020-03-18 06:44:45 -07:00
committed by Facebook GitHub Bot
parent 3da7552779
commit 805a911c08
4 changed files with 219 additions and 10 deletions

View File

@@ -37,6 +37,8 @@ import {
ACTIVE_SHEET_PLUGIN_SHEET, ACTIVE_SHEET_PLUGIN_SHEET,
ACTIVE_SHEET_JS_EMULATOR_LAUNCHER, ACTIVE_SHEET_JS_EMULATOR_LAUNCHER,
ACTIVE_SHEET_CHANGELOG, ACTIVE_SHEET_CHANGELOG,
setActiveSheet,
ACTIVE_SHEET_CHANGELOG_RECENT_ONLY,
} from './reducers/application'; } from './reducers/application';
import {Logger} from './fb-interfaces/Logger'; import {Logger} from './fb-interfaces/Logger';
import BugReporter from './fb-stubs/BugReporter'; import BugReporter from './fb-stubs/BugReporter';
@@ -46,7 +48,7 @@ import PluginManager from './chrome/plugin-manager/PluginManager';
import StatusBar from './chrome/StatusBar'; import StatusBar from './chrome/StatusBar';
import SettingsSheet from './chrome/SettingsSheet'; import SettingsSheet from './chrome/SettingsSheet';
import DoctorSheet from './chrome/DoctorSheet'; import DoctorSheet from './chrome/DoctorSheet';
import ChangelogSheet from './chrome/ChangelogSheet'; import ChangelogSheet, {hasNewChangesToShow} from './chrome/ChangelogSheet';
const version = remote.app.getVersion(); const version = remote.app.getVersion();
@@ -63,7 +65,11 @@ type StateFromProps = {
staticView: StaticView; staticView: StaticView;
}; };
type Props = StateFromProps & OwnProps; type DispatchProps = {
setActiveSheet: typeof setActiveSheet;
};
type Props = StateFromProps & OwnProps & DispatchProps;
export class App extends React.Component<Props> { export class App extends React.Component<Props> {
componentDidMount() { componentDidMount() {
@@ -79,6 +85,10 @@ export class App extends React.Component<Props> {
}); });
ipcRenderer.send('getLaunchTime'); ipcRenderer.send('getLaunchTime');
ipcRenderer.send('componentDidMount'); ipcRenderer.send('componentDidMount');
if (hasNewChangesToShow(window.localStorage)) {
this.props.setActiveSheet(ACTIVE_SHEET_CHANGELOG_RECENT_ONLY);
}
} }
getSheet = (onHide: () => any) => { getSheet = (onHide: () => any) => {
@@ -101,6 +111,8 @@ export class App extends React.Component<Props> {
return <DoctorSheet onHide={onHide} />; return <DoctorSheet onHide={onHide} />;
case ACTIVE_SHEET_CHANGELOG: case ACTIVE_SHEET_CHANGELOG:
return <ChangelogSheet onHide={onHide} />; return <ChangelogSheet onHide={onHide} />;
case ACTIVE_SHEET_CHANGELOG_RECENT_ONLY:
return <ChangelogSheet onHide={onHide} recent />;
case ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT: case ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT:
return <ExportDataPluginSheet onHide={onHide} />; return <ExportDataPluginSheet onHide={onHide} />;
case ACTIVE_SHEET_SHARE_DATA: case ACTIVE_SHEET_SHARE_DATA:
@@ -163,7 +175,7 @@ export class App extends React.Component<Props> {
} }
} }
export default connect<StateFromProps, {}, OwnProps, Store>( export default connect<StateFromProps, DispatchProps, OwnProps, Store>(
({ ({
application: {leftSidebarVisible, activeSheet, share}, application: {leftSidebarVisible, activeSheet, share},
connections: {errors, staticView}, connections: {errors, staticView},
@@ -174,4 +186,7 @@ export default connect<StateFromProps, {}, OwnProps, Store>(
errors, errors,
staticView, staticView,
}), }),
{
setActiveSheet,
},
)(App); )(App);

View File

@@ -14,10 +14,21 @@ import path from 'path';
import {reportUsage} from '../utils/metrics'; import {reportUsage} from '../utils/metrics';
import {getStaticPath} from '../utils/pathUtils'; import {getStaticPath} from '../utils/pathUtils';
const changelog: string = readFileSync( const changelogKey = 'FlipperChangelogStatus';
type ChangelogStatus = {
lastHeader: string;
};
let getChangelogFromDisk = (): string => {
const changelogFromDisk: string = readFileSync(
path.join(getStaticPath(), 'CHANGELOG.md'), path.join(getStaticPath(), 'CHANGELOG.md'),
'utf8', 'utf8',
); ).trim();
getChangelogFromDisk = () => changelogFromDisk;
return changelogFromDisk;
};
const Container = styled(FlexColumn)({ const Container = styled(FlexColumn)({
padding: 20, padding: 20,
@@ -43,23 +54,42 @@ const changelogSectionStyle = {
type Props = { type Props = {
onHide: () => void; onHide: () => void;
recent?: boolean;
}; };
export default class ChangelogSheet extends Component<Props, {}> { export default class ChangelogSheet extends Component<Props, {}> {
componentDidMount() { componentDidMount() {
if (!this.props.recent) {
// opened through the menu
reportUsage('changelog:opened'); reportUsage('changelog:opened');
} }
}
componentWillUnmount(): void { componentWillUnmount(): void {
if (this.props.recent) {
markChangelogRead(window.localStorage, getChangelogFromDisk());
}
if (!this.props.recent) {
reportUsage('changelog:closed'); reportUsage('changelog:closed');
} }
}
render() { render() {
return ( return (
<Container> <Container>
<Title>Changelog</Title> <Title>Changelog</Title>
<FlexRow> <FlexRow>
<Markdown source={changelog} style={changelogSectionStyle} /> <Markdown
source={
this.props.recent
? getRecentChangelog(
window.localStorage,
getChangelogFromDisk(),
)
: getChangelogFromDisk()
}
style={changelogSectionStyle}
/>
</FlexRow> </FlexRow>
<FlexRow> <FlexRow>
<Button type="primary" compact padded onClick={this.props.onHide}> <Button type="primary" compact padded onClick={this.props.onHide}>
@@ -70,3 +100,71 @@ export default class ChangelogSheet extends Component<Props, {}> {
); );
} }
} }
function getChangelogStatus(
localStorage: Storage,
): ChangelogStatus | undefined {
return JSON.parse(localStorage.getItem(changelogKey) || '{}');
}
function getFirstHeader(changelog: string): string {
const match = changelog.match(/(^|\n)(#.*?)\n/);
if (match) {
return match[2];
}
return '';
}
export function hasNewChangesToShow(
localStorage: Storage | undefined,
changelog: string = getChangelogFromDisk(),
): boolean {
if (!localStorage) {
return false;
}
const status = getChangelogStatus(localStorage);
if (!status || !status.lastHeader) {
return true;
}
const firstHeader = getFirstHeader(changelog);
if (firstHeader && firstHeader !== status.lastHeader) {
return true;
}
return false;
}
export /*for test*/ function getRecentChangelog(
localStorage: Storage | undefined,
changelog: string,
): string {
if (!localStorage) {
return 'Changelog not available';
}
const status = getChangelogStatus(localStorage);
if (!status || !status.lastHeader) {
return changelog.trim();
}
const lastHeaderIndex = changelog.indexOf(status.lastHeader);
if (lastHeaderIndex === -1) {
return changelog.trim();
} else {
return changelog.substr(0, lastHeaderIndex).trim();
}
}
export /*for test*/ function markChangelogRead(
localStorage: Storage | undefined,
changelog: string,
) {
if (!localStorage) {
return;
}
const firstHeader = getFirstHeader(changelog);
if (!firstHeader) {
return;
}
const status: ChangelogStatus = {
lastHeader: firstHeader,
};
localStorage.setItem(changelogKey, JSON.stringify(status));
}

View File

@@ -0,0 +1,93 @@
/**
* 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 {
hasNewChangesToShow,
getRecentChangelog,
markChangelogRead,
} from '../ChangelogSheet';
class StubStorage {
data: Record<string, string> = {};
setItem(key: string, value: string) {
this.data[key] = value;
}
getItem(key: string) {
return this.data[key];
}
}
const changelog = `
# Version 2.0
* Nice feature one
* Important fix
# Version 1.0
* Not very exciting actually
`;
describe('ChangelogSheet', () => {
let storage!: Storage;
beforeEach(() => {
storage = new StubStorage() as any;
});
test('without storage, should show changes', () => {
expect(hasNewChangesToShow(undefined, changelog)).toBe(false);
expect(getRecentChangelog(storage, changelog)).toEqual(changelog.trim());
expect(hasNewChangesToShow(storage, changelog)).toBe(true);
});
test('with last header, should not show changes', () => {
markChangelogRead(storage, changelog);
expect(storage.data).toMatchInlineSnapshot(`
Object {
"FlipperChangelogStatus": "{\\"lastHeader\\":\\"# Version 2.0\\"}",
}
`);
expect(hasNewChangesToShow(storage, changelog)).toBe(false);
const newChangelog = `
# Version 3.0
* Cool!
# Version 2.5
* This is visible as well
${changelog}
`;
expect(hasNewChangesToShow(storage, newChangelog)).toBe(true);
expect(getRecentChangelog(storage, newChangelog)).toMatchInlineSnapshot(`
"# Version 3.0
* Cool!
# Version 2.5
* This is visible as well"
`);
markChangelogRead(storage, newChangelog);
expect(storage.data).toMatchInlineSnapshot(`
Object {
"FlipperChangelogStatus": "{\\"lastHeader\\":\\"# Version 3.0\\"}",
}
`);
expect(hasNewChangesToShow(storage, newChangelog)).toBe(false);
});
});

View File

@@ -29,6 +29,8 @@ export const UNSET_SHARE: 'UNSET_SHARE' = 'UNSET_SHARE';
export const ACTIVE_SHEET_JS_EMULATOR_LAUNCHER: 'ACTIVE_SHEET_JS_EMULATOR_LAUNCHER' = export const ACTIVE_SHEET_JS_EMULATOR_LAUNCHER: 'ACTIVE_SHEET_JS_EMULATOR_LAUNCHER' =
'ACTIVE_SHEET_JS_EMULATOR_LAUNCHER'; 'ACTIVE_SHEET_JS_EMULATOR_LAUNCHER';
export const ACTIVE_SHEET_CHANGELOG = 'ACTIVE_SHEET_CHANGELOG'; export const ACTIVE_SHEET_CHANGELOG = 'ACTIVE_SHEET_CHANGELOG';
export const ACTIVE_SHEET_CHANGELOG_RECENT_ONLY =
'ACTIVE_SHEET_CHANGELOG_RECENT_ONLY';
export type ActiveSheet = export type ActiveSheet =
| typeof ACTIVE_SHEET_PLUGIN_SHEET | typeof ACTIVE_SHEET_PLUGIN_SHEET
@@ -42,6 +44,7 @@ export type ActiveSheet =
| typeof ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT | typeof ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT
| typeof ACTIVE_SHEET_JS_EMULATOR_LAUNCHER | typeof ACTIVE_SHEET_JS_EMULATOR_LAUNCHER
| typeof ACTIVE_SHEET_CHANGELOG | typeof ACTIVE_SHEET_CHANGELOG
| typeof ACTIVE_SHEET_CHANGELOG_RECENT_ONLY
| null; | null;
export type LauncherMsg = { export type LauncherMsg = {