[MM-59543] Disallow use of file: protocol in the app, remove all references to it, add mattermost-desktop: protocol to read local files (#3095)

This commit is contained in:
Devin Binnie 2024-07-18 16:01:44 -04:00 committed by GitHub
parent 87b2f12663
commit 080e4bf727
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 99 additions and 152 deletions

8
package-lock.json generated
View file

@ -39,7 +39,7 @@
"@babel/preset-env": "7.24.0", "@babel/preset-env": "7.24.0",
"@babel/preset-react": "7.23.3", "@babel/preset-react": "7.23.3",
"@babel/preset-typescript": "7.23.3", "@babel/preset-typescript": "7.23.3",
"@electron/fuses": "1.6.0", "@electron/fuses": "1.8.0",
"@mattermost/desktop-api": "file:api-types", "@mattermost/desktop-api": "file:api-types",
"@mattermost/eslint-plugin": "1.1.0-0", "@mattermost/eslint-plugin": "1.1.0-0",
"@types/auto-launch": "5.0.5", "@types/auto-launch": "5.0.5",
@ -2050,9 +2050,9 @@
} }
}, },
"node_modules/@electron/fuses": { "node_modules/@electron/fuses": {
"version": "1.6.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.6.0.tgz", "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz",
"integrity": "sha512-UnZgLfVO1jf7QoYVEEB27CCP1JjT5plhbWU1U8ji1OaXnvNe5UT6KPuRJ3Z12mwa5ZBAASU2tgxVuI06/2x6nQ==", "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chalk": "^4.1.1", "chalk": "^4.1.1",

View file

@ -115,7 +115,7 @@
"@babel/preset-env": "7.24.0", "@babel/preset-env": "7.24.0",
"@babel/preset-react": "7.23.3", "@babel/preset-react": "7.23.3",
"@babel/preset-typescript": "7.23.3", "@babel/preset-typescript": "7.23.3",
"@electron/fuses": "1.6.0", "@electron/fuses": "1.8.0",
"@mattermost/desktop-api": "file:api-types", "@mattermost/desktop-api": "file:api-types",
"@mattermost/eslint-plugin": "1.1.0-0", "@mattermost/eslint-plugin": "1.1.0-0",
"@types/auto-launch": "5.0.5", "@types/auto-launch": "5.0.5",

View file

@ -43,6 +43,7 @@ exports.default = async function afterPack(context) {
version: FuseVersion.V1, version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false, // Disables ELECTRON_RUN_AS_NODE [FuseV1Options.RunAsNode]: false, // Disables ELECTRON_RUN_AS_NODE
[FuseV1Options.EnableNodeCliInspectArguments]: false, // Disables --inspect [FuseV1Options.EnableNodeCliInspectArguments]: false, // Disables --inspect
[FuseV1Options.GrantFileProtocolExtraPrivileges]: false,
[FuseV1Options.EnableCookieEncryption]: true, [FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, // Disables NODE_OPTIONS and NODE_EXTRA_CA_CERTS [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, // Disables NODE_OPTIONS and NODE_EXTRA_CA_CERTS
// Can only verify on macOS right now, electron-builder doesn't support Windows ASAR integrity verification // Can only verify on macOS right now, electron-builder doesn't support Windows ASAR integrity verification

View file

@ -7,7 +7,7 @@ import {URLValidationStatus} from 'common/utils/constants';
import {getDefaultViewsForConfigServer} from 'common/views/View'; import {getDefaultViewsForConfigServer} from 'common/views/View';
import PermissionsManager from 'main/permissionsManager'; import PermissionsManager from 'main/permissionsManager';
import {ServerInfo} from 'main/server/serverInfo'; import {ServerInfo} from 'main/server/serverInfo';
import {getLocalURLString, getLocalPreload} from 'main/utils'; import {getLocalPreload} from 'main/utils';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
@ -50,7 +50,6 @@ jest.mock('main/views/modalManager', () => ({
})); }));
jest.mock('main/utils', () => ({ jest.mock('main/utils', () => ({
getLocalPreload: jest.fn(), getLocalPreload: jest.fn(),
getLocalURLString: jest.fn(),
})); }));
jest.mock('main/windows/mainWindow', () => ({ jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(), get: jest.fn(),
@ -169,7 +168,6 @@ describe('app/serverViewState', () => {
let serversCopy; let serversCopy;
beforeEach(() => { beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
@ -226,7 +224,6 @@ describe('app/serverViewState', () => {
let serversCopy; let serversCopy;
beforeEach(() => { beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
@ -311,7 +308,6 @@ describe('app/serverViewState', () => {
let serversCopy; let serversCopy;
beforeEach(() => { beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});

View file

@ -28,7 +28,7 @@ import {URLValidationStatus} from 'common/utils/constants';
import {isValidURI, isValidURL, parseURL} from 'common/utils/url'; import {isValidURI, isValidURL, parseURL} from 'common/utils/url';
import PermissionsManager from 'main/permissionsManager'; import PermissionsManager from 'main/permissionsManager';
import {ServerInfo} from 'main/server/serverInfo'; import {ServerInfo} from 'main/server/serverInfo';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload} from 'main/utils';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
@ -133,7 +133,7 @@ export class ServerViewState {
const modalPromise = ModalManager.addModal<null, Server>( const modalPromise = ModalManager.addModal<null, Server>(
'newServer', 'newServer',
getLocalURLString('newServer.html'), 'mattermost-desktop://renderer/newServer.html',
getLocalPreload('internalAPI.js'), getLocalPreload('internalAPI.js'),
null, null,
mainWindow, mainWindow,
@ -165,7 +165,7 @@ export class ServerViewState {
const modalPromise = ModalManager.addModal<UniqueServerWithPermissions, {server: Server; permissions: Permissions}>( const modalPromise = ModalManager.addModal<UniqueServerWithPermissions, {server: Server; permissions: Permissions}>(
'editServer', 'editServer',
getLocalURLString('editServer.html'), 'mattermost-desktop://renderer/editServer.html',
getLocalPreload('internalAPI.js'), getLocalPreload('internalAPI.js'),
{server: server.toUniqueServer(), permissions: PermissionsManager.getForServer(server) ?? {}}, {server: server.toUniqueServer(), permissions: PermissionsManager.getForServer(server) ?? {}},
mainWindow); mainWindow);
@ -195,7 +195,7 @@ export class ServerViewState {
const modalPromise = ModalManager.addModal<string, boolean>( const modalPromise = ModalManager.addModal<string, boolean>(
'removeServer', 'removeServer',
getLocalURLString('removeServer.html'), 'mattermost-desktop://renderer/removeServer.html',
getLocalPreload('internalAPI.js'), getLocalPreload('internalAPI.js'),
server.name, server.name,
mainWindow, mainWindow,

View file

@ -60,6 +60,7 @@ const downloadsSchema = Joi.object<DownloadedItems>().pattern(
receivedBytes: Joi.number().min(0), receivedBytes: Joi.number().min(0),
totalBytes: Joi.number().min(0), totalBytes: Joi.number().min(0),
bookmark: Joi.string(), bookmark: Joi.string(),
thumbnailData: Joi.string(),
}); });
const configDataSchemaV0 = Joi.object<ConfigV0>({ const configDataSchemaV0 = Joi.object<ConfigV0>({

View file

@ -146,7 +146,6 @@ export const DOWNLOADS_DROPDOWN_OPEN_FILE = 'downloads-dropdown-open-file';
export const REQUEST_HAS_DOWNLOADS = 'request-has-downloads'; export const REQUEST_HAS_DOWNLOADS = 'request-has-downloads';
export const DOWNLOADS_DROPDOWN_FOCUSED = 'downloads-dropdown-focused'; export const DOWNLOADS_DROPDOWN_FOCUSED = 'downloads-dropdown-focused';
export const RECEIVE_DOWNLOADS_DROPDOWN_SIZE = 'receive-downloads-dropdown-size'; export const RECEIVE_DOWNLOADS_DROPDOWN_SIZE = 'receive-downloads-dropdown-size';
export const GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION = 'get-downloaded-image-thumbnail-location';
export const OPEN_DOWNLOADS_DROPDOWN_MENU = 'open-downloads-dropdown-menu'; export const OPEN_DOWNLOADS_DROPDOWN_MENU = 'open-downloads-dropdown-menu';
export const CLOSE_DOWNLOADS_DROPDOWN_MENU = 'close-downloads-dropdown-menu'; export const CLOSE_DOWNLOADS_DROPDOWN_MENU = 'close-downloads-dropdown-menu';

View file

@ -69,6 +69,10 @@ jest.mock('electron', () => ({
on: jest.fn(), on: jest.fn(),
}, },
}, },
protocol: {
registerSchemesAsPrivileged: jest.fn(),
handle: jest.fn(),
},
})); }));
jest.mock('main/i18nManager', () => ({ jest.mock('main/i18nManager', () => ({

View file

@ -2,8 +2,9 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import path from 'path'; import path from 'path';
import {pathToFileURL} from 'url';
import {app, ipcMain, nativeTheme, session} from 'electron'; import {app, ipcMain, nativeTheme, net, protocol, session} from 'electron';
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-extension-installer'; import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-extension-installer';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
@ -36,6 +37,7 @@ import {
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import ServerManager from 'common/servers/serverManager'; import ServerManager from 'common/servers/serverManager';
import {parseURL} from 'common/utils/url';
import AllowProtocolDialog from 'main/allowProtocolDialog'; import AllowProtocolDialog from 'main/allowProtocolDialog';
import AppVersionManager from 'main/AppVersionManager'; import AppVersionManager from 'main/AppVersionManager';
import AuthManager from 'main/authManager'; import AuthManager from 'main/authManager';
@ -254,6 +256,10 @@ function initializeBeforeAppReady() {
nativeTheme.on('updated', handleUpdateTheme); nativeTheme.on('updated', handleUpdateTheme);
handleUpdateTheme(); handleUpdateTheme();
} }
protocol.registerSchemesAsPrivileged([
{scheme: 'mattermost-desktop', privileges: {standard: true}},
]);
} }
function initializeInterCommunicationEventListeners() { function initializeInterCommunicationEventListeners() {
@ -291,6 +297,24 @@ function initializeInterCommunicationEventListeners() {
} }
async function initializeAfterAppReady() { async function initializeAfterAppReady() {
protocol.handle('mattermost-desktop', (request: Request) => {
const url = parseURL(request.url);
if (!url) {
return new Response('bad', {status: 400});
}
// Including this snippet from the handler docs to check for path traversal
// https://www.electronjs.org/docs/latest/api/protocol#protocolhandlescheme-handler
const pathToServe = path.join(app.getAppPath(), 'renderer', url.pathname);
const relativePath = path.relative(app.getAppPath(), pathToServe);
const isSafe = relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
if (!isSafe) {
return new Response('bad', {status: 400});
}
return net.fetch(pathToFileURL(pathToServe).toString());
});
ServerManager.reloadFromConfig(); ServerManager.reloadFromConfig();
updateServerInfos(ServerManager.getAllServers()); updateServerInfos(ServerManager.getAllServers());
ServerManager.on(SERVERS_URL_MODIFIED, (serverIds?: string[]) => { ServerManager.on(SERVERS_URL_MODIFIED, (serverIds?: string[]) => {

View file

@ -4,7 +4,7 @@
import {app} from 'electron'; import {app} from 'electron';
import ServerManager from 'common/servers/serverManager'; import ServerManager from 'common/servers/serverManager';
import {getLocalURLString, getLocalPreload} from 'main/utils'; import {getLocalPreload} from 'main/utils';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
@ -38,7 +38,6 @@ jest.mock('common/servers/serverManager', () => ({
})); }));
jest.mock('main/utils', () => ({ jest.mock('main/utils', () => ({
getLocalPreload: jest.fn(), getLocalPreload: jest.fn(),
getLocalURLString: jest.fn(),
})); }));
jest.mock('main/views/viewManager', () => ({})); jest.mock('main/views/viewManager', () => ({}));
jest.mock('main/views/modalManager', () => ({ jest.mock('main/views/modalManager', () => ({
@ -53,7 +52,6 @@ jest.mock('./app', () => ({}));
describe('main/app/intercom', () => { describe('main/app/intercom', () => {
describe('handleWelcomeScreenModal', () => { describe('handleWelcomeScreenModal', () => {
beforeEach(() => { beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
@ -65,13 +63,12 @@ describe('main/app/intercom', () => {
ModalManager.addModal.mockReturnValue(promise); ModalManager.addModal.mockReturnValue(promise);
handleWelcomeScreenModal(); handleWelcomeScreenModal();
expect(ModalManager.addModal).toHaveBeenCalledWith('welcomeScreen', '/some/index.html', '/some/preload.js', null, {}, true); expect(ModalManager.addModal).toHaveBeenCalledWith('welcomeScreen', 'mattermost-desktop://renderer/welcomeScreen.html', '/some/preload.js', null, {}, true);
}); });
}); });
describe('handleMainWindowIsShown', () => { describe('handleMainWindowIsShown', () => {
it('MM-48079 should not show onboarding screen or server screen if GPO server is pre-configured', () => { it('MM-48079 should not show onboarding screen or server screen if GPO server is pre-configured', () => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({ MainWindow.get.mockReturnValue({
isVisible: () => true, isVisible: () => true,

View file

@ -9,7 +9,7 @@ import {Logger} from 'common/log';
import ServerManager from 'common/servers/serverManager'; import ServerManager from 'common/servers/serverManager';
import {ping} from 'common/utils/requests'; import {ping} from 'common/utils/requests';
import NotificationManager from 'main/notifications'; import NotificationManager from 'main/notifications';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload} from 'main/utils';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
@ -88,7 +88,7 @@ export function handleMainWindowIsShown() {
export function handleWelcomeScreenModal() { export function handleWelcomeScreenModal() {
log.debug('handleWelcomeScreenModal'); log.debug('handleWelcomeScreenModal');
const html = getLocalURLString('welcomeScreen.html'); const html = 'mattermost-desktop://renderer/welcomeScreen.html';
const preload = getLocalPreload('internalAPI.js'); const preload = getLocalPreload('internalAPI.js');
@ -169,7 +169,7 @@ export function handleShowSettingsModal() {
ModalManager.addModal( ModalManager.addModal(
'settingsModal', 'settingsModal',
getLocalURLString('settings.html'), 'mattermost-desktop://renderer/settings.html',
getLocalPreload('internalAPI.js'), getLocalPreload('internalAPI.js'),
null, null,
mainWindow, mainWindow,

View file

@ -51,7 +51,6 @@ jest.mock('main/views/modalManager', () => ({
jest.mock('main/utils', () => ({ jest.mock('main/utils', () => ({
getLocalPreload: (file) => file, getLocalPreload: (file) => file,
getLocalURLString: (file) => file,
})); }));
describe('main/authManager', () => { describe('main/authManager', () => {

View file

@ -6,7 +6,7 @@ import {Logger} from 'common/log';
import {BASIC_AUTH_PERMISSION} from 'common/permissions'; import {BASIC_AUTH_PERMISSION} from 'common/permissions';
import {isCustomLoginURL, isTrustedURL, parseURL} from 'common/utils/url'; import {isCustomLoginURL, isTrustedURL, parseURL} from 'common/utils/url';
import TrustedOriginsStore from 'main/trustedOrigins'; import TrustedOriginsStore from 'main/trustedOrigins';
import {getLocalURLString, getLocalPreload} from 'main/utils'; import {getLocalPreload} from 'main/utils';
import modalManager from 'main/views/modalManager'; import modalManager from 'main/views/modalManager';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
@ -16,8 +16,8 @@ import type {PermissionType} from 'types/trustedOrigin';
const log = new Logger('AuthManager'); const log = new Logger('AuthManager');
const preload = getLocalPreload('internalAPI.js'); const preload = getLocalPreload('internalAPI.js');
const loginModalHtml = getLocalURLString('loginModal.html'); const loginModalHtml = 'mattermost-desktop://renderer/loginModal.html';
const permissionModalHtml = getLocalURLString('permissionModal.html'); const permissionModalHtml = 'mattermost-desktop://renderer/permissionModal.html';
type LoginModalResult = { type LoginModalResult = {
username: string; username: string;

View file

@ -16,7 +16,6 @@ jest.mock('main/views/modalManager', () => ({
jest.mock('main/utils', () => ({ jest.mock('main/utils', () => ({
getLocalPreload: (file) => file, getLocalPreload: (file) => file,
getLocalURLString: (file) => file,
})); }));
describe('main/certificateManager', () => { describe('main/certificateManager', () => {

View file

@ -7,13 +7,13 @@ import {Logger} from 'common/log';
import type {CertificateModalData} from 'types/certificate'; import type {CertificateModalData} from 'types/certificate';
import {getLocalURLString, getLocalPreload} from './utils'; import {getLocalPreload} from './utils';
import modalManager from './views/modalManager'; import modalManager from './views/modalManager';
import MainWindow from './windows/mainWindow'; import MainWindow from './windows/mainWindow';
const log = new Logger('CertificateManager'); const log = new Logger('CertificateManager');
const preload = getLocalPreload('internalAPI.js'); const preload = getLocalPreload('internalAPI.js');
const html = getLocalURLString('certificateModal.html'); const html = 'mattermost-desktop://renderer/certificateModal.html';
type CertificateModalResult = { type CertificateModalResult = {
cert: Certificate; cert: Certificate;

View file

@ -14,7 +14,7 @@ const defaultMenuOptions = {
let isInternalSrc; let isInternalSrc;
try { try {
const srcurl = parseURL(p.srcURL); const srcurl = parseURL(p.srcURL);
isInternalSrc = srcurl?.protocol === 'file:'; isInternalSrc = srcurl?.protocol === 'mattermost-desktop:';
} catch (err) { } catch (err) {
isInternalSrc = false; isInternalSrc = false;
} }

View file

@ -148,11 +148,11 @@ describe('main/downloadsManager', () => {
it('should mark "completed" files that were deleted as "deleted"', () => { 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'}}); expect(new DownloadsManager(JSON.stringify(downloadsJson))).toHaveProperty('downloads', {...downloadsJson, 'file1.txt': {...downloadsJson['file1.txt'], state: 'deleted'}});
}); });
it('should handle a new download', () => { it('should handle a new download', async () => {
const dl = new DownloadsManager({}); const dl = new DownloadsManager({});
path.parse.mockImplementation(() => ({base: 'file.txt'})); path.parse.mockImplementation(() => ({base: 'file.txt'}));
dl.willDownloadURLs.set('http://some-url.com/some-text.txt', {filePath: locationMock}); dl.willDownloadURLs.set('http://some-url.com/some-text.txt', {filePath: locationMock});
dl.handleNewDownload({preventDefault: jest.fn()}, item, {id: 0, getURL: jest.fn(), downloadURL: jest.fn()}); await dl.handleNewDownload({preventDefault: jest.fn()}, item, {id: 0, getURL: jest.fn(), downloadURL: jest.fn()});
expect(dl).toHaveProperty('downloads', {'file.txt': { expect(dl).toHaveProperty('downloads', {'file.txt': {
addedAt: nowSeconds * 1000, addedAt: nowSeconds * 1000,
filename: 'file.txt', filename: 'file.txt',

View file

@ -4,7 +4,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import type {DownloadItem, Event, WebContents, FileFilter, IpcMainInvokeEvent} from 'electron'; import type {DownloadItem, Event, WebContents, FileFilter, IpcMainInvokeEvent} from 'electron';
import {ipcMain, dialog, shell, Menu, app} from 'electron'; import {ipcMain, dialog, shell, Menu, app, nativeImage} from 'electron';
import type {ProgressInfo, UpdateInfo} from 'electron-updater'; import type {ProgressInfo, UpdateInfo} from 'electron-updater';
import { import {
@ -123,7 +123,7 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
item.setSavePath(info.filePath); item.setSavePath(info.filePath);
} }
this.upsertFileToDownloads(item, 'progressing'); await this.upsertFileToDownloads(item, 'progressing');
this.progressingItems.set(this.getFileId(item), item); this.progressingItems.set(this.getFileId(item), item);
this.handleDownloadItemEvents(item, webContents); this.handleDownloadItemEvents(item, webContents);
this.openDownloadsDropdown(); this.openDownloadsDropdown();
@ -501,10 +501,10 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
} }
}; };
private upsertFileToDownloads = (item: DownloadItem, state: DownloadItemState, overridePath?: string) => { private upsertFileToDownloads = async (item: DownloadItem, state: DownloadItemState, overridePath?: string) => {
const fileId = this.getFileId(item); const fileId = this.getFileId(item);
log.debug('upsertFileToDownloads', {fileId}); log.debug('upsertFileToDownloads', {fileId});
const formattedItem = this.formatDownloadItem(item, state, overridePath); const formattedItem = await this.formatDownloadItem(item, state, overridePath);
this.save(fileId, formattedItem); this.save(fileId, formattedItem);
this.checkIfMaxFilesReached(); this.checkIfMaxFilesReached();
}; };
@ -545,10 +545,10 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
/** /**
* DownloadItem event handlers * DownloadItem event handlers
*/ */
private updatedEventController = (updatedEvent: Event, state: DownloadItemUpdatedEventState, item: DownloadItem) => { private updatedEventController = async (updatedEvent: Event, state: DownloadItemUpdatedEventState, item: DownloadItem) => {
log.debug('updatedEventController', {state}); log.debug('updatedEventController', {state});
this.upsertFileToDownloads(item, state); await this.upsertFileToDownloads(item, state);
if (state === 'interrupted') { if (state === 'interrupted') {
this.fileSizes.delete(item.getFilename()); this.fileSizes.delete(item.getFilename());
@ -557,7 +557,7 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
this.shouldShowBadge(); this.shouldShowBadge();
}; };
private doneEventController = (doneEvent: Event, state: DownloadItemDoneEventState, item: DownloadItem, webContents: WebContents) => { private doneEventController = async (doneEvent: Event, state: DownloadItemDoneEventState, item: DownloadItem, webContents: WebContents) => {
log.debug('doneEventController', {state}); log.debug('doneEventController', {state});
if (state === 'completed' && !this.open) { if (state === 'completed' && !this.open) {
@ -571,7 +571,7 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
func(); func();
} }
this.upsertFileToDownloads(item, state, bookmark?.originalPath); await this.upsertFileToDownloads(item, state, bookmark?.originalPath);
this.fileSizes.delete(item.getFilename()); this.fileSizes.delete(item.getFilename());
this.progressingItems.delete(this.getFileId(item)); this.progressingItems.delete(this.getFileId(item));
this.shouldAutoClose(); this.shouldAutoClose();
@ -628,11 +628,16 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
/** /**
* Internal utils * Internal utils
*/ */
private formatDownloadItem = (item: DownloadItem, state: DownloadItemState, overridePath?: string): DownloadedItem => { private formatDownloadItem = async (item: DownloadItem, state: DownloadItemState, overridePath?: string): Promise<DownloadedItem> => {
const totalBytes = this.getFileSize(item); const totalBytes = this.getFileSize(item);
const receivedBytes = item.getReceivedBytes(); const receivedBytes = item.getReceivedBytes();
const progress = getPercentage(receivedBytes, totalBytes); const progress = getPercentage(receivedBytes, totalBytes);
let thumbnailData;
if (state === 'completed' && item.getMimeType().toLowerCase().startsWith('image/')) {
thumbnailData = (await nativeImage.createThumbnailFromPath(overridePath ?? item.getSavePath(), {height: 32, width: 32})).toDataURL();
}
return { return {
addedAt: doubleSecToMs(item.getStartTime()), addedAt: doubleSecToMs(item.getStartTime()),
filename: this.getFileId(item), filename: this.getFileId(item),
@ -644,6 +649,7 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
totalBytes, totalBytes,
type: DownloadItemTypeEnum.FILE, type: DownloadItemTypeEnum.FILE,
bookmark: this.getBookmark(item), bookmark: this.getBookmark(item),
thumbnailData,
}; };
}; };

View file

@ -18,7 +18,7 @@ import type {UpdateManager} from 'main/autoUpdater';
import Diagnostics from 'main/diagnostics'; import Diagnostics from 'main/diagnostics';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload} from 'main/utils';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import CallsWidgetWindow from 'main/windows/callsWidgetWindow'; import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
@ -57,7 +57,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
ModalManager.addModal( ModalManager.addModal(
'settingsModal', 'settingsModal',
getLocalURLString('settings.html'), 'mattermost-desktop://renderer/settings.html',
getLocalPreload('internalAPI.js'), getLocalPreload('internalAPI.js'),
null, null,
mainWindow, mainWindow,

View file

@ -9,7 +9,7 @@ import {Menu} from 'electron';
import ServerViewState from 'app/serverViewState'; import ServerViewState from 'app/serverViewState';
import ServerManager from 'common/servers/serverManager'; import ServerManager from 'common/servers/serverManager';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload} from 'main/utils';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
@ -35,7 +35,7 @@ export function createTemplate() {
ModalManager.addModal( ModalManager.addModal(
'settingsModal', 'settingsModal',
getLocalURLString('settings.html'), 'mattermost-desktop://renderer/settings.html',
getLocalPreload('internalAPI.js'), getLocalPreload('internalAPI.js'),
null, null,
mainWindow, mainWindow,

View file

@ -72,7 +72,6 @@ import {
START_UPDATE_DOWNLOAD, START_UPDATE_DOWNLOAD,
START_UPGRADE, START_UPGRADE,
TOGGLE_DOWNLOADS_DROPDOWN_MENU, TOGGLE_DOWNLOADS_DROPDOWN_MENU,
GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION,
DOWNLOADS_DROPDOWN_OPEN_FILE, DOWNLOADS_DROPDOWN_OPEN_FILE,
MODAL_CANCEL, MODAL_CANCEL,
MODAL_RESULT, MODAL_RESULT,
@ -110,10 +109,6 @@ contextBridge.exposeInMainWorld('timers', {
setImmediate, setImmediate,
}); });
contextBridge.exposeInMainWorld('mas', {
getThumbnailLocation: (location) => ipcRenderer.invoke(GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION, location),
});
contextBridge.exposeInMainWorld('desktop', { contextBridge.exposeInMainWorld('desktop', {
quit: (reason, stack) => ipcRenderer.send(QUIT, reason, stack), quit: (reason, stack) => ipcRenderer.send(QUIT, reason, stack),
openAppMenu: () => ipcRenderer.send(OPEN_APP_MENU), openAppMenu: () => ipcRenderer.send(OPEN_APP_MENU),

View file

@ -3,7 +3,6 @@
'use strict'; 'use strict';
import {BACK_BAR_HEIGHT, TAB_BAR_HEIGHT} from 'common/utils/constants'; import {BACK_BAR_HEIGHT, TAB_BAR_HEIGHT} from 'common/utils/constants';
import {runMode} from 'common/utils/util';
import * as Utils from './utils'; import * as Utils from './utils';
@ -82,29 +81,6 @@ describe('main/utils', () => {
}); });
}); });
describe('getLocalURLString', () => {
it('should return URL relative to current run directory', () => {
runMode.mockImplementation(() => 'development');
expect(Utils.getLocalURLString('index.html')).toStrictEqual('file:///path/to/app/dist/renderer/index.html');
});
it('should return URL relative to current run directory in production', () => {
runMode.mockImplementation(() => 'production');
expect(Utils.getLocalURLString('index.html')).toStrictEqual('file:///path/to/app/renderer/index.html');
});
it('should include query string when specified', () => {
const queryMap = new Map([['key', 'value']]);
runMode.mockImplementation(() => 'development');
expect(Utils.getLocalURLString('index.html', queryMap)).toStrictEqual('file:///path/to/app/dist/renderer/index.html?key=value');
});
it('should return URL relative to current run directory when using main process', () => {
runMode.mockImplementation(() => 'development');
expect(Utils.getLocalURLString('index.html', null, true)).toStrictEqual('file:///path/to/app/dist/index.html');
});
});
describe('shouldHaveBackBar', () => { describe('shouldHaveBackBar', () => {
it('should have back bar for custom logins', () => { it('should have back bar for custom logins', () => {
expect(Utils.shouldHaveBackBar(new URL('https://server-1.com'), new URL('https://server-1.com/login/sso/saml'))).toBe(true); expect(Utils.shouldHaveBackBar(new URL('https://server-1.com'), new URL('https://server-1.com/login/sso/saml'))).toBe(true);

View file

@ -11,9 +11,8 @@ const exec = promisify(execOriginal);
import type {BrowserWindow} from 'electron'; import type {BrowserWindow} from 'electron';
import {app} from 'electron'; import {app} from 'electron';
import {BACK_BAR_HEIGHT, customLoginRegexPaths, PRODUCTION, TAB_BAR_HEIGHT} from 'common/utils/constants'; import {BACK_BAR_HEIGHT, customLoginRegexPaths, TAB_BAR_HEIGHT} from 'common/utils/constants';
import {isAdminUrl, isPluginUrl, isTeamUrl, isUrlType, parseURL} from 'common/utils/url'; import {isAdminUrl, isPluginUrl, isTeamUrl, isUrlType, parseURL} from 'common/utils/url';
import Utils from 'common/utils/util';
import type {Args} from 'types/args'; import type {Args} from 'types/args';
@ -85,34 +84,8 @@ export function shouldHaveBackBar(serverUrl: URL, inputURL: URL) {
return !isTeamUrl(serverUrl, inputURL) && !isAdminUrl(serverUrl, inputURL) && !isPluginUrl(serverUrl, inputURL); return !isTeamUrl(serverUrl, inputURL) && !isAdminUrl(serverUrl, inputURL) && !isPluginUrl(serverUrl, inputURL);
} }
export function getLocalURLString(urlPath: string, query?: Map<string, string>, isMain?: boolean) {
let pathname;
const processPath = isMain ? '' : '/renderer';
const mode = Utils.runMode();
const protocol = 'file';
const hostname = '';
const port = '';
if (mode === PRODUCTION) {
pathname = path.join(app.getAppPath(), `${processPath}/${urlPath}`);
} else {
pathname = path.resolve(__dirname, `../../dist/${processPath}/${urlPath}`); // TODO: find a better way to work with webpack on this
}
const localUrl = new URL(`${protocol}://${hostname}${port}`);
localUrl.pathname = pathname;
if (query) {
query.forEach((value: string, key: string) => {
localUrl.searchParams.append(encodeURIComponent(key), encodeURIComponent(value));
});
}
return localUrl.href;
}
export function getLocalPreload(file: string) { export function getLocalPreload(file: string) {
if (Utils.runMode() === PRODUCTION) { return path.join(app.getAppPath(), file);
return path.join(app.getAppPath(), `${file}`);
}
return path.resolve(__dirname, `../../dist/${file}`);
} }
export function composeUserAgent() { export function composeUserAgent() {

View file

@ -12,7 +12,6 @@ import {DownloadsDropdownMenuView} from './downloadsDropdownMenuView';
jest.mock('main/utils', () => ({ jest.mock('main/utils', () => ({
getLocalPreload: (file) => file, getLocalPreload: (file) => file,
getLocalURLString: (file) => file,
})); }));
jest.mock('electron', () => { jest.mock('electron', () => {
class NotificationMock { class NotificationMock {

View file

@ -27,7 +27,7 @@ import {
TAB_BAR_HEIGHT, TAB_BAR_HEIGHT,
} from 'common/utils/constants'; } from 'common/utils/constants';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload} from 'main/utils';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import type {CoordinatesToJsonType, DownloadedItem, DownloadsMenuOpenEventPayload} from 'types/downloads'; import type {CoordinatesToJsonType, DownloadedItem, DownloadsMenuOpenEventPayload} from 'types/downloads';
@ -75,7 +75,7 @@ export class DownloadsDropdownMenuView {
// @ts-ignore // @ts-ignore
transparent: true, transparent: true,
}}); }});
this.view.webContents.loadURL(getLocalURLString('downloadsDropdownMenu.html')); this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdownMenu.html');
MainWindow.get()?.addBrowserView(this.view); MainWindow.get()?.addBrowserView(this.view);
}; };

View file

@ -12,7 +12,6 @@ import {DownloadsDropdownView} from './downloadsDropdownView';
jest.mock('main/utils', () => ({ jest.mock('main/utils', () => ({
getLocalPreload: (file) => file, getLocalPreload: (file) => file,
getLocalURLString: (file) => file,
})); }));
jest.mock('fs', () => ({ jest.mock('fs', () => ({
existsSync: jest.fn().mockReturnValue(false), existsSync: jest.fn().mockReturnValue(false),

View file

@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import type {IpcMainEvent, IpcMainInvokeEvent} from 'electron'; import type {IpcMainEvent} from 'electron';
import {BrowserView, ipcMain} from 'electron'; import {BrowserView, ipcMain} from 'electron';
import { import {
@ -13,7 +13,6 @@ import {
REQUEST_DOWNLOADS_DROPDOWN_INFO, REQUEST_DOWNLOADS_DROPDOWN_INFO,
UPDATE_DOWNLOADS_DROPDOWN, UPDATE_DOWNLOADS_DROPDOWN,
UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM,
GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION,
DOWNLOADS_DROPDOWN_OPEN_FILE, DOWNLOADS_DROPDOWN_OPEN_FILE,
MAIN_WINDOW_CREATED, MAIN_WINDOW_CREATED,
MAIN_WINDOW_RESIZED, MAIN_WINDOW_RESIZED,
@ -22,7 +21,7 @@ import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {TAB_BAR_HEIGHT, DOWNLOADS_DROPDOWN_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, DOWNLOADS_DROPDOWN_FULL_WIDTH} from 'common/utils/constants'; import {TAB_BAR_HEIGHT, DOWNLOADS_DROPDOWN_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, DOWNLOADS_DROPDOWN_FULL_WIDTH} from 'common/utils/constants';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload} from 'main/utils';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import type {DownloadedItem} from 'types/downloads'; import type {DownloadedItem} from 'types/downloads';
@ -47,7 +46,6 @@ export class DownloadsDropdownView {
ipcMain.on(DOWNLOADS_DROPDOWN_OPEN_FILE, this.openFile); ipcMain.on(DOWNLOADS_DROPDOWN_OPEN_FILE, this.openFile);
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN, this.updateDownloadsDropdown); ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN, this.updateDownloadsDropdown);
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, this.updateDownloadsDropdownMenuItem); ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, this.updateDownloadsDropdownMenuItem);
ipcMain.handle(GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION, this.getDownloadImageThumbnailLocation);
} }
init = () => { init = () => {
@ -67,7 +65,7 @@ export class DownloadsDropdownView {
transparent: true, transparent: true,
}}); }});
this.view.webContents.loadURL(getLocalURLString('downloadsDropdown.html')); this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdown.html');
this.view.webContents.session.webRequest.onHeadersReceived(downloadsManager.webRequestOnHeadersReceivedHandler); this.view.webContents.session.webRequest.onHeadersReceived(downloadsManager.webRequestOnHeadersReceivedHandler);
MainWindow.get()?.addBrowserView(this.view); MainWindow.get()?.addBrowserView(this.view);
}; };
@ -183,10 +181,6 @@ export class DownloadsDropdownView {
this.view?.setBounds(this.bounds); this.view?.setBounds(this.bounds);
} }
}; };
private getDownloadImageThumbnailLocation = (event: IpcMainInvokeEvent, location: string) => {
return location;
};
} }
const downloadsDropdownView = new DownloadsDropdownView(); const downloadsDropdownView = new DownloadsDropdownView();

View file

@ -5,7 +5,7 @@ import {BrowserView, app, ipcMain} from 'electron';
import {DARK_MODE_CHANGE, LOADING_SCREEN_ANIMATION_FINISHED, MAIN_WINDOW_RESIZED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication'; import {DARK_MODE_CHANGE, LOADING_SCREEN_ANIMATION_FINISHED, MAIN_WINDOW_RESIZED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {getLocalPreload, getLocalURLString, getWindowBoundaries} from 'main/utils'; import {getLocalPreload, getWindowBoundaries} from 'main/utils';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
enum LoadingScreenState { enum LoadingScreenState {
@ -85,7 +85,7 @@ export class LoadingScreen {
// @ts-ignore // @ts-ignore
transparent: true, transparent: true,
}}); }});
const localURL = getLocalURLString('loadingScreen.html'); const localURL = 'mattermost-desktop://renderer/loadingScreen.html';
this.view.webContents.loadURL(localURL); this.view.webContents.loadURL(localURL);
}; };

View file

@ -12,7 +12,6 @@ jest.mock('app/serverViewState', () => ({}));
jest.mock('main/utils', () => ({ jest.mock('main/utils', () => ({
getLocalPreload: (file) => file, getLocalPreload: (file) => file,
getLocalURLString: (file) => file,
})); }));
jest.mock('electron', () => ({ jest.mock('electron', () => ({

View file

@ -22,7 +22,7 @@ import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import ServerManager from 'common/servers/serverManager'; import ServerManager from 'common/servers/serverManager';
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants'; import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload} from 'main/utils';
import type {UniqueServer} from 'types/config'; import type {UniqueServer} from 'types/config';
@ -83,7 +83,7 @@ export class ServerDropdownView {
// @ts-ignore // @ts-ignore
transparent: true, transparent: true,
}}); }});
this.view.webContents.loadURL(getLocalURLString('dropdown.html')); this.view.webContents.loadURL('mattermost-desktop://renderer/dropdown.html');
this.setOrderedServers(); this.setOrderedServers();
this.windowBounds = MainWindow.getBounds(); this.windowBounds = MainWindow.getBounds();

View file

@ -52,7 +52,7 @@ import LoadingScreen from './loadingScreen';
import {MattermostBrowserView} from './MattermostBrowserView'; import {MattermostBrowserView} from './MattermostBrowserView';
import modalManager from './modalManager'; import modalManager from './modalManager';
import {getLocalURLString, getLocalPreload, getAdjustedWindowBoundaries, shouldHaveBackBar} from '../utils'; import {getLocalPreload, getAdjustedWindowBoundaries, shouldHaveBackBar} from '../utils';
const log = new Logger('ViewManager'); const log = new Logger('ViewManager');
const URL_VIEW_DURATION = 10 * SECOND; const URL_VIEW_DURATION = 10 * SECOND;
@ -354,8 +354,7 @@ export class ViewManager {
// @ts-ignore // @ts-ignore
transparent: true, transparent: true,
}}); }});
const query = new Map([['url', urlString]]); const localURL = `mattermost-desktop://renderer/urlView.html?url=${encodeURIComponent(urlString)}`;
const localURL = getLocalURLString('urlView.html', query);
urlView.webContents.loadURL(localURL); urlView.webContents.loadURL(localURL);
MainWindow.get()?.addBrowserView(urlView); MainWindow.get()?.addBrowserView(urlView);
const boundaries = this.views.get(this.currentView || '')?.getBounds() ?? MainWindow.getBounds(); const boundaries = this.views.get(this.currentView || '')?.getBounds() ?? MainWindow.getBounds();

View file

@ -68,7 +68,6 @@ jest.mock('../contextMenu', () => jest.fn());
jest.mock('../utils', () => ({ jest.mock('../utils', () => ({
isInsideRectangle: jest.fn(), isInsideRectangle: jest.fn(),
getLocalPreload: jest.fn(), getLocalPreload: jest.fn(),
getLocalURLString: jest.fn(),
})); }));
jest.mock('main/i18nManager', () => ({ jest.mock('main/i18nManager', () => ({

View file

@ -37,7 +37,7 @@ import {localizeMessage} from 'main/i18nManager';
import type {SavedWindowState} from 'types/mainWindow'; import type {SavedWindowState} from 'types/mainWindow';
import ContextMenu from '../contextMenu'; import ContextMenu from '../contextMenu';
import {getLocalPreload, getLocalURLString, isInsideRectangle} from '../utils'; import {getLocalPreload, isInsideRectangle} from '../utils';
const log = new Logger('MainWindow'); const log = new Logger('MainWindow');
const ALT_MENU_KEYS = ['Alt+F', 'Alt+E', 'Alt+V', 'Alt+H', 'Alt+W', 'Alt+P']; const ALT_MENU_KEYS = ['Alt+F', 'Alt+E', 'Alt+V', 'Alt+H', 'Alt+W', 'Alt+P'];
@ -152,7 +152,7 @@ export class MainWindow extends EventEmitter {
const contextMenu = new ContextMenu({}, this.win); const contextMenu = new ContextMenu({}, this.win);
contextMenu.reload(); contextMenu.reload();
const localURL = getLocalURLString('index.html'); const localURL = 'mattermost-desktop://renderer/index.html';
this.win.loadURL(localURL).catch( this.win.loadURL(localURL).catch(
(reason) => { (reason) => {
log.error('failed to load', reason); log.error('failed to load', reason);

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react'; import React from 'react';
import {CheckCircleIcon, CloseCircleIcon} from '@mattermost/compass-icons/components'; import {CheckCircleIcon, CloseCircleIcon} from '@mattermost/compass-icons/components';
@ -17,11 +17,7 @@ const iconSize = 14;
const colorGreen = '#3DB887'; const colorGreen = '#3DB887';
const colorRed = '#D24B4E'; const colorRed = '#D24B4E';
const isWin = window.process.platform === 'win32';
const Thumbnail = ({item}: OwnProps) => { const Thumbnail = ({item}: OwnProps) => {
const [imageUrl, setImageUrl] = useState<string | undefined>();
const showBadge = (state: DownloadedItem['state']) => { const showBadge = (state: DownloadedItem['state']) => {
switch (state) { switch (state) {
case 'completed': case 'completed':
@ -45,27 +41,18 @@ const Thumbnail = ({item}: OwnProps) => {
} }
}; };
useEffect(() => {
const fetchThumbnail = async () => {
const imageUrl = await window.mas.getThumbnailLocation(item.location);
setImageUrl(imageUrl);
};
fetchThumbnail();
}, [item]);
const showImagePreview = isImageFile(item) && item.state === 'completed'; const showImagePreview = isImageFile(item) && item.state === 'completed';
if (showImagePreview && !imageUrl) { if (showImagePreview && !item.thumbnailData) {
return null; return null;
} }
return ( return (
<div className='DownloadsDropdown__Thumbnail__Container'> <div className='DownloadsDropdown__Thumbnail__Container'>
{showImagePreview && imageUrl ? {showImagePreview && item.thumbnailData ?
<div <div
className='DownloadsDropdown__Thumbnail preview' className='DownloadsDropdown__Thumbnail preview'
style={{ style={{
backgroundImage: `url("${isWin ? `file:///${imageUrl.replaceAll('\\', '/')}` : imageUrl}")`, backgroundImage: `url("${item.thumbnailData}")`,
backgroundSize: 'cover', backgroundSize: 'cover',
}} }}
/> : /> :

View file

@ -3,6 +3,8 @@
import React from 'react'; import React from 'react';
import StippleMask from 'renderer/assets/StippleMask.jpg';
function LoadingBackground() { function LoadingBackground() {
return ( return (
<div className='LoadingScreen__backgound'> <div className='LoadingScreen__backgound'>
@ -36,7 +38,7 @@ function LoadingBackground() {
<image <image
width='900' width='900'
height='535' height='535'
href='../assets/loader/StippleMask.jpg' href={StippleMask}
/> />
</mask> </mask>
<g <g

View file

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; media-src data:"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; media-src data:; img-src 'self' data:">
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
</head> </head>
<body> <body>

View file

@ -18,6 +18,7 @@ export type DownloadedItem = {
receivedBytes: number; receivedBytes: number;
totalBytes: number; totalBytes: number;
bookmark?: string; bookmark?: string;
thumbnailData?: string;
} }
export type DownloadedItems = Record<string, DownloadedItem>; export type DownloadedItems = Record<string, DownloadedItem>;

View file

@ -4,3 +4,4 @@
declare module '*.mp3'; declare module '*.mp3';
declare module '*.svg'; declare module '*.svg';
declare module '*.lazy.css'; declare module '*.lazy.css';
declare module '*.jpg';

View file

@ -27,9 +27,6 @@ declare global {
timers: { timers: {
setImmediate: typeof setImmediate; setImmediate: typeof setImmediate;
}; };
mas: {
getThumbnailLocation: (location: string) => Promise<string>;
};
desktop: { desktop: {
quit: (reason: string, stack: string) => void; quit: (reason: string, stack: string) => void;
openAppMenu: () => void; openAppMenu: () => void;

View file

@ -158,7 +158,7 @@ module.exports = merge(base, {
test: /\.mp3$/, test: /\.mp3$/,
type: 'asset/inline', type: 'asset/inline',
}, { }, {
test: /\.(svg|gif)$/, test: /\.(svg|gif|jpg)$/,
type: 'asset/resource', type: 'asset/resource',
}, { }, {
test: /\.(eot|ttf|woff|woff2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, test: /\.(eot|ttf|woff|woff2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,