You've already forked focalboard
mirror of
https://github.com/mattermost/focalboard.git
synced 2025-07-03 23:30:29 +02:00
Merge pull request #3046 from sbishel/personal-server-advertising
[Main] Add Cloud Message to Personal server/desktop
This commit is contained in:
@ -1045,12 +1045,13 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||||
|
|
||||||
if userID == model.SingleUser {
|
if userID == model.SingleUser {
|
||||||
|
ws, _ := a.app.GetRootTeam()
|
||||||
now := utils.GetMillis()
|
now := utils.GetMillis()
|
||||||
user = &model.User{
|
user = &model.User{
|
||||||
ID: model.SingleUser,
|
ID: model.SingleUser,
|
||||||
Username: model.SingleUser,
|
Username: model.SingleUser,
|
||||||
Email: model.SingleUser,
|
Email: model.SingleUser,
|
||||||
CreateAt: now,
|
CreateAt: ws.UpdateAt,
|
||||||
UpdateAt: now,
|
UpdateAt: now,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`components/messages/CloudMessage not plugin mode, close message 1`] = `<div />`;
|
||||||
|
|
||||||
|
exports[`components/messages/CloudMessage not plugin mode, show message, close message 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="CloudMessage"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="banner"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-information-outline CompassIcon"
|
||||||
|
/>
|
||||||
|
Get your own free cloud server.
|
||||||
|
<button
|
||||||
|
title="Learn more"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Learn more
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label="Close dialog"
|
||||||
|
title="Close dialog"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-close CloseIcon"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/messages/CloudMessage plugin mode, no display 1`] = `<div />`;
|
38
webapp/src/components/messages/cloudMessage.scss
Normal file
38
webapp/src/components/messages/cloudMessage.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
130
webapp/src/components/messages/cloudMessage.test.tsx
Normal file
130
webapp/src/components/messages/cloudMessage.test.tsx
Normal file
@ -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(
|
||||||
|
<ReduxProvider store={store}>
|
||||||
|
<CloudMessage/>
|
||||||
|
</ReduxProvider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
<ReduxProvider store={store}>
|
||||||
|
<CloudMessage/>
|
||||||
|
</ReduxProvider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
<ReduxProvider store={store}>
|
||||||
|
<CloudMessage/>
|
||||||
|
</ReduxProvider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
101
webapp/src/components/messages/cloudMessage.tsx
Normal file
101
webapp/src/components/messages/cloudMessage.tsx
Normal file
@ -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<IUser|null>(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 (
|
||||||
|
<div className='CloudMessage'>
|
||||||
|
<div className='banner'>
|
||||||
|
<CompassIcon
|
||||||
|
icon='information-outline'
|
||||||
|
className='CompassIcon'
|
||||||
|
/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='CloudMessage.cloud-server'
|
||||||
|
defaultMessage='Get your own free cloud server.'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
title='Learn more'
|
||||||
|
size='xsmall'
|
||||||
|
emphasis='primary'
|
||||||
|
onClick={() => {
|
||||||
|
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.CloudMoreInfo)
|
||||||
|
window.open(signupURL)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='cloudMessage.learn-more'
|
||||||
|
defaultMessage='Learn more'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
className='margin-right'
|
||||||
|
onClick={onClose}
|
||||||
|
icon={<CloseIcon/>}
|
||||||
|
title={closeDialogText}
|
||||||
|
size='small'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
export default CloudMessage
|
@ -5,6 +5,7 @@ import {FormattedMessage, useIntl} from 'react-intl'
|
|||||||
import {useRouteMatch} from 'react-router-dom'
|
import {useRouteMatch} from 'react-router-dom'
|
||||||
|
|
||||||
import Workspace from '../../components/workspace'
|
import Workspace from '../../components/workspace'
|
||||||
|
import CloudMessage from '../../components/messages/cloudMessage'
|
||||||
import octoClient from '../../octoClient'
|
import octoClient from '../../octoClient'
|
||||||
import {Utils} from '../../utils'
|
import {Utils} from '../../utils'
|
||||||
import wsClient from '../../wsclient'
|
import wsClient from '../../wsclient'
|
||||||
@ -59,7 +60,6 @@ const BoardPage = (props: Props): JSX.Element => {
|
|||||||
if (!me) {
|
if (!me) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(fetchUserBlockSubscriptions(me!.id))
|
dispatch(fetchUserBlockSubscriptions(me!.id))
|
||||||
}, [me?.id])
|
}, [me?.id])
|
||||||
}
|
}
|
||||||
@ -143,6 +143,7 @@ const BoardPage = (props: Props): JSX.Element => {
|
|||||||
readonly={props.readonly || false}
|
readonly={props.readonly || false}
|
||||||
loadAction={loadAction}
|
loadAction={loadAction}
|
||||||
/>
|
/>
|
||||||
|
<CloudMessage/>
|
||||||
|
|
||||||
{!mobileWarningClosed &&
|
{!mobileWarningClosed &&
|
||||||
<div className='mobileWarning'>
|
<div className='mobileWarning'>
|
||||||
|
@ -141,3 +141,13 @@ export const getOnboardingTourCategory = createSelector(
|
|||||||
getMe,
|
getMe,
|
||||||
(me): string => (me ? me.props?.focalboard_tourCategory : ''),
|
(me): string => (me ? me.props?.focalboard_tourCategory : ''),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const getCloudMessageCanceled = createSelector(
|
||||||
|
getMe,
|
||||||
|
(me): boolean => {
|
||||||
|
if (!me) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return Boolean(me.props?.focalboard_cloudMessageCanceled)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@ -43,6 +43,7 @@ export const TelemetryActions = {
|
|||||||
ExportArchive: 'settings_exportArchive',
|
ExportArchive: 'settings_exportArchive',
|
||||||
StartTour: 'welcomeScreen_startTour',
|
StartTour: 'welcomeScreen_startTour',
|
||||||
SkipTour: 'welcomeScreen_skipTour',
|
SkipTour: 'welcomeScreen_skipTour',
|
||||||
|
CloudMoreInfo: 'cloud_more_info',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEventProps {
|
interface IEventProps {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
.IconButton {
|
.IconButton {
|
||||||
|
cursor: pointer;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
Reference in New Issue
Block a user