diff --git a/e2e/specs/focus.test.js b/e2e/specs/focus.test.js index 9e58d773..7e9dadb7 100644 --- a/e2e/specs/focus.test.js +++ b/e2e/specs/focus.test.js @@ -99,8 +99,8 @@ describe('focus', function desc() { const newServerView = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('newServer'), }); - await newServerView.waitForSelector('#cancelNewServerModal'); - await newServerView.click('#cancelNewServerModal'); + await newServerView.waitForSelector('#newServerModal_cancel'); + await newServerView.click('#newServerModal_cancel'); const isTextboxFocused = await firstServer.$eval('#post_textbox', (el) => el === document.activeElement); isTextboxFocused.should.be.true; diff --git a/e2e/specs/menu_bar/dropdown.test.js b/e2e/specs/menu_bar/dropdown.test.js index 125b9a4a..7b5fd6ab 100644 --- a/e2e/specs/menu_bar/dropdown.test.js +++ b/e2e/specs/menu_bar/dropdown.test.js @@ -80,7 +80,7 @@ describe('menu_bar/dropdown', function desc() { const newServerModal = await this.app.waitForEvent('window', { predicate: (window) => window.url().includes('newServer'), }); - const modalTitle = await newServerModal.innerText('#newServerModal .modal-title'); + const modalTitle = await newServerModal.innerText('#newServerModal .Modal__header__text_container'); modalTitle.should.equal('Add Server'); await afterFunc(); diff --git a/e2e/specs/server_management/add_server_modal.test.js b/e2e/specs/server_management/add_server_modal.test.js index 987c06d7..285f6ece 100644 --- a/e2e/specs/server_management/add_server_modal.test.js +++ b/e2e/specs/server_management/add_server_modal.test.js @@ -46,7 +46,7 @@ describe('Add Server Modal', function desc() { }); it('MM-T4388 should close the window after clicking cancel', async () => { - await newServerView.click('#cancelNewServerModal'); + await newServerView.click('#newServerModal_cancel'); await asyncSleep(1000); const existing = Boolean(this.app.windows().find((window) => window.url().includes('newServer'))); existing.should.be.false; @@ -54,19 +54,17 @@ describe('Add Server Modal', function desc() { describe('MM-T4389 Invalid messages', () => { it('MM-T4389_1 should not be valid and save should be disabled if no server name or URL has been set', async () => { - const existing = await newServerView.isVisible('#nameValidation.error'); - existing.should.be.true; - const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled'); + const disabled = await newServerView.getAttribute('#newServerModal_confirm', 'disabled'); (disabled === '').should.be.true; }); it('should warn the user if a server with the same URL exists, but still allow them to save', async () => { await newServerView.type('#serverNameInput', 'some-new-server'); await newServerView.type('#serverUrlInput', config.teams[0].url); - await newServerView.waitForSelector('#urlValidation.warning'); - const existing = await newServerView.isVisible('#urlValidation.warning'); + await newServerView.waitForSelector('#customMessage_url.Input___warning'); + const existing = await newServerView.isVisible('#customMessage_url.Input___warning'); existing.should.be.true; - const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled'); + const disabled = await newServerView.getAttribute('#newServerModal_confirm', 'disabled'); (disabled === '').should.be.false; }); @@ -76,8 +74,8 @@ describe('Add Server Modal', function desc() { }); it('MM-T4389_2 Name should not be marked invalid, but should not be able to save', async () => { - await newServerView.waitForSelector('#nameValidation.error', {state: 'detached'}); - const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled'); + await newServerView.waitForSelector('#customMessage_name.Input___error', {state: 'detached'}); + const disabled = await newServerView.getAttribute('#newServerModal_confirm', 'disabled'); (disabled === '').should.be.true; }); }); @@ -88,9 +86,10 @@ describe('Add Server Modal', function desc() { }); it('MM-T4389_3 URL should not be marked invalid, name should be marked invalid', async () => { - const existingUrl = await newServerView.isVisible('#urlValidation.error'); - const existingName = await newServerView.isVisible('#nameValidation.error'); - const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled'); + await newServerView.waitForSelector('#customMessage_name.Input___error'); + const existingUrl = await newServerView.isVisible('#customMessage_url.Input___error'); + const existingName = await newServerView.isVisible('#customMessage_name.Input___error'); + const disabled = await newServerView.getAttribute('#newServerModal_confirm', 'disabled'); existingName.should.be.true; existingUrl.should.be.false; (disabled === '').should.be.true; @@ -100,8 +99,8 @@ describe('Add Server Modal', function desc() { it('MM-T2826_1 should not be valid if an invalid server address has been set', async () => { await newServerView.type('#serverUrlInput', 'superInvalid url'); - await newServerView.waitForSelector('#urlValidation.error'); - const existing = await newServerView.isVisible('#urlValidation.error'); + await newServerView.waitForSelector('#customMessage_url.Input___error'); + const existing = await newServerView.isVisible('#customMessage_url.Input___error'); existing.should.be.true; }); @@ -109,16 +108,16 @@ describe('Add Server Modal', function desc() { beforeEach(async () => { await newServerView.type('#serverUrlInput', 'http://example.org'); await newServerView.type('#serverNameInput', 'TestServer'); - await newServerView.waitForSelector('#urlValidation.warning'); + await newServerView.waitForSelector('#customMessage_url.Input___warning'); }); it('should be possible to click add', async () => { - const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled'); + const disabled = await newServerView.getAttribute('#newServerModal_confirm', 'disabled'); (disabled === null).should.be.true; }); it('MM-T2826_2 should add the server to the config file', async () => { - await newServerView.click('#saveNewServerModal'); + await newServerView.click('#newServerModal_confirm'); await asyncSleep(2000); const existing = Boolean(this.app.windows().find((window) => window.url().includes('newServer'))); existing.should.be.false; diff --git a/e2e/specs/server_management/edit_server_modal.test.js b/e2e/specs/server_management/edit_server_modal.test.js index 43c724e5..071292c0 100644 --- a/e2e/specs/server_management/edit_server_modal.test.js +++ b/e2e/specs/server_management/edit_server_modal.test.js @@ -40,7 +40,7 @@ describe('EditServerModal', function desc() { let editServerView; it('should not edit server when Cancel is pressed', async () => { - await editServerView.click('#cancelNewServerModal'); + await editServerView.click('#newServerModal_cancel'); await asyncSleep(1000); const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer'))); existing.should.be.false; @@ -70,7 +70,7 @@ describe('EditServerModal', function desc() { }); it('MM-T4391_1 should not edit server when Save is pressed but nothing edited', async () => { - await editServerView.click('#saveNewServerModal'); + await editServerView.click('#newServerModal_confirm'); await asyncSleep(1000); const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer'))); existing.should.be.false; @@ -100,15 +100,15 @@ describe('EditServerModal', function desc() { }); it('MM-T2826_3 should not edit server if an invalid server address has been set', async () => { - await editServerView.type('#serverUrlInput', 'superInvalid url'); - await editServerView.waitForSelector('#urlValidation.error'); - const existing = await editServerView.isVisible('#urlValidation.error'); + await editServerView.fill('#serverUrlInput', 'superInvalid url'); + await editServerView.waitForSelector('#customMessage_url.Input___error'); + const existing = await editServerView.isVisible('#customMessage_url.Input___error'); existing.should.be.true; }); it('MM-T4391_2 should edit server when Save is pressed and name edited', async () => { await editServerView.fill('#serverNameInput', 'NewTestServer'); - await editServerView.click('#saveNewServerModal'); + await editServerView.click('#newServerModal_confirm'); await asyncSleep(1000); const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer'))); existing.should.be.false; @@ -160,7 +160,7 @@ describe('EditServerModal', function desc() { it('MM-T4391_3 should edit server when Save is pressed and URL edited', async () => { await editServerView.fill('#serverUrlInput', 'http://google.com'); - await editServerView.click('#saveNewServerModal'); + await editServerView.click('#newServerModal_confirm'); await asyncSleep(1000); const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer'))); existing.should.be.false; @@ -213,7 +213,7 @@ describe('EditServerModal', function desc() { it('MM-T4391_4 should edit server when Save is pressed and both edited', async () => { await editServerView.fill('#serverNameInput', 'NewTestServer'); await editServerView.fill('#serverUrlInput', 'http://google.com'); - await editServerView.click('#saveNewServerModal'); + await editServerView.click('#newServerModal_confirm'); await asyncSleep(1000); const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer'))); existing.should.be.false; diff --git a/e2e/specs/server_management/long_server_name.test.js b/e2e/specs/server_management/long_server_name.test.js index dc0a0e0f..33836e07 100644 --- a/e2e/specs/server_management/long_server_name.test.js +++ b/e2e/specs/server_management/long_server_name.test.js @@ -45,7 +45,7 @@ describe('LongServerName', function desc() { it('MM-T4050 Long server name', async () => { await newServerView.type('#serverNameInput', longServerName); await newServerView.type('#serverUrlInput', longServerUrl); - await newServerView.click('#saveNewServerModal'); + await newServerView.click('#newServerModal_confirm'); await asyncSleep(1000); const existing = Boolean(this.app.windows().find((window) => window.url().includes('newServer'))); diff --git a/e2e/specs/server_management/remove_server_modal.test.js b/e2e/specs/server_management/remove_server_modal.test.js index c67adf1a..562b8126 100644 --- a/e2e/specs/server_management/remove_server_modal.test.js +++ b/e2e/specs/server_management/remove_server_modal.test.js @@ -73,7 +73,7 @@ describe('RemoveServerModal', function desc() { it('MM-T4390_4 should disappear on click background', async () => { // ignore any target closed error try { - await removeServerView.click('.modal', {position: {x: 20, y: 20}}); + await removeServerView.click('.Modal', {position: {x: 20, y: 20}}); } catch {} // eslint-disable-line no-empty await asyncSleep(1000); const existing = Boolean(await this.app.windows().find((window) => window.url().includes('removeServer'))); diff --git a/i18n/en.json b/i18n/en.json index d0c4555e..cbf6377b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -146,6 +146,8 @@ "main.windows.mainWindow.minimizeToTray.dialog.checkboxLabel": "Don't show again", "main.windows.mainWindow.minimizeToTray.dialog.message": "{appName} will continue to run in the system tray. This can be disabled in Settings.", "main.windows.mainWindow.minimizeToTray.dialog.title": "Minimize to Tray", + "modal.cancel": "Cancel", + "modal.confirm": "Confirm", "renderer.components.autoSaveIndicator.saved": "Saved", "renderer.components.autoSaveIndicator.saving": "Saving...", "renderer.components.configureServer.cardtitle": "Enter your server details", @@ -200,8 +202,8 @@ "renderer.components.newServerModal.warning.notMattermost": "The server URL provided does not appear to point to a valid Mattermost server. Please verify the URL and check your connection.", "renderer.components.newServerModal.warning.urlNotMatched": "The server URL does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}", "renderer.components.newServerModal.warning.urlUpdated": "The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}", - "renderer.components.removeServerModal.body": "This will remove the server from your Desktop App but will not delete any of its data - you can add the server back to the app at any time.", - "renderer.components.removeServerModal.confirm": "Confirm you wish to remove the {serverName} server?", + "renderer.components.removeServerModal.body": "This will remove the server from your Desktop App but will not delete any of its data - you can add the server back at any time.", + "renderer.components.removeServerModal.confirm": "Are you sure you wish to remove the server?", "renderer.components.removeServerModal.title": "Remove Server", "renderer.components.saveButton.save": "Save", "renderer.components.saveButton.saving": "Saving", @@ -266,12 +268,12 @@ "renderer.components.showCertificateModal.algorithm": "Algorithm", "renderer.components.showCertificateModal.commonName": "Common Name", "renderer.components.showCertificateModal.issuerName": "Issuer Name", - "renderer.components.showCertificateModal.noCertSelected": "No certificate Selected", "renderer.components.showCertificateModal.notValidAfter": "Not Valid After", "renderer.components.showCertificateModal.notValidBefore": "Not Valid Before", "renderer.components.showCertificateModal.publicKeyInfo": "Public Key Info", "renderer.components.showCertificateModal.serialNumber": "Serial Number", "renderer.components.showCertificateModal.subjectName": "Subject Name", + "renderer.components.showCertificateModal.title": "Certificate information", "renderer.components.welcomeScreen.button.getStarted": "Get Started", "renderer.components.welcomeScreen.slides.calls.subtitle": "When typing isn’t fast enough, seamlessly move from chat to audio calls and screenshare without switching tools.", "renderer.components.welcomeScreen.slides.calls.title": "Start secure calls instantly", diff --git a/src/app/serverViewState.ts b/src/app/serverViewState.ts index e41ca851..125b43a2 100644 --- a/src/app/serverViewState.ts +++ b/src/app/serverViewState.ts @@ -204,11 +204,11 @@ export class ServerViewState { return; } - const modalPromise = ModalManager.addModal( + const modalPromise = ModalManager.addModal( 'removeServer', 'mattermost-desktop://renderer/removeServer.html', getLocalPreload('internalAPI.js'), - server.name, + null, mainWindow, ); diff --git a/src/renderer/components/DestructiveConfirmModal.tsx b/src/renderer/components/DestructiveConfirmModal.tsx index c2903c4a..74af93a2 100644 --- a/src/renderer/components/DestructiveConfirmModal.tsx +++ b/src/renderer/components/DestructiveConfirmModal.tsx @@ -3,20 +3,23 @@ // See LICENSE.txt for license information. import React from 'react'; -import {Button, Modal} from 'react-bootstrap'; + +import {Modal} from './Modal'; type Props = { + id: string; title: string; body: React.ReactNode; acceptLabel: string; cancelLabel: string; onHide: () => void; - onAccept: React.MouseEventHandler; - onCancel: React.MouseEventHandler; + onAccept: () => void; + onCancel: () => void; }; export default function DestructiveConfirmationModal(props: Props) { const { + id, title, body, acceptLabel, @@ -27,23 +30,18 @@ export default function DestructiveConfirmationModal(props: Props) { ...rest} = props; return ( - - {title} - {body} - - - - ); } diff --git a/src/renderer/components/Modal.tsx b/src/renderer/components/Modal.tsx new file mode 100644 index 00000000..fbc4a1da --- /dev/null +++ b/src/renderer/components/Modal.tsx @@ -0,0 +1,274 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import classNames from 'classnames'; +import React, {useState, useRef, useEffect, useCallback} from 'react'; +import {FormattedMessage} from 'react-intl'; + +import 'renderer/css/components/Modal.scss'; + +export type Props = { + id: string; + children: React.ReactNode; + onExited: () => void; + + className?: string; + modalHeaderText?: React.ReactNode; + modalSubheaderText?: React.ReactNode; + show?: boolean; + handleCancel?: () => void; + handleConfirm?: () => void; + handleEnterKeyPress?: () => void; + handleKeydown?: (event?: React.KeyboardEvent) => void; + confirmButtonText?: React.ReactNode; + confirmButtonClassName?: string; + cancelButtonText?: React.ReactNode; + cancelButtonClassName?: string; + isConfirmDisabled?: boolean; + isDeleteModal?: boolean; + autoCloseOnCancelButton?: boolean; + autoCloseOnConfirmButton?: boolean; + ariaLabel?: string; + errorText?: string | React.ReactNode; + tabIndex?: number; + autoFocusConfirmButton?: boolean; + headerInput?: React.ReactNode; + bodyPadding?: boolean; + bodyDivider?: boolean; + footerContent?: React.ReactNode; + footerDivider?: boolean; + appendedContent?: React.ReactNode; + headerButton?: React.ReactNode; +}; + +export const Modal: React.FC = ({ + id = 'modal', + children, + onExited, + className, + modalHeaderText, + modalSubheaderText, + show = true, + handleCancel, + handleConfirm, + handleEnterKeyPress, + handleKeydown, + confirmButtonText, + confirmButtonClassName, + cancelButtonText, + cancelButtonClassName, + isConfirmDisabled, + isDeleteModal, + autoCloseOnCancelButton = true, + autoCloseOnConfirmButton = true, + ariaLabel, + errorText, + tabIndex, + autoFocusConfirmButton, + headerInput, + bodyPadding = true, + bodyDivider, + footerContent, + footerDivider, + appendedContent, + headerButton, +}) => { + const [showState, setShowState] = useState(); + const backdropRef = useRef(null); + + useEffect(() => { + setShowState(show ?? true); + }, [show]); + + const onHide = () => { + return new Promise((resolve) => { + backdropRef.current?.addEventListener('transitionend', () => { + resolve(); + }, {once: true}); + setShowState(false); + }); + }; + + const onClose = useCallback(async () => { + await onHide(); + onExited(); + }, [onExited]); + + const handleCancelClick = async (event: React.MouseEvent) => { + event.preventDefault(); + if (autoCloseOnCancelButton) { + await onHide(); + } + handleCancel?.(); + }; + + const handleConfirmClick = async (event: React.MouseEvent) => { + event.preventDefault(); + if (autoCloseOnConfirmButton) { + await onHide(); + } + handleConfirm?.(); + }; + + const onEnterKeyDown = async (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + if (event.nativeEvent.isComposing) { + return; + } + if (autoCloseOnConfirmButton) { + await onHide(); + } + if (handleEnterKeyPress) { + handleEnterKeyPress(); + } + } + handleKeydown?.(event); + }; + + let confirmButton; + if (handleConfirm) { + const isConfirmOrDeleteClassName = isDeleteModal ? 'delete' : 'confirm'; + let confirmButtonTextNode: React.ReactNode = ( + + ); + if (confirmButtonText) { + confirmButtonTextNode = confirmButtonText; + } + + confirmButton = ( + + ); + } + + let cancelButton; + if (handleCancel) { + let cancelButtonTextNode: React.ReactNode = ( + + ); + if (cancelButtonText) { + cancelButtonTextNode = cancelButtonText; + } + + cancelButton = ( + + ); + } + + const headerText = modalHeaderText && ( +
+

+ {modalHeaderText} +

+ {headerButton} +
+ ); + + return ( + <> +
+
+ +
+ + ); +}; diff --git a/src/renderer/components/NewServerModal.tsx b/src/renderer/components/NewServerModal.tsx index 3a6c28b3..d17f9948 100644 --- a/src/renderer/components/NewServerModal.tsx +++ b/src/renderer/components/NewServerModal.tsx @@ -3,7 +3,6 @@ // See LICENSE.txt for license information. import React from 'react'; -import {Modal, Button, FormGroup, FormControl, FormLabel, FormText, Spinner} from 'react-bootstrap'; import type {IntlShape} from 'react-intl'; import {FormattedMessage, injectIntl} from 'react-intl'; @@ -14,20 +13,22 @@ import type {UniqueServer} from 'types/config'; import type {Permissions} from 'types/permissions'; import type {URLValidationResult} from 'types/server'; +import Input, {SIZE, STATUS} from './Input'; +import {Modal} from './Modal'; + import 'renderer/css/components/NewServerModal.scss'; type Props = { - onClose?: () => void; + onClose: () => void; onSave?: (server: UniqueServer, permissions?: Permissions) => void; server?: UniqueServer; permissions?: Permissions; editMode?: boolean; show?: boolean; - restoreFocus?: boolean; currentOrder?: number; - setInputRef?: (inputRef: HTMLInputElement) => void; intl: IntlShape; prefillURL?: string; + unremoveable?: boolean; }; type State = { @@ -49,10 +50,6 @@ class NewServerModal extends React.PureComponent { validationTimeout?: NodeJS.Timeout; mounted: boolean; - static defaultProps = { - restoreFocus: true, - }; - constructor(props: Props) { super(props); @@ -159,19 +156,13 @@ class NewServerModal extends React.PureComponent { getServerURLMessage = () => { if (this.state.validationStarted) { - return ( -
- - -
- ); + return { + type: STATUS.INFO, + value: this.props.intl.formatMessage({ + id: 'renderer.components.newServerModal.validating', + defaultMessage: 'Validating...', + }), + }; } if (!this.state.validationResult) { @@ -180,114 +171,78 @@ class NewServerModal extends React.PureComponent { switch (this.state.validationResult?.status) { case URLValidationStatus.Missing: - return ( -
- - -
- ); + return { + type: STATUS.ERROR, + value: this.props.intl.formatMessage({ + id: 'renderer.components.newServerModal.error.urlRequired', + defaultMessage: 'URL is required.', + }), + }; case URLValidationStatus.Invalid: - return ( -
- - -
- ); + return { + type: STATUS.ERROR, + value: this.props.intl.formatMessage({ + id: 'renderer.components.newServerModal.error.urlIncorrectFormatting', + defaultMessage: 'URL is not formatted correctly.', + }), + }; case URLValidationStatus.URLExists: - return ( -
- - -
- ); + return { + type: STATUS.WARNING, + value: this.props.intl.formatMessage({ + id: 'renderer.components.newServerModal.error.serverUrlExists', + defaultMessage: 'A server named {serverName} with the same Site URL already exists.', + }, { + serverName: this.state.validationResult.existingServerName, + }), + }; case URLValidationStatus.Insecure: - return ( -
- - -
- ); + return { + type: STATUS.WARNING, + value: this.props.intl.formatMessage({ + id: 'renderer.components.newServerModal.warning.insecure', + defaultMessage: 'Your server URL is potentially insecure. For best results, use a URL with the HTTPS protocol.', + }), + }; case URLValidationStatus.NotMattermost: - return ( -
- - -
- ); + return { + type: STATUS.WARNING, + value: this.props.intl.formatMessage({ + id: 'renderer.components.newServerModal.warning.notMattermost', + defaultMessage: 'The server URL provided does not appear to point to a valid Mattermost server. Please verify the URL and check your connection.', + }), + }; case URLValidationStatus.URLNotMatched: - return ( -
- - -
- ); + return { + type: STATUS.WARNING, + value: this.props.intl.formatMessage({ + id: 'renderer.components.newServerModal.warning.urlNotMatched', + defaultMessage: 'The server URL does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}', + }, { + serverVersion: this.state.validationResult.serverVersion, + }), + }; case URLValidationStatus.URLUpdated: - return ( -
- - -
- ); + return { + type: STATUS.INFO, + value: this.props.intl.formatMessage({ + id: 'renderer.components.newServerModal.warning.urlUpdated', + defaultMessage: 'The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}', + }, { + serverVersion: this.state.validationResult.serverVersion, + }), + }; } - return ( -
- - -
- ); + return { + type: STATUS.SUCCESS, + value: this.props.intl.formatMessage({ + id: 'renderer.components.newServerModal.success.ok', + defaultMessage: 'Server URL is valid. Server version: {serverVersion}', + }, { + serverVersion: this.state.validationResult.serverVersion, + }), + }; }; openNotificationPrefs = () => { @@ -303,19 +258,18 @@ class NewServerModal extends React.PureComponent { }; getServerNameMessage = () => { + if (!this.state.validationResult) { + return null; + } + if (!this.state.serverName.length) { - return ( -
- - -
- ); + return { + type: STATUS.ERROR, + value: this.props.intl.formatMessage({ + id: 'renderer.components.newServerModal.error.nameRequired', + defaultMessage: 'Name is required.', + }), + }; } return null; }; @@ -401,114 +355,71 @@ class NewServerModal extends React.PureComponent { return ( this.serverUrlInputRef?.focus()} - onHide={this.props.onClose} - restoreFocus={this.props.restoreFocus} - onKeyDown={(e: React.KeyboardEvent) => { - switch (e.key) { - case 'Enter': - this.save(); - - // The add button from behind this might still be focused - e.preventDefault(); - e.stopPropagation(); - break; - case 'Escape': - this.props.onClose?.(); - break; - } - }} + className='NewServerModal' + onExited={this.props.unremoveable ? () => {} : this.props.onClose} + modalHeaderText={this.getModalTitle()} + confirmButtonText={this.getSaveButtonLabel()} + handleConfirm={this.save} + isConfirmDisabled={!this.state.serverName.length || !this.state.validationResult || this.isServerURLErrored()} + handleCancel={this.props.onClose} + bodyDivider={true} + footerDivider={true} > - - {this.getModalTitle()} - - - + <> {!(this.props.editMode && this.props.server?.isPredefined) && <> -
- - - - - ) => { - e.stopPropagation(); - }} - ref={(ref: HTMLInputElement) => { - this.serverUrlInputRef = ref; - if (this.props.setInputRef) { - this.props.setInputRef(ref); - } - }} - isInvalid={this.isServerURLErrored()} - autoFocus={true} - /> - - - - - - - - - - ) => { - e.stopPropagation(); - }} - isInvalid={!this.state.serverName.length} - /> - - - - - -
-
- {this.getServerNameMessage()} - {this.getServerURLMessage()} -
+ + } {this.props.editMode && <>
-
+

-

+ { defaultMessage='Microphone and Camera' /> {this.state.cameraDisabled && - + { ), }} /> - + } {this.state.microphoneDisabled && - + { ), }} /> - + }
@@ -568,22 +479,22 @@ class NewServerModal extends React.PureComponent { defaultMessage='Notifications' /> {window.process.platform === 'darwin' && - - - + + + } {window.process.platform === 'win32' && - + - + } @@ -609,33 +520,7 @@ class NewServerModal extends React.PureComponent { } - - - - {this.props.onClose && - - } - {this.props.onSave && - - } - - + ); } diff --git a/src/renderer/components/RemoveServerModal.tsx b/src/renderer/components/RemoveServerModal.tsx index 4084d83f..6324d5b5 100644 --- a/src/renderer/components/RemoveServerModal.tsx +++ b/src/renderer/components/RemoveServerModal.tsx @@ -10,36 +10,32 @@ import DestructiveConfirmationModal from './DestructiveConfirmModal'; type Props = { show: boolean; - serverName: string; onHide: () => void; - onAccept: React.MouseEventHandler; - onCancel: React.MouseEventHandler; + onAccept: () => void; + onCancel: () => void; }; function RemoveServerModal(props: Props) { const intl = useIntl(); - const {serverName, ...rest} = props; + const {...rest} = props; return ( -

- -

-

- -

+ +

+ )} /> diff --git a/src/renderer/components/showCertificateModal.tsx b/src/renderer/components/showCertificateModal.tsx index cdde6ec3..66c90b8a 100644 --- a/src/renderer/components/showCertificateModal.tsx +++ b/src/renderer/components/showCertificateModal.tsx @@ -3,9 +3,11 @@ import type {Certificate} from 'electron/renderer'; import React, {Fragment} from 'react'; -import {Modal, Button, Row, Col} from 'react-bootstrap'; import {FormattedMessage} from 'react-intl'; +import {Modal} from 'renderer/components/Modal'; +import IntlProvider from 'renderer/intl_provider'; + type Props = { certificate: Certificate; onOk: () => void; @@ -32,7 +34,7 @@ export default class ShowCertificateModal extends React.PureComponent { return ( -
{descriptor}
+
{descriptor}
); @@ -41,29 +43,12 @@ export default class ShowCertificateModal extends React.PureComponent; return ( -
{descriptor}
+
{descriptor}
{val}
); }; - if (this.state.certificate === null) { - return ( - {}} - > - - - - - ); - } - const utcSeconds = (date: number) => { const d = new Date(0); d.setUTCSeconds(date); @@ -74,18 +59,27 @@ export default class ShowCertificateModal extends React.PureComponent {}} - > - - {'Certificate information'} - - -

{'Details'}

+ + + } + confirmButtonText={ + + } + handleConfirm={this.handleOk} + >
{certificateSection( - - -
- - - - - -
-
- + + ); } } diff --git a/src/renderer/css/_buttons.scss b/src/renderer/css/_buttons.scss new file mode 100644 index 00000000..f1f9262d --- /dev/null +++ b/src/renderer/css/_buttons.scss @@ -0,0 +1,358 @@ +@use "variables"; + +.style--none { + padding: 0; + border: none; + background: transparent; + + &:focus { + outline: 0; + text-decoration: none; + } + + &.btn--block { + width: 100%; + text-align: left; + } + + &:hover, + &:active { + text-decoration: none; + } +} + +.rounded-button { + border-radius: 50%; +} + +button { + .unread-badge { + display: inline-block; + width: 8px; + height: 8px; + border-radius: var(--radius-full); + margin: 0 0 0 40px; + background: #f74343; + } +} + +.btn { + // Important is given to override bootstrap styles on different states + display: inline-flex; + height: 40px; + align-items: center; + justify-content: center; + padding: 0 20px; + border: none; + border-radius: var(--radius-s); + font-size: 14px; + font-weight: 600; + gap: 8px; + outline: none; + transition: all 0.15s ease; + + &.btn-icon { + width: 40px; + min-width: 40px; + padding: 0; + background-color: transparent; + color: rgba(var(--center-channel-color-rgb), var(--icon-opacity)); + + &:hover { + background-color: rgba(var(--center-channel-color-rgb), 0.08); + color: rgba(var(--center-channel-color-rgb), var(--icon-opacity-hover)); + } + + &:active { + background-color: rgba(var(--button-bg-rgb), 0.08); + color: rgba(var(--button-bg-rgb), 1); + } + + i { + font-size: 24px; + } + + &.btn-xs { + width: 24px; + min-width: 24px; + height: 24px; + padding: 0; + font-size: 12px; + gap: 4px; + + &.btn-compact { + width: 20px; + height: 20px; + } + + i { + font-size: 14.4px; + } + } + + &.btn-sm { + width: 32px; + min-width: 32px; + height: 32px; + padding: 0; + font-size: 14px; + gap: 4px; + + &.btn-compact { + width: 28px; + height: 28px; + } + + i { + font-size: 18px; + } + } + + &.btn-lg { + width: 48px; + height: 48px; + padding: 0; + + &.btn-compact { + width: 36px; + height: 36px; + } + + i { + font-size: 31.2px; + } + } + } + + &:not(.a11y--active) { + box-shadow: none; + } + + &:active { + box-shadow: none; + } + + & + .btn { + margin-left: 8px; + } + + &.btn-full { + width: 100%; + } + + i { + display: inline-flex; + width: 16px; + height: 16px; + align-items: center; + justify-content: center; + margin-right: 0; + font-size: 18px; + } + + &.btn-xs { + height: 24px; + padding: 0 10px; + font-size: 11px; + gap: 6px; + + i { + width: 12px; + height: 12px; + font-size: 14.4px; + } + } + + &.btn-sm { + height: 32px; + padding: 0 16px; + font-size: 12px; + gap: 6px; + + i { + width: 12px; + height: 12px; + font-size: 14.4px; + } + } + + &.btn-lg { + height: 48px; + padding: 0 24px; + font-size: 16px; + gap: 10px; + + i { + width: 20px; + height: 20px; + font-size: 24px; + } + } + + &.btn-link { + padding: 0; + border: none; + background: transparent; + color: rgba(var(--button-bg-rgb), 1); + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + + &.btn-primary { + position: relative; + border-color: transparent; + background-color: rgb(var(--button-bg-rgb)); + color: rgb(var(--button-color-rgb)); + + // These hover and active values are for things outside the app__body, the correct theme styles for the primary button are applied in utils.jsx + &:hover { + background-color: #1a51c8; + } + + &:active, + &:focus { + background-color: #184ab6; + } + + &:disabled, + &:disabled:hover, + &:disabled:active { + background: rgba(var(--center-channel-color-rgb), 0.08); + color: rgba(var(--center-channel-color-rgb), 0.32); + opacity: 1; + } + } + + &.btn-secondary { + border: 1px solid rgb(var(--button-bg-rgb)); + background: transparent; + color: rgb(var(--button-bg-rgb)); + + &.btn-danger { + border-color: currentColor; + background: transparent; + color: var(--error-text); + + &:hover { + border-color: currentColor; + background-color: rgba(var(--error-text-color-rgb), 0.08); + color: var(--error-text); + } + + &:active, + &:focus { + border-color: currentColor; + background-color: rgba(var(--error-text-color-rgb), 0.16); + color: var(--error-text); + } + } + + &:hover { + background-color: rgb(var(--button-bg-rgb), 0.08); + } + + &:active { + background-color: rgb(var(--button-bg-rgb), 0.16); + } + } + + &.btn-tertiary { + background: rgba(var(--button-bg-rgb), 0.08); + color: rgb(var(--button-bg-rgb)); + + &:hover { + background-color: rgb(var(--button-bg-rgb), 0.12); + } + + &:active { + background-color: rgb(var(--button-bg-rgb), 0.16); + outline: none; + } + + &:disabled, + &:disabled:hover, + &:disabled:active { + background: rgba(var(--center-channel-color-rgb), 0.08); + color: rgba(var(--center-channel-color-rgb), 0.32); + opacity: 1; + } + + &.btn-danger { + background-color: rgba(var(--error-text-color-rgb), 0.08); + color: var(--error-text); + + &:hover { + background-color: rgba(var(--error-text-color-rgb), 0.12); + color: var(--error-text); + } + + &:active, + &:focus { + background-color: rgba(var(--error-text-color-rgb), 0.16); + color: var(--error-text); + } + } + } + + &.btn-quaternary { + background: transparent; + color: rgb(var(--button-bg-rgb)); + + &:hover { + background: rgba(var(--button-bg-rgb), 0.08); + } + + &:active { + background-color: rgb(var(--button-bg-rgb), 0.12); + } + } + + &.btn-danger { + background: var(--error-text); + color: variables.$white; + + .app__body & { + color: variables.$white; + + &:hover, + &:focus, + &:active { + color: variables.$white; + } + } + + &:hover, + &:focus, + &:active { + color: variables.$white; + } + } + + &.btn-transparent { + padding: 7px 12px; + border: none; + background: transparent; + } + + &.btn-inactive { + border-color: transparent; + background: variables.$light-gray; + color: variables.$white; + } + + .fa { + margin-right: 3px; + + &.margin-right { + margin-right: 6px; + } + + &.margin-left { + margin-left: 6px; + } + } +} diff --git a/src/renderer/css/_css_variables.scss b/src/renderer/css/_css_variables.scss index 25065709..9b63b94a 100644 --- a/src/renderer/css/_css_variables.scss +++ b/src/renderer/css/_css_variables.scss @@ -17,6 +17,19 @@ --elevation-5: 0 12px 32px 0 rgba(0, 0, 0, 0.12); --elevation-6: 0 20px 32px 0 rgba(0, 0, 0, 0.12); + /* Corner Radius variables */ + --radius-xs: 2px; + --radius-s: 4px; + --radius-m: 8px; + --radius-l: 12px; + --radius-xl: 16px; + --radius-full: 50%; + + /* Border variables */ + --border-default: solid 1px rgba(var(--center-channel-color-rgb), 0.12); + --border-light: solid 1px rgba(var(--center-channel-color-rgb), 0.08); + --border-dark: solid 1px rgba(var(--center-channel-color-rgb), 0.16); + /* Denim - used for light mode */ --away-indicator-rgb: 255, 188, 31; --button-bg-rgb: 28, 88, 217; @@ -69,7 +82,7 @@ --mention-highlight-link: #1b1d22; } -.LoadingScreen--darkMode, .ErrorView.darkMode { +.darkMode .Modal, .LoadingScreen--darkMode, .ErrorView.darkMode { /* Onyx - used for dark mode*/ --away-indicator-rgb: 245, 171, 0; --button-bg-rgb: 74, 124, 232; diff --git a/src/renderer/css/_mixins.scss b/src/renderer/css/_mixins.scss index d2e24ab6..1fd55f45 100644 --- a/src/renderer/css/_mixins.scss +++ b/src/renderer/css/_mixins.scss @@ -79,3 +79,15 @@ transform: translate3d(4px, 0, 0); } } + +@mixin font-smoothing($value: antialiased) { + @if $value == antialiased { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + @else { + -webkit-font-smoothing: subpixel-antialiased; + -moz-osx-font-smoothing: auto; + } +} diff --git a/src/renderer/css/_typography.scss b/src/renderer/css/_typography.scss new file mode 100644 index 00000000..96048299 --- /dev/null +++ b/src/renderer/css/_typography.scss @@ -0,0 +1,148 @@ +@use "mixins"; +@use "variables"; + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-300.woff2') format('woff2'); + } + + @font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-300italic.woff2') format('woff2'); + } + + @font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-regular.woff2') format('woff2'); + } + + @font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-italic.woff2') format('woff2'); + } + + @font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-600.woff2') format('woff2'); + } + + @font-face { + font-family: 'Open Sans'; + font-style: italic; + 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'); +} + +@font-face { + font-family: 'FontAwesome'; + font-style: normal; + font-weight: 300; + src: url('../../../assets/fonts/fontawesome-webfont.woff') format('woff'); +} + +b, +strong { + font-weight: 600; +} + +a { + color: variables.$primary-color; + cursor: pointer; + text-decoration: none; + word-break: break-word; + + &:focus, + &:hover { + color: variables.$primary-color--hover; + } +} + +body { + @include mixins.font-smoothing; + + font-family: 'Open Sans', sans-serif; +} + +h1, +h2, +h3 { + font-family: Metropolis, sans-serif; +} + +h1 { + font-weight: 600; +} + +label { + &.has-error { + color: variables.$red; + font-weight: normal; + } +} + +.small { + font-size: 12px; +} + +.light { + opacity: 0.73; +} + +ul, +ol { + padding-left: 22px; + margin-top: 3px; + margin-bottom: 0.11em; +} diff --git a/src/renderer/css/_variables.scss b/src/renderer/css/_variables.scss index e75a61d5..8b55102e 100644 --- a/src/renderer/css/_variables.scss +++ b/src/renderer/css/_variables.scss @@ -1,2 +1,11 @@ +@use 'sass:color'; + // Since they can be used on any modal, menu or popover for now they are highest -$z-index-tooltip: 1350; \ No newline at end of file +$z-index-tooltip: 1350; + +// Color Variables +$primary-color: #166de0; +$primary-color--hover: color.adjust($primary-color, $lightness: -10%); +$white: rgb(255, 255, 255); +$light-gray: rgba(0, 0, 0, 0.15); +$red: rgb(214, 73, 70); \ No newline at end of file diff --git a/src/renderer/css/components/Button.scss b/src/renderer/css/components/Button.scss index a5d4663f..9ae535c7 100644 --- a/src/renderer/css/components/Button.scss +++ b/src/renderer/css/components/Button.scss @@ -119,7 +119,7 @@ &:disabled { background: none; - color: rgba(var(--center-channel-text-rgb), 0.32); + color: rgba(var(--center-channel-color-rgb), 0.32); cursor: not-allowed; } diff --git a/src/renderer/css/components/CertificateModal.css b/src/renderer/css/components/CertificateModal.css deleted file mode 100644 index 3d776fbb..00000000 --- a/src/renderer/css/components/CertificateModal.css +++ /dev/null @@ -1,210 +0,0 @@ -.certificate-modal .modal, -.show-certificate .modal { - background-color: aliceblue; - margin-top: 40px; -} - -.show-certificate .modal-dialog { - width: 800px; -} -.modal-header { - border-bottom: 0px; -} -.modal-header::after { - border-bottom: solid 1px #E5E5E5; - width: 100%; - transform: translateY(15px); - } -.modal-footer::before { - border-top: solid 1px #E5E5E5; - width: 100%; - transform: translateY(-15px); -} -.modal-body :last-child { - margin-bottom: 0; -} - -.certificate-modal .col-sm-4 { - padding: 0px; - text-align: left; -} -.certificate-modal .col-sm-8 { - padding: 0px; -} - -.certificate-list thead { - width: 557.89px; - -} - -.certificate-list thead>tr { - padding: 0px 5px; -} - -.certificate-list thead>tr>th { - font-family: Helvetica Neue; - font-style: normal; - font-weight: normal; - font-size: 12px; - line-height: 18px; - padding: 3px 5px; - border-bottom: 1px solid #CCCCCC; - color: #333333; - height: 22px; -} - -.certificate-list thead tr th:first-child span { - padding-left: 5px; -} -.certificate-list thead tr th span { - border-right: solid 1px #E5E5E5; - display: block; -} -.certificate-list thead tr th:last-child span { - border-right: none; -} - -.certificate-list tbody tr { - user-select: none; - cursor: pointer; - color: #555555; -} - -.certificate-list tbody>tr>td { - max-width: 165px; - height: 47px; - font-style: normal; - font-weight: normal; - font-size: 14px; - line-height: 17px; - padding: 15px 10px; - white-space: nowrap; - overflow-x: hidden; - text-overflow: ellipsis; -} - -.certificate-list tbody tr td:first-child { - padding-left: 15px; - max-width: 227px; -} - -.certificate-list tbody tr.selected { - background: #457AB2; - color: #FFFFFF; -} - -table.certificate-list { - background: #FFFFFF; - border: 1px solid #CCCCCC; - border-radius: 4px; - border-collapse: unset; - box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.0008); - height: 150px; -} - -table.certificate-list:focus { - border: 1px solid #66AFE9; - box-sizing: border-box; - box-shadow: 0px 0px 8px rgba(102, 175, 233, 0.6), inset 1px 1px 0px rgba(0, 0, 0, 0.00075); -} - - -.show-certificate button, -.certificate-modal button { - background: #FFFFFF; - border: 1px solid #CCCCCC; - box-sizing: border-box; - border-radius: 4px; - padding: 9px 12px; - line-height: 16px; -} - -.show-certificate button:disabled, -.certificate-modal button:disabled { - opacity: 0.5; -} - -.show-certificate button.primary, -.certificate-modal button.primary { - background: #457AB2; - color: #FFFFFF; - border: 1px solid #2E6DA4; -} - - -.show-certificate button.primary:hover, -.certificate-modal button.primary:hover { - background: #659AD2; -} - -.certificate-modal button.info { - color: #457AB2; -} - -.certificate-modal button.info:disabled { - color: #000; -} - -.show-certificate .subtitle, -.certificate-modal .subtitle { - color: #737373; - margin: 0px 0px 15px 0px; -} - -.show-certificate .no-border, -.certificate-modal .no-border { - border: none; -} - -.show-certificate dl { - overflow-y: auto; -} - -.show-certificate dt, dd { - float: left; - margin: 5px; -} - -.show-certificate dt { - width: 150px; - clear:both -} - -.show-certificate dd { - white-space: nowrap; - text-overflow: ellipsis; - overflow-x: hidden; -} - -.certificate-key { - font-style: normal; - font-weight: bold; - font-size: 12px; - line-height: 15px; - color: #333333; - text-align: right; -} -.certificate-value { - padding-top: 1px; - font-style: normal; - font-weight: normal; - font-size: 12px; - line-height: 14px; - text-align: left; - color: #333333; -} - -.certificate-section { - border-bottom: 1px solid #E5E5E5; - width: 598px; - height: 8px; -} - -.show-certificate .details { - margin: 15px; - font-weight: bold; - font-style: normal; - font-size: 12px; - line-height: 15px; - color: #333333; -} \ No newline at end of file diff --git a/src/renderer/css/components/CertificateModal.scss b/src/renderer/css/components/CertificateModal.scss new file mode 100644 index 00000000..edc98d8b --- /dev/null +++ b/src/renderer/css/components/CertificateModal.scss @@ -0,0 +1,50 @@ +@use "../css_variables"; + +.Modal .CertificateModal { + width: 832px; + max-width: 832px; +} + +.CertificateModal_certInfoButton { + margin-right: auto +} + +.CertificateModal_list { + text-align: left; + border: 1px solid rgba(var(--center-channel-color-rgb), 0.12); + border-radius: var(--radius-s); + border-spacing: 0; + width: 100%; + + th, td { + padding: 10px 12px; + } + + th { + background-color: rgba(var(--center-channel-color-rgb), 0.08); + + & + th { + border-left: 1px solid rgba(var(--center-channel-color-rgb), 0.12); + } + } + + td { + border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.12); + + & + td { + border-left: 1px solid rgba(var(--center-channel-color-rgb), 0.12); + } + } + + tbody tr { + cursor: pointer; + + &:hover { + background-color: rgba(var(--center-channel-color-rgb), 0.16); + } + + &.selected { + background-color: rgba(var(--button-bg-rgb), 0.12); + } + } +} \ No newline at end of file diff --git a/src/renderer/css/components/ConfigureServer.scss b/src/renderer/css/components/ConfigureServer.scss index 85945353..aad6f92e 100644 --- a/src/renderer/css/components/ConfigureServer.scss +++ b/src/renderer/css/components/ConfigureServer.scss @@ -8,7 +8,7 @@ font-family: 'Open Sans'; .alternate-link__message { - color: var(--center-channel-text); + color: var(--center-channel-color); font-size: 12px; font-weight: 400; line-height: 16px; @@ -117,7 +117,7 @@ .ConfigureServer__card { width: 540px; box-sizing: border-box; - border: 1px solid rgba(var(--center-channel-text-rgb), 0.08); + border: 1px solid rgba(var(--center-channel-color-rgb), 0.08); margin-left: 60px; background-color: var(--center-channel-bg); border-radius: 8px; @@ -143,6 +143,7 @@ font-weight: 600; line-height: 28px; margin-bottom: 32px; + margin-top: 0; } .ConfigureServer__card-form { diff --git a/src/renderer/css/components/Header.scss b/src/renderer/css/components/Header.scss index 7c05105e..74edf4d9 100644 --- a/src/renderer/css/components/Header.scss +++ b/src/renderer/css/components/Header.scss @@ -6,6 +6,7 @@ width: 100%; min-height: 100px; padding: 0 40px; + box-sizing: border-box; .Header__main { display: flex; diff --git a/src/renderer/css/components/Input.scss b/src/renderer/css/components/Input.scss index 94d0f5b6..060b2a76 100644 --- a/src/renderer/css/components/Input.scss +++ b/src/renderer/css/components/Input.scss @@ -34,6 +34,7 @@ font-size: 14px; line-height: 20px; outline: 0; + font-family: 'Open Sans', sans-serif; &::placeholder { color: rgba(var(--center-channel-color-rgb), 0.64); diff --git a/src/renderer/css/components/LoadingScreen.css b/src/renderer/css/components/LoadingScreen.css index 50c3673f..57a77ff3 100644 --- a/src/renderer/css/components/LoadingScreen.css +++ b/src/renderer/css/components/LoadingScreen.css @@ -1,5 +1,6 @@ body { background-color: transparent; + margin: 0; } .LoadingScreen { diff --git a/src/renderer/css/components/Modal.scss b/src/renderer/css/components/Modal.scss new file mode 100644 index 00000000..fb9015ef --- /dev/null +++ b/src/renderer/css/components/Modal.scss @@ -0,0 +1,464 @@ +@use '../css_variables'; +@use "../buttons"; +@use "../typography"; + +@import '~@mattermost/compass-icons/css/compass-icons.css'; + +body { + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + + button { + font-family: inherit; + + &:not(:disabled) { + cursor: pointer; + } + } + + background-color: transparent; +} + +.Modal { + position: fixed; + top: 0; + left: 0; + z-index: 1050; + height: 100%; + width: 100%; + color: var(--center-channel-color); + outline: 0; + display: flex; + align-items: center; + + .Modal_body { + overflow: auto; + max-height: calc(90vh - 80px); + padding: 2px 32px; + font-size: 14px; + + &.overflow--visible { + overflow: visible; + } + + &.divider { + border-top: var(--border-light); + } + + .form-control { + height: 40px; + box-sizing: border-box; + border: var(--border-default); + border-radius: 4px; + + &:focus { + border-color: var(--button-bg); + } + + &.has-error { + border-color: var(--error-text); + } + } + + .input-wrapper { + position: relative; + } + + .input-clear { + top: 12px; + right: 12px; + width: 16px; + height: 16px; + color: var(--center-channel-color); + font-size: 18px; + font-style: normal; + font-weight: normal; + line-height: 16px; + opacity: 0.48; + } + } + + .Modal_footer { + display: flex; + flex-wrap: wrap; + justify-content: end; + padding: 24px 32px; + border: none; + grid-row-gap: 8px; + + &.divider { + border-top: var(--border-light); + } + + &.Modal_footer--invisible { + overflow: hidden; + height: 0; + padding: 0; + } + + .error-text { + margin: 10px; + float: left; + font-weight: bold; + text-align: left; + } + + & .btn + .btn { + margin-left: 8px; + } + } + + + textarea { + overflow-x: hidden; + } + + .custom-textarea { + padding: 12px 30px 12px 15px; + } + + .info__label { + margin-bottom: 3px; + font-size: 0.9em; + font-weight: 600; + opacity: 0.75; + } + + .info__value { + padding-left: 10px; + word-break: break-word; + + p { + white-space: pre-wrap; + } + } + + .Modal_dialog { + max-width: 95%; + margin-right: auto; + margin-bottom: 0; + margin-left: auto; + position: relative; + width: auto; + max-width: 500px; + + &.Modal_xl { + width: 800px; + } + } + + .Modal_push-down { + margin-top: 60px; + } + + .Modal_next-bar { + position: absolute; + top: 0; + right: 0; + height: 100%; + } + + .Modal_header { + display: flex; + min-height: 76px; + align-items: center; + justify-content: space-between; + padding: 16px 64px 16px 32px; + border: none; + background: transparent; + box-sizing: border-box; + + button.close { + border: 0; + background-color: transparent; + float: right; + line-height: 1; + text-transform: none; + overflow: visible; + padding: 0; + margin: 0; + margin-top: -2px; + + .sr-only { + position: absolute; + clip: rect(0,0,0,0); + } + } + + &.divider { + border-bottom: var(--border-light); + } + + &::before, + &::after { + content: none; + } + + .Modal_header-back-button { + margin-left: -12px; + } + + .btn { + position: relative; + top: -2px; + } + + .close { + position: absolute; + top: 18px; + right: 18px; + width: 40px; + height: 40px; + border-radius: var(--radius-s); + color: rgba(var(--center-channel-color-rgb), 0.64); + font-size: 30px; + font-weight: 400; + opacity: 1; + text-shadow: none; + + &.icon-close { + font-size: 24px; + } + + &:hover { + background: rgba(var(--center-channel-color-rgb), 0.08); + color: rgba(var(--center-channel-color-rgb), 0.8); + } + + &:active { + background: rgba(var(--button-bg-rgb), 0.08); + color: var(--button-bg); + } + + .Modal_title { + width: 100%; + background: transparent; + color: var(--center-channel-color); + font-size: 22px; + line-height: 28px; + } + } + + .Modal_title { + width: 100%; + background: transparent; + color: var(--center-channel-color); + font-size: 22px; + line-height: 28px; + word-break: break-word; + } + } + + .Modal_content { + border: var(--border-default); + border-radius: var(--radius-l); + background: var(--center-channel-bg); + } + + .Modal_chevron-icon { + top: 50%; + font-size: 120%; + } + + .Modal_prev-bar { + position: absolute; + top: 0; + left: 0; + height: 100%; + } + + .Modal_overflow { + .Modal_body { + overflow: visible; + } + } + + .no-header__img { + margin-top: -40px; + } + + .Modal_header { + .Modal__header__text_container { + margin-top: 8px; + width: 100%; + + .Modal_subheading-container { + color: rgba(var(--center-channel-color-rgb), 0.75); + } + } + + p#Modal_subHeading { + font-size: 12px; + margin-block: 10px; + } + } + + .Modal_body { + max-height: 100%; + padding: 0; + + &.divider { + border-top: var(--border-light); + } + + .form-control { + height: 40px; + box-sizing: border-box; + border: var(--border-default); + border-radius: 4px; + + &:focus { + border-color: var(--button-bg); + } + + &.has-error { + border-color: var(--error-text); + } + } + + .MaxLengthInput { + &.form-control.has-error { + padding-right: 66px; + } + + &__validation { + position: absolute; + top: 12px; + right: 56px; + color: var(--error-text); + font-size: 14px; + font-style: normal; + font-weight: normal; + line-height: 16px; + } + } + + .input-wrapper { + position: relative; + } + + .input-clear { + top: 12px; + right: 12px; + width: 16px; + height: 16px; + color: var(--center-channel-color); + font-size: 18px; + font-style: normal; + font-weight: normal; + line-height: 16px; + opacity: 0.48; + } + } + + &__header h1 { + margin-top: 0; + margin-bottom: 0; + } + + &__compassDesign { + .Modal_content { + .Modal_body { + .Modal__body { + padding: 0; + + &.padding { + padding: 0px 32px; + } + } + } + + .Modal_error { + display: flex; + box-sizing: border-box; + flex: 1; + padding: 14px 32px; + border: 1px solid rgba(var(--dnd-indicator-rgb), 0.16); + margin-bottom: 24px; + background: rgba(var(--dnd-indicator-rgb), 0.08); + border-radius: var(--radius-s); + + span { + color: var(--center-channel-color); + font-size: 14px; + font-weight: 600; + line-height: 24px; + } + + i { + margin-right: 8px; + color: var(--error-text); + font-size: 24px; + line-height: 24px; + + &::before { + margin: 0; + } + } + } + } + + @media screen and (max-width: 640px) { + margin: 0; + + .Modal_header { + box-shadow: var(--elevation-2); + } + } + } + + &.fade { + .Modal_dialog { + transition: transform .3s ease-out,-webkit-transform .3s ease-out; + transform: translate(0, -50px); + } + } + + &.show .Modal_dialog { + transform: none; + } + + .Modal_body.divider .Input_container { + margin-top: 24px; + } + + .Input_container + .Input_container { + margin-top: 24px; + } +} + +.fade { + transition: opacity 0.15s linear; + + &:not(.show) { + opacity: 0; + } +} + +.btn.btn-primary.btn-danger:hover { + background: linear-gradient(0deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.08)), var(--error-text); +} + +.Modal_backdrop { + box-sizing: border-box; + position: fixed; + top: 0; + left: 0; + z-index: 1040; + width: 100vw; + height: 100vh; + background-color: #000; + + &.fade { + opacity: 0; + } + + &.show { + opacity: 0.5; + } + + &.in { + opacity: 0.64; + } +} diff --git a/src/renderer/css/components/NewServerModal.scss b/src/renderer/css/components/NewServerModal.scss index 152ac583..f2c7fdc3 100644 --- a/src/renderer/css/components/NewServerModal.scss +++ b/src/renderer/css/components/NewServerModal.scss @@ -1,38 +1,34 @@ -.NewServerModal-noBottomSpace { - padding-bottom: 0px; - margin-bottom: 0px; -} +@use "../css_variables"; -.NewServerModal-validation { - margin-top: 8px; - margin-right: auto; +.Modal .NewServerModal { + width: 600px; + max-width: 600px; + + hr { + margin-top: 24px; + border: 0; + border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08); + } + + .Modal_body { + margin-bottom: 12px; + } +} + + +.NewServerModal__toggle__description { + display: block; + margin-top: 0.25rem; + font-weight: 400; font-size: 12px; - display: flex; - flex-direction: column; - - > div { - margin-top: 4px; - > span { - margin-left: 4px; - } - } - - .error { - color: #d24b4e; - } - - .warning { - color: #c79e3f; - } - - .success { - color: #06d6a0; - } + line-height: 16px; + max-width: 400px; } -.NewServerModal-validationSpinner { - width: 0.75rem; - height: 0.75rem; - margin-left: 2px; - margin-right: 4px; +.NewServerModal__permissions__title { + margin-top: 24px; + margin-bottom: 12px; + font-size: 16px; + font-weight: 600; + line-height: 24px; } \ No newline at end of file diff --git a/src/renderer/css/components/Toggle.scss b/src/renderer/css/components/Toggle.scss index c5af8f85..579e90a3 100644 --- a/src/renderer/css/components/Toggle.scss +++ b/src/renderer/css/components/Toggle.scss @@ -4,10 +4,11 @@ display: flex; column-gap: 12px; font-weight: inherit; - line-height: 16px; + line-height: 24px; cursor: pointer; margin-bottom: 0; padding: 10px 0; + font-size: 16px; &.disabled { cursor: default; @@ -63,6 +64,10 @@ &.disabled { background-color: var(--button-bg-30); } - } + } + } + + i { + font-size: 20px; } } \ No newline at end of file diff --git a/src/renderer/css/components/WelcomeScreenSlide.scss b/src/renderer/css/components/WelcomeScreenSlide.scss index da1da804..3f0ee3ff 100644 --- a/src/renderer/css/components/WelcomeScreenSlide.scss +++ b/src/renderer/css/components/WelcomeScreenSlide.scss @@ -23,7 +23,7 @@ } .WelcomeScreenSlide__subtitle { - color: rgba(var(--center-channel-text-rgb), 0.72); + color: rgba(var(--center-channel-color-rgb), 0.72); font-family: Open Sans; font-size: 16px; font-weight: 400; diff --git a/src/renderer/css/components/index.css b/src/renderer/css/components/index.css index a21c3dbd..465125f6 100644 --- a/src/renderer/css/components/index.css +++ b/src/renderer/css/components/index.css @@ -4,6 +4,5 @@ @import url("PermissionRequestDialog.css"); @import url("TabBar.css"); @import url("UpdaterPage.css"); -@import url("CertificateModal.css"); @import url("LoadingScreen.css"); @import url("LoadingAnimation.css"); diff --git a/src/renderer/css/modals-dark.scss b/src/renderer/css/modals-dark.scss index a4857e98..66774c90 100644 --- a/src/renderer/css/modals-dark.scss +++ b/src/renderer/css/modals-dark.scss @@ -4,6 +4,18 @@ @include meta.load-css('bootstrap-dark/src/bootstrap-dark.css'); color: #fff; + > div.modal { + color: #fff; + + .modal-header .modal-title, .modal-header .close { + color: #fff; + } + + .modal-content { + background-color: #191B1F; + } + } + .SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__control { background: #242a30; } @@ -34,5 +46,52 @@ .Toggle___switch.disabled { background: rgba(var(--center-channel-bg-rgb), 0.08); } + + .Input { + color: var(--button-color); + background-color: unset; + + &::placeholder { + color: rgba(var(--button-color-rgb), 0.56); + } + } + + .Input_wrapper { + color: rgba(var(--button-color-rgb), 0.56); + } + + .Input___info { + color: rgba(var(--button-color-rgb), 0.56); + } + + .Input_fieldset { + background-color: #191B1F; + border: 1px solid rgba(#fff, 0.16); + + &:hover { + border-color: rgba(#fff, 0.48); + } + + &:focus-within { + border-color: #fff; + box-shadow: inset 0 0 0 1px #fff; + color: var(--button-color); + + .Input_legend { + color: var(--button-color); + } + } + } + + .Input_legend { + background-color: #191B1F; + color: rgba(var(--button-color-rgb), 0.64); + } + + &.disabled { + .Input_fieldset { + background: rgba(var(--button-color-rgb), 0.08); + } + } } diff --git a/src/renderer/modals/certificate/certificate.tsx b/src/renderer/modals/certificate/certificate.tsx index 3e9739a5..fd173cff 100644 --- a/src/renderer/modals/certificate/certificate.tsx +++ b/src/renderer/modals/certificate/certificate.tsx @@ -5,10 +5,6 @@ import type {Certificate} from 'electron/renderer'; import React from 'react'; import ReactDOM from 'react-dom'; -import 'bootstrap/dist/css/bootstrap.min.css'; -import 'renderer/css/modals.css'; -import 'renderer/css/components/CertificateModal.css'; - import type {CertificateModalInfo} from 'types/modals'; import SelectCertificateModal from './certificateModal'; diff --git a/src/renderer/modals/certificate/certificateModal.tsx b/src/renderer/modals/certificate/certificateModal.tsx index bcf981db..b0fa59d2 100644 --- a/src/renderer/modals/certificate/certificateModal.tsx +++ b/src/renderer/modals/certificate/certificateModal.tsx @@ -1,18 +1,21 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import classNames from 'classnames'; import type {Certificate} from 'electron/renderer'; import React, {Fragment} from 'react'; -import {Modal, Button, Table, Row, Col} from 'react-bootstrap'; import {FormattedMessage} from 'react-intl'; +import {Modal} from 'renderer/components/Modal'; import IntlProvider from 'renderer/intl_provider'; import ShowCertificateModal from '../../components/showCertificateModal'; +import 'renderer/css/components/CertificateModal.scss'; + type Props = { onSelect: (cert: Certificate) => void; - onCancel?: () => void; + onCancel: () => void; getCertInfo: () => Promise<{url: string; list: Certificate[]}>; } @@ -117,107 +120,99 @@ export default class SelectCertificateModal extends React.PureComponent + + + + + ); + return ( {}} + onExited={this.props.onCancel} + modalHeaderText={ + + } + modalSubheaderText={ + + } + footerContent={footerContent} > - - - - - - -

- -

- - - - - - - - - - {this.renderCerts(this.state.list!)} - - -
- - - - - -
-
- -
- - - - - - - - - -
-
+ + + + + + + + + + {this.renderCerts(this.state.list!)} + +
+ + + + + +
); diff --git a/src/renderer/modals/darkMode.ts b/src/renderer/modals/darkMode.ts index e270e6b2..495c511a 100644 --- a/src/renderer/modals/darkMode.ts +++ b/src/renderer/modals/darkMode.ts @@ -1,8 +1,6 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import 'renderer/css/modals-dark.scss'; - export default function addDarkModeListener() { const setDarkMode = (darkMode: boolean) => { if (darkMode) { diff --git a/src/renderer/modals/editServer/editServer.tsx b/src/renderer/modals/editServer/editServer.tsx index 576cf6be..cef54fd6 100644 --- a/src/renderer/modals/editServer/editServer.tsx +++ b/src/renderer/modals/editServer/editServer.tsx @@ -1,9 +1,6 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import 'bootstrap/dist/css/bootstrap.min.css'; -import 'renderer/css/modals.css'; - import React, {useEffect, useState} from 'react'; import ReactDOM from 'react-dom'; diff --git a/src/renderer/modals/loadingScreen/index.tsx b/src/renderer/modals/loadingScreen/index.tsx index 59114635..ffe80fe9 100644 --- a/src/renderer/modals/loadingScreen/index.tsx +++ b/src/renderer/modals/loadingScreen/index.tsx @@ -6,8 +6,6 @@ import ReactDOM from 'react-dom'; import LoadingScreen from '../../components/LoadingScreen'; -import 'bootstrap/dist/css/bootstrap.min.css'; -import 'renderer/css/modals.css'; import 'renderer/css/components/LoadingAnimation.css'; import 'renderer/css/components/LoadingScreen.css'; diff --git a/src/renderer/modals/login/login.tsx b/src/renderer/modals/login/login.tsx index 2df97d33..5da4f402 100644 --- a/src/renderer/modals/login/login.tsx +++ b/src/renderer/modals/login/login.tsx @@ -9,9 +9,6 @@ import IntlProvider from 'renderer/intl_provider'; import type {LoginModalInfo} from 'types/modals'; -import 'bootstrap/dist/css/bootstrap.min.css'; -import 'renderer/css/modals.css'; - import LoginModal from './loginModal'; import setupDarkMode from '../darkMode'; diff --git a/src/renderer/modals/login/loginModal.tsx b/src/renderer/modals/login/loginModal.tsx index 76fe8c3f..68105b61 100644 --- a/src/renderer/modals/login/loginModal.tsx +++ b/src/renderer/modals/login/loginModal.tsx @@ -4,11 +4,12 @@ import type {AuthenticationResponseDetails, AuthInfo} from 'electron/renderer'; import React from 'react'; -import {Button, Col, FormLabel, Form, FormGroup, FormControl, Modal} from 'react-bootstrap'; import type {IntlShape} from 'react-intl'; import {FormattedMessage, injectIntl} from 'react-intl'; import {parseURL} from 'common/utils/url'; +import Input, {SIZE} from 'renderer/components/Input'; +import {Modal} from 'renderer/components/Modal'; import type {LoginModalInfo} from 'types/modals'; @@ -44,8 +45,7 @@ class LoginModal extends React.PureComponent { this.setState({request, authInfo}); }; - handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); + handleSubmit = () => { this.props.onLogin(this.state.request!, this.state.username, this.state.password); this.setState({ username: '', @@ -55,8 +55,7 @@ class LoginModal extends React.PureComponent { }); }; - handleCancel = (event: React.MouseEvent) => { - event.preventDefault(); + handleCancel = () => { this.props.onCancel(this.state.request!); this.setState({ username: '', @@ -101,94 +100,44 @@ class LoginModal extends React.PureComponent { return ( + } + handleConfirm={this.handleSubmit} + confirmButtonText={ + + } + handleCancel={this.handleCancel} + modalSubheaderText={this.renderLoginModalMessage()} > - - - - - - -

- {this.renderLoginModalMessage()} -

-
- - - - - - ) => { - e.stopPropagation(); - }} - /> - - - - - - - - ) => { - e.stopPropagation(); - }} - /> - - - - -
- - { ' ' } - -
- -
-
-
+ +
); } diff --git a/src/renderer/modals/newServer/newServer.tsx b/src/renderer/modals/newServer/newServer.tsx index 4f086554..f9171257 100644 --- a/src/renderer/modals/newServer/newServer.tsx +++ b/src/renderer/modals/newServer/newServer.tsx @@ -1,9 +1,6 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import 'bootstrap/dist/css/bootstrap.min.css'; -import 'renderer/css/modals.css'; - import React, {useEffect, useState} from 'react'; import ReactDOM from 'react-dom'; @@ -42,7 +39,8 @@ const NewServerModalWrapper: React.FC = () => { return ( ; - handleGrant: React.MouseEventHandler; + handleDeny: () => void; + handleGrant: () => void; getPermissionInfo: () => Promise; openExternalLink: (protocol: string, url: string) => void; intl: IntlShape; @@ -67,74 +67,52 @@ class PermissionModal extends React.PureComponent { }; return ( -
-

- - {} -

-

- ( - + +

+ ( + - {msg} - - ), - }} - /> -

-
+ onClick={click} + href='#' + > + {msg} + + ), + }} + /> + ); } render() { return ( {}} + show={Boolean(this.state.url && this.state.permission)} + onExited={() => {}} + modalHeaderText={this.getModalTitle()} + handleConfirm={this.props.handleGrant} + confirmButtonText={ + + } + handleCancel={this.props.handleDeny} > - - {this.getModalTitle()} - - - {this.getModalBody()} - - -
- - -
-
+ {this.getModalBody()}
); } diff --git a/src/renderer/modals/removeServer/removeServer.tsx b/src/renderer/modals/removeServer/removeServer.tsx index a64e0376..72ec2ec9 100644 --- a/src/renderer/modals/removeServer/removeServer.tsx +++ b/src/renderer/modals/removeServer/removeServer.tsx @@ -1,10 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import 'bootstrap/dist/css/bootstrap.min.css'; -import 'renderer/css/modals.css'; - -import React, {useEffect, useState} from 'react'; +import React from 'react'; import ReactDOM from 'react-dom'; import IntlProvider from 'renderer/intl_provider'; @@ -23,19 +20,10 @@ const onSave = (data: boolean) => { }; const RemoveServerModalWrapper: React.FC = () => { - const [serverName, setServerName] = useState(''); - - useEffect(() => { - window.desktop.modals.getModalInfo<{name: string}>().then(({name}) => { - setServerName(name); - }); - }, []); - return ( { onClose(); }} diff --git a/src/renderer/modals/settings/settings.tsx b/src/renderer/modals/settings/settings.tsx index 2925bef5..54149b94 100644 --- a/src/renderer/modals/settings/settings.tsx +++ b/src/renderer/modals/settings/settings.tsx @@ -4,6 +4,7 @@ import 'bootstrap/dist/css/bootstrap.min.css'; import 'renderer/css/index.css'; import 'renderer/css/settings.css'; +import 'renderer/css/modals-dark.scss'; import React from 'react'; import ReactDOM from 'react-dom'; diff --git a/src/renderer/modals/welcomeScreen/welcomeScreen.tsx b/src/renderer/modals/welcomeScreen/welcomeScreen.tsx index 962fbd35..8cab8e68 100644 --- a/src/renderer/modals/welcomeScreen/welcomeScreen.tsx +++ b/src/renderer/modals/welcomeScreen/welcomeScreen.tsx @@ -11,8 +11,6 @@ import type {UniqueServer} from 'types/config'; import ConfigureServer from '../../components/ConfigureServer'; import WelcomeScreen from '../../components/WelcomeScreen'; -import 'bootstrap/dist/css/bootstrap.min.css'; - const MOBILE_SCREEN_WIDTH = 1200; const onConnect = (data: UniqueServer) => {