[MM-22239] Downloads dropdown (#2227)

* WIP: show/hide temp downloads dropdown

* WIP: Position downloads dropdown correctly under the button

* WIP: Use correct width for dropdown so that right radius and shadows are displayed

* WIP: Add items to download list after finished downloading

* WIP: Add download item base components

* Add "clear all" functionality

* Use type Record<> for downloads saved in config

* Add styling to files in the downloads dropdown

* Open file in folder when clicking it from downloads dropdown. Center svg in parent element

* Update scrollbar styling

* Update scrollbar styling

* Update state of downloaded items if deleted from folder

* Add progress bar in downloads

* Use "x-uncompressed-content-length" in file downloads.

* Keep downloads open when clicking outside their browserview

* Use correct color for downloads dropdown button

* Add better styling to downloads dropdown button

* Allow only 50 download files maximum. Oldest file is being removed if reached

* Autoclose downloads dropdown after 4s of download finish

* Add file thumbnails

* Dont show second dialog if first dismissed

* Add red badge when downloads running and dropdown closed

* Add menu item for Downloads

* Add support for more code file extensions

* Open downloads dropdown instead of folder from the menu

* Run lint:js and fix problems

* Add tests for utils

* Fix issue with dropdown not displaying

* Remove unecessary comment

* Move downloads to separate json file, outside Config

* Add downloads dropdown menu for the 3-dot button

* Dont show dev tools for downloads

* Add cancel download functionality

* Add dark mode styling

* Use View state for downloadsMenu open state

* Fix some style issues

* Add image preview for downloaded images

* Remove extra devTool in weback config

* Fix issue with paths on windows

* Align items left in downloads menu

* Use pretty-bytes for file sizes

* Show download remaining time

* Close downloads dropdown when clicking outside

* Show different units in received bytes when they are different from the total units (kb/mb)

* Dont hide downloads when mattermost view is clicked

* Keep downloads open if download button is clicked

* Use closest() to check for download clicks

* Fix unit tests.
Add tests for new Views and downloadManager
Add @types/jest as devDependency for intellisense

* Remove unecessary tsconfig for jest

* Fix types error

* Add all critical tests for downloadsManager

* WIP: add e2e tests for downloads

* WIP: add e2e tests for downloads

* Rename downloads spec file

* WIP: make vscode debugger work for e2e tests

* Remove unused mock

* Remove defaults for v4 config

* Use electron-mocha for e2e debugger

* Fix e2e tests spawning JsonFileManager twice

* Add async fs functions and add tests for download item UI

* Add async fs functions and add tests for download item UI

* Improve tests with "waitForSelector" to wait for visible elements

* Wait for page load before assertions

* Add tests for file uploads/downloads

* Dont show native notification for completed downloads if dropdown is open

* Increment filenames if file already exists

* Fix antializing in downloads dropdown

* Fix styling of downloads header

* Increase dimensions of green/red icons in downloads

* Fix styling of 3-dot button

* Fix unit tests

* Show 3-dot button only on hover or click

* PR review fixes

* Revert vscode debug fixes

* Mock fs.constants

* Mock fs instead of JsonFileManager in downlaods tests

* Mock fs instead of JsonFileManager in downlaods tests

* Add necessary mocks for downloads manager

* Mark file as deleted if user deleted it

* Fix min-height of downloads dropdown and 3-dot icon position

* Add more tests

* Make size of downloads dropdown dynamic based on content

* Combine log statements

* Close 3-dot menu if user clicks elsewhere

* Move application updates inside downloads dropdown

* Fix update issues

* Fix ipc event payload

* Add missing prop

* Remove unused translations

* Fix failing test

* Fix version unknown

* Remove commented out component
This commit is contained in:
Tasos Boulis
2022-10-07 11:40:27 +03:00
committed by GitHub
parent cf6ca93627
commit 131b5fa2ac
74 changed files with 4805 additions and 264 deletions

View File

@@ -0,0 +1,58 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React, {useEffect} from 'react';
import '../../css/components/DownloadsDropdown/DownloadsDropdownButton.scss';
type Props = {
closeDownloadsDropdown: () => void;
darkMode: boolean;
isDownloadsDropdownOpen: boolean;
openDownloadsDropdown: () => void;
showDownloadsBadge: boolean;
}
const DownloadsDropDownButtonBadge = ({show}: { show: boolean }) => (
show ? <span className='DownloadsDropdownButton__badge'/> : null
);
const DownloadsDropdownButton: React.FC<Props> = ({darkMode, isDownloadsDropdownOpen, showDownloadsBadge, closeDownloadsDropdown, openDownloadsDropdown}: Props) => {
const buttonRef: React.RefObject<HTMLButtonElement> = React.createRef();
useEffect(() => {
if (!isDownloadsDropdownOpen) {
buttonRef.current?.blur();
}
}, [isDownloadsDropdownOpen, buttonRef]);
const handleToggleButton = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
if (isDownloadsDropdownOpen) {
closeDownloadsDropdown();
} else {
openDownloadsDropdown();
}
};
return (
<button
ref={buttonRef}
className={classNames('DownloadsDropdownButton', {
isDownloadsDropdownOpen,
darkMode,
})}
onClick={handleToggleButton}
onDoubleClick={(event) => {
event.stopPropagation();
}}
>
<i className='icon-arrow-down-bold-circle-outline'/>
<DownloadsDropDownButtonBadge show={showDownloadsBadge}/>
</button>
);
};
export default DownloadsDropdownButton;

View File

@@ -0,0 +1,28 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {DownloadedItem} from 'types/downloads';
import DownloadsDropdownItemFile from './DownloadsDropdownItemFile';
import UpdateWrapper from './Update/UpdateWrapper';
type OwnProps = {
activeItem?: DownloadedItem;
item: DownloadedItem;
}
const DownloadsDropdownItem = ({item, activeItem}: OwnProps) => {
if (item.type === 'update' && item.state !== 'progressing') {
return <UpdateWrapper item={item}/>;
}
return (
<DownloadsDropdownItemFile
item={item}
activeItem={activeItem}
/>
);
};
export default DownloadsDropdownItem;

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import {DownloadedItem} from 'types/downloads';
import classNames from 'classnames';
import {useIntl} from 'react-intl';
import {DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER} from 'common/communication';
import FileSizeAndStatus from './FileSizeAndStatus';
import ProgressBar from './ProgressBar';
import ThreeDotButton from './ThreeDotButton';
import Thumbnail from './Thumbnail';
type OwnProps = {
activeItem?: DownloadedItem;
item: DownloadedItem;
}
const DownloadsDropdownItemFile = ({item, activeItem}: OwnProps) => {
const [threeDotButtonVisible, setThreeDotButtonVisible] = useState(false);
const translate = useIntl();
const onFileClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
window.postMessage({type: DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER, payload: {item}}, window.location.href);
};
const itemFilename = item.type === 'update' ?
translate.formatMessage({id: 'renderer.downloadsDropdown.Update.MattermostVersionX', defaultMessage: `Mattermost version ${item.filename}`}, {version: item.filename}) :
item.filename;
return (
<div
className={classNames('DownloadsDropdown__File', {
progressing: item.state === 'progressing',
})}
onClick={onFileClick}
onMouseEnter={() => setThreeDotButtonVisible(true)}
onMouseLeave={() => setThreeDotButtonVisible(false)}
>
<div className='DownloadsDropdown__File__Body'>
<Thumbnail item={item}/>
<div className='DownloadsDropdown__File__Body__Details'>
<div className='DownloadsDropdown__File__Body__Details__Filename'>
{itemFilename}
</div>
<div
className={classNames('DownloadsDropdown__File__Body__Details__FileSizeAndStatus', {
cancelled: (/(cancelled|deleted|interrupted)/).test(item.state),
})}
>
<FileSizeAndStatus item={item}/>
</div>
</div>
<ThreeDotButton
item={item}
activeItem={activeItem}
visible={threeDotButtonVisible}
/>
</div>
<ProgressBar item={item}/>
</div>
);
};
export default DownloadsDropdownItemFile;

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {DownloadedItem} from 'types/downloads';
import {getDownloadingFileStatus, getFileSizeOrBytesProgress, prettyETA} from 'renderer/utils';
type OwnProps = {
item: DownloadedItem;
}
const FileSizeAndStatus = ({item}: OwnProps) => {
const translate = useIntl();
const {totalBytes, receivedBytes, addedAt} = item;
const getRemainingTime = useCallback(() => {
const elapsedMs = Date.now() - addedAt;
const bandwidth = receivedBytes / elapsedMs;
const etaMS = Math.round((totalBytes - receivedBytes) / bandwidth);
return prettyETA(etaMS, translate);
}, [receivedBytes, addedAt, totalBytes, translate]);
const fileSizeOrByteProgress = getFileSizeOrBytesProgress(item);
const statusOrETA = item.state === 'progressing' ? getRemainingTime() : getDownloadingFileStatus(item);
return (
<>
{fileSizeOrByteProgress}{' • '}{statusOrETA}
</>
);
};
export default FileSizeAndStatus;

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {DownloadedItem} from 'types/downloads';
type OwnProps = {
item: DownloadedItem;
}
const ProgressBar = ({item}: OwnProps) => {
if (item.state !== 'progressing') {
return null;
}
return (
<div className='DownloadsDropdown__File__ProgressBarContainer'>
<div
className='DownloadsDropdown__File__ProgressBar'
style={{width: `${Math.max(1, item.progress)}%`}}
/>
</div>
);
};
export default ProgressBar;

View File

@@ -0,0 +1,48 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useRef} from 'react';
import {DownloadedItem} from 'types/downloads';
import classNames from 'classnames';
import {TOGGLE_DOWNLOADS_DROPDOWN_MENU} from 'common/communication';
type OwnProps = {
activeItem?: DownloadedItem;
item: DownloadedItem;
visible: boolean;
}
const ThreeDotButton = ({item, activeItem, visible}: OwnProps) => {
const buttonElement = useRef<HTMLButtonElement>(null);
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
const coords = buttonElement.current?.getBoundingClientRect();
window.postMessage({
type: TOGGLE_DOWNLOADS_DROPDOWN_MENU,
payload: {
coordinates: coords?.toJSON(),
item,
},
}, window.location.href);
};
return (
<button
className={classNames('DownloadsDropdown__File__Body__ThreeDotButton', {
active: item.location && (item.location === activeItem?.location),
visible,
})}
onClick={onClick}
ref={buttonElement}
>
<i className='icon-dots-vertical'/>
</button>
);
};
export default ThreeDotButton;

View File

@@ -0,0 +1,63 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {DownloadedItem} from 'types/downloads';
import {CheckCircleIcon, CloseCircleIcon} from '@mattermost/compass-icons/components';
import {getIconClassName, isImageFile} from 'renderer/utils';
type OwnProps = {
item: DownloadedItem;
}
const iconSize = 12;
const colorGreen = '#3DB887';
const colorRed = '#D24B4E';
const isWin = window.process.platform === 'win32';
const Thumbnail = ({item}: OwnProps) => {
const showBadge = (state: DownloadedItem['state']) => {
switch (state) {
case 'completed':
return (
<CheckCircleIcon
size={iconSize}
color={colorGreen}
/>
);
case 'progressing':
return null;
case 'available':
return null;
default:
return (
<CloseCircleIcon
size={iconSize}
color={colorRed}
/>
);
}
};
const showImagePreview = isImageFile(item) && item.state === 'completed';
return (
<div className='DownloadsDropdown__Thumbnail__Container'>
{showImagePreview ?
<div
className='DownloadsDropdown__Thumbnail preview'
style={{
backgroundImage: `url("${isWin ? `file:///${item.location.replaceAll('\\', '/')}` : item.location}")`,
backgroundSize: 'cover',
}}
/> :
<div className={`DownloadsDropdown__Thumbnail ${getIconClassName(item)}`}/>}
{showBadge(item.state)}
</div>
);
};
export default Thumbnail;

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {DownloadedItem} from 'types/downloads';
import {FormattedMessage} from 'react-intl';
import {Button} from 'react-bootstrap';
import {START_UPDATE_DOWNLOAD} from 'common/communication';
import Thumbnail from '../Thumbnail';
type OwnProps = {
item: DownloadedItem;
}
const UpdateAvailable = ({item}: OwnProps) => {
const onButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e?.preventDefault?.();
window.postMessage({type: START_UPDATE_DOWNLOAD}, window.location.href);
};
return (
<div className='DownloadsDropdown__Update'>
<Thumbnail item={item}/>
<div className='DownloadsDropdown__Update__Details'>
<div className='DownloadsDropdown__Update__Details__Title'>
<FormattedMessage
id='renderer.downloadsDropdown.Update.NewDesktopVersionAvailable'
defaultMessage='New Desktop version available'
/>
</div>
<div className='DownloadsDropdown__Update__Details__Description'>
<FormattedMessage
id='renderer.downloadsDropdown.Update.ANewVersionIsAvailableToInstall'
defaultMessage={`A new version of the Mattermost Desktop App (version ${item.filename}) is available to install.`}
values={{version: item.filename}}
/>
</div>
<Button
id='downloadUpdateButton'
className='primary-button'
onClick={onButtonClick}
>
<FormattedMessage
id='renderer.downloadsDropdown.Update.DownloadUpdate'
defaultMessage='Download Update'
/>
</Button>
</div>
</div>
);
};
export default UpdateAvailable;

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {DownloadedItem} from 'types/downloads';
import {FormattedMessage, useIntl} from 'react-intl';
import {Button} from 'react-bootstrap';
import classNames from 'classnames';
import {START_UPGRADE} from 'common/communication';
import Thumbnail from '../Thumbnail';
import FileSizeAndStatus from '../FileSizeAndStatus';
type OwnProps = {
item: DownloadedItem;
}
const UpdateAvailable = ({item}: OwnProps) => {
const translate = useIntl();
const onButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e?.preventDefault?.();
window.postMessage({type: START_UPGRADE}, window.location.href);
};
return (
<div className='DownloadsDropdown__File update'>
<div className='DownloadsDropdown__File__Body'>
<Thumbnail item={item}/>
<div className='DownloadsDropdown__File__Body__Details'>
<div className='DownloadsDropdown__File__Body__Details__Filename'>
{translate.formatMessage({id: 'renderer.downloadsDropdown.Update.MattermostVersionX', defaultMessage: `Mattermost version ${item.filename}`}, {version: item.filename})}
</div>
<div
className={classNames('DownloadsDropdown__File__Body__Details__FileSizeAndStatus', {
cancelled: (/(cancelled|deleted|interrupted)/).test(item.state),
})}
>
<FileSizeAndStatus item={item}/>
</div>
<Button
id='restartAndUpdateButton'
className='primary-button'
onClick={onButtonClick}
>
<FormattedMessage
id='renderer.downloadsDropdown.Update.RestartAndUpdate'
defaultMessage={'Restart & update'}
/>
</Button>
</div>
</div>
</div>
);
};
export default UpdateAvailable;

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {DownloadedItem} from 'types/downloads';
import UpdateAvailable from './UpdateAvailable';
import UpdateDownloaded from './UpdateDownloaded';
import 'renderer/css/components/Button.scss';
type OwnProps = {
item: DownloadedItem;
}
const UpdateWrapper = ({item}: OwnProps) => {
if (item.state === 'available') {
return <UpdateAvailable item={item}/>;
}
if (item.state === 'completed') {
return <UpdateDownloaded item={item}/>;
}
return null;
};
export default UpdateWrapper;

View File

@@ -10,9 +10,9 @@ import {Container, Row} from 'react-bootstrap';
import {DropResult} from 'react-beautiful-dnd';
import {injectIntl, IntlShape} from 'react-intl';
import {IpcRendererEvent} from 'electron/renderer';
import prettyBytes from 'pretty-bytes';
import {TeamWithTabs} from 'types/config';
import {DownloadedItems} from 'types/downloads';
import {getTabViewName} from 'common/tabs/TabView';
@@ -40,13 +40,15 @@ import {
CLOSE_TEAMS_DROPDOWN,
OPEN_TEAMS_DROPDOWN,
SWITCH_TAB,
UPDATE_AVAILABLE,
UPDATE_DOWNLOADED,
UPDATE_PROGRESS,
START_UPGRADE,
START_DOWNLOAD,
CLOSE_TAB,
RELOAD_CURRENT_VIEW,
CLOSE_DOWNLOADS_DROPDOWN,
OPEN_DOWNLOADS_DROPDOWN,
SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE,
HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE,
UPDATE_DOWNLOADS_DROPDOWN,
REQUEST_HAS_DOWNLOADS,
CLOSE_DOWNLOADS_DROPDOWN_MENU,
} from 'common/communication';
import restoreButton from '../../assets/titlebar/chrome-restore.svg';
@@ -60,6 +62,8 @@ import TabBar from './TabBar';
import ExtraBar from './ExtraBar';
import ErrorView from './ErrorView';
import TeamDropdownButton from './TeamDropdownButton';
import DownloadsDropdownButton from './DownloadsDropdown/DownloadsDropdownButton';
import '../css/components/UpgradeButton.scss';
enum Status {
@@ -70,13 +74,6 @@ enum Status {
NOSERVERS = -2,
}
enum UpgradeStatus {
NONE = 0,
AVAILABLE = 1,
DOWNLOADING = 2,
DOWNLOADED = 3,
}
type Props = {
teams: TeamWithTabs[];
lastActiveTeam?: number;
@@ -101,14 +98,9 @@ type State = {
fullScreen?: boolean;
showExtraBar?: boolean;
isMenuOpen: boolean;
upgradeStatus: UpgradeStatus;
upgradeProgress?: {
total: number;
delta: number;
transferred: number;
percent: number;
bytesPerSecond: number;
};
isDownloadsDropdownOpen: boolean;
showDownloadsBadge: boolean;
hasDownloads: boolean;
};
type TabViewStatus = {
@@ -146,7 +138,9 @@ class MainPage extends React.PureComponent<Props, State> {
tabViewStatus: new Map(this.props.teams.map((team) => team.tabs.map((tab) => getTabViewName(team.name, tab.name))).flat().map((tabViewName) => [tabViewName, {status: Status.LOADING}])),
darkMode: this.props.darkMode,
isMenuOpen: false,
upgradeStatus: UpgradeStatus.NONE,
isDownloadsDropdownOpen: false,
showDownloadsBadge: false,
hasDownloads: false,
};
}
@@ -163,7 +157,21 @@ class MainPage extends React.PureComponent<Props, State> {
this.setState({tabViewStatus: status});
}
async requestDownloadsLength() {
try {
const hasDownloads = await window.ipcRenderer.invoke(REQUEST_HAS_DOWNLOADS);
this.setState({
hasDownloads,
});
} catch (error) {
console.error(error);
}
}
componentDidMount() {
// request downloads
this.requestDownloadsLength();
// set page on retry
window.ipcRenderer.on(LOAD_RETRY, (_, viewName, retry, err, loadUrl) => {
console.log(`${viewName}: failed to load ${err}, but retrying`);
@@ -249,24 +257,25 @@ class MainPage extends React.PureComponent<Props, State> {
this.setState({isMenuOpen: true});
});
window.ipcRenderer.on(UPDATE_AVAILABLE, () => {
this.setState({upgradeStatus: UpgradeStatus.AVAILABLE});
window.ipcRenderer.on(CLOSE_DOWNLOADS_DROPDOWN, () => {
this.setState({isDownloadsDropdownOpen: false});
});
window.ipcRenderer.on(UPDATE_DOWNLOADED, () => {
this.setState({upgradeStatus: UpgradeStatus.DOWNLOADED});
window.ipcRenderer.on(OPEN_DOWNLOADS_DROPDOWN, () => {
this.setState({isDownloadsDropdownOpen: true});
});
window.ipcRenderer.on(UPDATE_PROGRESS, (event, total, delta, transferred, percent, bytesPerSecond) => {
window.ipcRenderer.on(SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE, () => {
this.setState({showDownloadsBadge: true});
});
window.ipcRenderer.on(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE, () => {
this.setState({showDownloadsBadge: false});
});
window.ipcRenderer.on(UPDATE_DOWNLOADS_DROPDOWN, (event, downloads: DownloadedItems) => {
this.setState({
upgradeStatus: UpgradeStatus.DOWNLOADING,
upgradeProgress: {
total,
delta,
transferred,
percent,
bytesPerSecond,
},
hasDownloads: (Object.values(downloads)?.length || 0) > 0,
});
});
@@ -278,15 +287,17 @@ class MainPage extends React.PureComponent<Props, State> {
});
}
window.addEventListener('click', this.handleCloseTeamsDropdown);
window.addEventListener('click', this.handleCloseDropdowns);
}
componentWillUnmount() {
window.removeEventListener('click', this.handleCloseTeamsDropdown);
window.removeEventListener('click', this.handleCloseDropdowns);
}
handleCloseTeamsDropdown = () => {
handleCloseDropdowns = () => {
window.ipcRenderer.send(CLOSE_TEAMS_DROPDOWN);
window.ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN);
window.ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN_MENU);
}
handleMaximizeState = (_: IpcRendererEvent, maximized: boolean) => {
@@ -359,13 +370,26 @@ class MainPage extends React.PureComponent<Props, State> {
focusOnWebView = () => {
window.ipcRenderer.send(FOCUS_BROWSERVIEW);
this.handleCloseTeamsDropdown();
this.handleCloseDropdowns();
}
reloadCurrentView = () => {
window.ipcRenderer.send(RELOAD_CURRENT_VIEW);
}
showHideDownloadsBadge(value = false) {
this.setState({showDownloadsBadge: value});
}
closeDownloadsDropdown() {
window.ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN);
window.ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN_MENU);
}
openDownloadsDropdown() {
window.ipcRenderer.send(OPEN_DOWNLOADS_DROPDOWN);
}
render() {
const {intl} = this.props;
const currentTabs = this.props.teams.find((team) => team.name === this.state.activeServerName)?.tabs || [];
@@ -394,6 +418,16 @@ class MainPage extends React.PureComponent<Props, State> {
fullScreen: this.state.fullScreen,
});
const downloadsDropdownButton = this.state.hasDownloads ? (
<DownloadsDropdownButton
darkMode={this.state.darkMode}
isDownloadsDropdownOpen={this.state.isDownloadsDropdownOpen}
showDownloadsBadge={this.state.showDownloadsBadge}
closeDownloadsDropdown={this.closeDownloadsDropdown}
openDownloadsDropdown={this.openDownloadsDropdown}
/>
) : null;
let maxButton;
if (this.state.maximized || this.state.fullScreen) {
maxButton = (
@@ -421,58 +455,6 @@ class MainPage extends React.PureComponent<Props, State> {
);
}
let upgradeTooltip;
switch (this.state.upgradeStatus) {
case UpgradeStatus.AVAILABLE:
upgradeTooltip = intl.formatMessage({id: 'renderer.components.mainPage.updateAvailable', defaultMessage: 'Update available'});
break;
case UpgradeStatus.DOWNLOADED:
upgradeTooltip = intl.formatMessage({id: 'renderer.components.mainPage.updateReady', defaultMessage: 'Update ready to install'});
break;
case UpgradeStatus.DOWNLOADING:
upgradeTooltip = intl.formatMessage({
id: 'renderer.components.mainPage.downloadingUpdate',
defaultMessage: 'Downloading update. {percentDone}% of {total} @ {speed}/s',
}, {
percentDone: String(this.state.upgradeProgress?.percent).split('.')[0],
total: prettyBytes(this.state.upgradeProgress?.total || 0),
speed: prettyBytes(this.state.upgradeProgress?.bytesPerSecond || 0),
});
break;
}
let upgradeIcon;
if (this.state.upgradeStatus !== UpgradeStatus.NONE) {
upgradeIcon = (
<button
className={classNames('upgrade-btns', {darkMode: this.state.darkMode})}
onClick={() => {
if (this.state.upgradeStatus === UpgradeStatus.DOWNLOADING) {
return;
}
window.ipcRenderer.send(this.state.upgradeStatus === UpgradeStatus.DOWNLOADED ? START_UPGRADE : START_DOWNLOAD);
}}
>
<div
className={classNames('button upgrade-button', {
rotate: this.state.upgradeStatus === UpgradeStatus.DOWNLOADING,
})}
title={upgradeTooltip}
>
<i
className={classNames({
'icon-arrow-down-bold-circle-outline': this.state.upgradeStatus === UpgradeStatus.AVAILABLE,
'icon-sync': this.state.upgradeStatus === UpgradeStatus.DOWNLOADING,
'icon-arrow-up-bold-circle-outline': this.state.upgradeStatus === UpgradeStatus.DOWNLOADED,
})}
/>
{(this.state.upgradeStatus !== UpgradeStatus.DOWNLOADING) && <div className={'circle'}/>}
</div>
</button>
);
}
let titleBarButtons;
if (window.process.platform === 'win32' && !this.props.useNativeWindow) {
titleBarButtons = (
@@ -548,7 +530,7 @@ class MainPage extends React.PureComponent<Props, State> {
/>
)}
{tabsRow}
{upgradeIcon}
{downloadsDropdownButton}
{titleBarButtons}
</div>
</Row>

100
src/renderer/constants.ts Normal file
View File

@@ -0,0 +1,100 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export const Constants = {
SECOND_MS: 1000,
MINUTE_MS: 60 * 1000,
HOUR_MS: 60 * 60 * 1000,
ICON_NAME_FROM_MIME_TYPE: {
'application/pdf': 'pdf',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'ppt',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'excel',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'word',
'application/x-apple-diskimage': 'generic',
'application/zip': 'zip',
'audio/mpeg': 'audio',
'image/jpeg': 'image',
'text/html': 'code',
'text/plain': 'text',
'video/mp4': 'video',
},
ICON_NAME_FROM_EXTENSION: {
pdf: 'pdf',
doc: 'word',
docx: 'word',
ppt: 'ppt',
pptx: 'ppt',
xls: 'excel',
xlsx: 'excel',
patch: 'patch',
txt: 'text',
// ZIP
zip: 'zip',
rar: 'zip',
'7z': 'zip',
tar: 'zip',
gz: 'zip',
// Audio
mp3: 'audio',
aac: 'audio',
wav: 'audio',
flac: 'audio',
ogg: 'audio',
// Image
jpg: 'image',
jpeg: 'image',
svg: 'image',
gif: 'image',
png: 'image',
bmp: 'image',
tif: 'image',
tiff: 'image',
// Code
html: 'code',
xhtml: 'code',
htm: 'code',
css: 'code',
sass: 'code',
scss: 'code',
js: 'code',
jsx: 'code',
tsx: 'code',
ts: 'code',
go: 'code',
json: 'code',
sh: 'code',
py: 'code',
rpy: 'code',
c: 'code',
cgi: 'code',
pl: 'code',
class: 'code',
cpp: 'code',
cc: 'code',
cs: 'code',
h: 'code',
java: 'code',
php: 'code',
swift: 'code',
vb: 'code',
jsp: 'code',
r: 'code',
lib: 'code',
dll: 'code',
perl: 'code',
run: 'code',
// Video
mp4: 'video',
mov: 'video',
wmv: 'video',
avi: 'video',
mkv: 'video',
flv: 'video',
webm: 'video',
},
};

View File

@@ -1,6 +1,7 @@
@import url("../_css_variables.scss");
.primary-button {
cursor: pointer;
position: relative;
display: inline-flex;
align-items: center;

View File

@@ -0,0 +1,50 @@
.DownloadsDropdownButton {
align-items: center;
background: transparent;
border-radius: 4px;
border: none;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: center;
margin: 4px;
position: relative;
width: 32px;
i {
color: rgba(63, 67, 80, 0.56);
cursor: pointer;
font-size: 21px;
line-height: 21px;
}
&:hover, &:focus, &.isDownloadsDropdownOpen {
background-color: rgba(28, 88, 217, 0.08);
i {
color: rgba(56, 111, 229, 1);
}
}
.DownloadsDropdownButton__badge {
background: rgba(210, 75, 78, 1);
border-radius: 10px;
width: 10px;
height: 10px;
position: absolute;
top: 5px;
right: 5px;
}
&.darkMode {
i {
color: rgba(221, 223, 228, 0.56);
}
&:hover, &:focus, &.isDownloadsDropdownOpen {
background-color: rgba(56, 111, 229, 0.08);
i {
color: rgba(56, 111, 229, 1);
}
}
}
}

View File

@@ -0,0 +1,518 @@
@import url("fonts.css");
@import '~@mattermost/compass-icons/css/compass-icons.css';
@mixin file-icon($path) {
background-image: url($path);
background-position: center;
background-repeat: no-repeat;
background-size: 24px 24px;
}
/* with this, padding doesn't change an element's width & height */
* {
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
display: none;
}
::-webkit-scrollbar-thumb {
background: rgba(63, 67, 80, 0.6);
border-radius: 4px;
width: 8px;
}
body {
margin: 0;
background: transparent;
font-family: Open Sans;
overflow: hidden;
#app {
width: 328px; // 280px + 24px*2
padding: 0 24px 24px;
.DownloadsDropdown {
background: #ffffff;
border-radius: 4px;
border: 1px solid rgba(61, 60, 64, 0.16);
box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
max-height: 360px;
max-width: 280px;
min-height: 120px;
padding: 0 0 8px;
width: 280px;
.DownloadsDropdown__Thumbnail__Container {
align-items: center;
background: rgba(63, 67, 80, 0.08);
border-radius: 4px;
display: flex;
height: 32px;
justify-content: center;
margin-right: 8px;
min-height: 32px;
min-width: 32px;
position: relative;
width: 32px;
> svg {
background: #ffffff;
border-radius: 50%;
border: 2px solid #ffffff;
position: absolute;
right: 0;
top: 0;
transform: translate(50%, -50%);
}
.DownloadsDropdown__Thumbnail {
border-radius: 4px;
width: 100%;
height: 100%;
&.mattermost {
@include file-icon('./thumbnails/mattermost.svg');
background-size: 32px 32px;
}
&.text {
@include file-icon('./thumbnails/text.svg');
}
&.audio {
@include file-icon('./thumbnails/audio.svg');
}
&.video {
@include file-icon('./thumbnails/video.svg');
}
&.ppt {
@include file-icon('./thumbnails/ppt.svg');
}
&.generic,
&.other {
@include file-icon('./thumbnails/generic.svg');
}
&.code {
@include file-icon('./thumbnails/code.svg');
}
&.excel {
@include file-icon('./thumbnails/excel.svg');
}
&.word {
@include file-icon('./thumbnails/word.svg');
}
&.pdf {
@include file-icon('./thumbnails/pdf.svg');
}
&.patch {
@include file-icon('./thumbnails/patch.svg');
}
&.image {
@include file-icon('./thumbnails/image.svg');
}
&.zip {
@include file-icon('./thumbnails/zip.svg');
}
}
}
.DownloadsDropdown__header {
align-items: center;
display: flex;
flex-direction: row;
min-height: 44px;
justify-content: space-between;
padding: 6px 10px 6px 20px;
.DownloadsDropdown__Downloads {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #3D3C40;
}
.DownloadsDropdown__clearAllButton {
background: transparent;
border-radius: 4px;
color: #1C58D9;
cursor: pointer;
font-size: 11px;
font-style: normal;
font-weight: 600;
height: 24px;
letter-spacing: 0.02em;
padding: 4px 10px;
&:hover {
background: rgba(28, 88, 217, 0.08);
}
&.disabled {
cursor: default;
color: rgba(63, 67, 80, 0.32);
}
}
}
.DownloadsDropdown__divider {
border-top: 1px solid rgba(61, 60, 64, 0.08);
border-bottom: 0;
width: 100%;
margin: 0 0 8px;
}
.DownloadsDropdown__list {
align-items: flex-start;
display: flex;
flex-direction: column;
font-size: 12px;
justify-content: flex-start;
overflow-x: hidden;
overflow-y: auto;
width: 100%;
.DownloadsDropdown__File {
align-items: center;
cursor: pointer;
display: flex;
flex-direction: column;
height: 100%;
justify-content: flex-start;
height: 56px;
padding: 12px 12px 12px 20px;
width: 100%;
&.progressing {
height: 68px;
}
&.update {
height: 92px;
.DownloadsDropdown__File__Body {
height: 100%;
.DownloadsDropdown__File__Body__Details {
width: calc(100% - 32px - 16px);
button#restartAndUpdateButton {
font-style: normal;
font-weight: 600;
font-size: 12px;
height: 32px;
margin-top: 6px;
padding: 10px 16px;
width: auto;
}
}
}
}
&:hover {
background: rgba(63, 67, 80, 0.08);
}
.DownloadsDropdown__File__Body {
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
.DownloadsDropdown__File__Body__Thumbnail__Container {
align-items: center;
background: rgba(63, 67, 80, 0.08);
border-radius: 4px;
display: flex;
height: 32px;
justify-content: center;
margin-right: 8px;
position: relative;
width: 32px;
> svg {
background: #ffffff;
border-radius: 50%;
border: 1px solid #ffffff;
height: 16px;
position: absolute;
right: 0;
top: 0;
transform: translate(50%, -50%);
width: 16px;
}
.DownloadsDropdown__File__Body__Thumbnail {
border-radius: 4px;
width: 100%;
height: 100%;
&.text {
@include file-icon('./thumbnails/text.svg');
}
&.audio {
@include file-icon('./thumbnails/audio.svg');
}
&.video {
@include file-icon('./thumbnails/video.svg');
}
&.ppt {
@include file-icon('./thumbnails/ppt.svg');
}
&.generic,
&.other {
@include file-icon('./thumbnails/generic.svg');
}
&.code {
@include file-icon('./thumbnails/code.svg');
}
&.excel {
@include file-icon('./thumbnails/excel.svg');
}
&.word {
@include file-icon('./thumbnails/word.svg');
}
&.pdf {
@include file-icon('./thumbnails/pdf.svg');
}
&.patch {
@include file-icon('./thumbnails/patch.svg');
}
&.image {
@include file-icon('./thumbnails/image.svg');
}
&.zip {
@include file-icon('./thumbnails/zip.svg');
}
}
}
.DownloadsDropdown__File__Body__Details {
margin-right: 8px;
/* 100% - thumbnail width - three dot button width - margins of first 2 elements */
width: calc(100% - 32px - 28px - 16px);
> * {
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
&.DownloadsDropdown__File__Body__Details__Filename {
color: #3F4350;
font-size: 12px;
font-weight: 600;
}
&.DownloadsDropdown__File__Body__Details__FileSizeAndStatus {
margin-top: 2px;
color: rgba(63, 67, 80, 0.64);
font-size: 10px;
font-weight: 400;
&.cancelled {
color: #D24B4E;
}
}
}
}
.DownloadsDropdown__File__Body__ThreeDotButton {
background: transparent;
border: none;
border-radius: 4px;
color: rgba(63, 67, 80, 0.56);
cursor: pointer;
visibility: hidden;
font-size: 18px;
height: 28px;
outline: none;
position: relative;
width: 28px;
&.visible {
visibility: visible;
}
&.active {
visibility: visible;
background: rgba(28, 88, 217, 0.08);
color: rgba(28, 88, 217, 1);
}
&:hover {
background-color: rgba(63, 67, 80, 0.08);
color: rgba(63, 67, 80, 0.72)
}
&:active {
background: rgba(28, 88, 217, 0.08);
color: rgba(28, 88, 217, 1);
}
> .icon-dots-vertical {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.DownloadsDropdown__File__ProgressBarContainer {
width: 100%;
height: 4px;
background: rgba(63, 67, 80, 0.16);
border-radius: 4px;
margin-top: 4px;
overflow: hidden;
position: relative;
.DownloadsDropdown__File__ProgressBar {
background: #1c58d9;
height: 100%;
left: 0;
position: absolute;
top: 0;
transition: all 0.2s;
width: 0%;
}
}
}
.DownloadsDropdown__Update {
align-items: flex-start;
display: flex;
flex-direction: row;
justify-content: flex-start;
padding: 12px 12px 12px 20px;
width: 100%;
.DownloadsDropdown__Update__Details {
align-items: flex-start;
display: flex;
flex-direction: column;
justify-content: space-between;
.DownloadsDropdown__Update__Details__Title {
color: #3F4350;
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 16px;
max-width: 200px;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.DownloadsDropdown__Update__Details__Description {
color: rgba(63, 67, 80, 0.64);
font-size: 10px;
font-style: normal;
font-weight: 400;
letter-spacing: 0.02em;
line-height: 16px;
max-width: 200px;
}
button#downloadUpdateButton {
font-style: normal;
font-weight: 600;
font-size: 12px;
height: 32px;
margin-top: 6px;
padding: 10px 16px;
}
}
}
}
&.darkMode {
background: #1f1f1f;
border: 1px solid rgba(221, 223, 228, 0.16);
.DownloadsDropdown__header .DownloadsDropdown__Downloads {
color: #DDD;
}
.DownloadsDropdown__divider {
border-color: rgba(221, 221, 221, 0.08);
}
.DownloadsDropdown__Thumbnail__Container {
background: rgba(221, 223, 228, 0.08);
> svg {
background: #1f1f1f;
border: 2px solid #1f1f1f;
}
}
.DownloadsDropdown__list {
color: #DDD;
.DownloadsDropdown__File .DownloadsDropdown__File__Body {
align-items: flex-start;
.DownloadsDropdown__File__Body__Thumbnail__Container {
background: rgba(221, 223, 228, 0.08);
> svg {
background: #1f1f1f;
border-color: #1f1f1f;
}
}
.DownloadsDropdown__File__Body__Details {
.DownloadsDropdown__File__Body__Details__Filename {
color: rgba(221, 223, 228, 1);
}
.DownloadsDropdown__File__Body__Details__FileSizeAndStatus {
color: rgba(221, 223, 228, 0.64);
&.cancelled {
color: #D24B4E;
}
}
}
.DownloadsDropdown__File__Body__ThreeDotButton {
color: rgba(221, 223, 228, 0.56);
&:hover {
background: rgba(56, 111, 229, 0.08);
color: rgba(28, 88, 217, 0.72);
}
&:active {
background: rgba(56, 111, 229, 0.08);
color: rgba(28, 88, 217, 1);
}
&.active {
color: rgba(28, 88, 217, 1);
}
}
}
.DownloadsDropdown__Update .DownloadsDropdown__Update__Details {
.DownloadsDropdown__Update__Details__Title {
color: rgba(221, 223, 228, 1);
}
.DownloadsDropdown__Update__Details__Description {
color: rgba(221, 223, 228, 0.64);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,85 @@
@import url("fonts.css");
@import '~@mattermost/compass-icons/css/compass-icons.css';
@mixin file-icon($path) {
background-image: url($path);
background-position: center;
background-repeat: no-repeat;
background-size: 24px 24px;
}
/* with this, padding doesn't change an element's width & height */
* {
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
background-color: transparent;
font-family: Open Sans;
overflow: hidden;
height: 172px; // 160 + 12
width: 176px; // 154 + 12
#app {
background-color: transparent;
padding: 0 0 12px 12px;
height: 100%;
width: 100%;
.DownloadsDropdownMenu {
background: #ffffff;
border-radius: 4px;
border: 1px solid rgba(61, 60, 64, 0.16);
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
padding: 8px 0px;
width: 100%;
.DownloadsDropdownMenu__MenuItem {
align-items: center;
background-color: transparent;
color: rgba(63, 67, 80, 1);
cursor: pointer;
display: flex;
font-size: 14px;
font-weight: 400;
height: 25%;
justify-content: flex-start;
padding-left: 20px;
&:hover {
background-color: rgba(63, 67, 80, 0.08);
}
&.disabled {
background-color: transparent;
cursor: unset;
color: rgba(63, 67, 80, 0.32);
}
}
&.darkMode {
background: #1f1f1f;
border: 1px solid rgba(221, 223, 228, 0.16);
color: rgba(221, 223, 228, 1);
.DownloadsDropdownMenu__MenuItem {
color: rgba(221, 223, 228, 1);
&.disabled {
color: rgba(221, 223, 228, 0.32);
}
}
}
}
}
}

View File

@@ -0,0 +1,6 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#338AFF" stroke-width="2"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#338AFF" stroke-width="2"/>
<path d="M9 28.95V23.05C9 23.0224 9.02239 23 9.05 23H13.5L16.9175 20.0707C16.9499 20.0429 17 20.066 17 20.1087V31.8913C17 31.934 16.9499 31.9571 16.9175 31.9293L13.5 29H9.05C9.02239 29 9 28.9776 9 28.95Z" stroke="#338AFF" stroke-width="2"/>
<path d="M22.7107 21.3515C22.3872 21.1299 21.9583 21.2328 21.7533 21.5831C21.5484 21.9334 21.6441 22.3965 21.9675 22.6181C23.1165 23.4062 23.8025 24.745 23.8025 26.1999C23.8025 27.6549 23.1165 28.994 21.9675 29.7818C21.6441 30.0034 21.5484 30.4668 21.7533 30.8168C21.9415 31.138 22.3638 31.2865 22.7107 31.0484C24.2632 29.984 25.19 28.1715 25.19 26.1999C25.19 24.2284 24.2632 22.4159 22.7107 21.3515ZM21.092 23.7978C20.7572 23.5999 20.3349 23.7303 20.1493 24.0931C19.9739 24.4378 20.0754 24.8665 20.3738 25.0802L20.4225 25.1121C20.7957 25.3337 21.0275 25.7509 21.0275 26.1999C21.0275 26.6226 20.8221 27.017 20.4871 27.2466L20.4228 27.2877C20.0872 27.4881 19.9649 27.944 20.1496 28.3068C20.3355 28.6712 20.7581 28.8006 21.0922 28.6021C21.9082 28.1162 22.4153 27.1959 22.4153 26.1999C22.4153 25.204 21.9082 24.2834 21.092 23.7978Z" fill="#338AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,5 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#338AFF" stroke-width="2"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#338AFF" stroke-width="2"/>
<path d="M14.5236 31.9916C14.6431 32.0244 14.7653 31.9587 14.7986 31.8412L18.7097 18.5879C18.7431 18.4731 18.6736 18.35 18.5569 18.3172L17.4792 18.0082C17.3625 17.9754 17.2375 18.0438 17.2042 18.1586L13.2931 31.4119C13.2597 31.5267 13.3292 31.6498 13.4458 31.6826L14.5236 31.9916ZM12.2125 28.6803L12.9681 27.8873C13.0542 27.7971 13.0458 27.6549 12.9542 27.5729L10.007 24.9999L12.9542 22.4269C13.0486 22.3449 13.0542 22.2027 12.9681 22.1125L12.2125 21.3195C12.1292 21.232 11.9875 21.2265 11.8986 21.3086L8.07084 24.8413C7.97639 24.9288 7.97639 25.0737 8.07084 25.1612L11.8986 28.6912C11.9875 28.7733 12.1264 28.7678 12.2125 28.6803ZM20.1042 28.6912C20.0153 28.7733 19.8764 28.7678 19.7903 28.6803L19.0347 27.8873C18.9486 27.7971 18.9542 27.6549 19.0486 27.5729L21.9958 24.9999L19.0486 22.4269C18.957 22.3449 18.9486 22.2027 19.0347 22.1125L19.7903 21.3195C19.8736 21.2293 20.0153 21.2265 20.1042 21.3086L23.9292 24.8413C24.0236 24.9288 24.0236 25.0737 23.9292 25.1612L20.1042 28.6912Z" fill="#338AFF" stroke="#338AFF" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,5 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#1CA660" stroke-width="2"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#1CA660" stroke-width="2"/>
<path d="M22 31H18.8902L15.9034 26.461L12.9165 31H10L14.2606 24.8112L10.2723 19H13.2767L16.0439 23.3174L18.7584 19H21.6925L17.6603 24.9508L22 31Z" fill="#1CA660"/>
</svg>

After

Width:  |  Height:  |  Size: 606 B

View File

@@ -0,0 +1,5 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#338AFF" stroke-width="2"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#338AFF" stroke-width="2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.7791 17.1452C21.2806 16.6467 20.6909 16.3074 20.0147 16.1314C19.3421 15.9563 18.6668 15.9562 17.9942 16.131C17.324 16.2989 16.7342 16.6394 16.2283 17.1452L9.54617 23.8274C8.87 24.5036 8.4115 25.2967 8.17592 26.2028C7.94136 27.105 7.94136 28.0098 8.17592 28.912C8.4115 29.818 8.87 30.6112 9.54617 31.2874C10.2224 31.9636 11.0155 32.4221 11.9216 32.6576C12.8237 32.8922 13.7286 32.8922 14.6307 32.6576C15.5368 32.4221 16.33 31.9636 17.0062 31.2874L22.9282 25.3654C23.1234 25.1701 23.1234 24.8535 22.9282 24.6583L22.2211 23.9511C22.0258 23.7559 21.7092 23.7559 21.514 23.9511L15.5919 29.8732C15.1741 30.291 14.6862 30.5711 14.1234 30.7176C13.5577 30.8648 12.9946 30.8648 12.4289 30.7176C11.8662 30.5711 11.3782 30.291 10.9604 29.8732C10.5425 29.4553 10.2624 28.9674 10.116 28.4047C9.96871 27.8389 9.96871 27.2759 10.116 26.7101C10.2624 26.1474 10.5425 25.6595 10.9604 25.2416L17.6425 18.5595C17.8907 18.3113 18.1736 18.1513 18.4954 18.0726L18.5003 18.0714C18.839 17.983 19.1696 17.9797 19.4969 18.0593C19.8296 18.1465 20.1174 18.3119 20.3649 18.5595C20.613 18.8076 20.7731 19.0905 20.8517 19.4123L20.853 19.4172C20.9408 19.7537 20.9408 20.0875 20.853 20.4241L20.8518 20.429C20.7731 20.7508 20.613 21.0337 20.3649 21.2818L14.3191 27.3276C14.2026 27.4441 14.0692 27.4997 13.8998 27.4997C13.7541 27.4997 13.6273 27.4489 13.506 27.3276C13.3873 27.2089 13.3293 27.0777 13.3214 26.921C13.3292 26.7643 13.3873 26.6331 13.506 26.5144L18.7916 21.2288C18.9869 21.0335 18.9869 20.7169 18.7916 20.5217L18.0845 19.8146C17.8892 19.6193 17.5726 19.6193 17.3774 19.8146L12.0918 25.1002C11.7629 25.4291 11.5382 25.8193 11.4216 26.2662L11.4204 26.2711C11.3146 26.7036 11.3146 27.1384 11.4203 27.5709L11.4216 27.5758C11.5382 28.0227 11.7629 28.4129 12.0918 28.7418C12.42 29.0701 12.8066 29.2916 13.2474 29.3998C13.6877 29.5143 14.1277 29.5195 14.5625 29.4132L14.5674 29.4119C15.0143 29.2954 15.4045 29.0707 15.7334 28.7418L21.7791 22.696C22.285 22.1902 22.6255 21.6004 22.7934 20.9302C22.9682 20.2575 22.968 19.5823 22.793 18.9096C22.617 18.2335 22.2777 17.6438 21.7791 17.1452Z" fill="#338AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,6 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#338AFF" stroke-width="2"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#338AFF" stroke-width="2"/>
<path d="M23.5903 32H8.48957C8.28551 32 8.1674 31.7687 8.28705 31.6034L12.428 25.8823C12.5077 25.7722 12.6608 25.746 12.7726 25.8232L14.7942 27.2197C14.8294 27.244 14.8775 27.2363 14.9034 27.2023L18.6172 22.3158C18.7284 22.1693 18.9544 22.1891 19.0385 22.3527L23.8126 31.6357C23.8982 31.802 23.7774 32 23.5903 32Z" stroke="#338AFF" stroke-width="2"/>
<circle cx="11" cy="21" r="2" stroke="#338AFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 859 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 997 KiB

View File

@@ -0,0 +1,5 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#338AFF" stroke-width="2"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#338AFF" stroke-width="2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19 17.8284L23.2426 22.0711L20.4142 24.8995L16.1716 20.6569L19 17.8284ZM11.9289 24.8995L9.10051 27.7279L13.3431 31.9706L16.1716 29.1421L11.9289 24.8995ZM17.5858 16.4142C18.3668 15.6332 19.6332 15.6332 20.4142 16.4142L24.6569 20.6569C25.4379 21.4379 25.4379 22.7042 24.6569 23.4853L14.7574 33.3848C13.9763 34.1658 12.71 34.1658 11.9289 33.3848L7.68629 29.1421C6.90524 28.3611 6.90524 27.0948 7.68629 26.3137L17.5858 16.4142ZM15.2877 23.6621C15.6782 24.0526 16.3114 24.0526 16.7019 23.6621C17.0924 23.2715 17.0924 22.6384 16.7019 22.2478C16.3114 21.8573 15.6782 21.8573 15.2877 22.2478C14.8972 22.6384 14.8972 23.2715 15.2877 23.6621ZM14.9341 25.4298C14.5436 25.8203 13.9104 25.8203 13.5199 25.4298C13.1294 25.0393 13.1294 24.4061 13.5199 24.0156C13.9104 23.6251 14.5436 23.6251 14.9341 24.0156C15.3247 24.4061 15.3247 25.0393 14.9341 25.4298ZM17.409 25.7834C17.7995 26.1739 18.4327 26.1739 18.8232 25.7834C19.2137 25.3929 19.2137 24.7597 18.8232 24.3692C18.4327 23.9786 17.7995 23.9786 17.409 24.3692C17.0185 24.7597 17.0185 25.3929 17.409 25.7834ZM17.0555 27.5511C16.6649 27.9417 16.0318 27.9417 15.6412 27.5511C15.2507 27.1606 15.2507 26.5275 15.6412 26.1369C16.0318 25.7464 16.6649 25.7464 17.0555 26.1369C17.446 26.5275 17.446 27.1606 17.0555 27.5511Z" fill="#338AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,5 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#EE4F5C" stroke-width="2"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#EE4F5C" stroke-width="2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4878 16L15.5123 16.0001C16.2287 16.0131 16.8592 16.3652 17.2494 16.9687C17.9458 18.0474 17.8281 19.8849 17.2172 22.1847C17.3043 22.2828 17.3949 22.3787 17.489 22.4723C17.9556 22.9358 18.441 23.3538 18.933 23.721C20.63 23.4697 22.081 23.5831 22.9802 24.1658C23.6244 24.5823 24.0002 25.2422 24.0002 25.9892C24.0002 26.8132 23.5542 27.5091 22.805 27.8166C21.6981 28.2728 20.1011 27.7781 18.3347 26.6764C17.7146 26.8249 17.109 27.0241 16.5567 27.2659C15.9606 27.5271 15.3969 27.7853 14.8716 28.0376C13.5482 30.5492 12.1564 32 10.3567 32C10.075 32 9.79159 31.9601 9.51152 31.88C8.55309 31.6059 8.05649 30.9591 8.00369 30.2141C7.91274 28.8547 9.5044 27.5801 12.7964 25.9402C13.0103 25.4962 13.1918 25.0872 13.5144 24.3424L13.6995 23.9135C13.8503 23.57 14.0054 23.1584 14.153 22.7112C13.0842 20.9253 12.7816 18.9172 13.3628 17.5226C13.75 16.5872 14.5365 16 15.4878 16ZM13.2174 27.6068L12.7509 28.3362C11.8148 29.7998 10.9009 30.5345 9.97437 30.2673L9.92091 30.2507L9.86789 30.2318L9.61256 30.1366L9.75723 29.9056C10.0016 29.5155 10.9189 28.8706 12.3068 28.1039L12.4573 28.0213L13.2174 27.6068ZM16.0392 23.389L15.7855 23.1189L15.6667 23.47L15.5682 23.7522C15.4669 24.0335 15.3574 24.3118 15.2398 24.5868L15.0595 25.0047L14.984 25.1804C14.8851 25.4099 14.7923 25.6219 14.6984 25.8327L14.4593 26.3691L14.9936 26.1254L15.0759 26.0879C15.2966 25.9878 15.5297 25.8841 15.8799 25.7299C16.3776 25.5127 16.9239 25.3204 17.484 25.166L17.908 25.0491L17.5624 24.7771L17.2998 24.5655C16.953 24.2795 16.6196 23.9776 16.3006 23.661C16.2237 23.5838 16.1481 23.5049 16.0392 23.389ZM22.0633 25.5729C22.2394 25.6872 22.3054 25.8086 22.3139 25.993L22.3147 26.0569L22.3128 26.1648L22.2267 26.23C21.929 26.4556 21.1166 26.258 20.1282 25.7671L20.0134 25.7092L19.2833 25.3349L20.1025 25.2895C20.9912 25.2402 21.697 25.3344 22.0633 25.5729ZM15.8343 17.8769C15.7459 17.7411 15.6389 17.6826 15.4904 17.6777L15.4796 17.6776C15.2279 17.6816 15.0428 17.8606 14.9172 18.1635C14.6171 18.8836 14.7438 20.1101 15.2981 21.2851L15.5718 21.8653L15.7157 21.2401C16.0876 19.6238 16.165 18.3878 15.8343 17.8769Z" fill="#EE4F5C"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,5 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#ED522A" stroke-width="2"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#ED522A" stroke-width="2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8317 25.7059C19.6106 25.0219 20 24.0342 20 22.7428C20 21.5007 19.6343 20.5664 18.903 19.9398C18.1716 19.3133 17.1036 19 15.699 19H12V31H14.4554V26.7319H15.5089C16.9452 26.7319 18.0528 26.3899 18.8317 25.7059ZM15.2634 24.6471H14.4554V21.0848H15.5723C16.2429 21.0848 16.7353 21.2271 17.0495 21.5116C17.3637 21.7962 17.5208 22.2367 17.5208 22.8331C17.5208 23.4241 17.3333 23.8742 16.9584 24.1833C16.5835 24.4925 16.0185 24.6471 15.2634 24.6471Z" fill="#ED522A"/>
</svg>

After

Width:  |  Height:  |  Size: 955 B

View File

@@ -0,0 +1,8 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#999999" stroke-width="2"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#999999" stroke-width="2"/>
<rect x="8.5" y="18.5" width="15" height="1" rx="0.5" stroke="#999999"/>
<rect x="8.5" y="26.5" width="15" height="1" rx="0.5" stroke="#999999"/>
<rect x="8.5" y="22.5" width="15" height="1" rx="0.5" stroke="#999999"/>
<rect x="8.5" y="30.5" width="11" height="1" rx="0.5" stroke="#999999"/>
</svg>

After

Width:  |  Height:  |  Size: 734 B

View File

@@ -0,0 +1,5 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#338AFF" stroke-width="2"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#338AFF" stroke-width="2"/>
<path d="M11 31.5955V20.4045C11 20.2187 11.1956 20.0978 11.3618 20.1809L22.5528 25.7764C22.737 25.8685 22.737 26.1315 22.5528 26.2236L11.3618 31.8191C11.1956 31.9022 11 31.7813 11 31.5955Z" stroke="#338AFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 668 B

View File

@@ -0,0 +1,5 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#338AFF" stroke-width="2"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#338AFF" stroke-width="2"/>
<path d="M20.9939 32H18.1414L16.5172 25.601C16.4606 25.37 16.3764 24.9811 16.2647 24.4341L16.1427 23.821C16.0583 23.3804 16.0081 23.0698 15.9919 22.8892L15.9645 23.1038C15.9303 23.3432 15.8762 23.6565 15.8019 24.044L15.6459 24.8279C15.5737 25.1794 15.5137 25.4534 15.466 25.6501L13.8586 32H11.0141L8 20H10.4646L11.9758 26.5499C12.2397 27.7592 12.431 28.8071 12.5495 29.6936C12.5793 29.4057 12.6447 28.9721 12.7457 28.3927L12.7717 28.2449C12.8875 27.591 12.9966 27.0834 13.099 26.7223L14.8202 20H17.1879L18.9091 26.7223L18.9452 26.8725C19.014 27.1684 19.0962 27.578 19.1919 28.1012L19.2634 28.5005C19.342 28.9511 19.4044 29.3488 19.4505 29.6936L19.4867 29.4248C19.5401 29.0497 19.6143 28.6071 19.7091 28.0971L19.8037 27.6027C19.8856 27.1874 19.9618 26.8365 20.0323 26.5499L21.5354 20H24L20.9939 32Z" fill="#338AFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,16 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.59518 10.3571L10.4929 1.57645C10.8672 1.20709 11.3718 1 11.8977 1H28C29.6569 1 31 2.34315 31 4V36C31 37.6569 29.6569 39 28 39H4C2.34315 39 1 37.6569 1 36V11.7806C1 11.2456 1.21437 10.7329 1.59518 10.3571Z" stroke="#338AFF" stroke-width="2"/>
<rect x="18" y="35" width="3" height="2" rx="0.8" fill="#338AFF"/>
<rect x="18" y="31" width="3" height="2" rx="0.8" fill="#338AFF"/>
<rect x="18" y="27" width="3" height="2" rx="0.8" fill="#338AFF"/>
<rect x="18" y="23" width="3" height="2" rx="0.8" fill="#338AFF"/>
<rect x="18" y="19" width="3" height="2" rx="0.8" fill="#338AFF"/>
<rect x="21" y="37" width="3" height="2" rx="0.8" fill="#338AFF"/>
<rect x="21" y="33" width="3" height="2" rx="0.8" fill="#338AFF"/>
<rect x="21" y="29" width="3" height="2" rx="0.8" fill="#338AFF"/>
<rect x="21" y="25" width="3" height="2" rx="0.8" fill="#338AFF"/>
<rect x="21" y="21" width="3" height="2" rx="0.8" fill="#338AFF"/>
<rect x="21" y="17" width="3" height="2" rx="0.8" fill="#338AFF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19 8C18.4477 8 18 8.44772 18 9V14C18 15.1046 18.8954 16 20 16H22C23.1046 16 24 15.1046 24 14V9C24 8.44772 23.5523 8 23 8H19ZM20.25 10C20.1119 10 20 10.1119 20 10.25V11.5C20 11.7761 20.2239 12 20.5 12H21.5C21.7761 12 22 11.7761 22 11.5V10.25C22 10.1119 21.8881 10 21.75 10H20.25Z" fill="#338AFF"/>
<path d="M11 2V11C11 11.5523 10.5523 12 10 12H2" stroke="#338AFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,126 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {DownloadedItem} from 'types/downloads';
import {
CLOSE_DOWNLOADS_DROPDOWN,
REQUEST_CLEAR_DOWNLOADS_DROPDOWN,
REQUEST_DOWNLOADS_DROPDOWN_INFO,
SEND_DOWNLOADS_DROPDOWN_SIZE,
UPDATE_DOWNLOADS_DROPDOWN,
} from 'common/communication';
import IntlProvider from './intl_provider';
import DownloadsDropdownItem from './components/DownloadsDropdown/DownloadsDropdownItem';
import './css/downloadsDropdown.scss';
type State = {
downloads: DownloadedItem[];
darkMode?: boolean;
windowBounds?: Electron.Rectangle;
item?: DownloadedItem;
}
class DownloadsDropdown extends React.PureComponent<Record<string, never>, State> {
constructor(props: Record<string, never>) {
super(props);
this.state = {
downloads: [],
};
window.addEventListener('message', this.handleMessageEvent);
}
componentDidMount() {
window.postMessage({type: REQUEST_DOWNLOADS_DROPDOWN_INFO}, window.location.href);
}
componentDidUpdate() {
window.postMessage({type: SEND_DOWNLOADS_DROPDOWN_SIZE, data: {width: document.body.scrollWidth, height: document.body.scrollHeight}}, window.location.href);
}
handleMessageEvent = (event: MessageEvent) => {
if (event.data.type === UPDATE_DOWNLOADS_DROPDOWN) {
const {downloads, darkMode, windowBounds, item} = event.data.data;
const newDownloads = Object.values<DownloadedItem>(downloads);
newDownloads.sort((a, b) => {
// Show App update first
if (a.type === 'update') {
return -1;
} else if (b.type === 'update') {
return 1;
}
return b.addedAt - a.addedAt;
});
this.setState({
downloads: newDownloads,
darkMode,
windowBounds,
item,
});
}
}
closeMenu = () => {
window.postMessage({type: CLOSE_DOWNLOADS_DROPDOWN}, window.location.href);
}
clearAll = () => {
window.postMessage({type: REQUEST_CLEAR_DOWNLOADS_DROPDOWN}, window.location.href);
}
render() {
return (
<IntlProvider>
<div
className={classNames('DownloadsDropdown', {
darkMode: this.state.darkMode,
})}
>
<div className='DownloadsDropdown__header'>
<div className='DownloadsDropdown__Downloads'>
<FormattedMessage
id='renderer.downloadsDropdown.Downloads'
defaultMessage='Downloads'
/>
</div>
<div
className={'DownloadsDropdown__clearAllButton'}
onClick={this.clearAll}
>
<FormattedMessage
id='renderer.downloadsDropdown.ClearAll'
defaultMessage='Clear All'
/>
</div>
</div>
<hr className='DownloadsDropdown__divider'/>
<div className='DownloadsDropdown__list'>
{(this.state.downloads || []).map((downloadItem: DownloadedItem) => {
return (
<DownloadsDropdownItem
item={downloadItem}
key={downloadItem.filename}
activeItem={this.state.item}
/>
);
})}
</div>
</div>
</IntlProvider>
);
}
}
ReactDOM.render(
<DownloadsDropdown/>,
document.getElementById('app'),
);

View File

@@ -0,0 +1,163 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useState} from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import {FormattedMessage} from 'react-intl';
import {DownloadedItem} from 'types/downloads';
import {
DOWNLOADS_DROPDOWN_MENU_CANCEL_DOWNLOAD,
DOWNLOADS_DROPDOWN_MENU_CLEAR_FILE,
DOWNLOADS_DROPDOWN_MENU_OPEN_FILE,
DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER,
REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO,
UPDATE_DOWNLOADS_DROPDOWN_MENU,
} from 'common/communication';
import IntlProvider from './intl_provider';
import './css/downloadsDropdownMenu.scss';
const DownloadsDropdownMenu = () => {
const [item, setItem] = useState<DownloadedItem | null>(null);
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
const handleMessageEvent = (event: MessageEvent) => {
if (event.data.type === UPDATE_DOWNLOADS_DROPDOWN_MENU) {
const {item, darkMode} = event.data.data;
setItem(item);
setDarkMode(darkMode);
}
};
window.addEventListener('message', handleMessageEvent);
window.postMessage({type: REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO}, window.location.href);
return () => {
window.removeEventListener('message', handleMessageEvent);
};
}, []);
const preventPropagation = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
};
const getOSFileManager = () => {
switch (window.process.platform) {
case 'darwin':
return (
<FormattedMessage
id='renderer.downloadsDropdownMenu.ShowInFinder'
defaultMessage='Show in Finder'
/>);
case 'linux':
return (
<FormattedMessage
id='renderer.downloadsDropdownMenu.ShowInFileManager'
defaultMessage='Show in File Manager'
/>);
case 'win32':
return (
<FormattedMessage
id='renderer.downloadsDropdownMenu.ShowInFileExplorer'
defaultMessage='Show in File Explorer'
/>);
default:
return (
<FormattedMessage
id='renderer.downloadsDropdownMenu.ShowInFolder'
defaultMessage='Show in Folder'
/>);
}
};
const openFile = useCallback(() => {
if (item?.type === 'update') {
return;
}
window.postMessage({type: DOWNLOADS_DROPDOWN_MENU_OPEN_FILE, payload: {item}}, window.location.href);
}, [item]);
const showInFolder = useCallback(() => {
if (item?.type === 'update') {
return;
}
window.postMessage({type: DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER, payload: {item}}, window.location.href);
}, [item]);
const clearFile = useCallback(() => {
if (item?.type === 'update') {
return;
}
window.postMessage({type: DOWNLOADS_DROPDOWN_MENU_CLEAR_FILE, payload: {item}}, window.location.href);
}, [item]);
const cancelDownload = useCallback(() => {
if (item?.state !== 'progressing') {
return;
}
window.postMessage({type: DOWNLOADS_DROPDOWN_MENU_CANCEL_DOWNLOAD, payload: {item}}, window.location.href);
}, [item]);
return (
<IntlProvider>
<div
onClick={preventPropagation}
className={classNames('DownloadsDropdownMenu', {
darkMode,
})}
>
<div
className={classNames('DownloadsDropdownMenu__MenuItem', {
disabled: item?.type === 'update',
})}
onClick={openFile}
>
<FormattedMessage
id='renderer.downloadsDropdownMenu.Open'
defaultMessage='Open'
/>
</div>
<div
className={classNames('DownloadsDropdownMenu__MenuItem', {
disabled: item?.type === 'update',
})}
onClick={showInFolder}
>
{getOSFileManager()}
</div>
<div
className={classNames('DownloadsDropdownMenu__MenuItem', {
disabled: item?.type === 'update',
})}
onClick={clearFile}
>
<FormattedMessage
id='renderer.downloadsDropdownMenu.Clear'
defaultMessage='Clear'
/>
</div>
<div
className={classNames('DownloadsDropdownMenu__MenuItem', {
disabled: item?.state !== 'progressing',
})}
onClick={cancelDownload}
>
<FormattedMessage
id='renderer.downloadsDropdownMenu.CancelDownload'
defaultMessage='Cancel Download'
/>
</div>
</div>
</IntlProvider>
);
};
ReactDOM.render(
<DownloadsDropdownMenu/>,
document.getElementById('app'),
);

View File

@@ -99,7 +99,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
}
}
preventPropogation = (event: React.MouseEvent<HTMLDivElement>) => {
preventPropagation = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
}
@@ -160,7 +160,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
window.removeEventListener('keydown', this.handleKeyboardShortcuts);
}
setButtonRef = (teamIndex: number, refMethod?: (element: HTMLButtonElement) => any) => {
setButtonRef = (teamIndex: number, refMethod?: (element: HTMLButtonElement) => unknown) => {
return (ref: HTMLButtonElement) => {
this.addButtonRef(teamIndex, ref);
refMethod?.(ref);
@@ -232,7 +232,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
return (
<IntlProvider>
<div
onClick={this.preventPropogation}
onClick={this.preventPropagation}
className={classNames('TeamDropdown', {
darkMode: this.state.darkMode,
})}

View File

@@ -119,7 +119,6 @@ class Root extends React.PureComponent<Record<string, never>, State> {
if (!config) {
return null;
}
return (
<IntlProvider>
<MainPage

94
src/renderer/utils.ts Normal file
View File

@@ -0,0 +1,94 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import prettyBytes from 'pretty-bytes';
import {IntlShape} from 'react-intl';
import {DownloadedItem} from 'types/downloads';
import {Constants} from './constants';
const prettyBytesConverter = (value: number | string, excludeUnits?: boolean, totalUnits?: string): string => {
let returnValue = 'N/A';
if (typeof value === 'number') {
returnValue = prettyBytes(value);
} else if (typeof value === 'string') {
const parsed = parseInt(value, 10);
if (typeof parsed === 'number') {
returnValue = prettyBytes(parsed);
}
}
if (excludeUnits && totalUnits === returnValue.split(' ')[1]) {
return returnValue.split(' ')[0];
}
return returnValue;
};
const getFileSizeOrBytesProgress = (item: DownloadedItem) => {
const totalMegabytes = prettyBytesConverter(item.totalBytes);
if (item.state === 'progressing') {
return `${prettyBytesConverter(item.receivedBytes, true, totalMegabytes.split(' ')[1])}/${totalMegabytes}`;
}
return `${totalMegabytes}`;
};
const getDownloadingFileStatus = (item: DownloadedItem) => {
switch (item.state) {
case 'completed':
return 'Downloaded';
case 'deleted':
return 'Deleted';
default:
return 'Cancelled';
}
};
const getIconClassName = (file: DownloadedItem) => {
if (file.type === 'update') {
return 'mattermost';
}
if (!file.mimeType) {
return 'generic';
}
// Find thumbnail icon form MIME type
const fileType = file.mimeType.toLowerCase() as keyof typeof Constants.ICON_NAME_FROM_MIME_TYPE;
if (fileType in Constants.ICON_NAME_FROM_MIME_TYPE) {
return Constants.ICON_NAME_FROM_MIME_TYPE[fileType];
}
// Fallback to file extension
const extension = file.location.toLowerCase().split('.').pop() as keyof typeof Constants.ICON_NAME_FROM_EXTENSION;
if (extension && (extension in Constants.ICON_NAME_FROM_EXTENSION)) {
return Constants.ICON_NAME_FROM_EXTENSION[extension];
}
// use generic icon
return 'generic';
};
const isImageFile = (file: DownloadedItem): boolean => {
return file.mimeType?.toLowerCase().startsWith('image/') ?? false;
};
const prettyETA = (ms = 0, intl: IntlShape) => {
let eta;
if (ms < Constants.MINUTE_MS) {
eta = `${Math.round(ms / Constants.SECOND_MS)} ${intl.formatMessage({id: 'renderer.time.sec', defaultMessage: 'sec'})}`;
} else if (ms < Constants.HOUR_MS) {
eta = `${Math.round(ms / Constants.MINUTE_MS)} ${intl.formatMessage({id: 'renderer.time.mins', defaultMessage: 'mins'})}`;
} else {
eta = `${Math.round(ms / Constants.HOUR_MS)} ${intl.formatMessage({id: 'renderer.time.hours', defaultMessage: 'hours'})}`;
}
return `${eta} ${intl.formatMessage({id: 'renderer.downloadsDropdown.remaining', defaultMessage: 'remaining'})}`;
};
export {
getDownloadingFileStatus,
getFileSizeOrBytesProgress,
getIconClassName,
isImageFile,
prettyETA,
};