Show What's New dialog in app via Help -> Go to release notes

This commit is contained in:
Scott Nonnenberg 2021-10-22 17:41:45 -07:00 committed by GitHub
parent 3e38a4b761
commit 191bfee18c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 249 additions and 142 deletions

View File

@ -865,6 +865,11 @@ function openJoinTheBeta() {
}
function openReleaseNotes() {
if (mainWindow && mainWindow.isVisible()) {
mainWindow.webContents.send('show-release-notes');
return;
}
shell.openExternal(
`https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}`
);

View File

@ -56,7 +56,7 @@ const {
const {
SystemTraySettingsCheckboxes,
} = require('../../ts/components/conversation/SystemTraySettingsCheckboxes');
const { WhatsNew } = require('../../ts/components/WhatsNew');
const { WhatsNewLink } = require('../../ts/components/WhatsNewLink');
// State
const {
@ -338,7 +338,7 @@ exports.setup = (options = {}) => {
StagedLinkPreview,
DisappearingTimeDialog,
SystemTraySettingsCheckboxes,
WhatsNew,
WhatsNewLink,
};
const Roots = {

View File

@ -340,6 +340,13 @@ try {
}
});
ipc.on('show-release-notes', () => {
const { showReleaseNotes } = window.Events;
if (showReleaseNotes) {
showReleaseNotes();
}
});
window.addSetupMenuItems = () => ipc.send('add-setup-menu-items');
window.removeSetupMenuItems = () => ipc.send('remove-setup-menu-items');

View File

@ -1,9 +1,14 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { ContactModalStateType } from '../state/ducks/globalModals';
import { LocalizerType } from '../types/Util';
import { WhatsNewModal } from './WhatsNewModal';
type PropsType = {
i18n: LocalizerType;
// ContactModal
contactModalState?: ContactModalStateType;
renderContactModal: () => JSX.Element;
@ -13,9 +18,13 @@ type PropsType = {
// SafetyNumberModal
safetyNumberModalContactId?: string;
renderSafetyNumber: () => JSX.Element;
// WhatsNewModal
isWhatsNewVisible: boolean;
hideWhatsNewModal: () => unknown;
};
export const GlobalModalContainer = ({
i18n,
// ContactModal
contactModalState,
renderContactModal,
@ -25,6 +34,9 @@ export const GlobalModalContainer = ({
// SafetyNumberModal
safetyNumberModalContactId,
renderSafetyNumber,
// WhatsNewModal
hideWhatsNewModal,
isWhatsNewVisible,
}: PropsType): JSX.Element | null => {
if (safetyNumberModalContactId) {
return renderSafetyNumber();
@ -38,5 +50,9 @@ export const GlobalModalContainer = ({
return renderProfileEditor();
}
if (isWhatsNewVisible) {
return <WhatsNewModal hideWhatsNewModal={hideWhatsNewModal} i18n={i18n} />;
}
return null;
};

View File

@ -1,108 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { ReactChild, ReactNode, useState } from 'react';
import moment from 'moment';
import { Modal } from './Modal';
import { Intl, IntlComponentsType } from './Intl';
import { Emojify } from './conversation/Emojify';
import type { LocalizerType, RenderTextCallbackType } from '../types/Util';
export type PropsType = {
i18n: LocalizerType;
};
type ReleaseNotesType = {
date: Date;
version: string;
features: Array<{ key: string; components: IntlComponentsType }>;
};
const renderText: RenderTextCallbackType = ({ key, text }) => (
<Emojify key={key} text={text} />
);
export const WhatsNew = ({ i18n }: PropsType): JSX.Element => {
const [releaseNotes, setReleaseNotes] = useState<
ReleaseNotesType | undefined
>();
const viewReleaseNotes = () => {
setReleaseNotes({
date: new Date(window.getBuildCreation?.() || Date.now()),
version: window.getVersion(),
features: [
{
key: 'WhatsNew__v5.22',
components: undefined,
},
],
});
};
let modalNode: ReactNode;
if (releaseNotes) {
let contentNode: ReactChild;
if (releaseNotes.features.length === 1) {
const { key, components } = releaseNotes.features[0];
contentNode = (
<p>
<Intl
i18n={i18n}
id={key}
renderText={renderText}
components={components}
/>
</p>
);
} else {
contentNode = (
<ul>
{releaseNotes.features.map(({ key, components }) => (
<li key={key}>
<Intl
i18n={i18n}
id={key}
renderText={renderText}
components={components}
/>
</li>
))}
</ul>
);
}
modalNode = (
<Modal
hasXButton
i18n={i18n}
onClose={() => setReleaseNotes(undefined)}
title={i18n('WhatsNew__modal-title')}
>
<>
<span>
{moment(releaseNotes.date).format('LL')} &middot;{' '}
{releaseNotes.version}
</span>
{contentNode}
</>
</Modal>
);
}
return (
<>
{modalNode}
<Intl
i18n={i18n}
id="whatsNew"
components={[
<button className="WhatsNew" type="button" onClick={viewReleaseNotes}>
{i18n('viewReleaseNotes')}
</button>,
]}
/>
</>
);
};

View File

@ -0,0 +1,29 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { Intl } from './Intl';
import { LocalizerType } from '../types/Util';
export type PropsType = {
i18n: LocalizerType;
showWhatsNewModal: () => unknown;
};
export const WhatsNewLink = (props: PropsType): JSX.Element => {
const { i18n, showWhatsNewModal } = props;
return (
<Intl
i18n={i18n}
id="whatsNew"
components={[
<button className="WhatsNew" type="button" onClick={showWhatsNewModal}>
{i18n('viewReleaseNotes')}
</button>,
]}
/>
);
};

View File

@ -0,0 +1,89 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { ReactChild } from 'react';
import moment from 'moment';
import { Modal } from './Modal';
import { Intl, IntlComponentsType } from './Intl';
import { Emojify } from './conversation/Emojify';
import type { LocalizerType, RenderTextCallbackType } from '../types/Util';
export type PropsType = {
hideWhatsNewModal: () => unknown;
i18n: LocalizerType;
};
type ReleaseNotesType = {
date: Date;
version: string;
features: Array<{ key: string; components: IntlComponentsType }>;
};
const renderText: RenderTextCallbackType = ({ key, text }) => (
<Emojify key={key} text={text} />
);
const releaseNotes: ReleaseNotesType = {
date: new Date(window.getBuildCreation?.() || Date.now()),
version: window.getVersion(),
features: [
{
key: 'WhatsNew__v5.22',
components: undefined,
},
],
};
export const WhatsNewModal = ({
i18n,
hideWhatsNewModal,
}: PropsType): JSX.Element => {
let contentNode: ReactChild;
if (releaseNotes.features.length === 1) {
const { key, components } = releaseNotes.features[0];
contentNode = (
<p>
<Intl
i18n={i18n}
id={key}
renderText={renderText}
components={components}
/>
</p>
);
} else {
contentNode = (
<ul>
{releaseNotes.features.map(({ key, components }) => (
<li key={key}>
<Intl
i18n={i18n}
id={key}
renderText={renderText}
components={components}
/>
</li>
))}
</ul>
);
}
return (
<Modal
hasXButton
i18n={i18n}
onClose={hideWhatsNewModal}
title={i18n('WhatsNew__modal-title')}
>
<>
<span>
{moment(releaseNotes.date).format('LL')} &middot;{' '}
{releaseNotes.version}
</span>
{contentNode}
</>
</Modal>
);
};

View File

@ -8,12 +8,15 @@ export type GlobalModalsStateType = {
readonly isProfileEditorVisible: boolean;
readonly profileEditorHasError: boolean;
readonly safetyNumberModalContactId?: string;
readonly isWhatsNewVisible: boolean;
};
// Actions
const HIDE_CONTACT_MODAL = 'globalModals/HIDE_CONTACT_MODAL';
const SHOW_CONTACT_MODAL = 'globalModals/SHOW_CONTACT_MODAL';
const SHOW_WHATS_NEW_MODAL = 'globalModals/SHOW_WHATS_NEW_MODAL_MODAL';
const HIDE_WHATS_NEW_MODAL = 'globalModals/HIDE_WHATS_NEW_MODAL_MODAL';
const TOGGLE_PROFILE_EDITOR = 'globalModals/TOGGLE_PROFILE_EDITOR';
export const TOGGLE_PROFILE_EDITOR_ERROR =
'globalModals/TOGGLE_PROFILE_EDITOR_ERROR';
@ -33,6 +36,14 @@ type ShowContactModalActionType = {
payload: ContactModalStateType;
};
type HideWhatsNewModalActionType = {
type: typeof HIDE_WHATS_NEW_MODAL;
};
type ShowWhatsNewModalActionType = {
type: typeof SHOW_WHATS_NEW_MODAL;
};
type ToggleProfileEditorActionType = {
type: typeof TOGGLE_PROFILE_EDITOR;
};
@ -49,6 +60,8 @@ type ToggleSafetyNumberModalActionType = {
export type GlobalModalsActionType =
| HideContactModalActionType
| ShowContactModalActionType
| HideWhatsNewModalActionType
| ShowWhatsNewModalActionType
| ToggleProfileEditorActionType
| ToggleProfileEditorErrorActionType
| ToggleSafetyNumberModalActionType;
@ -58,6 +71,8 @@ export type GlobalModalsActionType =
export const actions = {
hideContactModal,
showContactModal,
hideWhatsNewModal,
showWhatsNewModal,
toggleProfileEditor,
toggleProfileEditorHasError,
toggleSafetyNumberModal,
@ -82,6 +97,18 @@ function showContactModal(
};
}
function hideWhatsNewModal(): HideWhatsNewModalActionType {
return {
type: HIDE_WHATS_NEW_MODAL,
};
}
function showWhatsNewModal(): ShowWhatsNewModalActionType {
return {
type: SHOW_WHATS_NEW_MODAL,
};
}
function toggleProfileEditor(): ToggleProfileEditorActionType {
return { type: TOGGLE_PROFILE_EDITOR };
}
@ -105,6 +132,7 @@ export function getEmptyState(): GlobalModalsStateType {
return {
isProfileEditorVisible: false,
profileEditorHasError: false,
isWhatsNewVisible: false,
};
}
@ -126,6 +154,20 @@ export function reducer(
};
}
if (action.type === SHOW_WHATS_NEW_MODAL) {
return {
...state,
isWhatsNewVisible: true,
};
}
if (action.type === HIDE_WHATS_NEW_MODAL) {
return {
...state,
isWhatsNewVisible: false,
};
}
if (action.type === SHOW_CONTACT_MODAL) {
return {
...state,

View File

@ -10,6 +10,8 @@ import { SmartProfileEditorModal } from './ProfileEditorModal';
import { SmartContactModal } from './ContactModal';
import { SmartSafetyNumberModal } from './SafetyNumberModal';
import { getIntl } from '../selectors/user';
const FilteredSmartProfileEditorModal = SmartProfileEditorModal;
function renderProfileEditor(): JSX.Element {
@ -21,8 +23,11 @@ function renderContactModal(): JSX.Element {
}
const mapStateToProps = (state: StateType) => {
const i18n = getIntl(state);
return {
...state.globalModals,
i18n,
renderContactModal,
renderProfileEditor,
renderSafetyNumber: () => (

View File

@ -24,4 +24,19 @@ describe('both/state/ducks/globalModals', () => {
assert.isFalse(nextNextState.isProfileEditorVisible);
});
});
describe('showWhatsNewModal/hideWhatsNewModal', () => {
const { showWhatsNewModal, hideWhatsNewModal } = actions;
it('toggles isWhatsNewVisible to true', () => {
const state = getEmptyState();
const nextState = reducer(state, showWhatsNewModal());
assert.isTrue(nextState.isWhatsNewVisible);
const nextNextState = reducer(nextState, hideWhatsNewModal());
assert.isFalse(nextNextState.isWhatsNewVisible);
});
});
});

View File

@ -99,6 +99,7 @@ export type IPCEventsCallbacksType = {
showConversationViaSignalDotMe: (hash: string) => void;
showKeyboardShortcuts: () => void;
showGroupViaLink: (x: string) => Promise<void>;
showReleaseNotes: () => void;
showStickerPack: (packId: string, key: string) => void;
shutdown: () => Promise<void>;
unknownSignalLink: () => void;
@ -505,6 +506,10 @@ export function createIPCEvents(
},
shutdown: () => Promise.resolve(),
showReleaseNotes: () => {
const { showWhatsNewModal } = window.reduxActions.globalModals;
showWhatsNewModal();
},
getMediaPermissions: window.getMediaPermissions,
getMediaCameraPermissions: window.getMediaCameraPermissions,

View File

@ -12784,14 +12784,6 @@
"updated": "2020-08-28T16:12:19.904Z",
"reasonDetail": "Used to reference popup menu"
},
{
"rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.tsx",
"line": " this.menuTriggerRef = React.createRef();",
"reasonCategory": "usageTrusted",
"updated": "2020-05-20T20:10:43.540Z",
"reasonDetail": "Used to reference popup menu"
},
{
"rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.js",
@ -12800,6 +12792,14 @@
"updated": "2021-01-18T22:24:05.937Z",
"reasonDetail": "Used to reference popup menu boundaries element"
},
{
"rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.tsx",
"line": " this.menuTriggerRef = React.createRef();",
"reasonCategory": "usageTrusted",
"updated": "2020-05-20T20:10:43.540Z",
"reasonDetail": "Used to reference popup menu"
},
{
"rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.tsx",
@ -13329,13 +13329,6 @@
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/inbox_view.js",
"line": " this.$('.whats-new-placeholder').append(this.whatsNewView.el);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/inbox_view.js",
@ -13350,12 +13343,19 @@
"reasonCategory": "usageTrusted",
"updated": "2021-10-08T17:40:22.770Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/inbox_view.js",
"line": " this.$('.whats-new-placeholder').append(this.whatsNewLink.el);",
"reasonCategory": "usageTrusted",
"updated": "2021-10-22T20:58:48.103Z"
},
{
"rule": "jQuery-append(",
"path": "ts/views/inbox_view.js",
"line": " this.$('.whats-new-placeholder').append(this.whatsNewView.el);",
"line": " this.$('.whats-new-placeholder').append(this.whatsNewLink.el);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
"updated": "2021-10-22T20:58:48.103Z"
},
{
"rule": "jQuery-appendTo(",
@ -13413,13 +13413,6 @@
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/inbox_view.ts",
"line": " this.$('.whats-new-placeholder').append(this.whatsNewView.el);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/inbox_view.ts",
@ -13434,12 +13427,19 @@
"reasonCategory": "usageTrusted",
"updated": "2021-10-08T17:40:22.770Z"
},
{
"rule": "jQuery-$(",
"path": "ts/views/inbox_view.ts",
"line": " this.$('.whats-new-placeholder').append(this.whatsNewLink.el);",
"reasonCategory": "usageTrusted",
"updated": "2021-10-22T20:58:48.103Z"
},
{
"rule": "jQuery-append(",
"path": "ts/views/inbox_view.ts",
"line": " this.$('.whats-new-placeholder').append(this.whatsNewView.el);",
"line": " this.$('.whats-new-placeholder').append(this.whatsNewLink.el);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-15T21:07:50.995Z"
"updated": "2021-10-22T20:58:48.103Z"
},
{
"rule": "jQuery-appendTo(",

View File

@ -160,16 +160,18 @@ Whisper.InboxView = Whisper.View.extend({
click: 'onClick',
},
renderWhatsNew() {
if (this.whatsNewView) {
if (this.whatsNewLink) {
return;
}
this.whatsNewView = new Whisper.ReactWrapperView({
Component: window.Signal.Components.WhatsNew,
const { showWhatsNewModal } = window.reduxActions.globalModals;
this.whatsNewLink = new Whisper.ReactWrapperView({
Component: window.Signal.Components.WhatsNewLink,
props: {
i18n: window.i18n,
showWhatsNewModal,
},
});
this.$('.whats-new-placeholder').append(this.whatsNewView.el);
this.$('.whats-new-placeholder').append(this.whatsNewLink.el);
},
setupLeftPane() {
if (this.leftPaneView) {

4
ts/window.d.ts vendored
View File

@ -92,7 +92,7 @@ import { ProgressModal } from './components/ProgressModal';
import { Quote } from './components/conversation/Quote';
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
import { WhatsNew } from './components/WhatsNew';
import { WhatsNewLink } from './components/WhatsNewLink';
import { MIMEType } from './types/MIME';
import { DownloadedAttachmentType } from './types/Attachment';
import { ElectronLocaleType } from './util/mapToSupportLocale';
@ -403,7 +403,7 @@ declare global {
ProgressModal: typeof ProgressModal;
Quote: typeof Quote;
StagedLinkPreview: typeof StagedLinkPreview;
WhatsNew: typeof WhatsNew;
WhatsNewLink: typeof WhatsNewLink;
};
OS: typeof OS;
Workflow: {