diff --git a/server/api/api.go b/server/api/api.go index 91ae16cd6..811c98c69 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -1045,12 +1045,13 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { defer a.audit.LogRecord(audit.LevelRead, auditRec) if userID == model.SingleUser { + ws, _ := a.app.GetRootTeam() now := utils.GetMillis() user = &model.User{ ID: model.SingleUser, Username: model.SingleUser, Email: model.SingleUser, - CreateAt: now, + CreateAt: ws.UpdateAt, UpdateAt: now, } } else { diff --git a/webapp/src/components/messages/__snapshots__/cloudMessage.test.tsx.snap b/webapp/src/components/messages/__snapshots__/cloudMessage.test.tsx.snap new file mode 100644 index 000000000..9451d01a7 --- /dev/null +++ b/webapp/src/components/messages/__snapshots__/cloudMessage.test.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/messages/CloudMessage not plugin mode, close message 1`] = `
`; + +exports[`components/messages/CloudMessage not plugin mode, show message, close message 1`] = ` +
+
+ + +
+
+`; + +exports[`components/messages/CloudMessage plugin mode, no display 1`] = `
`; diff --git a/webapp/src/components/messages/cloudMessage.scss b/webapp/src/components/messages/cloudMessage.scss new file mode 100644 index 000000000..f6c86bba5 --- /dev/null +++ b/webapp/src/components/messages/cloudMessage.scss @@ -0,0 +1,38 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +.CloudMessage { + background-color: rgb(var(--sidebar-text-active-border-rgb)); + display: flex; + flex-direction: row; + align-items: center; + text-align: center; + font-weight: 600; + + div { + width: 100%; + } + + > .banner { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: 10px; + color: #fff; + + .CompassIcon { + font-size: 18px; + margin-right: 2px; + } + + .Button { + margin-left: 8px; + background-color: rgba(255, 255, 255, 0.16); + } + } + + .IconButton { + float: right; + color: #fff; + } +} diff --git a/webapp/src/components/messages/cloudMessage.test.tsx b/webapp/src/components/messages/cloudMessage.test.tsx new file mode 100644 index 000000000..5a5ae54de --- /dev/null +++ b/webapp/src/components/messages/cloudMessage.test.tsx @@ -0,0 +1,130 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react' +import {Provider as ReduxProvider} from 'react-redux' + +import {render, screen} from '@testing-library/react' +import {mocked} from 'jest-mock' +import userEvent from '@testing-library/user-event' + +import configureStore from 'redux-mock-store' + +import {Utils} from '../../utils' + +import {IUser} from '../../user' + +import {wrapIntl} from '../../testUtils' + +import client from '../../octoClient' + +import CloudMessage from './cloudMessage' + +jest.mock('../../utils') +jest.mock('../../octoClient') +const mockedOctoClient = mocked(client, true) + +describe('components/messages/CloudMessage', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + const mockedUtils = mocked(Utils, true) + const mockStore = configureStore([]) + + test('plugin mode, no display', () => { + mockedUtils.isFocalboardPlugin.mockReturnValue(true) + + const me: IUser = { + id: 'user-id-1', + username: 'username_1', + email: '', + props: {}, + create_at: 0, + update_at: 0, + is_bot: false, + } + const state = { + users: { + me, + }, + } + + const store = mockStore(state) + + const component = wrapIntl( + + + , + ) + + const {container} = render(component) + expect(container).toMatchSnapshot() + }) + + test('not plugin mode, close message', () => { + const me: IUser = { + id: 'user-id-1', + username: 'username_1', + email: '', + props: { + focalboard_cloudMessageCanceled: 'true', + }, + create_at: 0, + update_at: 0, + is_bot: false, + } + const state = { + users: { + me, + }, + } + const store = mockStore(state) + mockedUtils.isFocalboardPlugin.mockReturnValue(false) + + const component = wrapIntl( + + + , + ) + + const {container} = render(component) + expect(container).toMatchSnapshot() + }) + + test('not plugin mode, show message, close message', () => { + const me: IUser = { + id: 'user-id-1', + username: 'username_1', + email: '', + props: {}, + create_at: 0, + update_at: 0, + is_bot: false, + } + const state = { + users: { + me, + }, + } + const store = mockStore(state) + mockedUtils.isFocalboardPlugin.mockReturnValue(false) + + const component = wrapIntl( + + + , + ) + + const {container} = render(component) + expect(container).toMatchSnapshot() + + const buttonElement = screen.getByRole('button', {name: 'Close dialog'}) + userEvent.click(buttonElement) + expect(mockedOctoClient.patchUserConfig).toBeCalledWith('user-id-1', { + updatedFields: { + focalboard_cloudMessageCanceled: 'true', + }, + }) + }) +}) diff --git a/webapp/src/components/messages/cloudMessage.tsx b/webapp/src/components/messages/cloudMessage.tsx new file mode 100644 index 000000000..e4ffe589f --- /dev/null +++ b/webapp/src/components/messages/cloudMessage.tsx @@ -0,0 +1,101 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react' + +import {useIntl, FormattedMessage} from 'react-intl' + +import {Utils} from '../../utils' +import IconButton from '../../widgets/buttons/iconButton' +import Button from '../../widgets/buttons/button' + +import CloseIcon from '../../widgets/icons/close' + +import {useAppSelector, useAppDispatch} from '../../store/hooks' +import octoClient from '../../octoClient' +import {IUser, UserConfigPatch} from '../../user' +import {getMe, patchProps, getCloudMessageCanceled} from '../../store/users' + +import CompassIcon from '../../widgets/icons/compassIcon' +import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../telemetry/telemetryClient' + +import './cloudMessage.scss' +const signupURL = 'https://mattermost.com/pricing' +const displayAfter = (1000 * 60 * 60 * 24) //24 hours + +const CloudMessage = React.memo(() => { + const intl = useIntl() + const dispatch = useAppDispatch() + const me = useAppSelector(getMe) + const cloudMessageCanceled = useAppSelector(getCloudMessageCanceled) + + const closeDialogText = intl.formatMessage({ + id: 'Dialog.closeDialog', + defaultMessage: 'Close dialog', + }) + + const onClose = async () => { + if (me) { + const patch: UserConfigPatch = { + updatedFields: { + focalboard_cloudMessageCanceled: 'true', + }, + } + + const patchedProps = await octoClient.patchUserConfig(me.id, patch) + if (patchedProps) { + dispatch(patchProps(patchedProps)) + } + } + } + + if (Utils.isFocalboardPlugin() || cloudMessageCanceled) { + return null + } + + if (me) { + const installTime = Date.now() - me.create_at + if (installTime < displayAfter) { + return null + } + } + + return ( +
+
+ + + + + +
+ + } + title={closeDialogText} + size='small' + /> +
+ ) +}) +export default CloudMessage diff --git a/webapp/src/pages/boardPage/boardPage.tsx b/webapp/src/pages/boardPage/boardPage.tsx index ac27c21dc..198c68604 100644 --- a/webapp/src/pages/boardPage/boardPage.tsx +++ b/webapp/src/pages/boardPage/boardPage.tsx @@ -5,6 +5,7 @@ import {FormattedMessage, useIntl} from 'react-intl' import {useRouteMatch} from 'react-router-dom' import Workspace from '../../components/workspace' +import CloudMessage from '../../components/messages/cloudMessage' import octoClient from '../../octoClient' import {Utils} from '../../utils' import wsClient from '../../wsclient' @@ -59,7 +60,6 @@ const BoardPage = (props: Props): JSX.Element => { if (!me) { return } - dispatch(fetchUserBlockSubscriptions(me!.id)) }, [me?.id]) } @@ -143,6 +143,7 @@ const BoardPage = (props: Props): JSX.Element => { readonly={props.readonly || false} loadAction={loadAction} /> + {!mobileWarningClosed &&
diff --git a/webapp/src/store/users.ts b/webapp/src/store/users.ts index 6983ec226..0ad2c4e40 100644 --- a/webapp/src/store/users.ts +++ b/webapp/src/store/users.ts @@ -141,3 +141,13 @@ export const getOnboardingTourCategory = createSelector( getMe, (me): string => (me ? me.props?.focalboard_tourCategory : ''), ) + +export const getCloudMessageCanceled = createSelector( + getMe, + (me): boolean => { + if (!me) { + return false + } + return Boolean(me.props?.focalboard_cloudMessageCanceled) + }, +) diff --git a/webapp/src/telemetry/telemetryClient.ts b/webapp/src/telemetry/telemetryClient.ts index 68bc26de5..a562d7e74 100644 --- a/webapp/src/telemetry/telemetryClient.ts +++ b/webapp/src/telemetry/telemetryClient.ts @@ -43,6 +43,7 @@ export const TelemetryActions = { ExportArchive: 'settings_exportArchive', StartTour: 'welcomeScreen_startTour', SkipTour: 'welcomeScreen_skipTour', + CloudMoreInfo: 'cloud_more_info', } interface IEventProps { diff --git a/webapp/src/widgets/buttons/iconButton.scss b/webapp/src/widgets/buttons/iconButton.scss index f9953a04c..fd5809fa6 100644 --- a/webapp/src/widgets/buttons/iconButton.scss +++ b/webapp/src/widgets/buttons/iconButton.scss @@ -1,4 +1,5 @@ .IconButton { + cursor: pointer; border-radius: 4px; height: 24px; width: 24px;