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:
committed by
Facebook GitHub Bot
parent
3da7552779
commit
805a911c08
@@ -37,6 +37,8 @@ import {
|
||||
ACTIVE_SHEET_PLUGIN_SHEET,
|
||||
ACTIVE_SHEET_JS_EMULATOR_LAUNCHER,
|
||||
ACTIVE_SHEET_CHANGELOG,
|
||||
setActiveSheet,
|
||||
ACTIVE_SHEET_CHANGELOG_RECENT_ONLY,
|
||||
} from './reducers/application';
|
||||
import {Logger} from './fb-interfaces/Logger';
|
||||
import BugReporter from './fb-stubs/BugReporter';
|
||||
@@ -46,7 +48,7 @@ import PluginManager from './chrome/plugin-manager/PluginManager';
|
||||
import StatusBar from './chrome/StatusBar';
|
||||
import SettingsSheet from './chrome/SettingsSheet';
|
||||
import DoctorSheet from './chrome/DoctorSheet';
|
||||
import ChangelogSheet from './chrome/ChangelogSheet';
|
||||
import ChangelogSheet, {hasNewChangesToShow} from './chrome/ChangelogSheet';
|
||||
|
||||
const version = remote.app.getVersion();
|
||||
|
||||
@@ -63,7 +65,11 @@ type StateFromProps = {
|
||||
staticView: StaticView;
|
||||
};
|
||||
|
||||
type Props = StateFromProps & OwnProps;
|
||||
type DispatchProps = {
|
||||
setActiveSheet: typeof setActiveSheet;
|
||||
};
|
||||
|
||||
type Props = StateFromProps & OwnProps & DispatchProps;
|
||||
|
||||
export class App extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
@@ -79,6 +85,10 @@ export class App extends React.Component<Props> {
|
||||
});
|
||||
ipcRenderer.send('getLaunchTime');
|
||||
ipcRenderer.send('componentDidMount');
|
||||
|
||||
if (hasNewChangesToShow(window.localStorage)) {
|
||||
this.props.setActiveSheet(ACTIVE_SHEET_CHANGELOG_RECENT_ONLY);
|
||||
}
|
||||
}
|
||||
|
||||
getSheet = (onHide: () => any) => {
|
||||
@@ -101,6 +111,8 @@ export class App extends React.Component<Props> {
|
||||
return <DoctorSheet onHide={onHide} />;
|
||||
case ACTIVE_SHEET_CHANGELOG:
|
||||
return <ChangelogSheet onHide={onHide} />;
|
||||
case ACTIVE_SHEET_CHANGELOG_RECENT_ONLY:
|
||||
return <ChangelogSheet onHide={onHide} recent />;
|
||||
case ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT:
|
||||
return <ExportDataPluginSheet onHide={onHide} />;
|
||||
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},
|
||||
connections: {errors, staticView},
|
||||
@@ -174,4 +186,7 @@ export default connect<StateFromProps, {}, OwnProps, Store>(
|
||||
errors,
|
||||
staticView,
|
||||
}),
|
||||
{
|
||||
setActiveSheet,
|
||||
},
|
||||
)(App);
|
||||
|
||||
@@ -14,10 +14,21 @@ import path from 'path';
|
||||
import {reportUsage} from '../utils/metrics';
|
||||
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'),
|
||||
'utf8',
|
||||
);
|
||||
).trim();
|
||||
|
||||
getChangelogFromDisk = () => changelogFromDisk;
|
||||
return changelogFromDisk;
|
||||
};
|
||||
|
||||
const Container = styled(FlexColumn)({
|
||||
padding: 20,
|
||||
@@ -43,23 +54,42 @@ const changelogSectionStyle = {
|
||||
|
||||
type Props = {
|
||||
onHide: () => void;
|
||||
recent?: boolean;
|
||||
};
|
||||
|
||||
export default class ChangelogSheet extends Component<Props, {}> {
|
||||
componentDidMount() {
|
||||
if (!this.props.recent) {
|
||||
// opened through the menu
|
||||
reportUsage('changelog:opened');
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.props.recent) {
|
||||
markChangelogRead(window.localStorage, getChangelogFromDisk());
|
||||
}
|
||||
if (!this.props.recent) {
|
||||
reportUsage('changelog:closed');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container>
|
||||
<Title>Changelog</Title>
|
||||
<FlexRow>
|
||||
<Markdown source={changelog} style={changelogSectionStyle} />
|
||||
<Markdown
|
||||
source={
|
||||
this.props.recent
|
||||
? getRecentChangelog(
|
||||
window.localStorage,
|
||||
getChangelogFromDisk(),
|
||||
)
|
||||
: getChangelogFromDisk()
|
||||
}
|
||||
style={changelogSectionStyle}
|
||||
/>
|
||||
</FlexRow>
|
||||
<FlexRow>
|
||||
<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));
|
||||
}
|
||||
|
||||
93
desktop/src/chrome/__tests__/ChangelogSheet.node.tsx
Normal file
93
desktop/src/chrome/__tests__/ChangelogSheet.node.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -29,6 +29,8 @@ export const UNSET_SHARE: 'UNSET_SHARE' = 'UNSET_SHARE';
|
||||
export const 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_RECENT_ONLY =
|
||||
'ACTIVE_SHEET_CHANGELOG_RECENT_ONLY';
|
||||
|
||||
export type ActiveSheet =
|
||||
| typeof ACTIVE_SHEET_PLUGIN_SHEET
|
||||
@@ -42,6 +44,7 @@ export type ActiveSheet =
|
||||
| typeof ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT
|
||||
| typeof ACTIVE_SHEET_JS_EMULATOR_LAUNCHER
|
||||
| typeof ACTIVE_SHEET_CHANGELOG
|
||||
| typeof ACTIVE_SHEET_CHANGELOG_RECENT_ONLY
|
||||
| null;
|
||||
|
||||
export type LauncherMsg = {
|
||||
|
||||
Reference in New Issue
Block a user