MM-25003_Improve Onboarding screens for the desktop app - Intro Screen (#2220)

This commit is contained in:
Julian Mondragón 2022-08-16 12:33:03 -05:00 committed by GitHub
parent d4282d965e
commit faf2dae74b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1815 additions and 42 deletions

View file

@ -7,6 +7,7 @@
const robot = require('robotjs');
const env = require('../../modules/environment');
const {asyncSleep} = require('../../modules/utils');
describe('startup/app', function desc() {
this.timeout(30000);
@ -15,6 +16,11 @@ describe('startup/app', function desc() {
env.createTestUserDataDir();
env.cleanTestConfig();
this.app = await env.getApp();
// Skip welcome screen modal
const welcomeScreenModal = this.app.windows().find((window) => window.url().includes('welcomeScreen'));
welcomeScreenModal.click('.WelcomeScreen .WelcomeScreen__button');
await asyncSleep(500);
});
afterEach(async () => {
@ -41,10 +47,10 @@ describe('startup/app', function desc() {
existingModal.should.not.be.null;
});
it('MM-T4399_2 should show no servers configured in dropdown when no servers exist', async () => {
it('MM-T4985 should show app name in title bar when no servers exist', async () => {
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
const dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton');
dropdownButtonText.should.equal('No servers configured');
const titleBarText = await mainWindow.innerText('.app-title');
titleBarText.should.equal('Mattermost');
});
it('MM-T4400 should be stopped when the app instance already exists', (done) => {
@ -62,4 +68,20 @@ describe('startup/app', function desc() {
done(new Error('Second app instance exists'));
});
});
it('MM-T4975 should show the welcome screen modal when no servers exist', async () => {
if (this.app) {
await this.app.close();
}
await env.clearElectronInstances();
env.createTestUserDataDir();
env.cleanTestConfig();
this.app = await env.getApp();
await asyncSleep(500);
const welcomeScreenModal = this.app.windows().find((window) => window.url().includes('welcomeScreen'));
const modalButton = await welcomeScreenModal.innerText('.WelcomeScreen .WelcomeScreen__button');
modalButton.should.equal('Get Started');
});
});

View file

@ -0,0 +1,142 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
const env = require('../../modules/environment');
const {asyncSleep} = require('../../modules/utils');
describe('Welcome Screen Modal', function desc() {
this.timeout(30000);
beforeEach(async () => {
env.createTestUserDataDir();
env.cleanTestConfig();
await asyncSleep(1000);
this.app = await env.getApp();
welcomeScreenModal = this.app.windows().find((window) => window.url().includes('welcomeScreen'));
});
afterEach(async () => {
if (this.app) {
await this.app.close();
}
await env.clearElectronInstances();
});
let welcomeScreenModal;
it('MM-T4976 should show the slides in the expected order', async () => {
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
welcomeSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#nextCarouselButton');
const channelSlideClass = await welcomeScreenModal.getAttribute('#channels', 'class');
channelSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#nextCarouselButton');
const playbooksSlideClass = await welcomeScreenModal.getAttribute('#playbooks', 'class');
playbooksSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#nextCarouselButton');
const boardsSlideClass = await welcomeScreenModal.getAttribute('#boards', 'class');
boardsSlideClass.should.contain('Carousel__slide-current');
});
it('MM-T4977 should be able to move through slides clicking the navigation buttons', async () => {
let welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
welcomeSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#nextCarouselButton');
const channelSlideClass = await welcomeScreenModal.getAttribute('#channels', 'class');
channelSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#prevCarouselButton');
welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
welcomeSlideClass.should.contain('Carousel__slide-current');
});
it('MM-T4978 should be able to move through slides clicking the pagination indicator', async () => {
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
welcomeSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#PaginationIndicator3');
const boardsSlideClass = await welcomeScreenModal.getAttribute('#boards', 'class');
boardsSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#PaginationIndicator2');
const playbooksSlideClass = await welcomeScreenModal.getAttribute('#playbooks', 'class');
playbooksSlideClass.should.contain('Carousel__slide-current');
});
it('MM-T4979 should be able to move forward through slides automatically every 5 seconds', async () => {
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
welcomeSlideClass.should.contain('Carousel__slide-current');
await asyncSleep(5500);
const channelSlideClass = await welcomeScreenModal.getAttribute('#channels', 'class');
channelSlideClass.should.contain('Carousel__slide-current');
});
it('MM-T4980 should show the slides in the expected order', async () => {
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
welcomeSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#nextCarouselButton');
const channelSlideClass = await welcomeScreenModal.getAttribute('#channels', 'class');
channelSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#nextCarouselButton');
const playbooksSlideClass = await welcomeScreenModal.getAttribute('#playbooks', 'class');
playbooksSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#nextCarouselButton');
const boardsSlideClass = await welcomeScreenModal.getAttribute('#boards', 'class');
boardsSlideClass.should.contain('Carousel__slide-current');
});
it('MM-T4981 should be able to move from last to first slide', async () => {
await welcomeScreenModal.click('#PaginationIndicator3');
const boardsSlideClass = await welcomeScreenModal.getAttribute('#boards', 'class');
boardsSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#nextCarouselButton');
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
welcomeSlideClass.should.contain('Carousel__slide-current');
});
it('MM-T4982 should be able to move from first to last slide', async () => {
const welcomeSlideClass = await welcomeScreenModal.getAttribute('#welcome', 'class');
welcomeSlideClass.should.contain('Carousel__slide-current');
await welcomeScreenModal.click('#prevCarouselButton');
const boardsSlideClass = await welcomeScreenModal.getAttribute('#boards', 'class');
boardsSlideClass.should.contain('Carousel__slide-current');
});
it('MM-T4983 should be able to click the get started button and be redirected to new server modal', async () => {
await welcomeScreenModal.click('#getStartedWelcomeScreen');
const newServerModal = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('newServer'),
});
const modalTitle = await newServerModal.innerText('#newServerModal .modal-title');
modalTitle.should.equal('Add Server');
});
});

View file

@ -1,7 +1,6 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console,consistent-return */
const fs = require('fs');

View file

@ -1,7 +1,6 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console */
// See reference: https://support.smartbear.com/tm4j-cloud/api-docs/

View file

@ -131,6 +131,7 @@
"renderer.components.extraBar.back": "Back",
"renderer.components.mainPage.contextMenu.ariaLabel": "Context menu",
"renderer.components.mainPage.downloadingUpdate": "Downloading update. {percentDone}% of {total} @ {speed}/s",
"renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.mainPage.updateAvailable": "Update available",
"renderer.components.mainPage.updateReady": "Update ready to install",
"renderer.components.newTeamModal.error.nameRequired": "Name is required.",
@ -213,6 +214,15 @@
"renderer.components.showCertificateModal.serialNumber": "Serial Number",
"renderer.components.showCertificateModal.subjectName": "Subject Name",
"renderer.components.teamDropdownButton.noServersConfigured": "No servers configured",
"renderer.components.welcomeScreen.button.getStarted": "Get Started",
"renderer.components.welcomeScreen.slides.boards.subtitle": "Ship on time, every time, with a project and task management solution built for digital operations.",
"renderer.components.welcomeScreen.slides.boards.title": "Boards",
"renderer.components.welcomeScreen.slides.channels.subtitle": "All of your teams communication in one place.<br></br>Secure collaboration, built for developers.",
"renderer.components.welcomeScreen.slides.channels.title": "Channels",
"renderer.components.welcomeScreen.slides.palybooks.subtitle": "Move faster and make fewer mistakes with checklists, automations, and tool integrations that power your teams workflows.",
"renderer.components.welcomeScreen.slides.playbooks.title": "Playbooks",
"renderer.components.welcomeScreen.slides.welcome.subtitle": "Mattermost is an open source platform for developer collaboration. Secure, flexible, and integrated with the tools you love.",
"renderer.components.welcomeScreen.slides.welcome.title": "Welcome",
"renderer.dropdown.addAServer": "Add a server",
"renderer.dropdown.servers": "Servers",
"renderer.modals.certificate.certificateModal.certInfoButton": "Certificate Information",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -35,6 +35,7 @@ export const DOUBLE_CLICK_ON_WINDOW = 'double_click';
export const SHOW_NEW_SERVER_MODAL = 'show_new_server_modal';
export const SHOW_EDIT_SERVER_MODAL = 'show-edit-server-modal';
export const SHOW_REMOVE_SERVER_MODAL = 'show-remove-server-modal';
export const MAIN_WINDOW_SHOWN = 'main-window-shown';
export const RETRIEVE_MODAL_INFO = 'retrieve-modal-info';
export const MODAL_INFO = 'modal-info';

View file

@ -55,7 +55,7 @@ function parseArgs(args: string[]) {
// build. As such, we provide the version manually.
version(app.getVersion()).
help('help').
parse(args);
parse(args) as Args;
}
function validateArgs(args: Args) {

View file

@ -7,7 +7,7 @@ import {RELOAD_CONFIGURATION} from 'common/communication';
import Config from 'common/config';
import {handleConfigUpdate} from 'main/app/config';
import {addNewServerModalWhenMainWindowIsShown} from 'main/app/intercom';
import {handleMainWindowIsShown} from 'main/app/intercom';
import {setLoggingLevel} from 'main/app/utils';
import WindowManager from 'main/windows/windowManager';
@ -32,7 +32,7 @@ jest.mock('main/app/utils', () => ({
setLoggingLevel: jest.fn(),
}));
jest.mock('main/app/intercom', () => ({
addNewServerModalWhenMainWindowIsShown: jest.fn(),
handleMainWindowIsShown: jest.fn(),
}));
jest.mock('main/AutoLauncher', () => ({
enable: jest.fn(),
@ -98,7 +98,7 @@ describe('main/app/config', () => {
Config.registryConfigData = {};
handleConfigUpdate({teams: []});
expect(addNewServerModalWhenMainWindowIsShown).toHaveBeenCalled();
expect(handleMainWindowIsShown).toHaveBeenCalled();
Object.defineProperty(process, 'platform', {
value: originalPlatform,

View file

@ -14,7 +14,7 @@ import {setUnreadBadgeSetting} from 'main/badge';
import {refreshTrayImages} from 'main/tray/tray';
import WindowManager from 'main/windows/windowManager';
import {addNewServerModalWhenMainWindowIsShown} from './intercom';
import {handleMainWindowIsShown} from './intercom';
import {handleUpdateMenuEvent, setLoggingLevel, updateServerInfos, updateSpellCheckerLocales} from './utils';
let didCheckForAddServerModal = false;
@ -61,7 +61,7 @@ export function handleConfigUpdate(newConfig: CombinedConfig) {
updateServerInfos(newConfig.teams);
WindowManager.initializeCurrentServerName();
if (newConfig.teams.length === 0) {
addNewServerModalWhenMainWindowIsShown();
handleMainWindowIsShown();
}
}

View file

@ -98,7 +98,7 @@ jest.mock('main/app/config', () => ({
handleConfigUpdate: jest.fn(),
}));
jest.mock('main/app/intercom', () => ({
addNewServerModalWhenMainWindowIsShown: jest.fn(),
handleMainWindowIsShown: jest.fn(),
}));
jest.mock('main/app/utils', () => ({
clearAppCache: jest.fn(),

View file

@ -34,6 +34,7 @@ import {
START_UPGRADE,
START_DOWNLOAD,
PING_DOMAIN,
MAIN_WINDOW_SHOWN,
} from 'common/communication';
import Config from 'common/config';
import urlUtils from 'common/utils/url';
@ -68,7 +69,7 @@ import {
} from './app';
import {handleConfigUpdate, handleDarkModeChange} from './config';
import {
addNewServerModalWhenMainWindowIsShown,
handleMainWindowIsShown,
handleAppVersion,
handleCloseTab,
handleEditServerModal,
@ -247,6 +248,7 @@ function initializeInterCommunicationEventListeners() {
ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal);
ipcMain.on(SHOW_EDIT_SERVER_MODAL, handleEditServerModal);
ipcMain.on(SHOW_REMOVE_SERVER_MODAL, handleRemoveServerModal);
ipcMain.on(MAIN_WINDOW_SHOWN, handleMainWindowIsShown);
ipcMain.on(WINDOW_CLOSE, WindowManager.close);
ipcMain.on(WINDOW_MAXIMIZE, WindowManager.maximize);
ipcMain.on(WINDOW_MINIMIZE, WindowManager.minimize);
@ -423,7 +425,7 @@ function initializeAfterAppReady() {
// only check for non-Windows, as with Windows we have to wait for GPO teams
if (process.platform !== 'win32' || typeof Config.registryConfigData !== 'undefined') {
if (Config.teams.length === 0) {
addNewServerModalWhenMainWindowIsShown();
handleMainWindowIsShown();
}
}
}

View file

@ -14,6 +14,7 @@ import {
handleNewServerModal,
handleEditServerModal,
handleRemoveServerModal,
handleWelcomeScreenModal,
} from './intercom';
jest.mock('common/config', () => ({
@ -235,4 +236,24 @@ describe('main/app/intercom', () => {
}));
});
});
describe('handleWelcomeScreenModal', () => {
beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js');
WindowManager.getMainWindow.mockReturnValue({});
Config.set.mockImplementation((name, value) => {
Config[name] = value;
});
});
it('should show welcomeScreen modal', async () => {
const promise = Promise.resolve({});
ModalManager.addModal.mockReturnValue(promise);
handleWelcomeScreenModal();
expect(ModalManager.addModal).toHaveBeenCalledWith('welcomeScreen', '/some/index.html', '/some/preload.js', {}, {}, true);
});
});
});

View file

@ -85,15 +85,26 @@ export function handleOpenTab(event: IpcMainEvent, serverName: string, tabName:
Config.set('teams', teams);
}
export function addNewServerModalWhenMainWindowIsShown() {
export function handleMainWindowIsShown() {
const showWelcomeScreen = !Config.teams.length;
const mainWindow = WindowManager.getMainWindow();
if (mainWindow) {
if (mainWindow.isVisible()) {
handleNewServerModal();
if (showWelcomeScreen) {
handleWelcomeScreenModal();
} else {
handleNewServerModal();
}
} else {
mainWindow.once('show', () => {
log.debug('Intercom.addNewServerModalWhenMainWindowIsShown.show');
handleNewServerModal();
if (showWelcomeScreen) {
log.debug('Intercom.handleMainWindowIsShown.show.welcomeScreenModal');
handleWelcomeScreenModal();
} else {
log.debug('Intercom.handleMainWindowIsShown.show.newServerModal');
handleNewServerModal();
}
});
}
}
@ -213,6 +224,32 @@ export function handleRemoveServerModal(e: IpcMainEvent, name: string) {
}
}
export function handleWelcomeScreenModal() {
log.debug('Intercom.handleWelcomeScreenModal');
const html = getLocalURLString('welcomeScreen.html');
const modalPreload = getLocalPreload('modalPreload.js');
const mainWindow = WindowManager.getMainWindow();
if (!mainWindow) {
return;
}
const modalPromise = ModalManager.addModal('welcomeScreen', html, modalPreload, {}, mainWindow, true);
if (modalPromise) {
modalPromise.then(() => {
handleNewServerModal();
}).catch((e) => {
// e is undefined for user cancellation
if (e) {
log.error(`there was an error in the welcome screen modal: ${e}`);
}
});
} else {
log.warn('There is already a welcome screen modal');
}
}
export function handleMentionNotification(event: IpcMainEvent, title: string, body: string, channel: {id: string}, teamId: string, url: string, silent: boolean, data: MentionData) {
log.debug('Intercom.handleMentionNotification', {title, body, channel, teamId, url, silent, data});
displayMention(title, body, channel, teamId, url, silent, event.sender, data);

View file

@ -7,7 +7,7 @@
import {dialog, ipcMain} from 'electron';
import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill';
import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, SHOW_NEW_SERVER_MODAL} from 'common/communication';
import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, MAIN_WINDOW_SHOWN} from 'common/communication';
import {MattermostServer} from 'common/servers/MattermostServer';
import {getServerView, getTabViewName} from 'common/tabs/TabView';
import urlUtils from 'common/utils/url';
@ -547,7 +547,7 @@ describe('main/views/viewManager', () => {
};
viewManager.getServers = () => [];
viewManager.showInitial();
expect(ipcMain.emit).toHaveBeenCalledWith(SHOW_NEW_SERVER_MODAL);
expect(ipcMain.emit).toHaveBeenCalledWith(MAIN_WINDOW_SHOWN);
});
});

View file

@ -20,7 +20,7 @@ import {
BROWSER_HISTORY_PUSH,
UPDATE_LAST_ACTIVE,
UPDATE_URL_VIEW_WIDTH,
SHOW_NEW_SERVER_MODAL,
MAIN_WINDOW_SHOWN,
} from 'common/communication';
import Config from 'common/config';
import urlUtils from 'common/utils/url';
@ -215,7 +215,7 @@ export class ViewManager {
}
} else {
this.mainWindow.webContents.send(SET_ACTIVE_VIEW, null, null);
ipcMain.emit(SHOW_NEW_SERVER_MODAL);
ipcMain.emit(MAIN_WINDOW_SHOWN);
}
}

View file

@ -217,7 +217,17 @@ describe('main/windows/windowManager', () => {
});
it('should use getSize when the platform is linux', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'linux',
});
windowManager.handleResizeMainWindow();
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
expect(view.setBounds).not.toHaveBeenCalled();
jest.runAllTimers();
expect(view.setBounds).toHaveBeenCalledWith({width: 1000, height: 900});
@ -281,6 +291,76 @@ describe('main/windows/windowManager', () => {
});
});
describe('handleResizedMainWindow', () => {
const windowManager = new WindowManager();
const view = {
setBounds: jest.fn(),
tab: {
url: 'http://server-1.com',
},
view: {
webContents: {
getURL: jest.fn(),
},
},
};
windowManager.mainWindow = {
getContentBounds: () => ({width: 800, height: 600}),
getSize: () => [1000, 900],
};
beforeEach(() => {
getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height}));
});
afterEach(() => {
windowManager.isResizing = true;
jest.resetAllMocks();
});
it('should not handle bounds if no window available', () => {
windowManager.handleResizedMainWindow();
expect(windowManager.isResizing).toBe(false);
expect(view.setBounds).not.toHaveBeenCalled();
});
it('should use getContentBounds when the platform is different to linux', () => {
windowManager.viewManager = {
getCurrentView: () => view,
};
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'windows',
});
windowManager.handleResizedMainWindow();
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
expect(windowManager.isResizing).toBe(false);
expect(view.setBounds).toHaveBeenCalledWith({width: 800, height: 600});
});
it('should use getSize when the platform is linux', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'linux',
});
windowManager.handleResizedMainWindow();
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
expect(windowManager.isResizing).toBe(false);
expect(view.setBounds).toHaveBeenCalledWith({width: 1000, height: 900});
});
});
describe('restoreMain', () => {
const windowManager = new WindowManager();
windowManager.mainWindow = {

View file

@ -203,8 +203,12 @@ export class WindowManager {
}
handleResizedMainWindow = () => {
log.silly('WindowManager.handleResizedMainWindow');
if (this.mainWindow) {
this.throttledWillResize(this.mainWindow?.getContentBounds());
const bounds = this.getBounds();
this.throttledWillResize(bounds);
ipcMain.emit(RESIZE_MODAL, null, bounds);
}
this.isResizing = false;
}
@ -228,17 +232,7 @@ export class WindowManager {
return;
}
let bounds;
// Workaround for linux maximizing/minimizing, which doesn't work properly because of these bugs:
// https://github.com/electron/electron/issues/28699
// https://github.com/electron/electron/issues/28106
if (process.platform === 'linux') {
const size = this.mainWindow.getSize();
bounds = {width: size[0], height: size[1]};
} else {
bounds = this.mainWindow.getContentBounds();
}
const bounds = this.getBounds();
// Another workaround since the window doesn't update p roperly under Linux for some reason
// See above comment
@ -261,6 +255,24 @@ export class WindowManager {
currentView.setBounds(bounds);
};
private getBounds = () => {
let bounds;
if (this.mainWindow) {
// Workaround for linux maximizing/minimizing, which doesn't work properly because of these bugs:
// https://github.com/electron/electron/issues/28699
// https://github.com/electron/electron/issues/28106
if (process.platform === 'linux') {
const size = this.mainWindow.getSize();
bounds = {width: size[0], height: size[1]};
} else {
bounds = this.mainWindow.getContentBounds();
}
}
return bounds as Electron.Rectangle;
}
// max retries allows the message to get to the renderer even if it is sent while the app is starting up.
sendToRendererWithRetry = (maxRetries: number, channel: string, ...args: any[]) => {
if (!this.mainWindow || !this.mainWindowReady) {

View file

@ -0,0 +1,20 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M59.2508 59.3145C69.9273 48.5729 83.3739 43.0356 99.5903 42.7026C115.652 43.0434 129.02 48.5807 139.697 59.3145C150.373 70.0483 155.96 83.4308 156.457 99.4618C155.952 115.477 150.366 128.86 139.697 139.609C129.028 150.359 115.659 155.907 99.5903 156.256C83.3661 155.923 69.9196 150.389 59.2508 139.656C48.5821 128.922 42.9956 115.539 42.4912 99.5083C42.98 83.4463 48.5666 70.0483 59.2508 59.3145Z"
fill="white" />
<path
d="M99.5899 161.17C82.0467 160.674 67.4402 154.642 55.7705 143.071C44.1009 131.501 38.0993 116.965 37.7656 99.4622C38.0993 82.1068 44.1009 67.6092 55.7705 55.9693C67.4402 44.3293 82.0467 38.2615 99.5899 37.7659C116.978 38.2538 131.503 44.3254 143.165 55.9809C154.827 67.6363 160.828 82.1339 161.17 99.4738C160.836 116.984 154.834 131.52 143.165 143.083C131.495 154.645 116.97 160.674 99.5899 161.17ZM99.5899 49.3942C85.352 49.8898 73.5194 54.8076 64.0922 64.1474C54.6649 73.4872 49.7806 85.2588 49.4392 99.4622C49.7651 113.666 54.6494 125.476 64.0922 134.893C73.535 144.31 85.3675 149.186 99.5899 149.519C113.657 149.186 125.412 144.31 134.855 134.893C144.298 125.476 149.178 113.666 149.496 99.4622C149.17 85.2511 144.29 73.4795 134.855 64.1474C125.42 54.8153 113.665 49.8937 99.5899 49.3826V49.3942Z"
fill="#FFBC1F" />
<path
d="M99.5902 136.635C88.9991 136.295 80.2275 132.701 73.2754 125.855C66.3232 119.009 62.6803 110.211 62.3467 99.462C62.3467 89.0535 65.7839 80.3797 72.6585 73.4406C79.533 66.5016 88.4289 62.7842 99.3459 62.2886C109.937 62.4512 118.751 66.0447 125.789 73.0689C132.826 80.0931 136.426 88.8908 136.589 99.462C136.426 110.033 132.826 118.788 125.789 125.727C118.816 132.636 109.414 136.55 99.5902 136.635ZM100.091 74.9276H99.5902C92.3045 75.2528 86.3455 77.6885 81.7133 82.2345C79.4225 84.4818 77.6392 87.1919 76.4831 90.1831C75.3269 93.1742 74.8245 96.3775 75.0095 99.5782C74.9866 102.805 75.6125 106.003 76.8499 108.984C78.0874 111.966 79.9112 114.669 82.2137 116.934C85.066 119.795 88.5792 121.913 92.4441 123.1C96.309 124.288 100.407 124.51 104.378 123.745C108.349 122.981 112.071 121.254 115.216 118.717C118.361 116.179 120.833 112.909 122.414 109.194C123.995 105.479 124.638 101.433 124.285 97.4125C123.931 93.3916 122.593 89.519 120.388 86.1354C118.183 82.7518 115.179 79.9611 111.64 78.0087C108.1 76.0564 104.134 75.0023 100.091 74.9392V74.9276Z"
fill="#FFBC1F" />
<path
d="M92.2699 92.3644C94.215 90.5278 96.7909 89.5044 99.4685 89.5044C102.146 89.5044 104.722 90.5278 106.667 92.3644C108.504 94.2582 109.532 96.7911 109.532 99.4273C109.532 102.064 108.504 104.597 106.667 106.49C104.722 108.327 102.146 109.35 99.4685 109.35C96.7909 109.35 94.215 108.327 92.2699 106.49C90.4406 104.592 89.4189 102.061 89.4189 99.4273C89.4189 96.7937 90.4406 94.2624 92.2699 92.3644Z"
fill="#FFBC1F" />
<path
d="M100.509 100.938C100.148 101.297 99.6594 101.499 99.1496 101.499C98.6398 101.499 98.151 101.297 97.7905 100.938C97.43 100.578 97.2275 100.091 97.2275 99.5825C97.2275 99.0742 97.43 98.5866 97.7905 98.2272L105.448 90.603L107.912 93.3136L100.509 100.938Z"
fill="#818698" />
<path
d="M161.655 55.6199C162.48 60.2336 154.665 63.6939 138.208 66.0008L114.274 92.8181C113.75 93.3673 113.117 93.8033 112.417 94.099C111.717 94.3946 110.963 94.5436 110.202 94.5367C108.63 94.5388 107.121 93.921 106.003 92.8181C105.421 92.3133 104.954 91.6895 104.634 90.9889C104.315 90.2884 104.151 89.5275 104.152 88.758C104.17 87.1601 104.785 85.6261 105.876 84.4557L132.782 60.8334C135.096 44.4316 138.566 36.596 143.193 37.3265C147.82 38.057 148.229 43.8242 144.419 54.628C155.093 50.6986 160.838 51.0293 161.655 55.6199Z"
fill="#1C58D9" />
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,24 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1023_92269)">
<path
d="M100.489 58.9117H152.495C154.053 58.9006 155.597 59.1999 157.04 59.7925C158.483 60.3851 159.795 61.2593 160.902 62.3648C162.009 63.4704 162.889 64.7857 163.491 66.2351C164.093 67.6845 164.405 69.2395 164.41 70.8109V125.223C164.403 126.793 164.09 128.347 163.487 129.795C162.885 131.243 162.005 132.557 160.898 133.661C159.791 134.765 158.479 135.639 157.037 136.23C155.595 136.822 154.052 137.121 152.495 137.11H139.985V157.543L121.196 137.11H100.489C98.932 137.121 97.3884 136.822 95.9465 136.23C94.5046 135.639 93.1925 134.765 92.0857 133.661C90.9789 132.557 90.099 131.243 89.4964 129.795C88.8938 128.347 88.5805 126.793 88.5742 125.223V70.8109C88.5789 69.2395 88.8911 67.6845 89.493 66.2351C90.0949 64.7857 90.9745 63.4704 92.0815 62.3648C93.1885 61.2593 94.501 60.3851 95.9437 59.7925C97.3864 59.1999 98.931 58.9006 100.489 58.9117Z"
fill="#1C58D9" />
<path
d="M132.289 42.5533H47.4584C45.9007 42.5422 44.3562 42.8415 42.9135 43.4341C41.4708 44.0267 40.1582 44.9009 39.0513 46.0064C37.9443 47.112 37.0646 48.4273 36.4627 49.8767C35.8608 51.3261 35.5486 52.8811 35.5439 54.4525V108.816C35.5502 110.387 35.8636 111.94 36.4661 113.389C37.0687 114.837 37.9486 116.15 39.0555 117.255C40.1623 118.359 41.4743 119.232 42.9162 119.824C44.3581 120.416 45.9017 120.715 47.4584 120.704H59.9804V141.137L78.7695 120.704H132.289C133.846 120.715 135.389 120.416 136.831 119.824C138.273 119.232 139.585 118.359 140.692 117.255C141.799 116.15 142.679 114.837 143.281 113.389C143.884 111.94 144.197 110.387 144.203 108.816V54.4525C144.199 52.8811 143.887 51.3261 143.285 49.8767C142.683 48.4273 141.803 47.112 140.696 46.0064C139.589 44.9009 138.277 44.0267 136.834 43.4341C135.391 42.8415 133.847 42.5422 132.289 42.5533Z"
fill="#FFBC1F" />
<path
d="M60.9328 73.8638C62.4443 73.8614 63.9226 74.3115 65.1803 75.1572C66.438 76.0029 67.4187 77.206 67.9983 78.6143C68.5778 80.0226 68.73 81.5727 68.4357 83.0683C68.1414 84.564 67.4138 85.9379 66.345 87.0161C65.2762 88.0943 63.9143 88.8283 62.4318 89.1252C60.9492 89.4221 59.4126 89.2685 58.0166 88.6839C56.6206 88.0993 55.428 87.1099 54.5897 85.8411C53.7515 84.5723 53.3053 83.081 53.3076 81.5562C53.3108 79.5171 54.1152 77.5623 55.5445 76.1204C56.9738 74.6784 58.9115 73.867 60.9328 73.8638Z"
fill="white" />
<path
d="M89.8967 73.8638C91.4082 73.8614 92.8864 74.3115 94.1442 75.1572C95.4019 76.0029 96.3826 77.206 96.9621 78.6143C97.5417 80.0226 97.6939 81.5727 97.3996 83.0683C97.1053 84.564 96.3777 85.9379 95.3089 87.0161C94.2401 88.0943 92.8782 88.8283 91.3957 89.1252C89.9131 89.4221 88.3765 89.2685 86.9805 88.6839C85.5845 88.0993 84.3919 87.1099 83.5536 85.8411C82.7153 84.5723 82.2691 83.081 82.2715 81.5562C82.2746 79.5171 83.079 77.5623 84.5084 76.1204C85.9377 74.6784 87.8753 73.867 89.8967 73.8638Z"
fill="white" />
<path
d="M118.861 73.8638C120.372 73.8614 121.85 74.3115 123.108 75.1572C124.366 76.0029 125.346 77.206 125.926 78.6143C126.506 80.0226 126.658 81.5727 126.363 83.0683C126.069 84.564 125.342 85.9379 124.273 87.0161C123.204 88.0943 121.842 88.8283 120.36 89.1252C118.877 89.4221 117.34 89.2685 115.944 88.6839C114.548 88.0993 113.356 87.1099 112.517 85.8411C111.679 84.5723 111.233 83.081 111.235 81.5562C111.239 79.5171 112.043 77.5623 113.472 76.1204C114.902 74.6784 116.839 73.867 118.861 73.8638Z"
fill="white" />
</g>
<defs>
<clipPath id="clip0_1023_92269">
<rect width="128.723" height="114.894" fill="white" transform="translate(35.6387 42.5532)" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 64 KiB

View file

@ -0,0 +1,22 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M107.7 165.8C104.744 170.325 98.0681 170.977 92.8772 167.313L34.2645 126.034C32.3518 124.743 31.0113 122.764 30.5225 120.509C30.0338 118.254 30.4345 115.897 31.6412 113.93L83.2032 35.1611C84.6257 33.2242 86.6662 31.8304 88.9877 31.2096C91.3092 30.5888 93.773 30.7782 95.9723 31.7466L162.51 61.0196C168.395 63.6152 170.838 69.3476 167.909 73.9419L107.7 165.8Z"
fill="#FFBC1F" />
<path d="M98.3731 160.442L37.9561 119.135L91.1003 37.9648L158.429 68.7091L98.3731 160.442Z" fill="white" />
<path
d="M136.625 62.9489C136.108 63.599 135.382 64.0507 134.571 64.2277C133.759 64.4047 132.911 64.2961 132.17 63.9205L106.757 51.9837C106.428 51.8637 106.13 51.6716 105.885 51.4213C105.64 51.1711 105.454 50.8692 105.341 50.5377C105.227 50.2063 105.19 49.8537 105.231 49.5059C105.272 49.158 105.39 48.8237 105.577 48.5276L109.56 42.4342C110.031 41.8109 110.702 41.3685 111.46 41.1815C112.218 40.9944 113.018 41.0741 113.724 41.4071L139.471 52.8443C139.828 52.9588 140.155 53.1524 140.427 53.4105C140.699 53.6686 140.91 53.9848 141.043 54.3354C141.176 54.6861 141.229 55.0623 141.197 55.4361C141.165 55.8099 141.049 56.1717 140.859 56.4947L136.625 62.9489Z"
fill="#8D93A5" />
<path
d="M131.767 55.0373L115.39 47.5976L119.429 41.421C119.908 40.8095 120.578 40.3752 121.332 40.1867C122.086 39.9982 122.881 40.0662 123.592 40.38L134.488 45.0992C134.841 45.2073 135.166 45.3943 135.437 45.646C135.708 45.8978 135.918 46.2078 136.052 46.5526C136.185 46.8974 136.239 47.2681 136.208 47.6367C136.178 48.0052 136.064 48.362 135.876 48.6803L131.767 55.0373Z"
fill="#1C58D9" />
<path d="M132.961 88.5019L98.9707 71.0547L100.747 68.3342L134.863 85.601L132.961 88.5019Z" fill="#FFBC1F" />
<path
d="M88.255 69.2781C88.1756 69.3296 88.0854 69.3624 87.9914 69.374C87.8974 69.3855 87.802 69.3755 87.7124 69.3447C87.6228 69.314 87.5413 69.2633 87.4742 69.1964C87.4071 69.1295 87.3562 69.0483 87.3251 68.9588L85.2432 62.796C85.2164 62.7036 85.2099 62.6065 85.2242 62.5114C85.2384 62.4162 85.2731 62.3253 85.3258 62.2449C85.3786 62.1644 85.4482 62.0964 85.5297 62.0454C85.6113 61.9944 85.7029 61.9616 85.7984 61.9494L86.5895 61.7828C86.7952 61.7395 87.0096 61.7683 87.1966 61.8643C87.3836 61.9604 87.5319 62.1178 87.6165 62.3103L88.727 65.5304C88.7563 65.6196 88.8057 65.7007 88.8715 65.7677C88.9372 65.8346 89.0175 65.8855 89.1061 65.9163C89.1947 65.9472 89.2892 65.9573 89.3823 65.9457C89.4754 65.9342 89.5646 65.9013 89.643 65.8497L95.9442 62.2964C96.1399 62.1972 96.3629 62.1653 96.5786 62.2054C96.7943 62.2455 96.9909 62.3556 97.1379 62.5185L97.6375 63.1153C97.7038 63.1879 97.7525 63.2746 97.7797 63.3691C97.807 63.4635 97.8121 63.5629 97.7947 63.6596C97.7772 63.7564 97.7377 63.8478 97.6791 63.9267C97.6205 64.0056 97.5446 64.0699 97.4571 64.1147L88.255 69.2781Z"
fill="#FFBC1F" />
<path d="M121.844 105.477L88.5195 87.0168L90.31 84.2825L123.745 102.576L121.844 105.477Z" fill="#BABEC9" />
<path d="M82.1342 87.6553L72.3076 82.1311L77.9704 73.4561L87.922 78.8137L82.1342 87.6553Z" fill="#BABEC9" />
<path d="M110.726 122.466L78.082 102.965L79.8586 100.23L112.628 119.565L110.726 122.466Z" fill="#BABEC9" />
<path d="M71.7807 103.478L62.1484 97.635L67.8251 88.96L77.5823 94.6369L71.7807 103.478Z" fill="#BABEC9" />
<path d="M99.6088 139.442L67.6445 118.913L69.4211 116.193L101.51 136.541L99.6088 139.442Z" fill="#BABEC9" />
<path d="M61.4263 119.288L51.9883 113.139L57.6788 104.478L67.2279 110.446L61.4263 119.288Z" fill="#BABEC9" />
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1,137 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState, useEffect, useRef} from 'react';
import classNames from 'classnames';
import CarouselButton, {ButtonDirection} from './CarouselButton';
import CarouselPaginationIndicator from './CarouselPaginationIndicator';
import 'renderer/css/components/Carousel.scss';
const AUTO_CHANGE_TIME = 5000;
type CarouselProps = {
slides: Array<{key: string; content: React.ReactNode}>;
startIndex?: number;
darkMode?: boolean;
};
function Carousel({
slides,
startIndex = 0,
darkMode = false,
}: CarouselProps) {
const [slideIn, setSlideIn] = useState(startIndex);
const [slideOut, setSlideOut] = useState(NaN);
const [direction, setDirection] = useState(ButtonDirection.NEXT);
const [autoChange, setAutoChange] = useState(true);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const disableNavigation = slides.length <= 1;
useEffect(() => {
timerRef.current = autoChange ? (
setTimeout(() => {
handleOnNextButtonClick(true);
}, AUTO_CHANGE_TIME)
) : null;
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [slideIn, autoChange]);
const handleOnPrevButtonClick = () => {
moveSlide(slideIn - 1);
setDirection(ButtonDirection.PREV);
setAutoChange(false);
};
const handleOnNextButtonClick = (fromAuto?: boolean) => {
moveSlide(slideIn + 1);
setDirection(ButtonDirection.NEXT);
if (!fromAuto) {
setAutoChange(false);
}
};
const handleOnPaginationIndicatorClick = (indicatorIndex: number) => {
moveSlide(indicatorIndex);
setDirection(indicatorIndex > slideIn ? ButtonDirection.NEXT : ButtonDirection.PREV);
setAutoChange(false);
};
const moveSlide = (toIndex: number) => {
if (toIndex === slideIn) {
return;
}
let current = toIndex;
if (toIndex < 0) {
current = slides.length - 1;
} else if (toIndex >= slides.length) {
current = 0;
}
setSlideOut(slideIn);
setSlideIn(current);
};
return (
<div className='Carousel'>
<div className='Carousel__slides'>
{slides.map(({key, content}, slideIndex) => {
const isPrev = slideIndex === slideOut;
const isCurrent = slideIndex === slideIn;
return (
<div
key={key}
id={key}
className={classNames(
'Carousel__slide',
{
'Carousel__slide-current': isCurrent,
inFromRight: isCurrent && direction === ButtonDirection.NEXT,
inFromLeft: isCurrent && direction === ButtonDirection.PREV,
outToLeft: isPrev && direction === ButtonDirection.NEXT,
outToRight: isPrev && direction === ButtonDirection.PREV,
},
)}
>
{content}
</div>
);
})}
</div>
<div className='Carousel__pagination'>
<CarouselButton
direction={ButtonDirection.PREV}
disabled={disableNavigation}
darkMode={darkMode}
onClick={handleOnPrevButtonClick}
/>
<CarouselPaginationIndicator
pages={slides.length}
activePage={slideIn}
disabled={disableNavigation}
darkMode={darkMode}
onClick={handleOnPaginationIndicatorClick}
/>
<CarouselButton
direction={ButtonDirection.NEXT}
disabled={disableNavigation}
darkMode={darkMode}
onClick={handleOnNextButtonClick}
/>
</div>
</div>
);
}
export default Carousel;

View file

@ -0,0 +1,51 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import classNames from 'classnames';
import 'renderer/css/components/Button.scss';
import 'renderer/css/components/CarouselButton.scss';
export enum ButtonDirection {
NEXT = 'next',
PREV = 'prev',
}
type CarouselButtonProps = {
direction: ButtonDirection;
disabled?: boolean;
darkMode?: boolean;
onClick?: () => void;
};
function CarouselButton({
direction = ButtonDirection.NEXT,
disabled = false,
darkMode = false,
onClick = () => null,
}: CarouselButtonProps) {
const handleOnClick = () => {
onClick();
};
return (
<button
id={`${direction}CarouselButton`}
className={classNames(
'CarouselButton',
'icon-button icon-button-small',
{
'icon-button-inverted': darkMode,
disabled,
},
)}
disabled={disabled}
onClick={handleOnClick}
>
<i className={direction === ButtonDirection.PREV ? 'icon-chevron-left' : 'icon-chevron-right'}/>
</button>
);
}
export default CarouselButton;

View file

@ -0,0 +1,74 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import classNames from 'classnames';
import 'renderer/css/components/Button.scss';
import 'renderer/css/components/CarouselPaginationIndicator.scss';
type CarouselPaginationIndicatorProps = {
pages: number;
activePage: number;
disabled?: boolean;
darkMode?: boolean;
onClick?: (pageIndex: number) => void;
};
function CarouselPaginationIndicator({
pages,
activePage,
disabled,
darkMode,
onClick = () => null,
}: CarouselPaginationIndicatorProps) {
const handleOnClick = useCallback((pageIndex: number) => () => {
onClick(pageIndex);
}, [onClick]);
const handleOnKeyDown = useCallback((pageIndex: number) => (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
onClick(pageIndex);
}
}, [onClick]);
const getIndicators = useCallback(() => {
const indicators = [];
for (let pageIndex = 0; pageIndex < pages; pageIndex++) {
indicators.push(
<div
key={pageIndex}
id={`PaginationIndicator${pageIndex}`}
onClick={handleOnClick(pageIndex)}
onKeyDown={handleOnKeyDown(pageIndex)}
className={classNames(
'indicatorDot',
{
'indicatorDot-inverted': darkMode,
active: activePage === pageIndex,
disabled,
},
)}
role='button'
tabIndex={0}
>
<div className='dot'/>
</div>,
);
}
return indicators;
}, [pages, activePage, darkMode, handleOnClick]);
return (
<div
className='CarouselPaginationIndicator'
tabIndex={-1}
>
{getIndicators()}
</div>
);
}
export default CarouselPaginationIndicator;

View file

@ -0,0 +1,4 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default} from './Carousel';

View file

@ -0,0 +1,35 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import classNames from 'classnames';
import Logo from 'renderer/components/Logo';
import 'renderer/css/components/Header.scss';
type HeaderProps = {
alternateLink?: React.ReactElement;
darkMode?: boolean;
}
const Header = ({
alternateLink,
darkMode,
}: HeaderProps) => (
<div
className={classNames(
'Header',
{'Header--darkMode': darkMode},
)}
>
<div className='Header__main'>
<div className='Header__logo'>
<Logo/>
</div>
{alternateLink}
</div>
</div>
);
export default Header;

View file

@ -0,0 +1,4 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default} from './Header';

File diff suppressed because one or more lines are too long

View file

@ -521,6 +521,11 @@ class MainPage extends React.PureComponent<Props, State> {
ref={this.topBar}
className={'topBar-bg'}
>
{window.process.platform !== 'linux' && this.props.teams.length === 0 && (
<div className='app-title'>
{intl.formatMessage({id: 'renderer.components.mainPage.titleBar', defaultMessage: 'Mattermost'})}
</div>
)}
<button
className='three-dot-menu'
onClick={this.openMenu}
@ -530,14 +535,16 @@ class MainPage extends React.PureComponent<Props, State> {
>
<i className='icon-dots-vertical'/>
</button>
<TeamDropdownButton
isDisabled={this.state.modalOpen}
activeServerName={this.state.activeServerName}
totalMentionCount={totalMentionCount}
hasUnreads={totalUnreadCount > 0}
isMenuOpen={this.state.isMenuOpen}
darkMode={this.state.darkMode}
/>
{this.props.teams.length !== 0 && (
<TeamDropdownButton
isDisabled={this.state.modalOpen}
activeServerName={this.state.activeServerName}
totalMentionCount={totalMentionCount}
hasUnreads={totalUnreadCount > 0}
isMenuOpen={this.state.isMenuOpen}
darkMode={this.state.darkMode}
/>
)}
{tabsRow}
{upgradeIcon}
{titleBarButtons}

View file

@ -0,0 +1,149 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {useIntl, FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import bullseye from 'renderer/assets/svg/bullseye.svg';
import channels from 'renderer/assets/svg/channels.svg';
import chat2 from 'renderer/assets/svg/chat2.svg';
import clipboard from 'renderer/assets/svg/clipboard.svg';
import Carousel from 'renderer/components/Carousel';
import Header from 'renderer/components/Header';
import LoadingBackground from 'renderer/components/LoadingScreen/LoadingBackground';
import WelcomeScreenSlide from './WelcomeScreenSlide';
import 'renderer/css/components/Button.scss';
import 'renderer/css/components/WelcomeScreen.scss';
import 'renderer/css/components/LoadingScreen.css';
type WelcomeScreenProps = {
darkMode?: boolean;
onGetStarted?: () => void;
};
function WelcomeScreen({
darkMode = false,
onGetStarted = () => null,
}: WelcomeScreenProps) {
const {formatMessage} = useIntl();
const slides = useMemo(() => [
{
key: 'welcome',
title: formatMessage({id: 'renderer.components.welcomeScreen.slides.welcome.title', defaultMessage: 'Welcome'}),
subtitle: formatMessage({
id: 'renderer.components.welcomeScreen.slides.welcome.subtitle',
defaultMessage: 'Mattermost is an open source platform for developer collaboration. Secure, flexible, and integrated with the tools you love.',
}),
image: (
<img
src={chat2}
draggable={false}
/>
),
main: true,
},
{
key: 'channels',
title: formatMessage({id: 'renderer.components.welcomeScreen.slides.channels.title', defaultMessage: 'Channels'}),
subtitle: (
<FormattedMessage
id='renderer.components.welcomeScreen.slides.channels.subtitle'
defaultMessage='All of your teams communication in one place.<br></br>Secure collaboration, built for developers.'
values={{
br: (x: React.ReactNode) => (<><br/>{x}</>),
}}
/>
),
image: (
<img
src={channels}
draggable={false}
/>
),
},
{
key: 'playbooks',
title: formatMessage({id: 'renderer.components.welcomeScreen.slides.playbooks.title', defaultMessage: 'Playbooks'}),
subtitle: formatMessage({
id: 'renderer.components.welcomeScreen.slides.palybooks.subtitle',
defaultMessage: 'Move faster and make fewer mistakes with checklists, automations, and tool integrations that power your teams workflows.',
}),
image: (
<img
src={clipboard}
draggable={false}
/>
),
},
{
key: 'boards',
title: formatMessage({id: 'renderer.components.welcomeScreen.slides.boards.title', defaultMessage: 'Boards'}),
subtitle: formatMessage({
id: 'renderer.components.welcomeScreen.slides.boards.subtitle',
defaultMessage: 'Ship on time, every time, with a project and task management solution built for digital operations.',
}),
image: (
<img
src={bullseye}
draggable={false}
/>
),
},
], []);
const handleOnGetStartedClick = () => {
onGetStarted();
};
return (
<div
className={classNames(
'LoadingScreen',
{'LoadingScreen--darkMode': darkMode},
'WelcomeScreen',
)}
>
<LoadingBackground/>
<Header darkMode={darkMode}/>
<div className='WelcomeScreen__body'>
<div className='WelcomeScreen__content'>
<Carousel
slides={slides.map(({key, title, subtitle, image, main}) => ({
key,
content: (
<WelcomeScreenSlide
key={key}
title={title}
subtitle={subtitle}
image={image}
isMain={main}
darkMode={darkMode}
/>
),
}))}
darkMode={darkMode}
/>
<button
id='getStartedWelcomeScreen'
className={classNames(
'WelcomeScreen__button',
'primary-button primary-medium-button',
{'primary-button-inverted': darkMode},
)}
onClick={handleOnGetStartedClick}
>
{formatMessage({id: 'renderer.components.welcomeScreen.button.getStarted', defaultMessage: 'Get Started'})}
</button>
</div>
</div>
<div className='WelcomeScreen__footer'/>
</div>
);
}
export default WelcomeScreen;

View file

@ -0,0 +1,45 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import classNames from 'classnames';
import 'renderer/css/components/WelcomeScreenSlide.scss';
type WelcomeScreenSlideProps = {
title: string;
subtitle: string | React.ReactElement;
image: React.ReactNode;
isMain?: boolean;
darkMode?: boolean;
};
const WelcomeScreenSlide = ({
title,
subtitle,
image,
isMain,
darkMode,
}: WelcomeScreenSlideProps) => (
<div
className={classNames(
'WelcomeScreenSlide',
{
'WelcomeScreenSlide--main': isMain,
'WelcomeScreenSlide--darkMode': darkMode,
},
)}
>
<div className='WelcomeScreenSlide__image'>
{image}
</div>
<div className='WelcomeScreenSlide__title'>
{title}
</div>
<div className='WelcomeScreenSlide__subtitle'>
{subtitle}
</div>
</div>
);
export default WelcomeScreenSlide;

View file

@ -0,0 +1,4 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default} from './WelcomeScreen';

View file

@ -0,0 +1,14 @@
:root {
--button-bg: #166de0;
--button-color: #fff;
--center-channel-text: #3f4350;
--sidebar-text-active-border: #579eff;
--denim-button-bg: #1c58d9;
--denim-sidebar-active-border: #5d89ea;
--title-color-indigo-500: #1e325c;
--button-color-rgb: 255, 255, 255;
--center-channel-color-rgb: 61, 60, 64;
--center-channel-text-rgb: 63, 67, 80;
--denim-button-bg-rgb: 28, 88, 217;
}

View file

@ -0,0 +1,177 @@
@import url("../_css_variables.scss");
.primary-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
background: var(--button-bg);
border-radius: 4px;
color: var(--button-color);
font-weight: 600;
font-family: 'Open Sans';
&:hover {
background: linear-gradient(0deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.08)), var(--button-bg);
}
&:active {
background: linear-gradient(0deg, rgba(0, 0, 0, 0.16), rgba(0, 0, 0, 0.16)), var(--button-bg);
}
&:focus {
box-sizing: border-box;
border: 2px solid var(--sidebar-text-active-border);
outline: none;
}
&:disabled {
background: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.32);
cursor: not-allowed;
}
i {
display: flex;
font-size: 18px;
}
}
.primary-button-inverted {
background: var(--button-color);
color: var(--denim-button-bg);
&:hover {
background: linear-gradient(0deg, rgba(var(--denim-button-bg-rgb), 0.08), rgba(var(--denim-button-bg-rgb), 0.08)), var(--button-color);
color: var(--denim-button-bg);
}
&:active {
background: linear-gradient(0deg, rgba(var(--denim-button-bg-rgb), 0.16), rgba(var(--denim-button-bg-rgb), 0.16)), var(--button-color);
color: var(--denim-button-bg);
}
&:focus {
border: 2px solid var(--denim-sidebar-active-border);
color: var(--denim-button-bg);
}
&:disabled {
background: rgba(var(--center-channel-text-rgb), 0.08);
color: rgba(var(--center-channel-text-rgb), 0.32);
}
}
.primary-large-button {
height: 48px;
padding: 0 24px;
font-size: 16px;
line-height: 18px;
&:focus {
padding: 0 22px;
}
}
.primary-medium-button {
height: 40px;
padding: 0 20px;
font-size: 14px;
line-height: 14px;
&:focus {
padding: 0 20px;
}
}
.icon-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
background: none;
border-radius: 4px;
color: rgba(var(--center-channel-text-rgb), 0.56);
font-weight: 400;
&:hover {
background: rgba(var(--center-channel-text-rgb), 0.08);
color: rgba(var(--center-channel-text-rgb), 0.72);
}
&:active {
background: rgba(var(--denim-button-bg-rgb), 0.08);
color: var(--denim-button-bg);
}
&:focus-visible {
box-sizing: border-box;
border-color: linear-gradient(0deg, rgba(var(--button-color-rgb), 0.32), rgba(var(--button-color-rgb), 0.32)), var(--denim-button-bg);
outline: none;
}
&:disabled {
background: none;
color: rgba(var(--center-channel-text-rgb), 0.32);
cursor: not-allowed;
}
i {
display: flex;
font-style: normal;
justify-content: center;
}
}
.icon-button-inverted {
background: none;
color: rgba(var(--button-color-rgb), 0.64);
&:hover {
background: rgba(var(--button-color-rgb), 0.08);
color: var(--button-color);
}
&:active {
background: rgba(var(--button-color-rgb), 0.16);
color: var(--button-color);
}
&:focus-visible {
box-sizing: border-box;
border-color: linear-gradient(0deg, rgba(var(--button-color-rgb), 0.32), rgba(var(--button-color-rgb), 0.32)), var(--denim-button-bg);
}
&:disabled {
background: none;
color: rgba(var(--button-color-rgb), 0.32);
}
}
.icon-button-small {
height: 28px;
padding: 6px;
font-size: 18px;
line-height: 18px;
&:focus:not(:focus-visible) {
padding: 6px 8px;
border: 0;
}
&:focus-visible {
padding: 4px;
border: 2px solid;
}
i {
width: 16px;
height: 16px;
&::before {
line-height: 16px;
}
}
}

View file

@ -0,0 +1,94 @@
.Carousel {
display: flex;
flex: 1;
flex-flow: column;
align-items: center;
justify-content: center;
width: 100%;
.Carousel__slides {
min-height: 380px;
width: 100%;
position: relative;
display: flex;
justify-content: center;
.Carousel__slide {
position: absolute;
bottom: 0;
opacity: 0;
}
.Carousel__slide-current {
opacity: 1;
}
}
.Carousel__pagination {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
margin-top: 22px;
}
}
.inFromRight {
animation: inFromRight 0.4s ease-in-out;
}
.inFromLeft {
animation: inFromLeft 0.4s ease-in-out;
}
.outToRight {
animation: outToRight 0.4s ease-in-out;
}
.outToLeft {
animation: outToLeft 0.4s ease-in-out;
}
@keyframes inFromRight {
0% {
transform: translateX(30%);
opacity: 0;
}
100% {
transform: translateX(0%);
opacity: 1;
}
}
@keyframes inFromLeft {
0% {
transform: translateX(-30%);
opacity: 0;
}
100% {
transform: translateX(0%);
opacity: 1;
}
}
@keyframes outToRight {
0% {
transform: translateX(0%);
opacity: 1;
}
100% {
transform: translateX(30%);
opacity: 0;
}
}
@keyframes outToLeft {
0% {
transform: translateX(0%);
opacity: 1;
}
100% {
transform: translateX(-30%);
opacity: 0;
}
}

View file

@ -0,0 +1,10 @@
@import '~@mattermost/compass-icons/css/compass-icons.css';
.CarouselButton {
height: 32px;
padding: 8px;
&:focus {
padding: 6px;
}
}

View file

@ -0,0 +1,70 @@
@import url("../_css_variables.scss");
.CarouselPaginationIndicator {
display: flex;
align-items: center;
justify-content: center;
margin: 0 23px;
.indicatorDot {
display: flex;
align-items: center;
justify-content: center;
width: 12px;
height: 12px;
border-radius: 50%;
background: none;
box-shadow: 0 0 0 3px transparent;
cursor: pointer;
&:not(:first-child) {
margin-left: 4px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(var(--denim-button-bg-rgb), 0.32);
}
&.active,
&.disabled {
cursor: default;
pointer-events: none;
}
&.active {
background: rgba(var(--denim-button-bg-rgb), 0.16);
.dot {
background: var(--denim-button-bg);
}
}
&:focus-visible {
box-sizing: border-box;
border: 2px solid var(--sidebar-text-active-border);
outline: none;
}
}
.indicatorDot-inverted {
.dot {
background: rgba(var(--button-color-rgb), 0.32);
}
&.active {
background: rgba(var(--button-color-rgb), 0.16);
.dot {
background: var(--button-color);
}
}
&:focus-visible {
border: 2px solid var(--denim-sidebar-active-border);
color: var(--denim-button-bg);
}
}
}

View file

@ -0,0 +1,36 @@
@import url("../_css_variables.scss");
.Header {
position: relative;
display: flex;
width: 100%;
min-height: 100px;
padding: 0 40px;
.Header__main {
display: flex;
width: 100%;
height: 100%;
flex-flow: wrap;
align-items: center;
justify-content: space-between;
.Header__logo {
width: 170px;
path {
fill: var(--center-channel-text);
}
}
}
&.Header--darkMode {
.Header__main {
.Header__logo {
path {
fill: var(--button-color);
}
}
}
}
}

View file

@ -0,0 +1,35 @@
.WelcomeScreen {
flex-direction: column;
z-index: 20;
* {
z-index: 21;
}
.WelcomeScreen__body {
display: flex;
flex: 1;
width: 100%;
align-items: center;
justify-content: center;
-webkit-font-smoothing: antialiased;
.WelcomeScreen__content {
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
justify-content: center;
.WelcomeScreen__button {
margin-top: 22px;
}
}
}
.WelcomeScreen__footer {
display: block;
width: 100%;
height: 100px;
}
}

View file

@ -0,0 +1,60 @@
@import url("../_css_variables.scss");
@import url("../fonts.css");
.WelcomeScreenSlide {
display: flex;
flex: 1;
max-width: 540px;
flex-flow: column;
justify-content: flex-end;
.WelcomeScreenSlide__image {
align-self: center;
}
.WelcomeScreenSlide__title {
color: var(--title-color-indigo-500);
font-family: 'Metropolis';
font-size: 80px;
font-weight: 600;
letter-spacing: -0.05em;
line-height: 88px;
text-align: center;
}
.WelcomeScreenSlide__subtitle {
color: rgba(var(--center-channel-text-rgb), 0.72);
font-family: Open Sans;
font-size: 16px;
font-weight: 400;
line-height: 24px;
text-align: center;
margin-top: 16px;
}
&.WelcomeScreenSlide--main {
position: relative;
height: 100%;
.WelcomeScreenSlide__title {
font-size: 128px;
line-height: 128px;
}
.WelcomeScreenSlide__image {
position: absolute;
display: block;
bottom: 115px;
}
}
&.WelcomeScreenSlide--darkMode {
.WelcomeScreenSlide__title {
color: var(--button-color);
}
.WelcomeScreenSlide__subtitle {
color: rgba(var(--button-color-rgb), 0.72);
}
}
}

View file

@ -39,3 +39,45 @@
font-weight: 600;
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-600italic.woff2') format('woff2');
}
@font-face {
font-family: 'Metropolis';
font-style: normal;
font-weight: 600;
src: url('../../assets/fonts/Metropolis-SemiBold.woff') format('woff');
}
@font-face {
font-family: 'Metropolis';
font-style: italic;
font-weight: 600;
src: url('../../assets/fonts/Metropolis-SemiBoldItalic.woff') format('woff');
}
@font-face {
font-family: 'Metropolis';
font-style: normal;
font-weight: 400;
src: url('../../assets/fonts/Metropolis-Regular.woff') format('woff');
}
@font-face {
font-family: 'Metropolis';
font-style: italic;
font-weight: 400;
src: url('../../assets/fonts/Metropolis-RegularItalic.woff') format('woff');
}
@font-face {
font-family: 'Metropolis';
font-style: normal;
font-weight: 300;
src: url('../../assets/fonts/Metropolis-Light.woff') format('woff');
}
@font-face {
font-family: 'Metropolis';
font-style: italic;
font-weight: 300;
src: url('../../assets/fonts/Metropolis-LightItalic.woff') format('woff');
}

View file

@ -117,6 +117,28 @@ body {
color: rgba(243,243,243,0.7);
}
.topBar .app-title {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 40px;
justify-content: center;
align-items: center;
color: rgba(61,60,64,0.7);
font-family: "Open Sans", sans-serif;
font-weight: 600;
font-size: 14px;
letter-spacing: -0.2px;
z-index: 0;
-webkit-app-region: drag;
}
.topBar.darkMode .app-title {
color: rgba(221,221,221,0.64);
}
.topBar .title-bar-btns {
position: relative;
line-height: 40px;

View file

@ -0,0 +1,60 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom';
import {ModalMessage} from 'types/modals';
import {
MODAL_RESULT,
GET_MODAL_UNCLOSEABLE,
GET_DARK_MODE,
DARK_MODE_CHANGE,
} from 'common/communication';
import IntlProvider from 'renderer/intl_provider';
import WelcomeScreen from '../../components/WelcomeScreen';
import 'bootstrap/dist/css/bootstrap.min.css';
const onGetStarted = () => {
window.postMessage({type: MODAL_RESULT}, window.location.href);
};
const WelcomeScreenModalWrapper = () => {
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
window.postMessage({type: GET_MODAL_UNCLOSEABLE}, window.location.href);
window.postMessage({type: GET_DARK_MODE}, window.location.href);
window.addEventListener('message', handleMessageEvent);
return () => {
window.removeEventListener('message', handleMessageEvent);
};
}, []);
const handleMessageEvent = (event: {data: ModalMessage<boolean>}) => {
if (event.data.type === DARK_MODE_CHANGE) {
setDarkMode(event.data.data);
}
};
return (
<IntlProvider>
<WelcomeScreen
darkMode={darkMode}
onGetStarted={onGetStarted}
/>
</IntlProvider>
);
};
const start = async () => {
ReactDOM.render(
<WelcomeScreenModalWrapper/>,
document.getElementById('app'),
);
};
start();

View file

@ -29,6 +29,7 @@ module.exports = merge(base, {
permissionModal: './src/renderer/modals/permission/permission.tsx',
certificateModal: './src/renderer/modals/certificate/certificate.tsx',
loadingScreen: './src/renderer/modals/loadingScreen/index.tsx',
welcomeScreen: './src/renderer/modals/welcomeScreen/welcomeScreen.tsx',
},
output: {
path: path.resolve(__dirname, 'dist/renderer'),
@ -102,6 +103,12 @@ module.exports = merge(base, {
chunks: ['loadingScreen'],
filename: 'loadingScreen.html',
}),
new HtmlWebpackPlugin({
title: 'Mattermost Desktop Settings',
template: 'src/renderer/index.html',
chunks: ['welcomeScreen'],
filename: 'welcomeScreen.html',
}),
new MiniCssExtractPlugin({
filename: 'styles.[contenthash].css',
ignoreOrder: true,