Skip to content

Commit

Permalink
[MM-27918] Add in product notices (mattermost#6448)
Browse files Browse the repository at this point in the history
* MM-27198 Add in product notices

  * Use Generic modal for showing notices
  * Call for notices data on load of webapp
  * subsequent calls are made when socket reconnects for first
    time in a day
  * Clear data of notices locally when modal is dismmised
  * Mark a notice when modal loads or when user hits next button
  * Add console settings for notices

* Update redux hash

* Fix lint

* Fix lint

* Fix  snapshots

* e2e tests

* Fix comment

* Fix for desktop app version

* Add check for API data

* Fix lint

* Update utils/user_agent.tsx

Co-authored-by: Guillermo Vayá <[email protected]>

* Fix markdown

* Update redux hash

* Add TM4J test ids

* Update func name updateNoticeAsViewed to updateNoticesAsViewed

* Update redux hash

Co-authored-by: Guillermo Vayá <[email protected]>
  • Loading branch information
sudheerDev and Willyfrog committed Sep 21, 2020
1 parent 06441b5 commit f343b2c
Show file tree
Hide file tree
Showing 14 changed files with 988 additions and 6 deletions.
30 changes: 30 additions & 0 deletions components/admin_console/admin_definition.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2396,6 +2396,36 @@ const AdminDefinition = {
],
},
},
notices: {
url: 'site_config/notices',
title: t('admin.sidebar.notices'),
title_default: 'Notices',
schema: {
id: 'NoticesSettings',
name: t('admin.site.notices'),
name_default: 'Notices',
settings: [
{
type: Constants.SettingsTypes.TYPE_BOOL,
key: 'AnnouncementSettings.AdminNoticesEnabled',
label: t('admin.notices.enableAdminNoticesTitle'),
label_default: 'Enable Admin Notices: ',
help_text: t('admin.notices.enableAdminNoticesDescription'),
help_text_default: 'When enabled, System Admins will receive notices about available server upgrades and relevant system administration features. [Learn more about notices](!https://about.mattermost.com/default-notices) in our documentation.',
help_text_markdown: true,
},
{
type: Constants.SettingsTypes.TYPE_BOOL,
key: 'AnnouncementSettings.UserNoticesEnabled',
label: t('admin.notices.enableEndUserNoticesTitle'),
label_default: 'Enable End User Notices: ',
help_text: t('admin.notices.enableEndUserNoticesDescription'),
help_text_default: 'When enabled, all users will receive notices about available client upgrades and relevant end user features to improve user experience. [Learn more about notices](!https://about.mattermost.com/default-notices) in our documentation.',
help_text_markdown: true,
},
],
},
},
},
authentication: {
icon: 'fa-shield',
Expand Down
3 changes: 2 additions & 1 deletion components/channel_layout/channel_controller.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as UserAgent from 'utils/user_agent';
import CenterChannel from 'components/channel_layout/center_channel';
import LoadingScreen from 'components/loading_screen';
import FaviconTitleHandler from 'components/favicon_title_handler';
import ProductNoticesModal from 'components/product_notices_modal';

export default class ChannelController extends React.Component {
static propTypes = {
Expand Down Expand Up @@ -74,7 +75,7 @@ export default class ChannelController extends React.Component {
<AnnouncementBarController/>
<SystemNotice/>
<FaviconTitleHandler/>

<ProductNoticesModal/>
<div className='container-fluid'>
<SidebarRight/>
<SidebarRightMenu teamType={this.props.teamType}/>
Expand Down
12 changes: 10 additions & 2 deletions components/generic_modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type Props = {
confirmButtonClassName?: string;
cancelButtonText?: React.ReactNode;
isConfirmDisabled?: boolean;
autoCloseOnCancelButton?: boolean;
autoCloseOnConfirmButton?: boolean;
};

type State = {
Expand All @@ -28,6 +30,8 @@ type State = {
export default class GenericModal extends React.PureComponent<Props, State> {
static defaultProps: Partial<Props> = {
show: true,
autoCloseOnCancelButton: true,
autoCloseOnConfirmButton: true,
};

constructor(props: Props) {
Expand All @@ -44,15 +48,19 @@ export default class GenericModal extends React.PureComponent<Props, State> {

handleCancel = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
this.onHide();
if (this.props.autoCloseOnCancelButton) {
this.onHide();
}
if (this.props.handleCancel) {
this.props.handleCancel();
}
}

handleConfirm = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
this.onHide();
if (this.props.autoCloseOnConfirmButton) {
this.onHide();
}
if (this.props.handleConfirm) {
this.props.handleConfirm();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ProductNoticesModal Match snapshot for single notice 1`] = `
<GenericModal
autoCloseOnCancelButton={false}
autoCloseOnConfirmButton={true}
cancelButtonText={null}
className="productNotices"
confirmButtonText={
<span>
Download
</span>
}
handleConfirm={[Function]}
modalHeaderText={
<span>
title
</span>
}
onHide={[Function]}
show={true}
>
<span
className="productNotices__helpText"
>
<Connect(Markdown)
message="descr"
/>
</span>
<div
className="productNotices__imageDiv"
/>
<div
className="productNotices__info"
/>
</GenericModal>
`;

exports[`ProductNoticesModal Match snapshot for user notice 1`] = `
<GenericModal
autoCloseOnCancelButton={false}
autoCloseOnConfirmButton={true}
cancelButtonText={
<React.Fragment>
<PreviousIcon
additionalClassName={null}
/>
<FormattedMessage
defaultMessage="Previous"
id="generic.previous"
/>
</React.Fragment>
}
className="productNotices"
confirmButtonText={
<FormattedMessage
defaultMessage="Done"
id="generic.done"
/>
}
handleCancel={[Function]}
handleConfirm={[Function]}
modalHeaderText={
<span>
title
</span>
}
onHide={[Function]}
show={true}
>
<span
className="productNotices__helpText"
>
<Connect(Markdown)
message="descr"
/>
</span>
<a
className="GenericModal__button actionButton"
href="http:https://download.com/path"
id="actionButton"
rel="noopener noreferrer"
target="_blank"
>
Download
</a>
<div
className="productNotices__imageDiv"
/>
<div
className="productNotices__info"
>
<span
className="tutorial__circles"
>
<a
className="circle"
data-screen={0}
href="#"
id="tutorialIntroCircle0"
key="circle0"
/>
<a
className="circle active"
data-screen={1}
href="#"
id="tutorialIntroCircle1"
key="circle1"
/>
</span>
</div>
</GenericModal>
`;

exports[`ProductNoticesModal Should match snapshot for system admin notice 1`] = `
<GenericModal
autoCloseOnCancelButton={false}
autoCloseOnConfirmButton={false}
cancelButtonText={null}
className="productNotices"
confirmButtonText={
<React.Fragment>
<FormattedMessage
defaultMessage="Next"
id="generic.next"
/>
<NextIcon
additionalClassName={null}
/>
</React.Fragment>
}
handleConfirm={[Function]}
modalHeaderText={
<span>
for sysadmin
</span>
}
onHide={[Function]}
show={true}
>
<span
className="productNotices__helpText"
>
<Connect(Markdown)
message="your eyes only! [test](https://test.com)"
/>
</span>
<a
className="GenericModal__button actionButton"
href="http:https://download.com/path"
id="actionButton"
rel="noopener noreferrer"
target="_blank"
>
Download
</a>
<div
className="productNotices__imageDiv"
>
<img
className="productNotices__img"
src="https://raw.githubusercontent.com/reflog/notices-experiment/master/images/2020-08-11_11-42.png"
/>
</div>
<div
className="productNotices__info"
>
<span
className="tutorial__circles"
>
<a
className="circle active"
data-screen={0}
href="#"
id="tutorialIntroCircle0"
key="circle0"
/>
<a
className="circle"
data-screen={1}
href="#"
id="tutorialIntroCircle1"
key="circle1"
/>
</span>
<AdminEyeIcon />
<FormattedMessage
defaultMessage="Visible to Admins only"
id="inProduct_notices.adminOnlyMessage"
/>
</div>
</GenericModal>
`;

exports[`ProductNoticesModal Should match snapshot when there are no notices 1`] = `""`;
47 changes: 47 additions & 0 deletions components/product_notices_modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {connect} from 'react-redux';
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
import {ActionFunc} from 'mattermost-redux/types/actions';
import {ProductNotices} from 'mattermost-redux/types/product_notices';
import {WebsocketStatus} from 'mattermost-redux/types/websocket';
import {getInProductNotices, updateNoticesAsViewed} from 'mattermost-redux/actions/teams';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {ClientConfig} from 'mattermost-redux/types/config';

import {getSocketStatus} from 'selectors/views/websocket';
import {GlobalState} from 'types/store';

import ProductNoticesModal from './product_notices_modal';

type Actions = {
getInProductNotices: (teamId: string, client: string, clientVersion: string) => Promise<{
data: ProductNotices;
}>;
updateNoticesAsViewed: (noticeIds: string[]) => Promise<Record<string, unknown>>;
}

function mapStateToProps(state: GlobalState) {
const config: Partial<ClientConfig> = getConfig(state);
const version: string = config.Version || ''; //this should always exist but TS throws error
const socketStatus: WebsocketStatus = getSocketStatus(state);

return {
currentTeamId: getCurrentTeamId(state),
version,
socketStatus,
};
}

function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
getInProductNotices,
updateNoticesAsViewed,
}, dispatch),
};
}

export default connect(mapStateToProps, mapDispatchToProps)(ProductNoticesModal);
Loading

0 comments on commit f343b2c

Please sign in to comment.