[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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 4805 additions and 264 deletions

View file

@ -24,7 +24,9 @@
"import/no-commonjs": 2,
"no-process-env": 0,
"no-var": 2,
"react/no-find-dom-node": 2
"react/no-find-dom-node": 2,
"multiline-ternary": 0,
"max-lines": ["warn", 650]
},
"overrides": [
{

25
.vscode/settings.json vendored
View file

@ -6,12 +6,35 @@
"source.fixAll.eslint": true
},
"cSpell.words": [
"automations",
"Autoupgrade",
"browserview",
"btns",
"chromedriver",
"chromedriverlog",
"deauthorize",
"deeplinking",
"diskimage",
"dont",
"favicons",
"filechooser",
"flac",
"FOCALBOARD",
"ICONNAME",
"loadscreen",
"mmjstool",
"NOSERVERS",
"Ochiai",
"officedocument",
"openxmlformats",
"presentationml",
"spreadsheetml",
"textbox",
"UNCLOSEABLE",
"Unreads",
"webcontents",
"wordprocessingml",
"Yuya"
]
],
"i18n-ally.keystyle": "nested"
}

View file

@ -15,7 +15,7 @@ const {ipcRenderer} = require('electron');
const {SHOW_SETTINGS_WINDOW} = require('../../src/common/communication');
const {asyncSleep} = require('./utils');
const {asyncSleep, mkDirAsync, rmDirAsync, unlinkAsync} = require('./utils');
chai.should();
const sourceRootDir = path.join(__dirname, '../..');
@ -28,6 +28,8 @@ const electronBinaryPath = (() => {
})();
const userDataDir = path.join(sourceRootDir, 'e2e/testUserData/');
const configFilePath = path.join(userDataDir, 'config.json');
const downloadsFilePath = path.join(userDataDir, 'downloads.json');
const downloadsLocation = path.join(userDataDir, 'Downloads');
const boundsInfoPath = path.join(userDataDir, 'bounds-info.json');
const appUpdatePath = path.join(userDataDir, 'app-update.yml');
const exampleURL = 'http://example.com/';
@ -51,12 +53,10 @@ const exampleTeam = {
{
name: 'TAB_FOCALBOARD',
order: 1,
isOpen: true,
},
{
name: 'TAB_PLAYBOOKS',
order: 2,
isOpen: true,
},
],
lastActiveTab: 0,
@ -100,10 +100,14 @@ const demoConfig = {
useSpellChecker: true,
enableHardwareAcceleration: true,
autostart: true,
hideOnStart: false,
spellCheckerLocales: [],
darkMode: false,
lastActiveTeam: 0,
spellCheckerLocales: [],
startInFullscreen: false,
autoCheckForUpdates: false,
appLanguage: 'en',
logLevel: 'silly',
};
const demoMattermostConfig = {
@ -119,6 +123,8 @@ const cmdOrCtrl = process.platform === 'darwin' ? 'command' : 'control';
module.exports = {
sourceRootDir,
configFilePath,
downloadsFilePath,
downloadsLocation,
userDataDir,
boundsInfoPath,
appUpdatePath,
@ -147,7 +153,7 @@ module.exports = {
},
cleanTestConfig() {
[configFilePath, boundsInfoPath].forEach((file) => {
[configFilePath, downloadsFilePath, boundsInfoPath].forEach((file) => {
try {
fs.unlinkSync(file);
} catch (err) {
@ -158,6 +164,13 @@ module.exports = {
}
});
},
async cleanTestConfigAsync() {
await Promise.all(
[configFilePath, downloadsFilePath, boundsInfoPath].map((file) => {
return unlinkAsync(file);
}),
);
},
cleanDataDir() {
try {
@ -169,28 +182,35 @@ module.exports = {
}
}
},
cleanDataDirAsync() {
return rmDirAsync(userDataDir);
},
createTestUserDataDir() {
if (!fs.existsSync(userDataDir)) {
fs.mkdirSync(userDataDir);
}
},
async createTestUserDataDirAsync() {
await mkDirAsync(userDataDir);
},
async getApp(args = []) {
const options = {
downloadsPath: downloadsLocation,
env: {
...process.env,
RESOURCES_PATH: userDataDir,
},
executablePath: electronBinaryPath,
args: [`${path.join(sourceRootDir, 'dist')}`, `--data-dir=${userDataDir}`, '--disable-dev-mode', ...args],
args: [`${path.join(sourceRootDir, 'dist')}`, `--user-data-dir=${userDataDir}`, '--disable-dev-mode', ...args],
};
// if (process.env.MM_DEBUG_SETTINGS) {
// options.chromeDriverLogPath = './chromedriverlog.txt';
// }
// if (process.platform === 'darwin' || process.platform === 'linux') {
// // on a mac, debbuging port might conflict with other apps
// // on a mac, debugging port might conflict with other apps
// // this changes the default debugging port so chromedriver can run without issues.
// options.chromeDriverArgs.push('remote-debugging-port=9222');
//}
@ -242,6 +262,27 @@ module.exports = {
await window.click('#saveSetting');
},
async openDownloadsDropdown(app) {
const mainWindow = app.windows().find((window) => window.url().includes('index'));
await mainWindow.waitForLoadState();
await mainWindow.bringToFront();
const dlButtonLocator = await mainWindow.waitForSelector('.DownloadsDropdownButton');
await dlButtonLocator.click();
await asyncSleep(500);
const downloadsWindow = app.windows().find((window) => window.url().includes('downloadsDropdown'));
await downloadsWindow.waitForLoadState();
await downloadsWindow.bringToFront();
return downloadsWindow;
},
async downloadsDropdownIsOpen(app) {
const downloadsWindow = app.windows().find((window) => window.url().includes('downloadsDropdown'));
const result = await downloadsWindow.isVisible('.DownloadsDropdown');
return result;
},
addClientCommands(client) {
client.addCommand('loadSettingsPage', function async() {
ipcRenderer.send(SHOW_SETTINGS_WINDOW);

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const fs = require('fs');
function asyncSleep(timeout) {
return new Promise((resolve) => {
@ -10,6 +11,92 @@ function asyncSleep(timeout) {
});
}
function dirExistsAsync(path) {
return new Promise((resolve, reject) => {
fs.stat(path, (error, stats) => {
if (error) {
if (error.code === 'ENOENT') {
resolve(false);
} else {
reject(error);
}
return;
}
resolve(stats.isDirectory());
});
});
}
function mkDirAsync(path) {
return new Promise((resolve, reject) => {
dirExistsAsync(path).then((exists) => {
if (!exists) {
fs.mkdir(path, {recursive: true}, (error) => {
if (error) {
reject(error);
return;
}
resolve();
});
}
}).catch((err) => {
reject(err);
});
});
}
function rmDirAsync(path) {
return new Promise((resolve, reject) => {
dirExistsAsync(path).then((exists) => {
if (exists) {
fs.rm(path, {recursive: true, force: true}, (error) => {
if (error) {
if (error.code === 'ENOENT') {
resolve();
}
reject(error);
}
resolve();
});
}
resolve();
}).catch((err) => {
reject(err);
});
});
}
function unlinkAsync(path) {
return new Promise((resolve, reject) => {
fs.unlink(path, (error) => {
if (error) {
if (error.code === 'ENOENT') {
resolve();
}
reject(error);
}
resolve();
});
});
}
function writeFileAsync(path, data) {
return new Promise((resolve, reject) => {
fs.writeFile(path, data, (err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
module.exports = {
asyncSleep,
dirExistsAsync,
mkDirAsync,
rmDirAsync,
unlinkAsync,
writeFileAsync,
};

View file

@ -0,0 +1,193 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
const path = require('path');
const env = require('../../modules/environment');
const {asyncSleep, mkDirAsync, rmDirAsync, writeFileAsync} = require('../../modules/utils');
const config = env.demoConfig;
const file1 = {
addedAt: Date.UTC(2022, 8, 8, 10), // Aug 08, 2022 10:00AM UTC
filename: 'file1.txt',
mimeType: 'plain/text',
location: path.join(env.downloadsLocation, 'file1.txt'),
progress: 100,
receivedBytes: 3917388,
state: 'completed',
totalBytes: 3917388,
type: 'file',
};
const file2 = {
addedAt: Date.UTC(2022, 8, 8, 11), // Aug 08, 2022 11:00AM UTC
filename: 'file2.txt',
mimeType: 'plain/text',
location: path.join(env.downloadsLocation, 'file2.txt'),
progress: 100,
receivedBytes: 7917388,
state: 'completed',
totalBytes: 7917388,
type: 'file',
};
describe('downloads/downloads_dropdown_items', function desc() {
this.timeout(30000);
describe('The list has one downloaded file', () => {
const downloads = {
[file1.filename]: file1,
};
beforeEach(async () => {
await env.createTestUserDataDirAsync();
await env.cleanTestConfigAsync();
await writeFileAsync(env.configFilePath, JSON.stringify(config));
await writeFileAsync(env.downloadsFilePath, JSON.stringify(downloads));
await mkDirAsync(env.downloadsLocation);
await writeFileAsync(path.join(env.downloadsLocation, 'file1.txt'), 'file1 content');
await asyncSleep(1000);
this.app = await env.getApp();
this.downloadsWindow = await env.openDownloadsDropdown(this.app);
});
afterEach(async () => {
await rmDirAsync(env.downloadsLocation);
await this.app?.close?.();
await env.clearElectronInstances();
});
it('MM-22239 should display the file correctly (downloaded)', async () => {
const filenameTextLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__Filename');
const filenameInnerText = await filenameTextLocator.innerText();
filenameInnerText.should.equal(downloads['file1.txt'].filename);
const fileStateLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__FileSizeAndStatus');
const fileStateInnerText = await fileStateLocator.innerText();
fileStateInnerText.should.equal('3.92 MB • Downloaded');
const fileThumbnailLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Thumbnail');
const thumbnailBackgroundImage = await fileThumbnailLocator.evaluate((node) => window.getComputedStyle(node).getPropertyValue('background-image'));
thumbnailBackgroundImage.should.include('text..svg');
});
});
describe('The list has one downloaded file but it is deleted from the folder', () => {
const downloads = {
[file1.filename]: file1,
};
beforeEach(async () => {
await env.createTestUserDataDirAsync();
await env.cleanTestConfigAsync();
await writeFileAsync(env.configFilePath, JSON.stringify(config));
await writeFileAsync(env.downloadsFilePath, JSON.stringify(downloads));
await mkDirAsync(env.downloadsLocation);
await asyncSleep(1000);
this.app = await env.getApp();
this.downloadsWindow = await env.openDownloadsDropdown(this.app);
});
afterEach(async () => {
await rmDirAsync(env.downloadsLocation);
await this.app?.close?.();
await env.clearElectronInstances();
});
it('MM-22239 should display the file correctly (deleted)', async () => {
const filenameTextLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__Filename');
const filenameInnerText = await filenameTextLocator.innerText();
filenameInnerText.should.equal(downloads['file1.txt'].filename);
const fileStateLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__FileSizeAndStatus');
const fileStateInnerText = await fileStateLocator.innerText();
fileStateInnerText.should.equal('3.92 MB • Deleted');
const fileThumbnailLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Thumbnail');
const thumbnailBackgroundImage = await fileThumbnailLocator.evaluate((node) => window.getComputedStyle(node).getPropertyValue('background-image'));
thumbnailBackgroundImage.should.include('text..svg');
});
});
describe('The list has one cancelled file', () => {
const downloads = {
[file1.filename]: {
...file1,
state: 'progressing',
progress: 50,
receivedBytes: 1958694,
totalBytes: 3917388,
},
};
beforeEach(async () => {
await env.createTestUserDataDirAsync();
await env.cleanTestConfigAsync();
await writeFileAsync(env.configFilePath, JSON.stringify(config));
await writeFileAsync(env.downloadsFilePath, JSON.stringify(downloads));
await mkDirAsync(env.downloadsLocation);
await writeFileAsync(path.join(env.downloadsLocation, 'file1.txt'), 'file1 content');
await asyncSleep(1000);
this.app = await env.getApp();
this.downloadsWindow = await env.openDownloadsDropdown(this.app);
});
afterEach(async () => {
await rmDirAsync(env.downloadsLocation);
await this.app?.close?.();
await env.clearElectronInstances();
});
it('MM-22239 should display the file correctly (cancelled)', async () => {
const filenameTextLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__Filename');
const filenameInnerText = await filenameTextLocator.innerText();
filenameInnerText.should.equal(downloads['file1.txt'].filename);
const fileStateLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Details__FileSizeAndStatus');
const fileStateInnerText = await fileStateLocator.innerText();
fileStateInnerText.should.equal('3.92 MB • Cancelled');
const fileThumbnailLocator = await this.downloadsWindow.waitForSelector('.DownloadsDropdown__File__Body__Thumbnail');
const thumbnailBackgroundImage = await fileThumbnailLocator.evaluate((node) => window.getComputedStyle(node).getPropertyValue('background-image'));
thumbnailBackgroundImage.should.include('text..svg');
});
});
describe('The list has two downloaded files', () => {
const downloads = {
'file1.txt': file1,
'file2.txt': file2,
};
beforeEach(async () => {
await env.createTestUserDataDirAsync();
await env.cleanTestConfigAsync();
await writeFileAsync(env.configFilePath, JSON.stringify(config));
await writeFileAsync(env.downloadsFilePath, JSON.stringify(downloads));
await mkDirAsync(env.downloadsLocation);
await writeFileAsync(path.join(env.downloadsLocation, 'file1.txt'), 'file1 content');
await writeFileAsync(path.join(env.downloadsLocation, 'file2.txt'), 'file2 content');
await asyncSleep(1000);
this.app = await env.getApp();
this.downloadsWindow = await env.openDownloadsDropdown(this.app);
});
afterEach(async () => {
await rmDirAsync(env.downloadsLocation);
await this.app?.close?.();
await env.clearElectronInstances();
});
it('MM-22239 should display the files in correct order', async () => {
const filenameTextLocators = this.downloadsWindow.locator('.DownloadsDropdown__File__Body__Details__Filename');
(await filenameTextLocators.count()).should.equal(2);
const firstItemLocator = filenameTextLocators.first();
const file1InnerText = await firstItemLocator.innerText();
file1InnerText.should.equal(downloads['file2.txt'].filename); // newest first
const secondItemLocator = filenameTextLocators.nth(1);
const file2InnerText = await secondItemLocator.innerText();
file2InnerText.should.equal(downloads['file1.txt'].filename);
});
});
});

View file

@ -0,0 +1,84 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
const robot = require('robotjs');
const env = require('../../modules/environment');
const {asyncSleep, rmDirAsync, writeFileAsync} = require('../../modules/utils');
const config = {
...env.demoMattermostConfig,
teams: [
...env.demoMattermostConfig.teams,
{
url: 'https://community.mattermost.com',
name: 'community',
order: 0,
tabs: [
{
name: 'TAB_MESSAGING',
order: 0,
isOpen: true,
},
{
name: 'TAB_FOCALBOARD',
order: 1,
},
{
name: 'TAB_PLAYBOOKS',
order: 2,
},
],
lastActiveTab: 0,
},
],
};
describe('downloads/downloads_manager', function desc() {
this.timeout(30000);
let firstServer;
const filename = `${Date.now().toString()}.txt`;
beforeEach(async () => {
await env.cleanDataDirAsync();
await env.cleanTestConfigAsync();
await env.createTestUserDataDirAsync();
await writeFileAsync(env.configFilePath, JSON.stringify(config));
await asyncSleep(1000);
this.app = await env.getApp();
this.serverMap = await env.getServerMap(this.app);
const loadingScreen = this.app.windows().find((window) => window.url().includes('loadingScreen'));
await loadingScreen.waitForSelector('.LoadingScreen', {state: 'hidden'});
firstServer = this.serverMap[`${config.teams[0].name}___TAB_MESSAGING`].win;
await env.loginToMattermost(firstServer);
const textbox = await firstServer.waitForSelector('#post_textbox');
const fileInput = await firstServer.waitForSelector('input[type="file"]');
await fileInput.setInputFiles({
name: filename,
mimeType: 'text/plain',
buffer: Buffer.from('this is test file'),
});
await asyncSleep(1000);
await textbox.focus();
robot.keyTap('enter');
});
afterEach(async () => {
await rmDirAsync(env.downloadsLocation);
await this.app?.close?.();
await env.clearElectronInstances();
});
it('MM-22239 should open downloads dropdown when a download starts', async () => {
await firstServer.locator('#file-attachment-link', {hasText: filename}).click();
await asyncSleep(1000);
await Promise.all([
firstServer.waitForEvent('download'), // It is important to call waitForEvent before click to set up waiting.
firstServer.locator(`div[role="dialog"] a[download="${filename}"]`).click(), // Triggers the download.
]);
(await env.downloadsDropdownIsOpen(this.app)).should.equal(true);
});
});

View file

@ -0,0 +1,126 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
const path = require('path');
const env = require('../../modules/environment');
const {asyncSleep, writeFileAsync} = require('../../modules/utils');
const config = env.demoConfig;
const downloads = {
'file1.txt': {
addedAt: Date.UTC(2022, 8, 8, 10), // Aug 08, 2022 10:00AM UTC
filename: 'file1.txt',
mimeType: 'plain/text',
location: path.join(env.downloadsLocation, 'file1.txt'),
progress: 100,
receivedBytes: 3917388,
state: 'completed',
totalBytes: 3917388,
type: 'file',
},
};
describe('downloads/downloads_menubar', function desc() {
this.timeout(30000);
describe('The download list is empty', () => {
beforeEach(async () => {
await env.createTestUserDataDirAsync();
await env.cleanTestConfigAsync();
await writeFileAsync(env.configFilePath, JSON.stringify(config));
await writeFileAsync(env.downloadsFilePath, JSON.stringify({}));
await asyncSleep(1000);
this.app = await env.getApp();
});
afterEach(async () => {
await this.app?.close?.();
await env.clearElectronInstances();
});
it('MM-22239 should not show the downloads dropdown and the menu item should be disabled', async () => {
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
await mainWindow.waitForLoadState();
await mainWindow.bringToFront();
const dlButton = mainWindow.locator('.DownloadsDropdownButton');
(await dlButton.isVisible()).should.equal(false);
const saveMenuItem = await this.app.evaluate(async ({app}) => {
const viewMenu = app.applicationMenu.getMenuItemById('view');
const saveItem = viewMenu.submenu.getMenuItemById('app-menu-downloads');
return saveItem;
});
saveMenuItem.should.haveOwnProperty('enabled', false);
});
});
describe('The download list has one file', () => {
beforeEach(async () => {
await env.createTestUserDataDirAsync();
await env.cleanTestConfigAsync();
await writeFileAsync(env.configFilePath, JSON.stringify(config));
await writeFileAsync(env.downloadsFilePath, JSON.stringify(downloads));
await asyncSleep(1000);
this.app = await env.getApp();
});
afterEach(async () => {
await this.app?.close?.();
await env.clearElectronInstances();
});
it('MM-22239 should show the downloads dropdown button and the menu item should be enabled', async () => {
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
await mainWindow.waitForLoadState();
await mainWindow.bringToFront();
const dlButton = await mainWindow.waitForSelector('.DownloadsDropdownButton', {state: 'attached'});
(await dlButton.isVisible()).should.equal(true);
const saveMenuItem = await this.app.evaluate(async ({app}) => {
const viewMenu = app.applicationMenu.getMenuItemById('view');
const saveItem = viewMenu.submenu.getMenuItemById('app-menu-downloads');
return saveItem;
});
saveMenuItem.should.haveOwnProperty('enabled', true);
});
it('MM-22239 should open the downloads dropdown when clicking the download button in the menubar', async () => {
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
await mainWindow.waitForLoadState();
await mainWindow.bringToFront();
const dlButton = await mainWindow.waitForSelector('.DownloadsDropdownButton', {state: 'attached'});
(await dlButton.isVisible()).should.equal(true);
await dlButton.click();
await asyncSleep(500);
(await env.downloadsDropdownIsOpen(this.app)).should.equal(true);
});
it('MM-22239 should open the downloads dropdown from the app menu', async () => {
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
await mainWindow.waitForLoadState();
await mainWindow.bringToFront();
await this.app.evaluate(async ({app}) => {
const viewMenu = app.applicationMenu.getMenuItemById('view');
const downloadsItem = viewMenu.submenu.getMenuItemById('app-menu-downloads');
downloadsItem.click();
});
await asyncSleep(500);
(await env.downloadsDropdownIsOpen(this.app)).should.equal(true);
});
});
});

View file

@ -33,16 +33,8 @@
"main.app.utils.migrateMacAppStore.button.selectAndImport": "Select Directory and Import",
"main.app.utils.migrateMacAppStore.dialog.detail": "It appears that an existing {appName} configuration exists, would you like to import it? You will be asked to pick the correct configuration directory.",
"main.app.utils.migrateMacAppStore.dialog.message": "Import Existing Configuration",
"main.autoUpdater.download.dialog.button.download": "Download",
"main.autoUpdater.download.dialog.button.remindMeLater": "Remind me Later",
"main.autoUpdater.download.dialog.detail": "A new version of the {appName} Desktop App is available for you to download and install now.",
"main.autoUpdater.download.dialog.message": "New desktop version available",
"main.autoUpdater.noUpdate.detail": "You are using the latest version of the {appName} Desktop App (version {version}). You'll be notified when a new version is available to install.",
"main.autoUpdater.noUpdate.message": "You're up to date",
"main.autoUpdater.update.dialog.button.remindMeLater": "Remind me Later",
"main.autoUpdater.update.dialog.button.restartAndUpdate": "Restart and Update",
"main.autoUpdater.update.dialog.detail": "A new version of the {appName} Desktop App is ready to install.",
"main.autoUpdater.update.dialog.message": "A new version is ready to install",
"main.badge.noUnreads": "You have no unread messages",
"main.badge.sessionExpired": "Session Expired: Please sign in to continue receiving notifications.",
"main.badge.unreadChannels": "You have unread channels",
@ -84,6 +76,7 @@
"main.menus.app.view.clearCacheAndReload": "Clear Cache and Reload",
"main.menus.app.view.devToolsAppWrapper": "Developer Tools for Application Wrapper",
"main.menus.app.view.devToolsCurrentServer": "Developer Tools for Current Server",
"main.menus.app.view.downloads": "Downloads",
"main.menus.app.view.find": "Find..",
"main.menus.app.view.fullscreen": "Toggle Full Screen",
"main.menus.app.view.reload": "Reload",
@ -140,10 +133,7 @@
"renderer.components.extraBar.back": "Back",
"renderer.components.input.required": "This field is required",
"renderer.components.mainPage.contextMenu.ariaLabel": "Context menu",
"renderer.components.mainPage.downloadingUpdate": "Downloading update. {percentDone}% of {total} @ {speed}/s",
"renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.mainPage.updateAvailable": "Update available",
"renderer.components.mainPage.updateReady": "Update ready to install",
"renderer.components.newTeamModal.error.nameRequired": "Name is required.",
"renderer.components.newTeamModal.error.serverNameExists": "A server with the same name already exists.",
"renderer.components.newTeamModal.error.serverUrlExists": "A server with the same URL already exists.",
@ -235,6 +225,21 @@
"renderer.components.welcomeScreen.slides.playbooks.title": "Playbooks",
"renderer.components.welcomeScreen.slides.welcome.subtitle": "Mattermost is an open source platform for developer collaboration. Secure, flexible, and integrated with the tools you love.",
"renderer.components.welcomeScreen.slides.welcome.title": "Welcome",
"renderer.downloadsDropdown.ClearAll": "Clear All",
"renderer.downloadsDropdown.Downloads": "Downloads",
"renderer.downloadsDropdown.remaining": "remaining",
"renderer.downloadsDropdown.Update.ANewVersionIsAvailableToInstall": "A new version of the Mattermost Desktop App (version {version}) is available to install.",
"renderer.downloadsDropdown.Update.DownloadUpdate": "Download Update",
"renderer.downloadsDropdown.Update.MattermostVersionX": "Mattermost version {version}",
"renderer.downloadsDropdown.Update.NewDesktopVersionAvailable": "New Desktop version available",
"renderer.downloadsDropdown.Update.RestartAndUpdate": "Restart & update",
"renderer.downloadsDropdownMenu.CancelDownload": "Cancel Download",
"renderer.downloadsDropdownMenu.Clear": "Clear",
"renderer.downloadsDropdownMenu.Open": "Open",
"renderer.downloadsDropdownMenu.ShowInFileExplorer": "Show in File Explorer",
"renderer.downloadsDropdownMenu.ShowInFileManager": "Show in File Manager",
"renderer.downloadsDropdownMenu.ShowInFinder": "Show in Finder",
"renderer.downloadsDropdownMenu.ShowInFolder": "Show in Folder",
"renderer.dropdown.addAServer": "Add a server",
"renderer.dropdown.servers": "Servers",
"renderer.modals.certificate.certificateModal.certInfoButton": "Certificate Information",
@ -252,5 +257,8 @@
"renderer.modals.permission.permissionModal.body": "A site that's not included in your Mattermost server configuration requires access for {permission}.",
"renderer.modals.permission.permissionModal.requestOriginatedFromOrigin": "This request originated from <link>{origin}</link>",
"renderer.modals.permission.permissionModal.title": "{permission} Required",
"renderer.modals.permission.permissionModal.unknownOrigin": "unknown origin"
"renderer.modals.permission.permissionModal.unknownOrigin": "unknown origin",
"renderer.time.hours": "hours",
"renderer.time.mins": "mins",
"renderer.time.sec": "sec"
}

556
package-lock.json generated
View file

@ -52,6 +52,7 @@
"@storybook/react": "6.4.20",
"@types/auto-launch": "5.0.2",
"@types/electron-devtools-installer": "2.2.1",
"@types/jest": "^29.0.0",
"@types/react": "17.0.43",
"@types/react-beautiful-dnd": "13.0.0",
"@types/react-dom": "17.0.14",
@ -2872,6 +2873,27 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/@jest/expect-utils": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.0.2.tgz",
"integrity": "sha512-+wcQF9khXKvAEi8VwROnCWWmHfsJYCZAs5dmuMlJBKk57S6ZN2/FQMIlo01F29fJyT8kV/xblE7g3vkIdTLOjw==",
"dev": true,
"dependencies": {
"jest-get-type": "^29.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/expect-utils/node_modules/jest-get-type": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.0.0.tgz",
"integrity": "sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==",
"dev": true,
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/fake-timers": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz",
@ -3013,6 +3035,18 @@
"node": ">=8"
}
},
"node_modules/@jest/schemas": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz",
"integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==",
"dev": true,
"dependencies": {
"@sinclair/typebox": "^0.24.1"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/source-map": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz",
@ -3685,6 +3719,12 @@
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
"node_modules/@sinclair/typebox": {
"version": "0.24.38",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.38.tgz",
"integrity": "sha512-IbYB6vdhLFmzGEyXXEdFAJKyq7S4/RsivkgxNzs/LzwYuUJHmeNQ0cHkjG/Yqm6VgUzzZDLMZAf0XgeeaZAocA==",
"dev": true
},
"node_modules/@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@ -8989,6 +9029,276 @@
"@types/istanbul-lib-report": "*"
}
},
"node_modules/@types/jest": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.0.0.tgz",
"integrity": "sha512-X6Zjz3WO4cT39Gkl0lZ2baFRaEMqJl5NC1OjElkwtNzAlbkr2K/WJXkBkH5VP0zx4Hgsd2TZYdOEfvp2Dxia+Q==",
"dev": true,
"dependencies": {
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
}
},
"node_modules/@types/jest/node_modules/@jest/types": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.0.2.tgz",
"integrity": "sha512-5WNMesBLmlkt1+fVkoCjHa0X3i3q8zc4QLTDkdHgCa2gyPZc7rdlZBWgVLqwS1860ZW5xJuCDwAzqbGaXIr/ew==",
"dev": true,
"dependencies": {
"@jest/schemas": "^29.0.0",
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^17.0.8",
"chalk": "^4.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@types/jest/node_modules/@types/yargs": {
"version": "17.0.12",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.12.tgz",
"integrity": "sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ==",
"dev": true,
"dependencies": {
"@types/yargs-parser": "*"
}
},
"node_modules/@types/jest/node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@types/jest/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@types/jest/node_modules/ci-info": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz",
"integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==",
"dev": true
},
"node_modules/@types/jest/node_modules/diff-sequences": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.0.0.tgz",
"integrity": "sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==",
"dev": true,
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@types/jest/node_modules/expect": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.0.2.tgz",
"integrity": "sha512-JeJlAiLKn4aApT4pzUXBVxl3NaZidWIOdg//smaIlP9ZMBDkHZGFd9ubphUZP9pUyDEo7bC6M0IIZR51o75qQw==",
"dev": true,
"dependencies": {
"@jest/expect-utils": "^29.0.2",
"jest-get-type": "^29.0.0",
"jest-matcher-utils": "^29.0.2",
"jest-message-util": "^29.0.2",
"jest-util": "^29.0.2"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@types/jest/node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@types/jest/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/@types/jest/node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/@types/jest/node_modules/jest-diff": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.0.2.tgz",
"integrity": "sha512-b9l9970sa1rMXH1owp2Woprmy42qIwwll/htsw4Gf7+WuSp5bZxNhkKHDuCGKL+HoHn1KhcC+tNEeAPYBkD2Jg==",
"dev": true,
"dependencies": {
"chalk": "^4.0.0",
"diff-sequences": "^29.0.0",
"jest-get-type": "^29.0.0",
"pretty-format": "^29.0.2"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@types/jest/node_modules/jest-get-type": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.0.0.tgz",
"integrity": "sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==",
"dev": true,
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@types/jest/node_modules/jest-matcher-utils": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.0.2.tgz",
"integrity": "sha512-s62YkHFBfAx0JLA2QX1BlnCRFwHRobwAv2KP1+YhjzF6ZCbCVrf1sG8UJyn62ZUsDaQKpoo86XMTjkUyO5aWmQ==",
"dev": true,
"dependencies": {
"chalk": "^4.0.0",
"jest-diff": "^29.0.2",
"jest-get-type": "^29.0.0",
"pretty-format": "^29.0.2"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@types/jest/node_modules/jest-message-util": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.0.2.tgz",
"integrity": "sha512-kcJAgms3ckJV0wUoLsAM40xAhY+pb9FVSZwicjFU9PFkaTNmqh9xd99/CzKse48wPM1ANUQKmp03/DpkY+lGrA==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.12.13",
"@jest/types": "^29.0.2",
"@types/stack-utils": "^2.0.0",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
"micromatch": "^4.0.4",
"pretty-format": "^29.0.2",
"slash": "^3.0.0",
"stack-utils": "^2.0.3"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@types/jest/node_modules/jest-util": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.0.2.tgz",
"integrity": "sha512-ozk8ruEEEACxqpz0hN9UOgtPZS0aN+NffwQduR5dVlhN+eN47vxurtvgZkYZYMpYrsmlAEx1XabkB3BnN0GfKQ==",
"dev": true,
"dependencies": {
"@jest/types": "^29.0.2",
"@types/node": "*",
"chalk": "^4.0.0",
"ci-info": "^3.2.0",
"graceful-fs": "^4.2.9",
"picomatch": "^2.2.3"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@types/jest/node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"dependencies": {
"braces": "^3.0.2",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/@types/jest/node_modules/pretty-format": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.0.2.tgz",
"integrity": "sha512-wp3CdtUa3cSJVFn3Miu5a1+pxc1iPIQTenOAn+x5erXeN1+ryTcLesV5pbK/rlW5EKwp27x38MoYfNGaNXDDhg==",
"dev": true,
"dependencies": {
"@jest/schemas": "^29.0.0",
"ansi-styles": "^5.0.0",
"react-is": "^18.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@types/jest/node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@types/jest/node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"node_modules/@types/jest/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@types/jest/node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/@types/json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz",
@ -34827,6 +35137,23 @@
"jest-mock": "^27.5.1"
}
},
"@jest/expect-utils": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.0.2.tgz",
"integrity": "sha512-+wcQF9khXKvAEi8VwROnCWWmHfsJYCZAs5dmuMlJBKk57S6ZN2/FQMIlo01F29fJyT8kV/xblE7g3vkIdTLOjw==",
"dev": true,
"requires": {
"jest-get-type": "^29.0.0"
},
"dependencies": {
"jest-get-type": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.0.0.tgz",
"integrity": "sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==",
"dev": true
}
}
},
"@jest/fake-timers": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz",
@ -34934,6 +35261,15 @@
}
}
},
"@jest/schemas": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz",
"integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==",
"dev": true,
"requires": {
"@sinclair/typebox": "^0.24.1"
}
},
"@jest/source-map": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz",
@ -35454,6 +35790,12 @@
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
"@sinclair/typebox": {
"version": "0.24.38",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.38.tgz",
"integrity": "sha512-IbYB6vdhLFmzGEyXXEdFAJKyq7S4/RsivkgxNzs/LzwYuUJHmeNQ0cHkjG/Yqm6VgUzzZDLMZAf0XgeeaZAocA==",
"dev": true
},
"@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@ -39623,6 +39965,220 @@
"@types/istanbul-lib-report": "*"
}
},
"@types/jest": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.0.0.tgz",
"integrity": "sha512-X6Zjz3WO4cT39Gkl0lZ2baFRaEMqJl5NC1OjElkwtNzAlbkr2K/WJXkBkH5VP0zx4Hgsd2TZYdOEfvp2Dxia+Q==",
"dev": true,
"requires": {
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
},
"dependencies": {
"@jest/types": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.0.2.tgz",
"integrity": "sha512-5WNMesBLmlkt1+fVkoCjHa0X3i3q8zc4QLTDkdHgCa2gyPZc7rdlZBWgVLqwS1860ZW5xJuCDwAzqbGaXIr/ew==",
"dev": true,
"requires": {
"@jest/schemas": "^29.0.0",
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^17.0.8",
"chalk": "^4.0.0"
}
},
"@types/yargs": {
"version": "17.0.12",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.12.tgz",
"integrity": "sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ==",
"dev": true,
"requires": {
"@types/yargs-parser": "*"
}
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"ci-info": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz",
"integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==",
"dev": true
},
"diff-sequences": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.0.0.tgz",
"integrity": "sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==",
"dev": true
},
"expect": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.0.2.tgz",
"integrity": "sha512-JeJlAiLKn4aApT4pzUXBVxl3NaZidWIOdg//smaIlP9ZMBDkHZGFd9ubphUZP9pUyDEo7bC6M0IIZR51o75qQw==",
"dev": true,
"requires": {
"@jest/expect-utils": "^29.0.2",
"jest-get-type": "^29.0.0",
"jest-matcher-utils": "^29.0.2",
"jest-message-util": "^29.0.2",
"jest-util": "^29.0.2"
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"jest-diff": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.0.2.tgz",
"integrity": "sha512-b9l9970sa1rMXH1owp2Woprmy42qIwwll/htsw4Gf7+WuSp5bZxNhkKHDuCGKL+HoHn1KhcC+tNEeAPYBkD2Jg==",
"dev": true,
"requires": {
"chalk": "^4.0.0",
"diff-sequences": "^29.0.0",
"jest-get-type": "^29.0.0",
"pretty-format": "^29.0.2"
}
},
"jest-get-type": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.0.0.tgz",
"integrity": "sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==",
"dev": true
},
"jest-matcher-utils": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.0.2.tgz",
"integrity": "sha512-s62YkHFBfAx0JLA2QX1BlnCRFwHRobwAv2KP1+YhjzF6ZCbCVrf1sG8UJyn62ZUsDaQKpoo86XMTjkUyO5aWmQ==",
"dev": true,
"requires": {
"chalk": "^4.0.0",
"jest-diff": "^29.0.2",
"jest-get-type": "^29.0.0",
"pretty-format": "^29.0.2"
}
},
"jest-message-util": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.0.2.tgz",
"integrity": "sha512-kcJAgms3ckJV0wUoLsAM40xAhY+pb9FVSZwicjFU9PFkaTNmqh9xd99/CzKse48wPM1ANUQKmp03/DpkY+lGrA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.12.13",
"@jest/types": "^29.0.2",
"@types/stack-utils": "^2.0.0",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
"micromatch": "^4.0.4",
"pretty-format": "^29.0.2",
"slash": "^3.0.0",
"stack-utils": "^2.0.3"
}
},
"jest-util": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.0.2.tgz",
"integrity": "sha512-ozk8ruEEEACxqpz0hN9UOgtPZS0aN+NffwQduR5dVlhN+eN47vxurtvgZkYZYMpYrsmlAEx1XabkB3BnN0GfKQ==",
"dev": true,
"requires": {
"@jest/types": "^29.0.2",
"@types/node": "*",
"chalk": "^4.0.0",
"ci-info": "^3.2.0",
"graceful-fs": "^4.2.9",
"picomatch": "^2.2.3"
}
},
"micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"requires": {
"braces": "^3.0.2",
"picomatch": "^2.3.1"
}
},
"pretty-format": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.0.2.tgz",
"integrity": "sha512-wp3CdtUa3cSJVFn3Miu5a1+pxc1iPIQTenOAn+x5erXeN1+ryTcLesV5pbK/rlW5EKwp27x38MoYfNGaNXDDhg==",
"dev": true,
"requires": {
"@jest/schemas": "^29.0.0",
"ansi-styles": "^5.0.0",
"react-is": "^18.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true
}
}
},
"react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}
}
}
},
"@types/json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz",

View file

@ -135,6 +135,7 @@
"@storybook/react": "6.4.20",
"@types/auto-launch": "5.0.2",
"@types/electron-devtools-installer": "2.2.1",
"@types/jest": "^29.0.0",
"@types/react": "17.0.43",
"@types/react-beautiful-dnd": "13.0.0",
"@types/react-dom": "17.0.14",

View file

@ -11,6 +11,9 @@ let started = false;
const mainCompiler = webpack(mainConfig);
mainCompiler.watch({}, (err, stats) => {
if (err) {
console.error(err);
}
process.stdout.write(stats.toString({colors: true}));
process.stdout.write('\n');
if (!stats.hasErrors()) {

View file

@ -90,13 +90,16 @@ export const REQUEST_TEAMS_DROPDOWN_INFO = 'request-teams-dropdown-info';
export const RECEIVE_DROPDOWN_MENU_SIZE = 'receive-dropdown-menu-size';
export const SEND_DROPDOWN_MENU_SIZE = 'send-dropdown-menu-size';
export const UPDATE_AVAILABLE = 'update_available';
export const UPDATE_DOWNLOADED = 'update_downloaded';
export const CANCEL_UPGRADE = 'cancel_upgrade';
export const START_UPGRADE = 'start_upgrade';
export const START_DOWNLOAD = 'start_download';
export const CHECK_FOR_UPDATES = 'check_for_updates';
export const UPDATE_PROGRESS = 'update_progress';
export const UPDATE_AVAILABLE = 'update-available';
export const UPDATE_DOWNLOADED = 'update-downloaded';
export const UPDATE_PROGRESS = 'update-progress';
export const UPDATE_REMIND_LATER = 'update-remind-later';
export const CANCEL_UPDATE_DOWNLOAD = 'cancel-update-download';
export const CANCEL_UPGRADE = 'cancel-upgrade';
export const START_UPDATE_DOWNLOAD = 'start-update-download';
export const START_UPGRADE = 'start-upgrade';
export const CHECK_FOR_UPDATES = 'check-for-updates';
export const NO_UPDATE_AVAILABLE = 'no-update-available';
export const BROWSER_HISTORY_BUTTON = 'browser-history-button';
export const BROWSER_HISTORY_PUSH = 'browser-history-push';
@ -129,3 +132,26 @@ export const RETRIEVED_LANGUAGE_INFORMATION = 'retrieved-language-information';
export const GET_AVAILABLE_LANGUAGES = 'get-available-languages';
export const VIEW_FINISHED_RESIZING = 'view-finished-resizing';
export const REQUEST_CLEAR_DOWNLOADS_DROPDOWN = 'request-clear-downloads-dropdown';
export const CLOSE_DOWNLOADS_DROPDOWN = 'close-downloads-dropdown';
export const OPEN_DOWNLOADS_DROPDOWN = 'open-downloads-dropdown';
export const SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE = 'show-downloads-dropdown-button-badge';
export const HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE = 'hide-downloads-dropdown-button-badge';
export const REQUEST_DOWNLOADS_DROPDOWN_INFO = 'request-downloads-dropdown-info';
export const UPDATE_DOWNLOADS_DROPDOWN = 'update-downloads-dropdown';
export const DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER = 'downloads-dropdown-show-file-in-folder';
export const REQUEST_HAS_DOWNLOADS = 'request-has-downloads';
export const DOWNLOADS_DROPDOWN_FOCUSED = 'downloads-dropdown-focused';
export const RECEIVE_DOWNLOADS_DROPDOWN_SIZE = 'receive-downloads-dropdown-size';
export const SEND_DOWNLOADS_DROPDOWN_SIZE = 'send-downloads-dropdown-size';
export const OPEN_DOWNLOADS_DROPDOWN_MENU = 'open-downloads-dropdown-menu';
export const CLOSE_DOWNLOADS_DROPDOWN_MENU = 'close-downloads-dropdown-menu';
export const TOGGLE_DOWNLOADS_DROPDOWN_MENU = 'toggle-downloads-dropdown-menu';
export const UPDATE_DOWNLOADS_DROPDOWN_MENU = 'update-downloads-dropdown-menu';
export const UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM = 'update-downloads-dropdown-menu-item';
export const REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO = 'request-downloads-dropdown-menu-info';
export const DOWNLOADS_DROPDOWN_MENU_CANCEL_DOWNLOAD = 'downloads-dropdown-menu-cancel-download';
export const DOWNLOADS_DROPDOWN_MENU_CLEAR_FILE = 'downloads-dropdown-menu-clear-file';
export const DOWNLOADS_DROPDOWN_MENU_OPEN_FILE = 'downloads-dropdown-menu-open-file';

8
src/common/constants.ts Normal file
View file

@ -0,0 +1,8 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/**
* This string includes special characters so that it's not confused with
* a file that may have the same filename (eg APP_UPDATE)
*/
export const APP_UPDATE_KEY = '#:(APP_UPDATE):#';

View file

@ -14,6 +14,7 @@ export const MAX_SERVER_RETRIES = 3;
export const MAX_LOADING_SCREEN_SECONDS = 4 * SECOND;
export const TAB_BAR_HEIGHT = 40;
export const TAB_BAR_PADDING = 4;
export const BACK_BAR_HEIGHT = 36;
export const THREE_DOT_MENU_WIDTH = 40;
export const THREE_DOT_MENU_WIDTH_MAC = 80;
@ -24,6 +25,20 @@ export const DEFAULT_WINDOW_HEIGHT = 800;
export const MINIMUM_WINDOW_WIDTH = 700;
export const MINIMUM_WINDOW_HEIGHT = 240;
export const DOWNLOADS_DROPDOWN_HEIGHT = 360;
export const DOWNLOADS_DROPDOWN_WIDTH = 280;
export const DOWNLOADS_DROPDOWN_PADDING = 24;
export const DOWNLOADS_DROPDOWN_MENU_HEIGHT = 160;
export const DOWNLOADS_DROPDOWN_MENU_WIDTH = 154;
export const DOWNLOADS_DROPDOWN_MENU_PADDING = 12;
// In order to display the box-shadow & radius on the left + right, use this WIDTH in the browserView for downloadsDropdown
export const DOWNLOADS_DROPDOWN_FULL_WIDTH = DOWNLOADS_DROPDOWN_PADDING + DOWNLOADS_DROPDOWN_WIDTH + TAB_BAR_PADDING;
export const DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH = (DOWNLOADS_DROPDOWN_MENU_PADDING * 2) + DOWNLOADS_DROPDOWN_MENU_WIDTH;
export const DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT = DOWNLOADS_DROPDOWN_MENU_HEIGHT + TAB_BAR_PADDING; // only bottom padding included for better positioning
export const DOWNLOADS_DROPDOWN_MAX_ITEMS = 50;
export const DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT = 4000; // 4 sec
// supported custom login paths (oath, saml)
export const customLoginRegexPaths = [
/^\/oauth\/authorize$/i,

View file

@ -6,6 +6,7 @@ import Joi from 'joi';
import {Args} from 'types/args';
import {ConfigV0, ConfigV1, ConfigV2, ConfigV3, TeamWithTabs} from 'types/config';
import {DownloadedItems} from 'types/downloads';
import {SavedWindowState} from 'types/mainWindow';
import {AppState} from 'types/appState';
import {ComparableCertificate} from 'types/certificate';
@ -45,6 +46,20 @@ const appStateSchema = Joi.object<AppState>({
updateCheckedDate: Joi.string(),
});
const downloadsSchema = Joi.object<DownloadedItems>().pattern(
Joi.string(),
{
type: Joi.string().valid('file', 'update'),
filename: Joi.string().allow(null),
state: Joi.string().valid('interrupted', 'progressing', 'completed', 'cancelled', 'deleted', 'available'),
progress: Joi.number().min(0).max(100),
location: Joi.string().allow(''),
mimeType: Joi.string().allow(null),
addedAt: Joi.number().min(0),
receivedBytes: Joi.number().min(0),
totalBytes: Joi.number().min(0),
});
const configDataSchemaV0 = Joi.object<ConfigV0>({
url: Joi.string().required(),
});
@ -96,7 +111,7 @@ const configDataSchemaV2 = Joi.object<ConfigV2>({
});
const configDataSchemaV3 = Joi.object<ConfigV3>({
version: Joi.number().min(2).default(2),
version: Joi.number().min(3).default(3),
teams: Joi.array().items(Joi.object({
name: Joi.string().required(),
url: Joi.string().required(),
@ -171,6 +186,11 @@ export function validateAppState(data: AppState) {
return validateAgainstSchema(data, appStateSchema);
}
// validate downloads.json
export function validateDownloads(data: DownloadedItems) {
return validateAgainstSchema(data, downloadsSchema);
}
// validate v.0 config.json
export function validateV0ConfigData(data: ConfigV0) {
return validateAgainstSchema(data, configDataSchemaV0);

View file

@ -8,7 +8,6 @@ import {app, session} from 'electron';
import Config from 'common/config';
import urlUtils from 'common/utils/url';
import {displayDownloadCompleted} from 'main/notifications';
import parseArgs from 'main/ParseArgs';
import WindowManager from 'main/windows/windowManager';
@ -17,6 +16,10 @@ import {clearAppCache, getDeeplinkingURL, wasUpdated} from './utils';
jest.mock('fs', () => ({
unlinkSync: jest.fn(),
existsSync: jest.fn().mockReturnValue(false),
readFileSync: jest.fn().mockImplementation((text) => text),
writeFile: jest.fn(),
}));
jest.mock('path', () => {
@ -143,9 +146,9 @@ jest.mock('main/windows/windowManager', () => ({
getMainWindow: jest.fn(),
showMainWindow: jest.fn(),
sendToMattermostViews: jest.fn(),
sendToRenderer: jest.fn(),
getServerNameByWebContentsId: jest.fn(),
}));
describe('main/app/initialize', () => {
beforeEach(() => {
parseArgs.mockReturnValue({});
@ -228,52 +231,6 @@ describe('main/app/initialize', () => {
expect(WindowManager.showMainWindow).toHaveBeenCalledWith('mattermost://server-1.com');
});
it('should setup save dialog correctly', async () => {
const item = {
getFilename: () => 'filename.txt',
on: jest.fn(),
setSaveDialogOptions: jest.fn(),
};
Config.downloadLocation = '/some/dir';
path.resolve.mockImplementation((base, p) => `${base}/${p}`);
session.defaultSession.on.mockImplementation((event, cb) => {
if (event === 'will-download') {
cb(null, item, {id: 0, getURL: jest.fn()});
}
});
await initialize();
expect(item.setSaveDialogOptions).toHaveBeenCalledWith(expect.objectContaining({
title: 'filename.txt',
defaultPath: '/some/dir/filename.txt',
}));
});
it('should use name of saved file instead of original file name', async () => {
const item = {
getFilename: () => 'filename.txt',
on: jest.fn(),
setSaveDialogOptions: jest.fn(),
savePath: '/some/dir/new_filename.txt',
};
Config.downloadLocation = '/some/dir';
path.resolve.mockImplementation((base, p) => `${base}/${p}`);
session.defaultSession.on.mockImplementation((event, cb) => {
if (event === 'will-download') {
cb(null, item, {id: 0, getURL: jest.fn()});
}
});
item.on.mockImplementation((event, cb) => {
if (event === 'done') {
cb(null, 'completed');
}
});
await initialize();
expect(displayDownloadCompleted).toHaveBeenCalledWith('new_filename.txt', '/some/dir/new_filename.txt', expect.anything());
});
it('should allow permission requests for supported types from trusted URLs', async () => {
let callback = jest.fn();
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {

View file

@ -32,7 +32,7 @@ import {
GET_AVAILABLE_SPELL_CHECKER_LANGUAGES,
USER_ACTIVITY_UPDATE,
START_UPGRADE,
START_DOWNLOAD,
START_UPDATE_DOWNLOAD,
PING_DOMAIN,
MAIN_WINDOW_SHOWN,
} from 'common/communication';
@ -48,8 +48,8 @@ import {setupBadge} from 'main/badge';
import CertificateManager from 'main/certificateManager';
import {updatePaths} from 'main/constants';
import CriticalErrorHandler from 'main/CriticalErrorHandler';
import i18nManager, {localizeMessage} from 'main/i18nManager';
import {displayDownloadCompleted} from 'main/notifications';
import downloadsManager from 'main/downloadsManager';
import i18nManager from 'main/i18nManager';
import parseArgs from 'main/ParseArgs';
import TrustedOriginsStore from 'main/trustedOrigins';
import {refreshTrayImages, setupTray} from 'main/tray/tray';
@ -259,7 +259,7 @@ function initializeInterCommunicationEventListeners() {
ipcMain.on(SHOW_SETTINGS_WINDOW, WindowManager.showSettingsWindow);
ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, () => session.defaultSession.availableSpellCheckerLanguages);
ipcMain.handle(GET_DOWNLOAD_LOCATION, handleSelectDownload);
ipcMain.on(START_DOWNLOAD, handleStartDownload);
ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload);
ipcMain.on(START_UPGRADE, handleStartUpgrade);
ipcMain.handle(PING_DOMAIN, handlePingDomain);
}
@ -272,7 +272,7 @@ function initializeAfterAppReady() {
if (process.platform !== 'darwin') {
defaultSession.on('spellcheck-dictionary-download-failure', (event, lang) => {
if (Config.spellCheckerURL) {
log.error(`There was an error while trying to load the dictionary definitions for ${lang} fromfully the specified url. Please review you have access to the needed files. Url used was ${Config.spellCheckerURL}`);
log.error(`There was an error while trying to load the dictionary definitions for ${lang} from fully the specified url. Please review you have access to the needed files. Url used was ${Config.spellCheckerURL}`);
} else {
log.warn(`There was an error while trying to download the dictionary definitions for ${lang}, spellchecking might not work properly.`);
}
@ -358,29 +358,7 @@ function initializeAfterAppReady() {
}
setupBadge();
defaultSession.on('will-download', (event, item, webContents) => {
log.debug('Initialize.will-download', {item, sourceURL: webContents.getURL()});
const filename = item.getFilename();
const fileElements = filename.split('.');
const filters = [];
if (fileElements.length > 1) {
filters.push({
name: localizeMessage('main.app.initialize.downloadBox.allFiles', 'All files'),
extensions: ['*'],
});
}
item.setSaveDialogOptions({
title: filename,
defaultPath: Config.downloadLocation ? path.resolve(Config.downloadLocation, filename) : undefined,
filters,
});
item.on('done', (doneEvent, state) => {
if (state === 'completed') {
displayDownloadCompleted(path.basename(item.savePath), item.savePath, WindowManager.getServerNameByWebContentsId(webContents.id) || '');
}
});
});
defaultSession.on('will-download', downloadsManager.handleNewDownload);
// needs to be done after app ready
// must be done before update menu

View file

@ -31,6 +31,9 @@ jest.mock('electron-updater', () => ({
downloadUpdate: jest.fn(),
checkForUpdates: jest.fn(),
},
CancellationToken: jest.fn().mockImplementation(() => {
return {};
}),
}));
jest.mock('common/config', () => ({

View file

@ -6,16 +6,24 @@ import path from 'path';
import {dialog, ipcMain, app, nativeImage} from 'electron';
import log from 'electron-log';
import {autoUpdater, ProgressInfo, UpdateInfo} from 'electron-updater';
import {autoUpdater, CancellationToken, ProgressInfo, UpdateInfo} from 'electron-updater';
import {localizeMessage} from 'main/i18nManager';
import {displayUpgrade, displayRestartToUpgrade} from 'main/notifications';
import {CANCEL_UPGRADE, UPDATE_AVAILABLE, UPDATE_DOWNLOADED, CHECK_FOR_UPDATES, UPDATE_SHORTCUT_MENU, UPDATE_PROGRESS} from 'common/communication';
import {
CANCEL_UPGRADE,
UPDATE_AVAILABLE,
UPDATE_DOWNLOADED,
CHECK_FOR_UPDATES,
UPDATE_SHORTCUT_MENU,
UPDATE_PROGRESS,
NO_UPDATE_AVAILABLE,
CANCEL_UPDATE_DOWNLOAD,
UPDATE_REMIND_LATER,
} from 'common/communication';
import Config from 'common/config';
import WindowManager from './windows/windowManager';
const NEXT_NOTIFY = 86400000; // 24 hours
const NEXT_CHECK = 3600000; // 1 hour
@ -44,12 +52,15 @@ const appIcon = nativeImage.createFromPath(appIconURL);
**/
export class UpdateManager {
cancellationToken?: CancellationToken;
lastNotification?: NodeJS.Timeout;
lastCheck?: NodeJS.Timeout;
versionAvailable?: string;
versionDownloaded?: string;
constructor() {
this.cancellationToken = new CancellationToken();
autoUpdater.on('error', (err: Error) => {
log.error(`[Mattermost] There was an error while trying to update: ${err}`);
});
@ -70,7 +81,7 @@ export class UpdateManager {
});
autoUpdater.on('download-progress', (progress: ProgressInfo) => {
WindowManager.sendToRenderer(UPDATE_PROGRESS, progress.total, progress.delta, progress.transferred, progress.percent, progress.bytesPerSecond);
ipcMain.emit(UPDATE_PROGRESS, null, progress);
});
ipcMain.on(CANCEL_UPGRADE, () => {
@ -80,6 +91,9 @@ export class UpdateManager {
ipcMain.on(CHECK_FOR_UPDATES, () => {
this.checkForUpdates(true);
});
ipcMain.on(CANCEL_UPDATE_DOWNLOAD, this.handleCancelDownload);
ipcMain.on(UPDATE_REMIND_LATER, this.handleRemindLater);
}
notify = (): void => {
@ -95,12 +109,12 @@ export class UpdateManager {
}
notifyUpgrade = (): void => {
WindowManager.sendToRenderer(UPDATE_AVAILABLE, this.versionAvailable);
ipcMain.emit(UPDATE_AVAILABLE, null, this.versionAvailable);
displayUpgrade(this.versionAvailable || 'unknown', this.handleDownload);
}
notifyDownloaded = (): void => {
WindowManager.sendToRenderer(UPDATE_DOWNLOADED, this.versionDownloaded);
ipcMain.emit(UPDATE_DOWNLOADED, null, this.versionDownloaded);
displayRestartToUpgrade(this.versionDownloaded || 'unknown', this.handleUpdate);
}
@ -108,23 +122,16 @@ export class UpdateManager {
if (this.lastCheck) {
clearTimeout(this.lastCheck);
}
dialog.showMessageBox({
title: app.name,
message: localizeMessage('main.autoUpdater.download.dialog.message', 'New desktop version available'),
detail: localizeMessage('main.autoUpdater.download.dialog.detail', 'A new version of the {appName} Desktop App is available for you to download and install now.', {appName: app.name}),
icon: appIcon,
buttons: [
localizeMessage('main.autoUpdater.download.dialog.button.download', 'Download'),
localizeMessage('main.autoUpdater.download.dialog.button.remindMeLater', 'Remind me Later'),
],
type: 'info',
defaultId: 0,
cancelId: 1,
}).then(({response}) => {
if (response === 0) {
autoUpdater.downloadUpdate();
}
});
autoUpdater.downloadUpdate(this.cancellationToken);
}
handleCancelDownload = (): void => {
this.cancellationToken?.cancel();
this.cancellationToken = new CancellationToken();
}
handleRemindLater = (): void => {
// TODO
}
handleOnQuit = (): void => {
@ -134,27 +141,12 @@ export class UpdateManager {
}
handleUpdate = (): void => {
dialog.showMessageBox({
title: app.name,
message: localizeMessage('main.autoUpdater.update.dialog.message', 'A new version is ready to install'),
detail: localizeMessage('main.autoUpdater.update.dialog.detail', 'A new version of the {appName} Desktop App is ready to install.', {appName: app.name}),
icon: appIcon,
buttons: [
localizeMessage('main.autoUpdater.update.dialog.button.restartAndUpdate', 'Restart and Update'),
localizeMessage('main.autoUpdater.update.dialog.button.remindMeLater', 'Remind me Later'),
],
type: 'info',
defaultId: 0,
cancelId: 1,
}).then(({response}) => {
if (response === 0) {
autoUpdater.quitAndInstall();
}
});
autoUpdater.quitAndInstall();
}
displayNoUpgrade = (): void => {
const version = app.getVersion();
ipcMain.emit(NO_UPDATE_AVAILABLE);
dialog.showMessageBox({
title: app.name,
icon: appIcon,
@ -177,7 +169,12 @@ export class UpdateManager {
if (manually) {
autoUpdater.once('update-not-available', this.displayNoUpgrade);
}
autoUpdater.checkForUpdates().catch((reason) => {
autoUpdater.checkForUpdates().then((result) => {
if (!result?.updateInfo) {
ipcMain.emit(NO_UPDATE_AVAILABLE);
}
}).catch((reason) => {
ipcMain.emit(NO_UPDATE_AVAILABLE);
log.error(`[Mattermost] Failed to check for updates: ${reason}`);
});
this.lastCheck = setTimeout(() => this.checkForUpdates(false), NEXT_CHECK);

View file

@ -18,6 +18,7 @@ export let certificateStorePath = '';
export let trustedOriginsStoreFile = '';
export let boundsInfoPath = '';
export let migrationInfoPath = '';
export let downloadsJson = '';
export function updatePaths(emit = false) {
userDataPath = app.getPath('userData');
@ -29,6 +30,7 @@ export function updatePaths(emit = false) {
trustedOriginsStoreFile = path.resolve(userDataPath, 'trustedOrigins.json');
boundsInfoPath = path.join(userDataPath, 'bounds-info.json');
migrationInfoPath = path.resolve(userDataPath, 'migration-info.json');
downloadsJson = path.resolve(userDataPath, 'downloads.json');
if (emit) {
ipcMain.emit(UPDATE_PATHS);

View file

@ -0,0 +1,194 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import path from 'path';
import fs from 'fs';
import {shell} from 'electron';
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state';
import {DownloadsManager} from 'main/downloadsManager';
const downloadLocationMock = '/path/to/downloads';
const locationMock = '/some/dir/file.txt';
const locationMock1 = '/downloads/file1.txt';
jest.mock('electron', () => {
class NotificationMock {
static isSupported = jest.fn();
static didConstruct = jest.fn();
constructor() {
NotificationMock.didConstruct();
}
on = jest.fn();
show = jest.fn();
click = jest.fn();
close = jest.fn();
}
return {
app: {
getAppPath: jest.fn(),
},
BrowserView: jest.fn().mockImplementation(() => ({
webContents: {
loadURL: jest.fn(),
focus: jest.fn(),
send: jest.fn(),
},
setBounds: jest.fn(),
})),
ipcMain: {
emit: jest.fn(),
handle: jest.fn(),
on: jest.fn(),
},
Menu: {
getApplicationMenu: () => ({
getMenuItemById: jest.fn(),
}),
},
Notification: NotificationMock,
session: {
defaultSession: {
on: jest.fn(),
},
},
shell: {
showItemInFolder: jest.fn(),
openPath: jest.fn(),
},
};
});
jest.mock('path', () => {
const original = jest.requireActual('path');
return {
...original,
resolve: jest.fn(),
parse: jest.fn(),
};
});
jest.mock('fs', () => ({
existsSync: jest.fn().mockReturnValue(false),
readFileSync: jest.fn().mockImplementation((text) => text),
writeFile: jest.fn(),
}));
jest.mock('macos-notification-state', () => ({
getDoNotDisturb: jest.fn(),
}));
jest.mock('main/windows/windowManager', () => ({
sendToRenderer: jest.fn(),
}));
jest.mock('common/config', () => {
const original = jest.requireActual('common/config');
return {
...original,
downloadLocation: downloadLocationMock,
};
});
const downloadsJson = {
'file1.txt': {
addedAt: 1662545584346,
filename: 'file1.txt',
mimeType: 'text/plain',
location: '/downloads/file1.txt',
progress: 100,
receivedBytes: 5425,
state: 'completed',
totalBytes: 5425,
type: 'file',
},
'file2.txt': {
addedAt: 1662545588346,
filename: 'file2.txt',
mimeType: 'text/plain',
location: '/downloads/file2.txt',
progress: 100,
receivedBytes: 5425,
state: 'cancelled',
totalBytes: 5425,
type: 'file',
},
};
const nowSeconds = Date.now() / 1000;
const item = {
getFilename: () => 'file.txt',
getMimeType: () => 'text/plain',
getReceivedBytes: () => 2121,
getStartTime: () => nowSeconds,
getTotalBytes: () => 4242,
getSavePath: () => locationMock,
setSavePath: jest.fn(),
on: jest.fn(),
setSaveDialogOptions: jest.fn(),
once: jest.fn(),
location: locationMock,
};
const item1 = {
...item,
getFilename: () => 'file1.txt',
getSavePath: () => locationMock1,
location: locationMock1,
};
describe('main/downloadsManager', () => {
beforeEach(() => {
getDarwinDoNotDisturb.mockReturnValue(false);
});
it('should be initialized', () => {
expect(new DownloadsManager({})).toHaveProperty('downloads', {});
});
it('should mark "completed" files that were deleted as "deleted"', () => {
expect(new DownloadsManager(JSON.stringify(downloadsJson))).toHaveProperty('downloads', {...downloadsJson, 'file1.txt': {...downloadsJson['file1.txt'], state: 'deleted'}});
});
it('should handle a new download', () => {
const dl = new DownloadsManager({});
path.parse.mockImplementation(() => ({base: 'file.txt'}));
dl.handleNewDownload({}, item, {id: 0, getURL: jest.fn()});
expect(dl).toHaveProperty('downloads', {'file.txt': {
addedAt: nowSeconds * 1000,
filename: 'file.txt',
mimeType: 'text/plain',
location: '/some/dir/file.txt',
progress: 50,
receivedBytes: 2121,
state: 'progressing',
totalBytes: 4242,
type: 'file',
}});
});
it('should monitor network to retrieve the file size of downloading items', () => {
const dl = new DownloadsManager({});
const details = {
responseHeaders: {
'content-encoding': ['gzip'],
'x-uncompressed-content-length': ['4242'],
'content-disposition': ['attachment; filename="file.txt"; foobar'],
},
};
dl.webRequestOnHeadersReceivedHandler(details, jest.fn());
expect(dl.fileSizes.get('file.txt')).toBe('4242');
});
it('should clear the downloads list', () => {
const dl = new DownloadsManager(JSON.stringify(downloadsJson));
dl.clearDownloadsDropDown();
expect(dl).toHaveProperty('downloads', {});
});
it('should open downloads folder if file deleted', () => {
const dl = new DownloadsManager(JSON.stringify(downloadsJson));
path.parse.mockImplementation(() => ({base: 'file1.txt'}));
dl.showFileInFolder(item1);
expect(shell.openPath).toHaveBeenCalledWith(downloadLocationMock);
});
it('should show the file in the downloads folder', () => {
const dl = new DownloadsManager(JSON.stringify(downloadsJson));
fs.existsSync.mockReturnValueOnce(true);
path.parse.mockImplementation(() => ({base: 'file1.txt'}));
dl.showFileInFolder(item1);
expect(shell.showItemInFolder).toHaveBeenCalledWith(locationMock1);
});
});

View file

@ -0,0 +1,575 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import path from 'path';
import fs from 'fs';
import {DownloadItem, Event, WebContents, FileFilter, ipcMain, dialog, shell, Menu} from 'electron';
import log from 'electron-log';
import {ProgressInfo} from 'electron-updater';
import {DownloadedItem, DownloadItemDoneEventState, DownloadedItems, DownloadItemState, DownloadItemUpdatedEventState} from 'types/downloads';
import {
CANCEL_UPDATE_DOWNLOAD,
CLOSE_DOWNLOADS_DROPDOWN,
CLOSE_DOWNLOADS_DROPDOWN_MENU,
DOWNLOADS_DROPDOWN_FOCUSED,
HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE,
NO_UPDATE_AVAILABLE,
OPEN_DOWNLOADS_DROPDOWN,
REQUEST_HAS_DOWNLOADS,
SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE,
UPDATE_AVAILABLE,
UPDATE_DOWNLOADED,
UPDATE_DOWNLOADS_DROPDOWN,
UPDATE_PATHS,
UPDATE_PROGRESS,
} from 'common/communication';
import Config from 'common/config';
import {localizeMessage} from 'main/i18nManager';
import {displayDownloadCompleted} from 'main/notifications';
import WindowManager from 'main/windows/windowManager';
import {doubleSecToMs, getPercentage, isStringWithLength, readFilenameFromContentDispositionHeader, shouldIncrementFilename} from 'main/utils';
import {DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT, DOWNLOADS_DROPDOWN_MAX_ITEMS} from 'common/utils/constants';
import JsonFileManager from 'common/JsonFileManager';
import {APP_UPDATE_KEY} from 'common/constants';
import {downloadsJson} from './constants';
import * as Validator from './Validator';
export enum DownloadItemTypeEnum {
FILE = 'file',
UPDATE = 'update',
}
export class DownloadsManager extends JsonFileManager<DownloadedItems> {
autoCloseTimeout: NodeJS.Timeout | null;
open: boolean;
fileSizes: Map<string, string>;
progressingItems: Map<string, DownloadItem>;
downloads: DownloadedItems;
constructor(file: string) {
super(file);
this.open = false;
this.fileSizes = new Map();
this.progressingItems = new Map();
this.autoCloseTimeout = null;
this.downloads = {};
this.init();
}
private init = () => {
// ensure data loaded from file is valid
const validatedJSON = Validator.validateDownloads(this.json);
log.debug('DownloadsManager.init', {'this.json': this.json, validatedJSON});
if (validatedJSON) {
this.saveAll(validatedJSON);
} else {
this.saveAll({});
}
this.checkForDeletedFiles();
ipcMain.handle(REQUEST_HAS_DOWNLOADS, () => {
return this.hasDownloads();
});
ipcMain.on(DOWNLOADS_DROPDOWN_FOCUSED, this.clearAutoCloseTimeout);
ipcMain.on(UPDATE_AVAILABLE, this.onUpdateAvailable);
ipcMain.on(UPDATE_DOWNLOADED, this.onUpdateDownloaded);
ipcMain.on(UPDATE_PROGRESS, this.onUpdateProgress);
ipcMain.on(NO_UPDATE_AVAILABLE, this.noUpdateAvailable);
}
handleNewDownload = (event: Event, item: DownloadItem, webContents: WebContents) => {
log.debug('DownloadsManager.handleNewDownload', {item, sourceURL: webContents.getURL()});
const shouldShowSaveDialog = this.shouldShowSaveDialog(Config.downloadLocation);
if (shouldShowSaveDialog) {
const saveDialogSuccess = this.showSaveDialog(item);
if (!saveDialogSuccess) {
item.cancel();
return;
}
} else {
const filename = this.createFilename(item);
const savePath = this.getSavePath(`${Config.downloadLocation}`, filename);
item.setSavePath(savePath);
}
this.upsertFileToDownloads(item, 'progressing');
this.progressingItems.set(this.getFileId(item), item);
this.handleDownloadItemEvents(item, webContents);
this.openDownloadsDropdown();
this.toggleAppMenuDownloadsEnabled(true);
};
/**
* This function monitors webRequests and retrieves the total file size (of files being downloaded)
* from the custom HTTP header "x-uncompressed-content-length".
*/
webRequestOnHeadersReceivedHandler = (details: Electron.OnHeadersReceivedListenerDetails, cb: (headersReceivedResponse: Electron.HeadersReceivedResponse) => void) => {
const headers = details.responseHeaders ?? {};
if (headers?.['content-encoding']?.includes('gzip') && headers?.['x-uncompressed-content-length'] && headers?.['content-disposition'].join(';')?.includes('filename=')) {
const filename = readFilenameFromContentDispositionHeader(headers['content-disposition']);
const fileSize = headers['x-uncompressed-content-length']?.[0] || '0';
if (filename && (!this.fileSizes.has(filename) || this.fileSizes.get(filename)?.toString() !== fileSize)) {
this.fileSizes.set(filename, fileSize);
}
}
// With no arguments it uses the same headers
cb({});
};
checkForDeletedFiles = () => {
log.debug('DownloadsManager.checkForDeletedFiles');
const downloads = this.downloads;
let modified = false;
for (const fileId in downloads) {
if (fileId === APP_UPDATE_KEY) {
continue;
}
if (Object.prototype.hasOwnProperty.call(downloads, fileId)) {
const file = downloads[fileId];
if ((file.state === 'completed')) {
if (!file.location || !fs.existsSync(file.location)) {
downloads[fileId].state = 'deleted';
modified = true;
}
} else if (file.state === 'progressing') {
downloads[fileId].state = 'interrupted';
modified = true;
}
}
}
if (modified) {
this.saveAll(downloads);
}
}
clearDownloadsDropDown = () => {
log.debug('DownloadsManager.clearDownloadsDropDown');
this.saveAll({});
this.fileSizes = new Map();
this.closeDownloadsDropdown();
this.toggleAppMenuDownloadsEnabled(false);
}
showFileInFolder = (item?: DownloadedItem) => {
log.debug('DownloadsDropdownView.showFileInFolder', {item});
if (!item) {
log.debug('DownloadsDropdownView.showFileInFolder', 'ITEM_UNDEFINED');
return;
}
if (item.type === DownloadItemTypeEnum.UPDATE) {
return;
}
if (fs.existsSync(item.location)) {
shell.showItemInFolder(item.location);
return;
}
this.markFileAsDeleted(item);
if (Config.downloadLocation) {
shell.openPath(Config.downloadLocation);
return;
}
log.debug('DownloadsDropdownView.showFileInFolder', 'NO_DOWNLOAD_LOCATION');
}
openFile = (item?: DownloadedItem) => {
log.debug('DownloadsDropdownView.openFile', {item});
if (!item) {
log.debug('DownloadsDropdownView.openFile', 'FILE_UNDEFINED');
return;
}
if (item.type === DownloadItemTypeEnum.UPDATE) {
return;
}
if (fs.existsSync(item.location)) {
shell.openPath(item.location).catch((err) => {
log.debug('DownloadsDropdownView.openFileError', {err});
this.showFileInFolder(item);
});
} else {
log.debug('DownloadsDropdownView.openFile', 'COULD_NOT_OPEN_FILE');
this.markFileAsDeleted(item);
this.showFileInFolder(item);
}
}
clearFile = (item?: DownloadedItem) => {
log.debug('DownloadsDropdownView.clearFile', {item});
if (!item || item.type === DownloadItemTypeEnum.UPDATE) {
return;
}
const fileId = this.getDownloadedFileId(item);
const downloads = this.downloads;
delete downloads[fileId];
this.saveAll(downloads);
if (!this.hasDownloads()) {
this.closeDownloadsDropdown();
}
}
cancelDownload = (item?: DownloadedItem) => {
log.debug('DownloadsDropdownView.cancelDownload', {item});
if (!item) {
return;
}
const fileId = this.getDownloadedFileId(item);
if (this.isAppUpdate(item)) {
ipcMain.emit(CANCEL_UPDATE_DOWNLOAD);
const update = this.downloads[APP_UPDATE_KEY];
update.state = 'cancelled';
this.save(APP_UPDATE_KEY, update);
} else if (this.progressingItems.has(fileId)) {
this.progressingItems.get(fileId)?.cancel?.();
this.progressingItems.delete(fileId);
}
}
onOpen = () => {
this.open = true;
WindowManager.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
}
onClose = () => {
this.open = false;
ipcMain.emit(CLOSE_DOWNLOADS_DROPDOWN_MENU);
this.clearAutoCloseTimeout();
}
getIsOpen = () => {
return this.open;
}
hasDownloads = () => {
log.debug('DownloadsManager.hasDownloads');
return (Object.keys(this.downloads)?.length || 0) > 0;
}
getDownloads = () => {
return this.downloads;
}
openDownloadsDropdown = () => {
log.debug('DownloadsManager.openDownloadsDropdown');
this.open = true;
ipcMain.emit(OPEN_DOWNLOADS_DROPDOWN);
WindowManager.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
}
private markFileAsDeleted = (item: DownloadedItem) => {
const fileId = this.getDownloadedFileId(item);
const file = this.downloads[fileId];
file.state = 'deleted';
this.save(fileId, file);
}
private toggleAppMenuDownloadsEnabled = (value: boolean) => {
const appMenuDownloads = Menu.getApplicationMenu()?.getMenuItemById('app-menu-downloads');
if (appMenuDownloads) {
appMenuDownloads.enabled = value;
}
}
private saveAll = (downloads: DownloadedItems) => {
log.debug('DownloadsManager.saveAll');
this.downloads = downloads;
this.setJson(downloads);
ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.downloads);
WindowManager?.sendToRenderer(UPDATE_DOWNLOADS_DROPDOWN, this.downloads);
}
private save = (key: string, item: DownloadedItem) => {
log.debug('DownloadsManager.save');
this.downloads[key] = item;
this.setValue(key, item);
ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.downloads);
WindowManager?.sendToRenderer(UPDATE_DOWNLOADS_DROPDOWN, this.downloads);
}
private handleDownloadItemEvents = (item: DownloadItem, webContents: WebContents) => {
item.on('updated', (updateEvent, state) => {
this.updatedEventController(updateEvent, state, item);
});
item.once('done', (doneEvent, state) => {
this.doneEventController(doneEvent, state, item, webContents);
});
}
/**
* This function return true if "downloadLocation" is undefined
*/
private shouldShowSaveDialog = (downloadLocation?: string) => {
log.debug('DownloadsManager.shouldShowSaveDialog', {downloadLocation});
return !downloadLocation;
};
private showSaveDialog = (item: DownloadItem) => {
const filename = item.getFilename();
const fileElements = filename.split('.');
const filters = this.getFileFilters(fileElements);
const newPath = dialog.showSaveDialogSync({
title: filename,
defaultPath: filename,
filters,
});
if (newPath) {
item.setSavePath(newPath);
return true;
}
return false;
}
private closeDownloadsDropdown = () => {
log.debug('DownloadsManager.closeDownloadsDropdown');
this.open = false;
ipcMain.emit(CLOSE_DOWNLOADS_DROPDOWN);
ipcMain.emit(CLOSE_DOWNLOADS_DROPDOWN_MENU);
this.clearAutoCloseTimeout();
}
private clearAutoCloseTimeout = () => {
if (this.autoCloseTimeout) {
clearTimeout(this.autoCloseTimeout);
this.autoCloseTimeout = null;
}
}
private upsertFileToDownloads = (item: DownloadItem, state: DownloadItemState) => {
const fileId = this.getFileId(item);
log.debug('DownloadsManager.upsertFileToDownloads', {fileId});
const formattedItem = this.formatDownloadItem(item, state);
this.save(fileId, formattedItem);
this.checkIfMaxFilesReached();
};
private checkIfMaxFilesReached = () => {
const downloads = this.downloads;
if (Object.keys(downloads).length > DOWNLOADS_DROPDOWN_MAX_ITEMS) {
const oldestFileId = Object.keys(downloads).reduce((prev, curr) => {
return downloads[prev].addedAt > downloads[curr].addedAt ? curr : prev;
});
delete downloads[oldestFileId];
this.saveAll(downloads);
}
}
private shouldAutoClose = () => {
// if some other file is being downloaded
if (Object.values(this.downloads).some((item) => item.state === 'progressing')) {
return;
}
if (this.autoCloseTimeout) {
this.autoCloseTimeout.refresh();
} else {
this.autoCloseTimeout = setTimeout(() => this.closeDownloadsDropdown(), DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT);
}
}
private shouldShowBadge = () => {
log.debug('DownloadsManager.shouldShowBadge');
if (this.open === true) {
WindowManager.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
} else {
WindowManager.sendToRenderer(SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
}
}
/**
* DownloadItem event handlers
*/
private updatedEventController = (updatedEvent: Event, state: DownloadItemUpdatedEventState, item: DownloadItem) => {
log.debug('DownloadsManager.updatedEventController', {state});
this.upsertFileToDownloads(item, state);
if (state === 'interrupted') {
this.fileSizes.delete(item.getFilename());
this.progressingItems.delete(this.getFileId(item));
}
this.shouldShowBadge();
}
private doneEventController = (doneEvent: Event, state: DownloadItemDoneEventState, item: DownloadItem, webContents: WebContents) => {
log.debug('DownloadsManager.doneEventController', {state});
if (state === 'completed' && !this.open) {
displayDownloadCompleted(path.basename(item.savePath), item.savePath, WindowManager.getServerNameByWebContentsId(webContents.id) || '');
}
this.upsertFileToDownloads(item, state);
this.fileSizes.delete(item.getFilename());
this.progressingItems.delete(this.getFileId(item));
this.shouldAutoClose();
this.shouldShowBadge();
}
/**
* Related to application updates
*/
private onUpdateAvailable = (event: Event, version = 'unknown') => {
this.save(APP_UPDATE_KEY, {
type: DownloadItemTypeEnum.UPDATE,
filename: version,
state: 'available',
progress: 0,
location: '',
mimeType: null,
addedAt: 0,
receivedBytes: 0,
totalBytes: 0,
});
this.openDownloadsDropdown();
}
private onUpdateDownloaded = (event: Event, version = 'unknown') => {
const update = this.downloads[APP_UPDATE_KEY];
update.state = 'completed';
update.progress = 100;
update.filename = version;
this.save(APP_UPDATE_KEY, update);
this.openDownloadsDropdown();
}
private onUpdateProgress = (event: Event, progress: ProgressInfo) => {
log.debug('DownloadsManager.onUpdateProgress', {progress});
const {total, transferred, percent} = progress;
const update = this.downloads[APP_UPDATE_KEY];
if (typeof update.addedAt !== 'number' || update.addedAt === 0) {
update.addedAt = Date.now();
}
update.state = 'progressing';
update.totalBytes = total;
update.receivedBytes = transferred;
update.progress = Math.round(percent);
this.shouldShowBadge();
}
private noUpdateAvailable = () => {
const downloads = this.downloads;
delete downloads[APP_UPDATE_KEY];
this.saveAll(downloads);
if (!this.hasDownloads()) {
this.closeDownloadsDropdown();
}
}
/**
* Internal utils
*/
private formatDownloadItem = (item: DownloadItem, state: DownloadItemState): DownloadedItem => {
const totalBytes = this.getFileSize(item);
const receivedBytes = item.getReceivedBytes();
const progress = getPercentage(receivedBytes, totalBytes);
return {
addedAt: doubleSecToMs(item.getStartTime()),
filename: this.getFileId(item),
mimeType: item.getMimeType(),
location: item.getSavePath(),
progress,
receivedBytes,
state,
totalBytes,
type: DownloadItemTypeEnum.FILE,
};
}
private getFileSize = (item: DownloadItem) => {
const itemTotalBytes = item.getTotalBytes();
if (!itemTotalBytes) {
return parseInt(this.fileSizes.get(item.getFilename()) || '0', 10);
}
return itemTotalBytes;
}
private getSavePath = (downloadLocation: string, filename?: string) => {
const name = isStringWithLength(filename) ? `${filename}` : 'file';
return path.join(downloadLocation, name);
};
private getFileFilters = (fileElements: string[]): FileFilter[] => {
const filters = [];
if (fileElements.length > 1) {
filters.push({
name: localizeMessage('main.app.initialize.downloadBox.allFiles', 'All files'),
extensions: ['*'],
});
}
return filters;
}
private createFilename = (item: DownloadItem): string => {
const defaultFilename = item.getFilename();
const incrementedFilenameIfExists = shouldIncrementFilename(path.join(`${Config.downloadLocation}`, defaultFilename));
return incrementedFilenameIfExists;
}
private readFilenameFromPath = (savePath: string) => {
const pathObj = path.parse(savePath);
return pathObj.base;
}
private getFileId = (item: DownloadItem) => {
const fileNameFromPath = this.readFilenameFromPath(item.savePath);
const itemFilename = item.getFilename();
return fileNameFromPath && fileNameFromPath !== itemFilename ? fileNameFromPath : itemFilename;
}
private getDownloadedFileId = (item: DownloadedItem) => {
if (item.type === DownloadItemTypeEnum.UPDATE) {
return APP_UPDATE_KEY;
}
const fileNameFromPath = this.readFilenameFromPath(item.location);
const itemFilename = item.filename;
return fileNameFromPath && fileNameFromPath !== itemFilename ? fileNameFromPath : itemFilename;
}
private isAppUpdate = (item: DownloadedItem): boolean => {
return item.type === DownloadItemTypeEnum.UPDATE;
}
}
let downloadsManager = new DownloadsManager(downloadsJson);
ipcMain.on(UPDATE_PATHS, () => {
downloadsManager = new DownloadsManager(downloadsJson);
});
export default downloadsManager;

View file

@ -3,26 +3,54 @@
'use strict';
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state';
import {localizeMessage} from 'main/i18nManager';
import WindowManager from 'main/windows/windowManager';
import {createTemplate} from './app';
jest.mock('electron', () => ({
app: {
name: 'AppName',
getVersion: () => '5.0.0',
},
jest.mock('electron', () => {
class NotificationMock {
static isSupported = jest.fn();
static didConstruct = jest.fn();
constructor() {
NotificationMock.didConstruct();
}
on = jest.fn();
show = jest.fn();
click = jest.fn();
close = jest.fn();
}
return {
app: {
name: 'AppName',
getVersion: () => '5.0.0',
getAppPath: () => '',
},
ipcMain: {
emit: jest.fn(),
handle: jest.fn(),
on: jest.fn(),
},
Notification: NotificationMock,
};
});
jest.mock('fs', () => ({
existsSync: jest.fn().mockReturnValue(false),
readFileSync: jest.fn().mockImplementation((text) => text),
writeFile: jest.fn(),
}));
jest.mock('macos-notification-state', () => ({
getDoNotDisturb: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(),
}));
jest.mock('main/windows/windowManager', () => ({
getCurrentTeamName: jest.fn(),
sendToRenderer: jest.fn(),
}));
jest.mock('common/tabs/TabView', () => ({
getTabDisplayName: (name) => name,
}));
@ -79,6 +107,9 @@ describe('main/menus/app', () => {
helpLink: 'http://link-to-help.site.com',
},
};
beforeEach(() => {
getDarwinDoNotDisturb.mockReturnValue(false);
});
describe('mac only', () => {
let originalPlatform;

View file

@ -13,6 +13,7 @@ import {Config} from 'common/config';
import {localizeMessage} from 'main/i18nManager';
import WindowManager from 'main/windows/windowManager';
import {UpdateManager} from 'main/autoUpdater';
import downloadsManager from 'main/downloadsManager';
export function createTemplate(config: Config, updateManager: UpdateManager) {
const separatorItem: MenuItemConstructorOptions = {
@ -78,12 +79,14 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}
template.push({
id: 'file',
label: firstMenuName,
submenu: [
...platformAppMenu,
],
});
template.push({
id: 'edit',
label: localizeMessage('main.menus.app.edit', '&Edit'),
submenu: [{
role: 'undo',
@ -159,6 +162,13 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
role: 'zoomOut',
visible: false,
accelerator: 'CmdOrCtrl+Shift+-',
}, separatorItem, {
id: 'app-menu-downloads',
label: localizeMessage('main.menus.app.view.downloads', 'Downloads'),
enabled: downloadsManager.hasDownloads(),
click() {
return downloadsManager.openDownloadsDropdown();
},
}, separatorItem, {
label: localizeMessage('main.menus.app.view.devToolsAppWrapper', 'Developer Tools for Application Wrapper'),
accelerator: (() => {
@ -195,10 +205,12 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}
template.push({
id: 'view',
label: localizeMessage('main.menus.app.view', '&View'),
submenu: viewSubMenu,
});
template.push({
id: 'history',
label: localizeMessage('main.menus.app.history', '&History'),
submenu: [{
label: localizeMessage('main.menus.app.history.back', 'Back'),
@ -225,6 +237,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
const teams = config.data?.teams || [];
const windowMenu = {
id: 'window',
label: localizeMessage('main.menus.app.window', '&Window'),
role: isMac ? 'windowMenu' : null,
submenu: [{
@ -339,7 +352,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
},
});
template.push({label: localizeMessage('main.menus.app.help', 'Hel&p'), submenu});
template.push({id: 'help', label: localizeMessage('main.menus.app.help', 'Hel&p'), submenu});
return template;
}

View file

@ -0,0 +1,81 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import {ipcRenderer, contextBridge} from 'electron';
import {
CLOSE_DOWNLOADS_DROPDOWN,
CLOSE_DOWNLOADS_DROPDOWN_MENU,
DOWNLOADS_DROPDOWN_FOCUSED,
DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER,
GET_LANGUAGE_INFORMATION,
RECEIVE_DOWNLOADS_DROPDOWN_SIZE,
REQUEST_CLEAR_DOWNLOADS_DROPDOWN,
REQUEST_DOWNLOADS_DROPDOWN_INFO,
RETRIEVED_LANGUAGE_INFORMATION,
SEND_DOWNLOADS_DROPDOWN_SIZE,
START_UPDATE_DOWNLOAD,
START_UPGRADE,
TOGGLE_DOWNLOADS_DROPDOWN_MENU,
UPDATE_DOWNLOADS_DROPDOWN,
} from 'common/communication';
console.log('preloaded for the downloadsDropdown!');
contextBridge.exposeInMainWorld('process', {
platform: process.platform,
});
window.addEventListener('click', () => {
ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN_MENU);
});
window.addEventListener('mousemove', () => {
ipcRenderer.send(DOWNLOADS_DROPDOWN_FOCUSED);
});
/**
* renderer => main
*/
window.addEventListener('message', async (event) => {
switch (event.data.type) {
case CLOSE_DOWNLOADS_DROPDOWN:
ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN);
break;
case TOGGLE_DOWNLOADS_DROPDOWN_MENU:
ipcRenderer.send(TOGGLE_DOWNLOADS_DROPDOWN_MENU, event.data.payload);
break;
case REQUEST_DOWNLOADS_DROPDOWN_INFO:
ipcRenderer.send(REQUEST_DOWNLOADS_DROPDOWN_INFO);
break;
case SEND_DOWNLOADS_DROPDOWN_SIZE:
ipcRenderer.send(RECEIVE_DOWNLOADS_DROPDOWN_SIZE, event.data.data.width, event.data.data.height);
break;
case REQUEST_CLEAR_DOWNLOADS_DROPDOWN:
ipcRenderer.send(REQUEST_CLEAR_DOWNLOADS_DROPDOWN);
break;
case DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER:
ipcRenderer.send(DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER, event.data.payload.item);
break;
case START_UPDATE_DOWNLOAD:
ipcRenderer.send(START_UPDATE_DOWNLOAD);
break;
case START_UPGRADE:
ipcRenderer.send(START_UPGRADE);
break;
case GET_LANGUAGE_INFORMATION:
window.postMessage({type: RETRIEVED_LANGUAGE_INFORMATION, data: await ipcRenderer.invoke(GET_LANGUAGE_INFORMATION)});
break;
default:
console.log('Got an unknown message. Unknown messages are ignored');
}
});
/**
* main => renderer
*/
ipcRenderer.on(UPDATE_DOWNLOADS_DROPDOWN, (event, downloads, darkMode, windowBounds, item) => {
window.postMessage({type: UPDATE_DOWNLOADS_DROPDOWN, data: {downloads, darkMode, windowBounds, item}}, window.location.href);
});

View file

@ -0,0 +1,58 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import {ipcRenderer, contextBridge} from 'electron';
import {
GET_LANGUAGE_INFORMATION,
DOWNLOADS_DROPDOWN_MENU_CLEAR_FILE,
RETRIEVED_LANGUAGE_INFORMATION,
DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER,
DOWNLOADS_DROPDOWN_MENU_CANCEL_DOWNLOAD,
DOWNLOADS_DROPDOWN_MENU_OPEN_FILE,
UPDATE_DOWNLOADS_DROPDOWN_MENU,
REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO,
} from 'common/communication';
console.log('preloaded for the downloadsDropdownMenu!');
contextBridge.exposeInMainWorld('process', {
platform: process.platform,
});
/**
* renderer => main
*/
window.addEventListener('message', async (event) => {
switch (event.data.type) {
case REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO:
ipcRenderer.send(REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO);
break;
case DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER:
ipcRenderer.send(DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER, event.data.payload.item);
break;
case DOWNLOADS_DROPDOWN_MENU_CANCEL_DOWNLOAD:
ipcRenderer.send(DOWNLOADS_DROPDOWN_MENU_CANCEL_DOWNLOAD, event.data.payload.item);
break;
case DOWNLOADS_DROPDOWN_MENU_CLEAR_FILE:
ipcRenderer.send(DOWNLOADS_DROPDOWN_MENU_CLEAR_FILE, event.data.payload.item);
break;
case DOWNLOADS_DROPDOWN_MENU_OPEN_FILE:
ipcRenderer.send(DOWNLOADS_DROPDOWN_MENU_OPEN_FILE, event.data.payload.item);
break;
case GET_LANGUAGE_INFORMATION:
window.postMessage({type: RETRIEVED_LANGUAGE_INFORMATION, data: await ipcRenderer.invoke(GET_LANGUAGE_INFORMATION)});
break;
default:
console.log('Got an unknown message. Unknown messages are ignored');
}
});
/**
* main => renderer
*/
ipcRenderer.on(UPDATE_DOWNLOADS_DROPDOWN_MENU, (event, item, darkMode) => {
window.postMessage({type: UPDATE_DOWNLOADS_DROPDOWN_MENU, data: {item, darkMode}}, window.location.href);
});

View file

@ -12,6 +12,7 @@ import {
LOADING_SCREEN_ANIMATION_FINISHED,
TOGGLE_LOADING_SCREEN_VISIBILITY,
CLOSE_TEAMS_DROPDOWN,
CLOSE_DOWNLOADS_DROPDOWN,
} from 'common/communication';
console.log('preloaded for the loading screen!');
@ -40,4 +41,5 @@ ipcRenderer.on(TOGGLE_LOADING_SCREEN_VISIBILITY, (_, toggle) => {
window.addEventListener('click', () => {
ipcRenderer.send(CLOSE_TEAMS_DROPDOWN);
ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN);
});

View file

@ -30,6 +30,7 @@ import {
DISPATCH_GET_DESKTOP_SOURCES,
DESKTOP_SOURCES_RESULT,
VIEW_FINISHED_RESIZING,
CLOSE_DOWNLOADS_DROPDOWN,
} from 'common/communication';
const UNREAD_COUNT_INTERVAL = 1000;
@ -243,8 +244,23 @@ setInterval(() => {
webFrame.clearCache();
}, CLEAR_CACHE_INTERVAL);
window.addEventListener('click', () => {
function isDownloadLink(el) {
if (typeof el !== 'object') {
return false;
}
const parentEl = el.parentElement;
if (typeof parentEl !== 'object') {
return el.className?.includes?.('download') || el.tagName?.toLowerCase?.() === 'svg';
}
return el.closest('a[download]') !== null;
}
window.addEventListener('click', (e) => {
ipcRenderer.send(CLOSE_TEAMS_DROPDOWN);
const el = e.target;
if (!isDownloadLink(el)) {
ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN);
}
});
ipcRenderer.on(BROWSER_HISTORY_PUSH, (event, pathName) => {

View file

@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
'use strict';
import {BACK_BAR_HEIGHT, TAB_BAR_HEIGHT} from 'common/utils/constants';
import {runMode} from 'common/utils/util';
@ -28,6 +27,14 @@ jest.mock('path', () => {
resolve: (basePath, ...restOfPath) => original.join('/path/to/app/src/main', ...restOfPath),
};
});
jest.mock('fs', () => ({
accessSync: jest.fn().mockImplementation(() => {
throw new Error('file missing');
}).mockImplementationOnce(() => {}),
constants: {
F_OK: 0,
},
}));
describe('main/utils', () => {
describe('shouldBeHiddenOnStartup', () => {
@ -107,4 +114,52 @@ describe('main/utils', () => {
expect(Utils.shouldHaveBackBar('https://server-1.com', 'https://server-1.com/login')).toBe(false);
});
});
describe('isStringWithLength', () => {
it('should return true for valid string', () => {
expect(Utils.isStringWithLength('string')).toBe(true);
});
it('should return false for empty string', () => {
expect(Utils.isStringWithLength('')).toBe(false);
});
it('should return false for invalid inputs', () => {
expect(
Utils.isStringWithLength(null) ||
Utils.isStringWithLength(undefined) ||
Utils.isStringWithLength(1) ||
Utils.isStringWithLength({}) ||
Utils.isStringWithLength(() => {}),
).toBe(false);
});
});
describe('getPercentage', () => {
it('should return 0 if denominator is 0', () => {
expect(Utils.getPercentage(1, 0)).toBe(0);
});
it('should return the correct percentage', () => {
expect(Utils.getPercentage(2, 4)).toBe(50);
});
it('should return the correct percentage as integer', () => {
expect(Utils.getPercentage(1, 3)).toBe(33);
});
});
describe('readFilenameFromContentDispositionHeader', () => {
it('should read the filename from the HTTP Content-Disposition header\'s value', () => {
expect(Utils.readFilenameFromContentDispositionHeader(['attachment; filename="filename.jpg"; foobar'])).toBe('filename.jpg');
});
});
describe('doubleSecToMs', () => {
it('should convert a double number of seconds to integer milliseconds', () => {
expect(Utils.doubleSecToMs(1662561807.067542)).toBe(1662561807068);
});
});
describe('shouldIncrementFilename', () => {
it('should increment filename if file already exists', () => {
expect(Utils.shouldIncrementFilename('filename.txt')).toBe('filename (1).txt');
});
});
});

View file

@ -3,6 +3,7 @@
// See LICENSE.txt for license information.
import path from 'path';
import fs from 'fs';
import {app, BrowserWindow} from 'electron';
@ -98,3 +99,40 @@ export function composeUserAgent() {
return `${filteredUserAgent.join(' ')} Mattermost/${app.getVersion()}`;
}
export function isStringWithLength(string: unknown): boolean {
return typeof string === 'string' && string.length > 0;
}
export function getPercentage(received: number, total: number) {
if (total === 0) {
return 0;
}
return Math.round((received / total) * 100);
}
export function readFilenameFromContentDispositionHeader(header: string[]) {
return header?.join(';')?.match(/(?<=filename=")(.*)(?=")/g)?.[0];
}
export function doubleSecToMs(d: number): number {
return Math.round(d * 1000);
}
export function shouldIncrementFilename(filepath: string, increment = 0): string {
const {dir, name, ext} = path.parse(filepath);
const incrementString = increment ? ` (${increment})` : '';
const filename = `${name}${incrementString}${ext}`;
let fileExists = true;
try {
fs.accessSync(path.join(dir, filename), fs.constants.F_OK);
} catch (error) {
fileExists = false;
}
if (fileExists) {
return shouldIncrementFilename(filepath, increment + 1);
}
return filename;
}

View file

@ -0,0 +1,85 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state';
import {DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT, DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, TAB_BAR_HEIGHT} from 'common/utils/constants';
import DownloadsDropdownMenuView from './downloadsDropdownMenuView';
jest.mock('main/utils', () => ({
getLocalPreload: (file) => file,
getLocalURLString: (file) => file,
}));
jest.mock('electron', () => {
class NotificationMock {
static isSupported = jest.fn();
static didConstruct = jest.fn();
constructor() {
NotificationMock.didConstruct();
}
on = jest.fn();
show = jest.fn();
click = jest.fn();
close = jest.fn();
}
return {
app: {
getAppPath: () => '',
},
BrowserView: jest.fn().mockImplementation(() => ({
webContents: {
loadURL: jest.fn(),
focus: jest.fn(),
send: jest.fn(),
},
setBounds: jest.fn(),
})),
ipcMain: {
emit: jest.fn(),
handle: jest.fn(),
on: jest.fn(),
},
Notification: NotificationMock,
};
});
jest.mock('macos-notification-state', () => ({
getDoNotDisturb: jest.fn(),
}));
jest.mock('main/windows/windowManager', () => ({
sendToRenderer: jest.fn(),
}));
jest.mock('fs', () => ({
existsSync: jest.fn().mockReturnValue(false),
readFileSync: jest.fn().mockImplementation((text) => text),
writeFile: jest.fn(),
}));
describe('main/views/DownloadsDropdownMenuView', () => {
const window = {
getContentBounds: () => ({width: 800, height: 600, x: 0, y: 0}),
addBrowserView: jest.fn(),
setTopBrowserView: jest.fn(),
};
const downloadsDropdownMenuView = new DownloadsDropdownMenuView(window, {}, false);
beforeEach(() => {
getDarwinDoNotDisturb.mockReturnValue(false);
});
describe('getBounds', () => {
it('should be placed top-left inside the downloads dropdown if coordinates not used', () => {
expect(downloadsDropdownMenuView.getBounds(DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT)).toStrictEqual({x: 800 - DOWNLOADS_DROPDOWN_FULL_WIDTH - DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, y: TAB_BAR_HEIGHT, width: DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, height: DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT});
});
});
it('should change the view bounds based on open/closed state', () => {
downloadsDropdownMenuView.bounds = {width: 400, height: 300};
downloadsDropdownMenuView.handleOpen();
expect(downloadsDropdownMenuView.view.setBounds).toBeCalledWith(downloadsDropdownMenuView.bounds);
downloadsDropdownMenuView.handleClose();
expect(downloadsDropdownMenuView.view.setBounds).toBeCalledWith({width: 0, height: 0, x: expect.any(Number), y: expect.any(Number)});
});
});

View file

@ -0,0 +1,223 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserView, BrowserWindow, ipcMain, IpcMainEvent} from 'electron';
import log from 'electron-log';
import {CombinedConfig} from 'types/config';
import {CoordinatesToJsonType, DownloadedItem, DownloadsMenuOpenEventPayload} from 'types/downloads';
import {
CLOSE_DOWNLOADS_DROPDOWN_MENU,
DOWNLOADS_DROPDOWN_MENU_CANCEL_DOWNLOAD,
DOWNLOADS_DROPDOWN_MENU_CLEAR_FILE,
DOWNLOADS_DROPDOWN_MENU_OPEN_FILE,
DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER,
EMIT_CONFIGURATION,
OPEN_DOWNLOADS_DROPDOWN_MENU,
REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO,
TOGGLE_DOWNLOADS_DROPDOWN_MENU,
UPDATE_DOWNLOADS_DROPDOWN_MENU,
UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM,
} from 'common/communication';
import {
DOWNLOADS_DROPDOWN_FULL_WIDTH,
DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT,
DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH,
TAB_BAR_HEIGHT,
} from 'common/utils/constants';
import {getLocalPreload, getLocalURLString} from 'main/utils';
import WindowManager from '../windows/windowManager';
import downloadsManager from 'main/downloadsManager';
export default class DownloadsDropdownMenuView {
open: boolean;
view: BrowserView;
bounds?: Electron.Rectangle;
item?: DownloadedItem;
coordinates?: CoordinatesToJsonType;
darkMode: boolean;
window: BrowserWindow;
windowBounds: Electron.Rectangle;
constructor(window: BrowserWindow, darkMode: boolean) {
this.open = false;
this.item = undefined;
this.coordinates = undefined;
this.window = window;
this.darkMode = darkMode;
this.windowBounds = this.window.getContentBounds();
this.bounds = this.getBounds(DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT);
const preload = getLocalPreload('downloadsDropdownMenu.js');
this.view = new BrowserView({webPreferences: {
preload,
// Workaround for this issue: https://github.com/electron/electron/issues/30993
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
transparent: true,
}});
this.view.webContents.loadURL(getLocalURLString('downloadsDropdownMenu.html'));
this.window.addBrowserView(this.view);
ipcMain.on(OPEN_DOWNLOADS_DROPDOWN_MENU, this.handleOpen);
ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN_MENU, this.handleClose);
ipcMain.on(TOGGLE_DOWNLOADS_DROPDOWN_MENU, this.handleToggle);
ipcMain.on(EMIT_CONFIGURATION, this.updateConfig);
ipcMain.on(REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO, this.updateDownloadsDropdownMenu);
ipcMain.on(DOWNLOADS_DROPDOWN_MENU_OPEN_FILE, this.openFile);
ipcMain.on(DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER, this.showFileInFolder);
ipcMain.on(DOWNLOADS_DROPDOWN_MENU_CANCEL_DOWNLOAD, this.cancelDownload);
ipcMain.on(DOWNLOADS_DROPDOWN_MENU_CLEAR_FILE, this.clearFile);
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU, this.updateItem);
}
updateItem = (event: IpcMainEvent, item: DownloadedItem) => {
log.debug('DownloadsDropdownMenuView.updateItem', {item});
this.item = item;
this.updateDownloadsDropdownMenu();
}
updateConfig = (event: IpcMainEvent, config: CombinedConfig) => {
log.debug('DownloadsDropdownMenuView.updateConfig');
this.darkMode = config.darkMode;
this.updateDownloadsDropdownMenu();
}
/**
* This is called every time the "window" is resized so that we can position
* the downloads dropdown at the correct position
*/
updateWindowBounds = () => {
log.debug('DownloadsDropdownMenuView.updateWindowBounds');
this.windowBounds = this.window.getContentBounds();
this.updateDownloadsDropdownMenu();
this.repositionDownloadsDropdownMenu();
}
updateDownloadsDropdownMenu = () => {
log.debug('DownloadsDropdownMenuView.updateDownloadsDropdownMenu');
this.view.webContents.send(
UPDATE_DOWNLOADS_DROPDOWN_MENU,
this.item,
this.darkMode,
);
ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, true, this.item);
this.repositionDownloadsDropdownMenu();
}
handleOpen = (event: IpcMainEvent, payload: DownloadsMenuOpenEventPayload = {} as DownloadsMenuOpenEventPayload) => {
log.debug('DownloadsDropdownMenuView.handleOpen', {bounds: this.bounds, payload});
if (!this.bounds) {
return;
}
const {item, coordinates} = payload;
log.debug('DownloadsDropdownMenuView.handleOpen', {item, coordinates});
this.open = true;
this.coordinates = coordinates;
this.item = item;
this.bounds = this.getBounds(DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT);
this.view.setBounds(this.bounds);
this.window.setTopBrowserView(this.view);
this.view.webContents.focus();
this.updateDownloadsDropdownMenu();
}
handleClose = () => {
log.debug('DownloadsDropdownMenuView.handleClose');
this.open = false;
this.item = undefined;
ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM);
this.view.setBounds(this.getBounds(0, 0));
WindowManager.sendToRenderer(CLOSE_DOWNLOADS_DROPDOWN_MENU);
}
handleToggle = (event: IpcMainEvent, payload: DownloadsMenuOpenEventPayload) => {
if (this.open) {
if (this.item?.location === payload.item.location) {
// clicking 3-dot in the same item
this.handleClose();
} else {
// clicking 3-dot in a different item
this.handleClose();
this.handleOpen(event, payload);
}
} else {
this.handleOpen(event, payload);
}
}
openFile = () => {
downloadsManager.openFile(this.item);
this.handleClose();
}
clearFile = () => {
downloadsManager.clearFile(this.item);
this.handleClose();
}
cancelDownload = () => {
downloadsManager.cancelDownload(this.item);
this.handleClose();
}
showFileInFolder = (e: IpcMainEvent, item: DownloadedItem) => {
log.debug('DownloadsDropdownMenuView.showFileInFolder', {item});
downloadsManager.showFileInFolder(item);
this.handleClose();
}
getBounds = (width: number, height: number) => {
// MUST return integers
return {
x: this.getX(),
y: this.getY(),
width: Math.round(width),
height: Math.round(height),
};
}
getX = () => {
const result = (this.windowBounds.width - DOWNLOADS_DROPDOWN_FULL_WIDTH - DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH) + (this.coordinates?.x || 0) + (this.coordinates?.width || 0);
if (result <= DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH) {
return 0;
}
return Math.round(result);
}
getY = () => {
const result = TAB_BAR_HEIGHT + (this.coordinates?.y || 0) + (this.coordinates?.height || 0);
return Math.round(result);
}
repositionDownloadsDropdownMenu = () => {
this.bounds = this.getBounds(DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT);
if (downloadsManager.getIsOpen()) {
this.view.setBounds(this.bounds);
}
}
destroy = () => {
// workaround to eliminate zombie processes
// https://github.com/mattermost/desktop/pull/1519
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.view.webContents.destroy();
}
}

View file

@ -0,0 +1,102 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
'use strict';
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state';
import {DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, TAB_BAR_HEIGHT} from 'common/utils/constants';
import DownloadsDropdownView from './downloadsDropdownView';
jest.mock('main/utils', () => ({
getLocalPreload: (file) => file,
getLocalURLString: (file) => file,
}));
jest.mock('fs', () => ({
existsSync: jest.fn().mockReturnValue(false),
readFileSync: jest.fn().mockImplementation((text) => text),
writeFile: jest.fn(),
}));
jest.mock('macos-notification-state', () => ({
getDoNotDisturb: jest.fn(),
}));
jest.mock('electron', () => {
class NotificationMock {
static isSupported = jest.fn();
static didConstruct = jest.fn();
constructor() {
NotificationMock.didConstruct();
}
on = jest.fn();
show = jest.fn();
click = jest.fn();
close = jest.fn();
}
return {
app: {
getAppPath: () => '',
},
BrowserView: jest.fn().mockImplementation(() => ({
webContents: {
loadURL: jest.fn(),
focus: jest.fn(),
session: {
webRequest: {
onHeadersReceived: jest.fn(),
},
},
},
setBounds: jest.fn(),
})),
ipcMain: {
emit: jest.fn(),
handle: jest.fn(),
on: jest.fn(),
},
Notification: NotificationMock,
};
});
jest.mock('main/windows/windowManager', () => ({
sendToRenderer: jest.fn(),
}));
describe('main/views/DownloadsDropdownView', () => {
beforeEach(() => {
getDarwinDoNotDisturb.mockReturnValue(false);
});
describe('getBounds', () => {
it('should be placed far right when window is large enough', () => {
const window = {
getContentBounds: () => ({width: 800, height: 600, x: 0, y: 0}),
addBrowserView: jest.fn(),
setTopBrowserView: jest.fn(),
};
const downloadsDropdownView = new DownloadsDropdownView(window, {}, false);
expect(downloadsDropdownView.getBounds(DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT)).toStrictEqual({x: 800 - DOWNLOADS_DROPDOWN_FULL_WIDTH, y: TAB_BAR_HEIGHT, width: DOWNLOADS_DROPDOWN_FULL_WIDTH, height: DOWNLOADS_DROPDOWN_HEIGHT});
});
it('should be placed left if window is very small', () => {
const window = {
getContentBounds: () => ({width: 500, height: 400, x: 0, y: 0}),
addBrowserView: jest.fn(),
setTopBrowserView: jest.fn(),
};
const downloadsDropdownView = new DownloadsDropdownView(window, {}, false);
expect(downloadsDropdownView.getBounds(DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT)).toStrictEqual({x: 0, y: TAB_BAR_HEIGHT, width: DOWNLOADS_DROPDOWN_FULL_WIDTH, height: DOWNLOADS_DROPDOWN_HEIGHT});
});
});
it('should change the view bounds based on open/closed state', () => {
const window = {
getContentBounds: () => ({width: 800, height: 600, x: 0, y: 0}),
addBrowserView: jest.fn(),
setTopBrowserView: jest.fn(),
};
const downloadsDropdownView = new DownloadsDropdownView(window, {}, false);
downloadsDropdownView.bounds = {width: 400, height: 300};
downloadsDropdownView.handleOpen();
expect(downloadsDropdownView.view.setBounds).toBeCalledWith(downloadsDropdownView.bounds);
downloadsDropdownView.handleClose();
expect(downloadsDropdownView.view.setBounds).toBeCalledWith({width: 0, height: 0, x: expect.any(Number), y: expect.any(Number)});
});
});

View file

@ -0,0 +1,201 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserView, BrowserWindow, ipcMain, IpcMainEvent} from 'electron';
import log from 'electron-log';
import {CombinedConfig} from 'types/config';
import {DownloadedItem, DownloadedItems} from 'types/downloads';
import {
CLOSE_DOWNLOADS_DROPDOWN,
DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER,
EMIT_CONFIGURATION,
OPEN_DOWNLOADS_DROPDOWN,
RECEIVE_DOWNLOADS_DROPDOWN_SIZE,
REQUEST_CLEAR_DOWNLOADS_DROPDOWN,
REQUEST_DOWNLOADS_DROPDOWN_INFO,
UPDATE_DOWNLOADS_DROPDOWN,
UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM,
} from 'common/communication';
import {TAB_BAR_HEIGHT, DOWNLOADS_DROPDOWN_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, DOWNLOADS_DROPDOWN_FULL_WIDTH} from 'common/utils/constants';
import {getLocalPreload, getLocalURLString} from 'main/utils';
import WindowManager from '../windows/windowManager';
import downloadsManager from 'main/downloadsManager';
export default class DownloadsDropdownView {
bounds?: Electron.Rectangle;
darkMode: boolean;
downloads: DownloadedItems;
item: DownloadedItem | undefined;
view: BrowserView;
window: BrowserWindow;
windowBounds: Electron.Rectangle;
constructor(window: BrowserWindow, downloads: DownloadedItems, darkMode: boolean) {
this.downloads = downloads;
this.window = window;
this.darkMode = darkMode;
this.item = undefined;
this.windowBounds = this.window.getContentBounds();
this.bounds = this.getBounds(DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT);
const preload = getLocalPreload('downloadsDropdown.js');
this.view = new BrowserView({webPreferences: {
preload,
// Workaround for this issue: https://github.com/electron/electron/issues/30993
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
transparent: true,
}});
this.view.webContents.loadURL(getLocalURLString('downloadsDropdown.html'));
this.window.addBrowserView(this.view);
this.view.webContents.session.webRequest.onHeadersReceived(downloadsManager.webRequestOnHeadersReceivedHandler);
ipcMain.on(OPEN_DOWNLOADS_DROPDOWN, this.handleOpen);
ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN, this.handleClose);
ipcMain.on(EMIT_CONFIGURATION, this.updateConfig);
ipcMain.on(REQUEST_DOWNLOADS_DROPDOWN_INFO, this.updateDownloadsDropdown);
ipcMain.on(REQUEST_CLEAR_DOWNLOADS_DROPDOWN, this.clearDownloads);
ipcMain.on(RECEIVE_DOWNLOADS_DROPDOWN_SIZE, this.handleReceivedDownloadsDropdownSize);
ipcMain.on(DOWNLOADS_DROPDOWN_SHOW_FILE_IN_FOLDER, this.showFileInFolder);
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN, this.updateDownloads);
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, this.updateDownloadsDropdownMenuItem);
}
updateDownloads = (event: IpcMainEvent, downloads: DownloadedItems) => {
log.debug('DownloadsDropdownView.updateDownloads', {downloads});
this.downloads = downloads;
this.updateDownloadsDropdown();
}
updateDownloadsDropdownMenuItem = (event: IpcMainEvent, item?: DownloadedItem) => {
log.debug('DownloadsDropdownView.updateDownloadsDropdownMenuItem', {item});
this.item = item;
this.updateDownloadsDropdown();
}
updateConfig = (event: IpcMainEvent, config: CombinedConfig) => {
log.debug('DownloadsDropdownView.updateConfig');
this.darkMode = config.darkMode;
this.updateDownloadsDropdown();
}
/**
* This is called every time the "window" is resized so that we can position
* the downloads dropdown at the correct position
*/
updateWindowBounds = () => {
log.debug('DownloadsDropdownView.updateWindowBounds');
this.windowBounds = this.window.getContentBounds();
this.updateDownloadsDropdown();
this.repositionDownloadsDropdown();
}
updateDownloadsDropdown = () => {
log.debug('DownloadsDropdownView.updateDownloadsDropdown');
this.view.webContents.send(
UPDATE_DOWNLOADS_DROPDOWN,
this.downloads,
this.darkMode,
this.windowBounds,
this.item,
);
}
handleOpen = () => {
log.debug('DownloadsDropdownView.handleOpen', {bounds: this.bounds});
if (!this.bounds) {
return;
}
this.view.setBounds(this.bounds);
this.window.setTopBrowserView(this.view);
this.view.webContents.focus();
downloadsManager.onOpen();
WindowManager.sendToRenderer(OPEN_DOWNLOADS_DROPDOWN);
}
handleClose = () => {
log.debug('DownloadsDropdownView.handleClose');
this.view.setBounds(this.getBounds(0, 0));
downloadsManager.onClose();
WindowManager.sendToRenderer(CLOSE_DOWNLOADS_DROPDOWN);
}
clearDownloads = () => {
downloadsManager.clearDownloadsDropDown();
this.handleClose();
}
showFileInFolder = (e: IpcMainEvent, item: DownloadedItem) => {
log.debug('DownloadsDropdownView.showFileInFolder', {item});
downloadsManager.showFileInFolder(item);
}
getBounds = (width: number, height: number) => {
// Must always use integers
return {
x: this.getX(this.windowBounds.width),
y: this.getY(),
width: Math.round(width),
height: Math.round(height),
};
}
getX = (windowWidth: number) => {
const result = windowWidth - DOWNLOADS_DROPDOWN_FULL_WIDTH;
if (result <= DOWNLOADS_DROPDOWN_WIDTH) {
return 0;
}
return Math.round(result);
}
getY = () => {
return Math.round(TAB_BAR_HEIGHT);
}
repositionDownloadsDropdown = () => {
if (!this.bounds) {
return;
}
this.bounds = {
...this.bounds,
x: this.getX(this.windowBounds.width),
y: this.getY(),
};
if (downloadsManager.getIsOpen()) {
this.view.setBounds(this.bounds);
}
}
handleReceivedDownloadsDropdownSize = (event: IpcMainEvent, width: number, height: number) => {
log.silly('DownloadsDropdownView.handleReceivedDownloadsDropdownSize', {width, height});
this.bounds = this.getBounds(width, height);
if (downloadsManager.getIsOpen()) {
this.view.setBounds(this.bounds);
}
}
destroy = () => {
// workaround to eliminate zombie processes
// https://github.com/mattermost/desktop/pull/1519
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.view.webContents.destroy();
}
}

View file

@ -64,10 +64,15 @@ jest.mock('../views/viewManager', () => ({
}));
jest.mock('../CriticalErrorHandler', () => jest.fn());
jest.mock('../views/teamDropdownView', () => jest.fn());
jest.mock('../views/downloadsDropdownView', () => jest.fn());
jest.mock('../views/downloadsDropdownMenuView', () => jest.fn());
jest.mock('./settingsWindow', () => ({
createSettingsWindow: jest.fn(),
}));
jest.mock('./mainWindow', () => jest.fn());
jest.mock('../downloadsManager', () => ({
getDownloads: () => {},
}));
describe('main/windows/windowManager', () => {
describe('handleUpdateConfig', () => {

View file

@ -41,6 +41,10 @@ import {ViewManager, LoadingScreenState} from '../views/viewManager';
import CriticalErrorHandler from '../CriticalErrorHandler';
import TeamDropdownView from '../views/teamDropdownView';
import DownloadsDropdownView from '../views/downloadsDropdownView';
import DownloadsDropdownMenuView from '../views/downloadsDropdownMenuView';
import downloadsManager from 'main/downloadsManager';
import {createSettingsWindow} from './settingsWindow';
import createMainWindow from './mainWindow';
@ -55,6 +59,8 @@ export class WindowManager {
settingsWindow?: BrowserWindow;
viewManager?: ViewManager;
teamDropdown?: TeamDropdownView;
downloadsDropdown?: DownloadsDropdownView;
downloadsDropdownMenu?: DownloadsDropdownMenuView;
currentServerName?: string;
constructor() {
@ -156,6 +162,8 @@ export class WindowManager {
}
this.teamDropdown = new TeamDropdownView(this.mainWindow, Config.teams, Config.darkMode, Config.enableServerManagement);
this.downloadsDropdown = new DownloadsDropdownView(this.mainWindow, downloadsManager.getDownloads(), Config.darkMode);
this.downloadsDropdownMenu = new DownloadsDropdownMenuView(this.mainWindow, Config.darkMode);
}
this.initializeViewManager();
@ -199,6 +207,8 @@ export class WindowManager {
this.throttledWillResize(newBounds);
this.viewManager?.setLoadingScreenBounds();
this.teamDropdown?.updateWindowBounds();
this.downloadsDropdown?.updateWindowBounds();
this.downloadsDropdownMenu?.updateWindowBounds();
ipcMain.emit(RESIZE_MODAL, null, newBounds);
}
@ -234,11 +244,12 @@ export class WindowManager {
const bounds = this.getBounds();
// Another workaround since the window doesn't update p roperly under Linux for some reason
// Another workaround since the window doesn't update properly under Linux for some reason
// See above comment
setTimeout(this.setCurrentViewBounds, 10, bounds);
this.viewManager.setLoadingScreenBounds();
this.teamDropdown?.updateWindowBounds();
this.downloadsDropdown?.updateWindowBounds();
ipcMain.emit(RESIZE_MODAL, null, bounds);
};
@ -274,7 +285,7 @@ export class WindowManager {
}
// max retries allows the message to get to the renderer even if it is sent while the app is starting up.
sendToRendererWithRetry = (maxRetries: number, channel: string, ...args: any[]) => {
sendToRendererWithRetry = (maxRetries: number, channel: string, ...args: unknown[]) => {
if (!this.mainWindow || !this.mainWindowReady) {
if (maxRetries > 0) {
log.info(`Can't send ${channel}, will retry`);
@ -296,11 +307,11 @@ export class WindowManager {
}
}
sendToRenderer = (channel: string, ...args: any[]) => {
sendToRenderer = (channel: string, ...args: unknown[]) => {
this.sendToRendererWithRetry(3, channel, ...args);
}
sendToAll = (channel: string, ...args: any[]) => {
sendToAll = (channel: string, ...args: unknown[]) => {
this.sendToRenderer(channel, ...args);
if (this.settingsWindow) {
this.settingsWindow.webContents.send(channel, ...args);
@ -309,7 +320,7 @@ export class WindowManager {
// TODO: should we include popups?
}
sendToMattermostViews = (channel: string, ...args: any[]) => {
sendToMattermostViews = (channel: string, ...args: unknown[]) => {
if (this.viewManager) {
this.viewManager.sendToAllViews(channel, ...args);
}
@ -498,7 +509,7 @@ export class WindowManager {
if (this.viewManager) {
this.viewManager.focus();
} else {
log.error('Trying to call focus when the viewmanager has not yet been initialized');
log.error('Trying to call focus when the viewManager has not yet been initialized');
}
}
@ -648,7 +659,7 @@ export class WindowManager {
}
handleBrowserHistoryPush = (e: IpcMainEvent, viewName: string, pathName: string) => {
log.debug('WwindowManager.handleBrowserHistoryPush', {viewName, pathName});
log.debug('WindowManager.handleBrowserHistoryPush', {viewName, pathName});
const currentView = this.viewManager?.views.get(viewName);
const cleanedPathName = urlUtils.cleanPathName(currentView?.tab.server.url.pathname || '', pathName);
@ -676,7 +687,7 @@ export class WindowManager {
}
handleBrowserHistoryButton = (e: IpcMainEvent, viewName: string) => {
log.debug('EindowManager.handleBrowserHistoryButton', viewName);
log.debug('WindowManager.handleBrowserHistoryButton', viewName);
const currentView = this.viewManager?.views.get(viewName);
if (currentView) {

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,
};

29
src/types/downloads.ts Normal file
View file

@ -0,0 +1,29 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DownloadItemTypeEnum} from 'main/downloadsManager';
export type DownloadItemUpdatedEventState = 'interrupted' | 'progressing';
export type DownloadItemDoneEventState = 'completed' | 'cancelled' | 'interrupted';
export type DownloadItemState = DownloadItemUpdatedEventState | DownloadItemDoneEventState | 'deleted' | 'available';
export type DownloadedItem = {
type: DownloadItemTypeEnum;
filename: string;
state: DownloadItemState;
progress: number;
location: string;
mimeType: string | null;
addedAt: number;
receivedBytes: number;
totalBytes: number;
}
export type DownloadedItems = Record<string, DownloadedItem>;
export type CoordinatesToJsonType = Omit<DOMRect, 'toJSON'>
export type DownloadsMenuOpenEventPayload = {
item: DownloadedItem;
coordinates: CoordinatesToJsonType;
}

View file

@ -31,5 +31,4 @@
"storybook-static",
"coverage"
]
}
}

View file

@ -17,6 +17,8 @@ module.exports = merge(base, {
index: './src/main/app/index.ts',
mainWindow: './src/main/preload/mainWindow.js',
dropdown: './src/main/preload/dropdown.js',
downloadsDropdown: './src/main/preload/downloadsDropdown.js',
downloadsDropdownMenu: './src/main/preload/downloadsDropdownMenu.js',
preload: './src/main/preload/mattermost.js',
modalPreload: './src/main/preload/modalPreload.js',
loadingScreenPreload: './src/main/preload/loadingScreenPreload.js',

View file

@ -21,6 +21,8 @@ module.exports = merge(base, {
index: './src/renderer/index.tsx',
settings: './src/renderer/settings.tsx',
dropdown: './src/renderer/dropdown.tsx',
downloadsDropdownMenu: './src/renderer/downloadsDropdownMenu.tsx',
downloadsDropdown: './src/renderer/downloadsDropdown.tsx',
urlView: './src/renderer/modals/urlView/urlView.tsx',
newServer: './src/renderer/modals/newServer/newServer.tsx',
editServer: './src/renderer/modals/editServer/editServer.tsx',
@ -55,6 +57,18 @@ module.exports = merge(base, {
chunks: ['dropdown'],
filename: 'dropdown.html',
}),
new HtmlWebpackPlugin({
title: 'Mattermost Desktop Downloads',
template: 'src/renderer/index.html',
chunks: ['downloadsDropdown'],
filename: 'downloadsDropdown.html',
}),
new HtmlWebpackPlugin({
title: 'Mattermost Desktop Downloads',
template: 'src/renderer/index.html',
chunks: ['downloadsDropdownMenu'],
filename: 'downloadsDropdownMenu.html',
}),
new HtmlWebpackPlugin({
title: 'Mattermost Desktop Settings',
template: 'src/renderer/index.html',