From 191bfee18cbe5f07a56112293ad1c0f45c49e616 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Fri, 22 Oct 2021 17:41:45 -0700 Subject: [PATCH] Show What's New dialog in app via Help -> Go to release notes --- app/main.ts | 5 + js/modules/signal.js | 4 +- preload.js | 7 ++ ts/components/GlobalModalContainer.tsx | 16 +++ ts/components/WhatsNew.tsx | 108 ------------------ ts/components/WhatsNewLink.tsx | 29 +++++ ts/components/WhatsNewModal.tsx | 89 +++++++++++++++ ts/state/ducks/globalModals.ts | 42 +++++++ ts/state/smart/GlobalModalContainer.tsx | 5 + ts/test-both/state/ducks/globalModals_test.ts | 15 +++ ts/util/createIPCEvents.ts | 5 + ts/util/lint/exceptions.json | 52 ++++----- ts/views/inbox_view.ts | 10 +- ts/window.d.ts | 4 +- 14 files changed, 249 insertions(+), 142 deletions(-) delete mode 100644 ts/components/WhatsNew.tsx create mode 100644 ts/components/WhatsNewLink.tsx create mode 100644 ts/components/WhatsNewModal.tsx diff --git a/app/main.ts b/app/main.ts index cb7037d14..ec4ffd2c8 100644 --- a/app/main.ts +++ b/app/main.ts @@ -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()}` ); diff --git a/js/modules/signal.js b/js/modules/signal.js index 4c34b5be9..b1d42bd83 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -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 = { diff --git a/preload.js b/preload.js index bf985456c..7313e18cb 100644 --- a/preload.js +++ b/preload.js @@ -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'); diff --git a/ts/components/GlobalModalContainer.tsx b/ts/components/GlobalModalContainer.tsx index 6047c160d..d35cb8042 100644 --- a/ts/components/GlobalModalContainer.tsx +++ b/ts/components/GlobalModalContainer.tsx @@ -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 ; + } + return null; }; diff --git a/ts/components/WhatsNew.tsx b/ts/components/WhatsNew.tsx deleted file mode 100644 index c06c0f670..000000000 --- a/ts/components/WhatsNew.tsx +++ /dev/null @@ -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 }) => ( - -); - -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 = ( -

- -

- ); - } else { - contentNode = ( -
    - {releaseNotes.features.map(({ key, components }) => ( -
  • - -
  • - ))} -
- ); - } - - modalNode = ( - setReleaseNotes(undefined)} - title={i18n('WhatsNew__modal-title')} - > - <> - - {moment(releaseNotes.date).format('LL')} ·{' '} - {releaseNotes.version} - - {contentNode} - - - ); - } - - return ( - <> - {modalNode} - - {i18n('viewReleaseNotes')} - , - ]} - /> - - ); -}; diff --git a/ts/components/WhatsNewLink.tsx b/ts/components/WhatsNewLink.tsx new file mode 100644 index 000000000..15eb0119e --- /dev/null +++ b/ts/components/WhatsNewLink.tsx @@ -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 ( + + {i18n('viewReleaseNotes')} + , + ]} + /> + ); +}; diff --git a/ts/components/WhatsNewModal.tsx b/ts/components/WhatsNewModal.tsx new file mode 100644 index 000000000..2cfab8c5f --- /dev/null +++ b/ts/components/WhatsNewModal.tsx @@ -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 }) => ( + +); + +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 = ( +

+ +

+ ); + } else { + contentNode = ( +
    + {releaseNotes.features.map(({ key, components }) => ( +
  • + +
  • + ))} +
+ ); + } + + return ( + + <> + + {moment(releaseNotes.date).format('LL')} ·{' '} + {releaseNotes.version} + + {contentNode} + + + ); +}; diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts index 367bfc773..5e943ab7d 100644 --- a/ts/state/ducks/globalModals.ts +++ b/ts/state/ducks/globalModals.ts @@ -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, diff --git a/ts/state/smart/GlobalModalContainer.tsx b/ts/state/smart/GlobalModalContainer.tsx index 03d5277ed..018cebdce 100644 --- a/ts/state/smart/GlobalModalContainer.tsx +++ b/ts/state/smart/GlobalModalContainer.tsx @@ -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: () => ( diff --git a/ts/test-both/state/ducks/globalModals_test.ts b/ts/test-both/state/ducks/globalModals_test.ts index ad5f132ba..4da6eb68f 100644 --- a/ts/test-both/state/ducks/globalModals_test.ts +++ b/ts/test-both/state/ducks/globalModals_test.ts @@ -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); + }); + }); }); diff --git a/ts/util/createIPCEvents.ts b/ts/util/createIPCEvents.ts index 643a9018a..43780ef24 100644 --- a/ts/util/createIPCEvents.ts +++ b/ts/util/createIPCEvents.ts @@ -99,6 +99,7 @@ export type IPCEventsCallbacksType = { showConversationViaSignalDotMe: (hash: string) => void; showKeyboardShortcuts: () => void; showGroupViaLink: (x: string) => Promise; + showReleaseNotes: () => void; showStickerPack: (packId: string, key: string) => void; shutdown: () => Promise; 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, diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index b29113200..8debd7457 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -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(", diff --git a/ts/views/inbox_view.ts b/ts/views/inbox_view.ts index d0e3b7f87..a44f2d2b2 100644 --- a/ts/views/inbox_view.ts +++ b/ts/views/inbox_view.ts @@ -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) { diff --git a/ts/window.d.ts b/ts/window.d.ts index dabc427d2..59143440e 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -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: {