mirror of
https://github.com/Llewellynvdm/nativefier.git
synced 2024-11-11 07:41:04 +00:00
Compare commits
No commits in common. "45d7981761249c0acb5e0f0923502353a1f8ba58" and "be418d4349c5c13f17faf0e91416818367c2cfb5" have entirely different histories.
45d7981761
...
be418d4349
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2 # Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v1 # Setup .npmrc file to publish to npm
|
||||
with:
|
||||
node-version: '20' # Align the version of Node here with ci.yml.
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2 # Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v1 # Setup .npmrc file to publish to npm
|
||||
with:
|
||||
node-version: '20' # Align the version of Node here with ci.yml.
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -44,6 +44,7 @@ build/Release
|
||||
# Dependency directory
|
||||
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
||||
node_modules
|
||||
.nvmrc
|
||||
|
||||
# Python virtual environment in case it's created for the Castlabs code signing tool
|
||||
venv
|
||||
|
@ -1,10 +1,4 @@
|
||||
|
||||
52.0.0 / 2023-08-25
|
||||
===================
|
||||
**[BREAKING]**
|
||||
* Update to Electron 25.7 (#1566)
|
||||
* Update to Electron 25 (#1559)
|
||||
|
||||
51.0.1 / 2023-08-04
|
||||
===================
|
||||
|
||||
|
1333
app/npm-shrinkwrap.json
generated
1333
app/npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,14 +12,14 @@
|
||||
],
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"electron-context-menu": "^3.6.1",
|
||||
"electron-dl": "^3.5.0",
|
||||
"electron-context-menu": "^3.1.1",
|
||||
"electron-dl": "^3.2.1",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"loglevel": "^1.8.1",
|
||||
"source-map-support": "^0.5.21"
|
||||
"loglevel": "^1.7.1",
|
||||
"source-map-support": "^0.5.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^25.7.0"
|
||||
"electron": "^21.4.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {
|
||||
BrowserWindow,
|
||||
ContextMenuParams,
|
||||
Event as ElectronEvent,
|
||||
NewWindowWebContentsEvent,
|
||||
} from 'electron';
|
||||
import contextMenu from 'electron-context-menu';
|
||||
|
||||
@ -25,7 +25,7 @@ export function initContextMenu(
|
||||
prepend: (actions: contextMenu.Actions, params: ContextMenuParams) => {
|
||||
log.debug('contextMenu.prepend', { actions, params });
|
||||
const items = [];
|
||||
if (params.linkURL && window) {
|
||||
if (params.linkURL) {
|
||||
items.push({
|
||||
label: 'Open Link in Default Browser',
|
||||
click: () => {
|
||||
@ -38,40 +38,36 @@ export function initContextMenu(
|
||||
label: 'Open Link in New Window',
|
||||
click: () =>
|
||||
createNewWindow(
|
||||
outputOptionsToWindowOptions(options, nativeTabsSupported()),
|
||||
outputOptionsToWindowOptions(options),
|
||||
setupNativefierWindow,
|
||||
params.linkURL,
|
||||
// window,
|
||||
window,
|
||||
),
|
||||
});
|
||||
if (nativeTabsSupported()) {
|
||||
items.push({
|
||||
label: 'Open Link in New Tab',
|
||||
click: () =>
|
||||
// // Fire a new window event for a foreground tab
|
||||
// // Previously we called createNewTab directly, but it had incosistent and buggy behavior
|
||||
// // as it was mostly designed for running off of events. So this will create a new event
|
||||
// // for a foreground-tab for the event handler to grab and take care of instead.
|
||||
// (window as BrowserWindow).webContents.emit(
|
||||
// // event name
|
||||
// 'new-window',
|
||||
// // event object
|
||||
// {
|
||||
// // Leave to the default for a NewWindowWebContentsEvent
|
||||
// newGuest: undefined,
|
||||
// ...new Event('new-window'),
|
||||
// }, // as NewWindowWebContentsEvent,
|
||||
// // url
|
||||
// params.linkURL,
|
||||
// // frameName
|
||||
// window?.webContents.mainFrame.name ?? '',
|
||||
// // disposition
|
||||
// 'foreground-tab',
|
||||
// ),
|
||||
window.emit('new-window-for-tab', {
|
||||
...new Event('new-window-for-tab'),
|
||||
url: params.linkURL,
|
||||
} as ElectronEvent<{ url: string }>),
|
||||
// Fire a new window event for a foreground tab
|
||||
// Previously we called createNewTab directly, but it had incosistent and buggy behavior
|
||||
// as it was mostly designed for running off of events. So this will create a new event
|
||||
// for a foreground-tab for the event handler to grab and take care of instead.
|
||||
(window as BrowserWindow).webContents.emit(
|
||||
// event name
|
||||
'new-window',
|
||||
// event object
|
||||
{
|
||||
// Leave to the default for a NewWindowWebContentsEvent
|
||||
newGuest: undefined,
|
||||
...new Event('new-window'),
|
||||
} as NewWindowWebContentsEvent,
|
||||
// url
|
||||
params.linkURL,
|
||||
// frameName
|
||||
window?.webContents.mainFrame.name ?? '',
|
||||
// disposition
|
||||
'foreground-tab',
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,19 +3,15 @@ import * as path from 'path';
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
import * as log from '../helpers/loggingHelper';
|
||||
import { nativeTabsSupported } from '../helpers/helpers';
|
||||
|
||||
export async function createLoginWindow(
|
||||
loginCallback: (username?: string, password?: string) => void,
|
||||
parent?: BrowserWindow,
|
||||
): Promise<BrowserWindow> {
|
||||
log.debug('createLoginWindow', {
|
||||
loginCallback,
|
||||
parent,
|
||||
});
|
||||
log.debug('createLoginWindow', { loginCallback, parent });
|
||||
|
||||
const loginWindow = new BrowserWindow({
|
||||
parent: nativeTabsSupported() ? undefined : parent,
|
||||
parent,
|
||||
width: 300,
|
||||
height: 400,
|
||||
frame: false,
|
||||
|
@ -1,13 +1,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import {
|
||||
desktopCapturer,
|
||||
ipcMain,
|
||||
BrowserWindow,
|
||||
Event,
|
||||
HandlerDetails,
|
||||
} from 'electron';
|
||||
import { ipcMain, desktopCapturer, BrowserWindow, Event } from 'electron';
|
||||
import windowStateKeeper from 'electron-window-state';
|
||||
|
||||
import { initContextMenu } from './contextMenu';
|
||||
@ -42,9 +36,9 @@ type SessionInteractionRequest = {
|
||||
propertyValue?: unknown;
|
||||
};
|
||||
|
||||
type SessionInteractionResult<T = unknown> = {
|
||||
type SessionInteractionResult = {
|
||||
id?: string;
|
||||
value?: T | Promise<T>;
|
||||
value?: unknown | Promise<unknown>;
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
@ -84,9 +78,7 @@ export async function createMainWindow(
|
||||
// So, we manually mainWindow.show() later, see a few lines below
|
||||
show: options.tray !== 'start-in-tray' && process.platform !== 'win32',
|
||||
backgroundColor: options.backgroundColor,
|
||||
...getDefaultWindowOptions(
|
||||
outputOptionsToWindowOptions(options, nativeTabsSupported()),
|
||||
),
|
||||
...getDefaultWindowOptions(outputOptionsToWindowOptions(options)),
|
||||
});
|
||||
|
||||
// Just load about:blank to start, gives playwright something to latch onto initially for testing.
|
||||
@ -110,32 +102,44 @@ export async function createMainWindow(
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
const windowOptions = outputOptionsToWindowOptions(
|
||||
options,
|
||||
nativeTabsSupported(),
|
||||
);
|
||||
const windowOptions = outputOptionsToWindowOptions(options);
|
||||
createMenu(options, mainWindow);
|
||||
createContextMenu(options, mainWindow);
|
||||
setupNativefierWindow(windowOptions, mainWindow);
|
||||
|
||||
// .on('new-window', ...) is deprected in favor of setWindowOpenHandler(...)
|
||||
// We can't quite cut over to that yet for a few reasons:
|
||||
// 1. Our version of Electron does not yet support a parameter to
|
||||
// setWindowOpenHandler that contains `disposition', which we need.
|
||||
// See https://github.com/electron/electron/issues/28380
|
||||
// 2. setWindowOpenHandler doesn't support newGuest as well
|
||||
// Though at this point, 'new-window' bugs seem to be coming up and downstream
|
||||
// users are being pointed to use setWindowOpenHandler.
|
||||
// E.g., https://github.com/electron/electron/issues/28374
|
||||
|
||||
// Note it is important to add these handlers only to the *main* window,
|
||||
// else we run into weird behavior like opening tabs twice
|
||||
mainWindow.webContents.setWindowOpenHandler((details: HandlerDetails) => {
|
||||
return onNewWindow(
|
||||
mainWindow.webContents.on(
|
||||
'new-window',
|
||||
(event, url, frameName, disposition) => {
|
||||
onNewWindow(
|
||||
windowOptions,
|
||||
setupNativefierWindow,
|
||||
details,
|
||||
mainWindow,
|
||||
event,
|
||||
url,
|
||||
frameName,
|
||||
disposition,
|
||||
).catch((err) => log.error('onNewWindow ERROR', err));
|
||||
},
|
||||
);
|
||||
});
|
||||
mainWindow.on('new-window-for-tab', (event?: Event<{ url?: string }>) => {
|
||||
log.debug('mainWindow.new-window-for-tab', { event });
|
||||
// @ts-expect-error new-tab isn't in the type definition, but it does exist
|
||||
mainWindow.on('new-tab', () => {
|
||||
createNewTab(
|
||||
windowOptions,
|
||||
setupNativefierWindow,
|
||||
event?.url ?? options.targetUrl,
|
||||
options.targetUrl,
|
||||
true,
|
||||
// mainWindow,
|
||||
mainWindow,
|
||||
);
|
||||
});
|
||||
|
||||
@ -150,7 +154,7 @@ export async function createMainWindow(
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
setupSessionInteraction(mainWindow);
|
||||
setupSessionInteraction(options, mainWindow);
|
||||
setupSessionPermissionHandler(mainWindow);
|
||||
|
||||
if (options.clearCache) {
|
||||
@ -261,7 +265,10 @@ function setupNotificationBadge(
|
||||
});
|
||||
}
|
||||
|
||||
function setupSessionInteraction(window: BrowserWindow): void {
|
||||
function setupSessionInteraction(
|
||||
options: OutputOptions,
|
||||
window: BrowserWindow,
|
||||
): void {
|
||||
// See API.md / "Accessing The Electron Session"
|
||||
ipcMain.on(
|
||||
'session-interaction',
|
||||
|
@ -2,7 +2,7 @@ jest.mock('./helpers');
|
||||
jest.mock('./windowEvents');
|
||||
jest.mock('./windowHelpers');
|
||||
|
||||
import { dialog, BrowserWindow, HandlerDetails, WebContents } from 'electron';
|
||||
import { dialog, BrowserWindow } from 'electron';
|
||||
import { WindowOptions } from '../../../shared/src/options/model';
|
||||
import { linkIsInternal, openExternal, nativeTabsSupported } from './helpers';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
@ -14,9 +14,11 @@ const {
|
||||
onNewWindowHelper: (
|
||||
options: WindowOptions,
|
||||
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
|
||||
details: Partial<HandlerDetails>,
|
||||
urlToGo: string,
|
||||
disposition: string | undefined,
|
||||
preventDefault: (newGuest: BrowserWindow) => void,
|
||||
parent?: BrowserWindow,
|
||||
) => ReturnType<Parameters<WebContents['setWindowOpenHandler']>[0]>;
|
||||
) => Promise<void>;
|
||||
onWillNavigate: (
|
||||
options: {
|
||||
blockExternalUrls: boolean;
|
||||
@ -41,13 +43,12 @@ describe('onNewWindowHelper', () => {
|
||||
const foregroundDisposition = 'foreground-tab';
|
||||
const backgroundDisposition = 'background-tab';
|
||||
const baseOptions = {
|
||||
autoHideMenuBar: true,
|
||||
blockExternalUrls: false,
|
||||
insecure: false,
|
||||
name: 'TEST_APP',
|
||||
targetUrl: originalURL,
|
||||
zoom: 1.0,
|
||||
} as WindowOptions;
|
||||
};
|
||||
const mockShowNavigationBlockedMessage: jest.SpyInstance =
|
||||
showNavigationBlockedMessage as jest.Mock;
|
||||
const mockCreateAboutBlank: jest.SpyInstance =
|
||||
@ -59,6 +60,7 @@ describe('onNewWindowHelper', () => {
|
||||
const mockNativeTabsSupported: jest.SpyInstance =
|
||||
nativeTabsSupported as jest.Mock;
|
||||
const mockOpenExternal: jest.SpyInstance = openExternal as jest.Mock;
|
||||
const preventDefault = jest.fn();
|
||||
const setupWindow = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
@ -70,6 +72,7 @@ describe('onNewWindowHelper', () => {
|
||||
mockLinkIsInternal.mockReset().mockReturnValue(true);
|
||||
mockNativeTabsSupported.mockReset().mockReturnValue(false);
|
||||
mockOpenExternal.mockReset();
|
||||
preventDefault.mockReset();
|
||||
setupWindow.mockReset();
|
||||
});
|
||||
|
||||
@ -82,84 +85,105 @@ describe('onNewWindowHelper', () => {
|
||||
mockOpenExternal.mockRestore();
|
||||
});
|
||||
|
||||
test('internal urls should not be handled', () => {
|
||||
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
||||
url: internalURL,
|
||||
});
|
||||
test('internal urls should not be handled', async () => {
|
||||
await onNewWindowHelper(
|
||||
baseOptions,
|
||||
setupWindow,
|
||||
internalURL,
|
||||
undefined,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(result.action).toEqual('allow');
|
||||
expect(preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('external urls should be opened externally', () => {
|
||||
test('external urls should be opened externally', async () => {
|
||||
mockLinkIsInternal.mockReturnValue(false);
|
||||
|
||||
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
||||
url: externalURL,
|
||||
});
|
||||
await onNewWindowHelper(
|
||||
baseOptions,
|
||||
setupWindow,
|
||||
externalURL,
|
||||
undefined,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
||||
expect(result.action).toEqual('deny');
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('external urls should be ignored if blockExternalUrls is true', () => {
|
||||
test('external urls should be ignored if blockExternalUrls is true', async () => {
|
||||
mockLinkIsInternal.mockReturnValue(false);
|
||||
const options = {
|
||||
...baseOptions,
|
||||
blockExternalUrls: true,
|
||||
};
|
||||
const result = onNewWindowHelper(options, setupWindow, {
|
||||
url: externalURL,
|
||||
});
|
||||
await onNewWindowHelper(
|
||||
options,
|
||||
setupWindow,
|
||||
externalURL,
|
||||
undefined,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockShowNavigationBlockedMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(result.action).toEqual('deny');
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('tab disposition should be ignored if tabs are not enabled', () => {
|
||||
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
||||
url: internalURL,
|
||||
disposition: foregroundDisposition,
|
||||
});
|
||||
test('tab disposition should be ignored if tabs are not enabled', async () => {
|
||||
await onNewWindowHelper(
|
||||
baseOptions,
|
||||
setupWindow,
|
||||
internalURL,
|
||||
foregroundDisposition,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(result.action).toEqual('allow');
|
||||
expect(preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('tab disposition should be ignored if url is external', () => {
|
||||
test('tab disposition should be ignored if url is external', async () => {
|
||||
mockLinkIsInternal.mockReturnValue(false);
|
||||
|
||||
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
||||
url: externalURL,
|
||||
disposition: foregroundDisposition,
|
||||
});
|
||||
await onNewWindowHelper(
|
||||
baseOptions,
|
||||
setupWindow,
|
||||
externalURL,
|
||||
foregroundDisposition,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).toHaveBeenCalledTimes(1);
|
||||
expect(result.action).toEqual('deny');
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('foreground tabs with internal urls should be opened in the foreground', () => {
|
||||
test('foreground tabs with internal urls should be opened in the foreground', async () => {
|
||||
mockNativeTabsSupported.mockReturnValue(true);
|
||||
|
||||
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
||||
url: internalURL,
|
||||
disposition: foregroundDisposition,
|
||||
});
|
||||
await onNewWindowHelper(
|
||||
baseOptions,
|
||||
setupWindow,
|
||||
internalURL,
|
||||
foregroundDisposition,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
|
||||
@ -168,19 +192,23 @@ describe('onNewWindowHelper', () => {
|
||||
setupWindow,
|
||||
internalURL,
|
||||
true,
|
||||
undefined,
|
||||
);
|
||||
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(result.action).toEqual('deny');
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('background tabs with internal urls should be opened in background tabs', () => {
|
||||
test('background tabs with internal urls should be opened in background tabs', async () => {
|
||||
mockNativeTabsSupported.mockReturnValue(true);
|
||||
|
||||
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
||||
url: internalURL,
|
||||
disposition: backgroundDisposition,
|
||||
});
|
||||
await onNewWindowHelper(
|
||||
baseOptions,
|
||||
setupWindow,
|
||||
internalURL,
|
||||
backgroundDisposition,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).toHaveBeenCalledTimes(1);
|
||||
@ -189,46 +217,59 @@ describe('onNewWindowHelper', () => {
|
||||
setupWindow,
|
||||
internalURL,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(result.action).toEqual('deny');
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('about:blank urls should be handled', () => {
|
||||
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
||||
url: 'about:blank',
|
||||
});
|
||||
test('about:blank urls should be handled', async () => {
|
||||
await onNewWindowHelper(
|
||||
baseOptions,
|
||||
setupWindow,
|
||||
'about:blank',
|
||||
undefined,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(result.action).toEqual('deny');
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('about:blank#blocked urls should be handled', () => {
|
||||
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
||||
url: 'about:blank#blocked',
|
||||
});
|
||||
test('about:blank#blocked urls should be handled', async () => {
|
||||
await onNewWindowHelper(
|
||||
baseOptions,
|
||||
setupWindow,
|
||||
'about:blank#blocked',
|
||||
undefined,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(result.action).toEqual('deny');
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('about:blank#other urls should not be handled', () => {
|
||||
const result = onNewWindowHelper(baseOptions, setupWindow, {
|
||||
url: 'about:blank#other',
|
||||
});
|
||||
test('about:blank#other urls should not be handled', async () => {
|
||||
await onNewWindowHelper(
|
||||
baseOptions,
|
||||
setupWindow,
|
||||
'about:blank#other',
|
||||
undefined,
|
||||
preventDefault,
|
||||
);
|
||||
|
||||
expect(mockCreateAboutBlank).not.toHaveBeenCalled();
|
||||
expect(mockCreateNewTab).not.toHaveBeenCalled();
|
||||
expect(mockShowNavigationBlockedMessage).not.toHaveBeenCalled();
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled();
|
||||
expect(result.action).toEqual('allow');
|
||||
expect(preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2,8 +2,8 @@ import {
|
||||
dialog,
|
||||
BrowserWindow,
|
||||
Event,
|
||||
NewWindowWebContentsEvent,
|
||||
WebContents,
|
||||
HandlerDetails,
|
||||
} from 'electron';
|
||||
|
||||
import { linkIsInternal, nativeTabsSupported, openExternal } from './helpers';
|
||||
@ -18,64 +18,84 @@ import {
|
||||
} from './windowHelpers';
|
||||
import { WindowOptions } from '../../../shared/src/options/model';
|
||||
|
||||
type NewWindowHandlerResult = ReturnType<
|
||||
Parameters<WebContents['setWindowOpenHandler']>[0]
|
||||
>;
|
||||
|
||||
export function onNewWindow(
|
||||
options: WindowOptions,
|
||||
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
|
||||
details: HandlerDetails,
|
||||
event: NewWindowWebContentsEvent,
|
||||
urlToGo: string,
|
||||
frameName: string,
|
||||
disposition:
|
||||
| 'default'
|
||||
| 'foreground-tab'
|
||||
| 'background-tab'
|
||||
| 'new-window'
|
||||
| 'save-to-disk'
|
||||
| 'other',
|
||||
parent?: BrowserWindow,
|
||||
): NewWindowHandlerResult {
|
||||
): Promise<void> {
|
||||
log.debug('onNewWindow', {
|
||||
details,
|
||||
event,
|
||||
urlToGo,
|
||||
frameName,
|
||||
disposition,
|
||||
parent,
|
||||
});
|
||||
const preventDefault = (newGuest?: BrowserWindow): void => {
|
||||
log.debug('onNewWindow.preventDefault', { newGuest, event });
|
||||
if (event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (newGuest) {
|
||||
event.newGuest = newGuest;
|
||||
}
|
||||
};
|
||||
return onNewWindowHelper(
|
||||
options,
|
||||
setupWindow,
|
||||
details,
|
||||
nativeTabsSupported() ? undefined : parent,
|
||||
urlToGo,
|
||||
disposition,
|
||||
preventDefault,
|
||||
parent,
|
||||
);
|
||||
}
|
||||
|
||||
export function onNewWindowHelper(
|
||||
options: WindowOptions,
|
||||
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
|
||||
details: HandlerDetails,
|
||||
urlToGo: string,
|
||||
disposition: string | undefined,
|
||||
preventDefault: (newGuest?: BrowserWindow) => void,
|
||||
parent?: BrowserWindow,
|
||||
): NewWindowHandlerResult {
|
||||
): Promise<void> {
|
||||
log.debug('onNewWindowHelper', {
|
||||
options,
|
||||
details,
|
||||
urlToGo,
|
||||
disposition,
|
||||
preventDefault,
|
||||
parent,
|
||||
});
|
||||
try {
|
||||
if (
|
||||
!linkIsInternal(
|
||||
options.targetUrl,
|
||||
details.url,
|
||||
urlToGo,
|
||||
options.internalUrls,
|
||||
options.strictInternalUrls,
|
||||
)
|
||||
) {
|
||||
preventDefault();
|
||||
if (options.blockExternalUrls) {
|
||||
return new Promise((resolve) => {
|
||||
showNavigationBlockedMessage(
|
||||
`Navigation to external URL blocked by options: ${details.url}`,
|
||||
`Navigation to external URL blocked by options: ${urlToGo}`,
|
||||
)
|
||||
.then(() => {
|
||||
// blockExternalURL(details.url).then(resolve).catch((err: unknown) => {
|
||||
// log.error('blockExternalURL', err);
|
||||
// });
|
||||
})
|
||||
.then(() => resolve())
|
||||
.catch((err: unknown) => {
|
||||
throw err;
|
||||
});
|
||||
return { action: 'deny' };
|
||||
} else {
|
||||
openExternal(details.url).catch((err: unknown) => {
|
||||
log.error('openExternal', err);
|
||||
});
|
||||
return { action: 'deny' };
|
||||
} else {
|
||||
return openExternal(urlToGo);
|
||||
}
|
||||
}
|
||||
// Normally the following would be:
|
||||
@ -83,25 +103,26 @@ export function onNewWindowHelper(
|
||||
// But due to a bug we resolved in https://github.com/nativefier/nativefier/issues/1197
|
||||
// Some sites use about:blank#something to use as placeholder windows to fill
|
||||
// with content via JavaScript. So we'll stay specific for now...
|
||||
else if (['about:blank', 'about:blank#blocked'].includes(details.url)) {
|
||||
createAboutBlankWindow(
|
||||
options,
|
||||
setupWindow,
|
||||
nativeTabsSupported() ? undefined : parent,
|
||||
else if (['about:blank', 'about:blank#blocked'].includes(urlToGo)) {
|
||||
return Promise.resolve(
|
||||
preventDefault(createAboutBlankWindow(options, setupWindow, parent)),
|
||||
);
|
||||
return { action: 'deny' };
|
||||
} else if (nativeTabsSupported()) {
|
||||
return Promise.resolve(
|
||||
preventDefault(
|
||||
createNewTab(
|
||||
options,
|
||||
setupWindow,
|
||||
details.url,
|
||||
details.disposition === 'foreground-tab',
|
||||
urlToGo,
|
||||
disposition === 'foreground-tab',
|
||||
parent,
|
||||
),
|
||||
),
|
||||
);
|
||||
return { action: 'deny' };
|
||||
}
|
||||
return { action: 'allow' };
|
||||
return Promise.resolve(undefined);
|
||||
} catch (err: unknown) {
|
||||
return { action: 'deny' };
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,15 +52,14 @@ describe('clearAppData', () => {
|
||||
});
|
||||
|
||||
describe('createNewTab', () => {
|
||||
// const window = new BrowserWindow();
|
||||
const window = new BrowserWindow();
|
||||
const options: WindowOptions = {
|
||||
autoHideMenuBar: true,
|
||||
blockExternalUrls: false,
|
||||
insecure: false,
|
||||
name: 'Test App',
|
||||
targetUrl: 'https://github.com/nativefier/natifefier',
|
||||
zoom: 1.0,
|
||||
} as WindowOptions;
|
||||
};
|
||||
const setupWindow = jest.fn();
|
||||
const url = 'https://github.com/nativefier/nativefier';
|
||||
const mockAddTabbedWindow: jest.SpyInstance = jest.spyOn(
|
||||
@ -79,7 +78,7 @@ describe('createNewTab', () => {
|
||||
test('creates new foreground tab', () => {
|
||||
const foreground = true;
|
||||
|
||||
const tab = createNewTab(options, setupWindow, url, foreground);
|
||||
const tab = createNewTab(options, setupWindow, url, foreground, window);
|
||||
|
||||
expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab);
|
||||
expect(setupWindow).toHaveBeenCalledWith(options, tab);
|
||||
@ -90,13 +89,7 @@ describe('createNewTab', () => {
|
||||
test('creates new background tab', () => {
|
||||
const foreground = false;
|
||||
|
||||
const tab = createNewTab(
|
||||
options,
|
||||
setupWindow,
|
||||
url,
|
||||
foreground,
|
||||
// window
|
||||
);
|
||||
const tab = createNewTab(options, setupWindow, url, foreground, window);
|
||||
|
||||
expect(mockAddTabbedWindow).toHaveBeenCalledWith(tab);
|
||||
expect(setupWindow).toHaveBeenCalledWith(options, tab);
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
import { getCSSToInject, isOSX, nativeTabsSupported } from './helpers';
|
||||
import * as log from './loggingHelper';
|
||||
import { TrayValue, WindowOptions } from '../../../shared/src/options/model';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const ZOOM_INTERVAL = 0.1;
|
||||
|
||||
@ -74,7 +73,7 @@ export function createAboutBlankWindow(
|
||||
{ ...options, show: false },
|
||||
setupWindow,
|
||||
'about:blank',
|
||||
nativeTabsSupported() ? undefined : parent,
|
||||
parent,
|
||||
);
|
||||
window.webContents.once('did-stop-loading', () => {
|
||||
if (window.webContents.getURL() === 'about:blank') {
|
||||
@ -91,16 +90,11 @@ export function createNewTab(
|
||||
setupWindow: (options: WindowOptions, window: BrowserWindow) => void,
|
||||
url: string,
|
||||
foreground: boolean,
|
||||
parent?: BrowserWindow,
|
||||
): BrowserWindow | undefined {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
log.debug('createNewTab', {
|
||||
url,
|
||||
foreground,
|
||||
focusedWindow,
|
||||
});
|
||||
log.debug('createNewTab', { url, foreground, parent });
|
||||
return withFocusedWindow((focusedWindow) => {
|
||||
const newTab = createNewWindow(options, setupWindow, url);
|
||||
log.debug('createNewTab.withFocusedWindow', { focusedWindow, newTab });
|
||||
const newTab = createNewWindow(options, setupWindow, url, parent);
|
||||
focusedWindow.addTabbedWindow(newTab);
|
||||
if (!foreground) {
|
||||
focusedWindow.focus();
|
||||
@ -115,12 +109,9 @@ export function createNewWindow(
|
||||
url: string,
|
||||
parent?: BrowserWindow,
|
||||
): BrowserWindow {
|
||||
log.debug('createNewWindow', {
|
||||
url,
|
||||
parent,
|
||||
});
|
||||
log.debug('createNewWindow', { url, parent });
|
||||
const window = new BrowserWindow({
|
||||
parent: nativeTabsSupported() ? undefined : parent,
|
||||
parent,
|
||||
...getDefaultWindowOptions(options),
|
||||
});
|
||||
setupWindow(options, window);
|
||||
@ -150,11 +141,8 @@ export function getDefaultWindowOptions(
|
||||
};
|
||||
|
||||
const defaultOptions: BrowserWindowConstructorOptions = {
|
||||
autoHideMenuBar: options.autoHideMenuBar,
|
||||
fullscreenable: true,
|
||||
tabbingIdentifier: nativeTabsSupported()
|
||||
? options.tabbingIdentifier ?? randomUUID()
|
||||
: undefined,
|
||||
tabbingIdentifier: nativeTabsSupported() ? options.name : undefined,
|
||||
title: options.name,
|
||||
webPreferences: {
|
||||
javascript: true,
|
||||
|
@ -202,8 +202,7 @@ const setDockBadge = isOSX()
|
||||
? (count?: number | string, bounce = false): void => {
|
||||
if (count !== undefined) {
|
||||
app.dock.setBadge(count.toString());
|
||||
if (bounce && typeof count === 'number' && count > currentBadgeCount)
|
||||
app.dock.bounce();
|
||||
if (bounce && count > currentBadgeCount) app.dock.bounce();
|
||||
currentBadgeCount = typeof count === 'number' ? count : 0;
|
||||
}
|
||||
}
|
||||
@ -310,10 +309,10 @@ if (shouldQuit) {
|
||||
});
|
||||
}
|
||||
|
||||
app.on('new-window-for-tab', (event: Event) => {
|
||||
log.debug('app.new-window-for-tab', { event });
|
||||
app.on('new-window-for-tab', () => {
|
||||
log.debug('app.new-window-for-tab');
|
||||
if (mainWindow) {
|
||||
mainWindow.emit('new-window-for-tab', event);
|
||||
mainWindow.emit('new-tab');
|
||||
}
|
||||
});
|
||||
|
||||
@ -333,10 +332,9 @@ app.on(
|
||||
if (appArgs.basicAuthUsername && appArgs.basicAuthPassword) {
|
||||
callback(appArgs.basicAuthUsername, appArgs.basicAuthPassword);
|
||||
} else {
|
||||
createLoginWindow(
|
||||
callback,
|
||||
// mainWindow
|
||||
).catch((err) => log.error('createLoginWindow ERROR', err));
|
||||
createLoginWindow(callback, mainWindow).catch((err) =>
|
||||
log.error('createLoginWindow ERROR', err),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -21,6 +21,7 @@ module.exports = {
|
||||
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-extraneous-class': 'error',
|
||||
'@typescript-eslint/no-implicit-any-catch': 'error',
|
||||
'@typescript-eslint/no-invalid-void-type': 'error',
|
||||
'@typescript-eslint/prefer-ts-expect-error': 'error',
|
||||
'@typescript-eslint/type-annotation-spacing': 'error',
|
||||
|
8679
npm-shrinkwrap.json
generated
8679
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
63
package.json
63
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nativefier",
|
||||
"version": "52.0.0",
|
||||
"version": "51.0.1",
|
||||
"description": "Wrap web apps natively",
|
||||
"license": "MIT",
|
||||
"author": "Goh Jia Hao",
|
||||
@ -41,10 +41,10 @@
|
||||
"lint:fix": "cd src && eslint . --ext .ts --fix && cd ../shared && eslint src --ext .ts --fix && cd ../app && eslint src --ext .ts --fix",
|
||||
"lint:format": "prettier --write 'src/**/*.ts' 'app/src/**/*.ts' 'shared/src/**/*.ts'",
|
||||
"lint": "eslint shared app src --ext .ts",
|
||||
"list-outdated-deps": "npm out -l; cd app && npm out -l; true",
|
||||
"list-outdated-deps": "npm out; cd app && npm out; true",
|
||||
"prepare": "cd app && npm ci && cd .. && npm run build",
|
||||
"relock:cli": "rm -rf ./node_modules/ ./npm-shrinkwrap.json && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out -l",
|
||||
"relock:app": "rm -rf ./app/node_modules/ ./app/npm-shrinkwrap.json && cd app && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out -l",
|
||||
"relock:cli": "rm -rf ./node_modules/ ./npm-shrinkwrap.json && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out",
|
||||
"relock:app": "rm -rf ./app/node_modules/ ./app/npm-shrinkwrap.json && cd app && npm install --ignore-scripts --package-lock && mv package-lock.json npm-shrinkwrap.json && npm out",
|
||||
"relock": "npm run relock:cli; npm run relock:app",
|
||||
"test:integration": "jest --testRegex=integration-test",
|
||||
"test:manual": "npm run build && bash .github/manual-test",
|
||||
@ -58,44 +58,43 @@
|
||||
"watch": "npx concurrently \"npm:*:watch\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.4",
|
||||
"axios": "^1.4.0",
|
||||
"electron-packager": "^17.1.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"gitcloud": "^0.2.4",
|
||||
"axios": "^1.1.3",
|
||||
"electron-packager": "^15.5.1",
|
||||
"fs-extra": "^10.0.0",
|
||||
"gitcloud": "^0.2.3",
|
||||
"hasbin": "^1.2.3",
|
||||
"loglevel": "^1.8.1",
|
||||
"loglevel": "^1.7.1",
|
||||
"ncp": "^2.0.0",
|
||||
"page-icon": "^0.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"source-map-support": "^0.5.19",
|
||||
"tmp": "^0.2.1",
|
||||
"yargs": "^17.7.2"
|
||||
"yargs": "^17.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.8",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/debug": "^4.1.6",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/hasbin": "^1.2.0",
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/ncp": "^2.0.5",
|
||||
"@types/node": "^20.5.6",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/page-icon": "^0.3.4",
|
||||
"@types/tmp": "^0.2.3",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"electron": "^25.7.0",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.6.2",
|
||||
"playwright": "^1.36.2",
|
||||
"prettier": "^3.0.1",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-loader": "^9.4.4",
|
||||
"typescript": "^5.1.6",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4"
|
||||
"@types/tmp": "^0.2.1",
|
||||
"@types/yargs": "^17.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.3.0",
|
||||
"@typescript-eslint/parser": "^5.3.0",
|
||||
"electron": "^21.4.4",
|
||||
"eslint": "^8.1.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^28.1.3",
|
||||
"playwright": "^1.24.0",
|
||||
"prettier": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-loader": "^9.2.3",
|
||||
"typescript": "^4.3.5",
|
||||
"webpack": "^5.45.1",
|
||||
"webpack-cli": "^4.7.2"
|
||||
},
|
||||
"jest_COMMENTS": {
|
||||
"testPathIgnorePatterns": "See https://jestjs.io/docs/configuration#testpathignorepatterns-arraystring . We set it to 1. ignore coverage for deps, and 2. be sure we test the compiled JS, which is in `lib`, not `src` or `dist`",
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { CreateOptions } from '@electron/asar';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { CreateOptions } from 'asar';
|
||||
import * as electronPackager from 'electron-packager';
|
||||
|
||||
export type TitleBarValue =
|
||||
@ -118,7 +117,6 @@ export type OutputOptions = NativefierOptions & {
|
||||
nativefierVersion: string;
|
||||
oldBuildWarningText: string;
|
||||
strictInternalUrls: boolean;
|
||||
tabbingIdentifier?: string;
|
||||
targetUrl: string;
|
||||
userAgent?: string;
|
||||
zoom?: number;
|
||||
@ -207,7 +205,6 @@ export type RawOptions = {
|
||||
};
|
||||
|
||||
export type WindowOptions = {
|
||||
autoHideMenuBar: boolean;
|
||||
blockExternalUrls: boolean;
|
||||
browserwindowOptions?: BrowserWindowOptions;
|
||||
insecure: boolean;
|
||||
@ -216,7 +213,6 @@ export type WindowOptions = {
|
||||
name: string;
|
||||
proxyRules?: string;
|
||||
show?: boolean;
|
||||
tabbingIdentifier?: string;
|
||||
targetUrl: string;
|
||||
userAgent?: string;
|
||||
zoom: number;
|
||||
@ -224,15 +220,10 @@ export type WindowOptions = {
|
||||
|
||||
export function outputOptionsToWindowOptions(
|
||||
options: OutputOptions,
|
||||
generateTabbingIdentifierIfMissing: boolean,
|
||||
): WindowOptions {
|
||||
return {
|
||||
...options,
|
||||
autoHideMenuBar: !options.showMenuBar,
|
||||
insecure: options.insecure ?? false,
|
||||
tabbingIdentifier: generateTabbingIdentifierIfMissing
|
||||
? options.tabbingIdentifier ?? randomUUID()
|
||||
: options.tabbingIdentifier,
|
||||
zoom: options.zoom ?? 1.0,
|
||||
};
|
||||
}
|
||||
|
@ -6,19 +6,19 @@ export const DEFAULT_APP_NAME = 'APP';
|
||||
// - upgrade app / package.json / "devDependencies" / "electron"
|
||||
// - upgrade package.json / "devDependencies" / "electron"
|
||||
// Doing a *major* upgrade? Read https://github.com/nativefier/nativefier/blob/master/HACKING.md#deps-major-upgrading-electron
|
||||
export const DEFAULT_ELECTRON_VERSION = '25.7.0';
|
||||
export const DEFAULT_ELECTRON_VERSION = '21.4.4';
|
||||
// https://atom.io/download/atom-shell/index.json
|
||||
// https://www.electronjs.org/releases/stable
|
||||
export const DEFAULT_CHROME_VERSION = '114.0.5735.289';
|
||||
export const DEFAULT_CHROME_VERSION = '106.0.5249.199';
|
||||
|
||||
// Update each of these periodically
|
||||
// https://product-details.mozilla.org/1.0/firefox_versions.json
|
||||
export const DEFAULT_FIREFOX_VERSION = '116.0.3';
|
||||
export const DEFAULT_FIREFOX_VERSION = '116.0';
|
||||
|
||||
// https://en.wikipedia.org/wiki/Safari_version_history
|
||||
export const DEFAULT_SAFARI_VERSION = {
|
||||
majorVersion: 16,
|
||||
version: '16.6',
|
||||
majorVersion: 65,
|
||||
version: '16.5.2',
|
||||
webkitVersion: '605.1.15',
|
||||
};
|
||||
|
||||
|
@ -71,8 +71,9 @@ async function edgeUserAgent(
|
||||
platform: string,
|
||||
electronVersion: string,
|
||||
): Promise<string> {
|
||||
const chromeVersion =
|
||||
await getChromeVersionForElectronVersion(electronVersion);
|
||||
const chromeVersion = await getChromeVersionForElectronVersion(
|
||||
electronVersion,
|
||||
);
|
||||
|
||||
return `Mozilla/5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36 Edg/${chromeVersion}`;
|
||||
}
|
||||
|
@ -6,7 +6,6 @@
|
||||
"incremental": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
|
Loading…
Reference in New Issue
Block a user