[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:
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
26
src/renderer/components/DownloadsDropdown/ProgressBar.tsx
Normal file
26
src/renderer/components/DownloadsDropdown/ProgressBar.tsx
Normal 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;
|
48
src/renderer/components/DownloadsDropdown/ThreeDotButton.tsx
Normal file
48
src/renderer/components/DownloadsDropdown/ThreeDotButton.tsx
Normal 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;
|
63
src/renderer/components/DownloadsDropdown/Thumbnail.tsx
Normal file
63
src/renderer/components/DownloadsDropdown/Thumbnail.tsx
Normal 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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
Reference in New Issue
Block a user