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`] = `
+
+
+
+
+ Get your own free cloud server.
+
+
+
+
+
+`;
+
+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;