mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
Compare commits
18 Commits
v14.0.0-ni
...
v14.0.0-ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c2150a6fa | ||
|
|
62b38812b6 | ||
|
|
3ed8da0931 | ||
|
|
e2f49edf83 | ||
|
|
1fcd6e2740 | ||
|
|
19d7a6b761 | ||
|
|
82ea8ea68c | ||
|
|
c280d770dc | ||
|
|
29603bcc27 | ||
|
|
55c66e3e92 | ||
|
|
9904438118 | ||
|
|
e6aefed0ee | ||
|
|
fa65faa4b0 | ||
|
|
e323bfe661 | ||
|
|
976222b509 | ||
|
|
2a55ae4b85 | ||
|
|
05d164e660 | ||
|
|
ba3b2189ad |
@@ -932,7 +932,7 @@ step-ninja-summary: &step-ninja-summary
|
||||
command: |
|
||||
set +e
|
||||
set +o pipefail
|
||||
python depot_tools/post_build_ninja_summary.py -C src/out/Default
|
||||
python depot_tools/post_build_ninja_summary.py -C src/out/Default || echo Ninja Summary Failed
|
||||
|
||||
step-ninja-report: &step-ninja-report
|
||||
store_artifacts:
|
||||
|
||||
@@ -1 +1 @@
|
||||
14.0.0-nightly.20210331
|
||||
14.0.0-nightly.20210406
|
||||
@@ -871,6 +871,10 @@ re-add a removed item to a custom category earlier than that will result in the
|
||||
entire custom category being omitted from the Jump List. The list of removed
|
||||
items can be obtained using `app.getJumpListSettings()`.
|
||||
|
||||
**Note:** The maximum length of a Jump List item's `description` property is
|
||||
260 characters. Beyond this limit, the item will not be added to the Jump
|
||||
List, nor will it be displayed.
|
||||
|
||||
Here's a very simple example of creating a custom Jump List:
|
||||
|
||||
```javascript
|
||||
|
||||
@@ -83,8 +83,10 @@ win.show()
|
||||
blur effect to the content below the window (i.e. other applications open on
|
||||
the user's system).
|
||||
* The window will not be transparent when DevTools is opened.
|
||||
* On Windows operating systems, transparent windows will not work when DWM is
|
||||
* On Windows operating systems,
|
||||
* transparent windows will not work when DWM is
|
||||
disabled.
|
||||
* transparent windows can not be maximized using the Windows system menu or by double clicking the title bar. The reasoning behind this can be seen on [this pull request](https://github.com/electron/electron/pull/28207).
|
||||
* On Linux, users have to put `--enable-transparent-visuals --disable-gpu` in
|
||||
the command line to disable GPU and allow ARGB to make transparent window,
|
||||
this is caused by an upstream bug that [alpha channel doesn't work on some
|
||||
|
||||
@@ -19,3 +19,7 @@
|
||||
property set then its `type` is assumed to be `tasks`. If the `name` property
|
||||
is set but the `type` property is omitted then the `type` is assumed to be
|
||||
`custom`.
|
||||
|
||||
**Note:** The maximum length of a Jump List item's `description` property is
|
||||
260 characters. Beyond this limit, the item will not be added to the Jump
|
||||
List, nor will it be displayed.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* `title` String (optional) - The text to be displayed for the item in the Jump List.
|
||||
Should only be set if `type` is `task`.
|
||||
* `description` String (optional) - Description of the task (displayed in a tooltip).
|
||||
Should only be set if `type` is `task`.
|
||||
Should only be set if `type` is `task`. Maximum length 260 characters.
|
||||
* `iconPath` String (optional) - The absolute path to an icon to be displayed in a
|
||||
Jump List, which can be an arbitrary resource file that contains an icon
|
||||
(e.g. `.ico`, `.exe`, `.dll`). You can usually specify `process.execPath` to
|
||||
|
||||
@@ -65,6 +65,7 @@ window.open('https://github.com', '_blank', 'top=500,left=200,frame=false,nodeIn
|
||||
* Non-standard features (that are not handled by Chromium or Electron) given in
|
||||
`features` will be passed to any registered `webContents`'s
|
||||
`did-create-window` event handler in the `additionalFeatures` argument.
|
||||
* `frameName` follows the specification of `windowName` located in the [native documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/open#parameters).
|
||||
|
||||
To customize or cancel the creation of the window, you can optionally set an
|
||||
override handler with `webContents.setWindowOpenHandler()` from the main
|
||||
|
||||
@@ -190,7 +190,6 @@ it may be worth your time to shop around. Popular resellers include:
|
||||
|
||||
* [digicert](https://www.digicert.com/code-signing/microsoft-authenticode.htm)
|
||||
* [Sectigo](https://sectigo.com/ssl-certificates-tls/code-signing)
|
||||
* [GoDaddy](https://au.godaddy.com/web-security/code-signing-certificate)
|
||||
* Amongst others, please shop around to find one that suits your needs, Google
|
||||
is your friend 😄
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ Following platforms are supported by Electron:
|
||||
### macOS
|
||||
|
||||
Only 64bit binaries are provided for macOS, and the minimum macOS version
|
||||
supported is macOS 10.10 (Yosemite).
|
||||
supported is macOS 10.11 (El Capitan).
|
||||
|
||||
Native support for Apple Silicon (`arm64`) devices was added in Electron 11.0.0.
|
||||
|
||||
|
||||
@@ -433,7 +433,11 @@ WebContents.prototype._callWindowOpenHandler = function (event: Electron.Event,
|
||||
event.preventDefault();
|
||||
return null;
|
||||
} else if (response.action === 'allow') {
|
||||
if (typeof response.overrideBrowserWindowOptions === 'object' && response.overrideBrowserWindowOptions !== null) { return response.overrideBrowserWindowOptions; } else { return {}; }
|
||||
if (typeof response.overrideBrowserWindowOptions === 'object' && response.overrideBrowserWindowOptions !== null) {
|
||||
return response.overrideBrowserWindowOptions;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
event.preventDefault();
|
||||
console.error('The window open handler response must be an object with an \'action\' property of \'allow\' or \'deny\'.');
|
||||
@@ -565,19 +569,22 @@ WebContents.prototype._init = function () {
|
||||
// Make new windows requested by links behave like "window.open".
|
||||
this.on('-new-window' as any, (event: ElectronInternal.Event, url: string, frameName: string, disposition: string,
|
||||
rawFeatures: string, referrer: Electron.Referrer, postData: PostData) => {
|
||||
openGuestWindow({
|
||||
event,
|
||||
embedder: event.sender,
|
||||
disposition,
|
||||
referrer,
|
||||
postData,
|
||||
overrideBrowserWindowOptions: {},
|
||||
windowOpenArgs: {
|
||||
url,
|
||||
frameName,
|
||||
features: rawFeatures
|
||||
}
|
||||
});
|
||||
const options = this._callWindowOpenHandler(event, url, frameName, rawFeatures);
|
||||
if (!event.defaultPrevented) {
|
||||
openGuestWindow({
|
||||
event,
|
||||
embedder: event.sender,
|
||||
disposition,
|
||||
referrer,
|
||||
postData,
|
||||
overrideBrowserWindowOptions: options || {},
|
||||
windowOpenArgs: {
|
||||
url,
|
||||
frameName,
|
||||
features: rawFeatures
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let windowOpenOverriddenOptions: BrowserWindowConstructorOptions | null = null;
|
||||
|
||||
@@ -42,7 +42,6 @@ export function openGuestWindow ({ event, embedder, guest, referrer, disposition
|
||||
windowOpenArgs: WindowOpenArgs,
|
||||
}): BrowserWindow | undefined {
|
||||
const { url, frameName, features } = windowOpenArgs;
|
||||
const isNativeWindowOpen = !!guest;
|
||||
const { options: browserWindowOptions, additionalFeatures } = makeBrowserWindowOptions({
|
||||
embedder,
|
||||
features,
|
||||
@@ -74,11 +73,14 @@ export function openGuestWindow ({ event, embedder, guest, referrer, disposition
|
||||
webContents: guest,
|
||||
...browserWindowOptions
|
||||
});
|
||||
if (!isNativeWindowOpen) {
|
||||
if (!guest) {
|
||||
// We should only call `loadURL` if the webContents was constructed by us in
|
||||
// the case of BrowserWindowProxy (non-sandboxed, nativeWindowOpen: false),
|
||||
// as navigating to the url when creating the window from an existing
|
||||
// webContents is not necessary (it will navigate there anyway).
|
||||
// This can also happen if we enter this function from OpenURLFromTab, in
|
||||
// which case the browser process is responsible for initiating navigation
|
||||
// in the new window.
|
||||
window.loadURL(url, {
|
||||
httpReferrer: referrer,
|
||||
...(postData && {
|
||||
|
||||
@@ -11,16 +11,16 @@ class WebFrame extends EventEmitter {
|
||||
this.setMaxListeners(0);
|
||||
}
|
||||
|
||||
findFrameByRoutingId (...args: Array<any>) {
|
||||
return getWebFrame(binding._findFrameByRoutingId(this.context, ...args));
|
||||
findFrameByRoutingId (routingId: number) {
|
||||
return getWebFrame(binding._findFrameByRoutingId(this.context, routingId));
|
||||
}
|
||||
|
||||
getFrameForSelector (...args: Array<any>) {
|
||||
return getWebFrame(binding._getFrameForSelector(this.context, ...args));
|
||||
getFrameForSelector (selector: string) {
|
||||
return getWebFrame(binding._getFrameForSelector(this.context, selector));
|
||||
}
|
||||
|
||||
findFrameByName (...args: Array<any>) {
|
||||
return getWebFrame(binding._findFrameByName(this.context, ...args));
|
||||
findFrameByName (name: string) {
|
||||
return getWebFrame(binding._findFrameByName(this.context, name));
|
||||
}
|
||||
|
||||
get opener () {
|
||||
@@ -62,12 +62,12 @@ for (const name in binding) {
|
||||
if (!worldSafeJS && name.startsWith('executeJavaScript')) {
|
||||
deprecate.log(`Security Warning: webFrame.${name} was called without worldSafeExecuteJavaScript enabled. This is considered unsafe. worldSafeExecuteJavaScript will be enabled by default in Electron 12.`);
|
||||
}
|
||||
return binding[name](this.context, ...args);
|
||||
return (binding as any)[name](this.context, ...args);
|
||||
};
|
||||
// TODO(MarshallOfSound): Remove once the above deprecation is removed
|
||||
if (name.startsWith('executeJavaScript')) {
|
||||
(WebFrame as any).prototype[`_${name}`] = function (...args: Array<any>) {
|
||||
return binding[name](this.context, ...args);
|
||||
return (binding as any)[name](this.context, ...args);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "electron",
|
||||
"version": "14.0.0-nightly.20210331",
|
||||
"version": "14.0.0-nightly.20210406",
|
||||
"repository": "https://github.com/electron/electron",
|
||||
"description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -306,18 +306,18 @@ def export_patches(repo, out_dir, patch_range=None, dry_run=False):
|
||||
# If we're doing a dry run, iterate through each patch and see if the newly
|
||||
# exported patch differs from what exists. Report number of mismatched
|
||||
# patches and fail if there's more than one.
|
||||
patch_count = 0
|
||||
bad_patches = []
|
||||
for patch in patches:
|
||||
filename = get_file_name(patch)
|
||||
filepath = posixpath.join(out_dir, filename)
|
||||
existing_patch = io.open(filepath, 'rb').read()
|
||||
existing_patch = unicode(io.open(filepath, 'rb').read(), "utf-8")
|
||||
formatted_patch = join_patch(patch)
|
||||
if formatted_patch != existing_patch:
|
||||
patch_count += 1
|
||||
if patch_count > 0:
|
||||
bad_patches.append(filename)
|
||||
if len(bad_patches) > 0:
|
||||
sys.stderr.write(
|
||||
"Patches in {} not up to date: {} patches need update\n".format(
|
||||
out_dir, patch_count
|
||||
"Patches in {} not up to date: {} patches need update\n-- {}\n".format(
|
||||
out_dir, len(bad_patches), "\n-- ".join(bad_patches)
|
||||
)
|
||||
)
|
||||
exit(1)
|
||||
|
||||
@@ -363,7 +363,9 @@ void JavascriptEnvironment::OnMessageLoopDestroying() {
|
||||
NodeEnvironment::NodeEnvironment(node::Environment* env) : env_(env) {}
|
||||
|
||||
NodeEnvironment::~NodeEnvironment() {
|
||||
auto* isolate_data = env_->isolate_data();
|
||||
node::FreeEnvironment(env_);
|
||||
node::FreeIsolateData(isolate_data);
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -530,7 +530,7 @@ void NativeWindowViews::Maximize() {
|
||||
|
||||
void NativeWindowViews::Unmaximize() {
|
||||
#if defined(OS_WIN)
|
||||
if (!(::GetWindowLong(GetAcceleratedWidget(), GWL_STYLE) & WS_THICKFRAME)) {
|
||||
if (transparent()) {
|
||||
SetBounds(restore_bounds_, false);
|
||||
return;
|
||||
}
|
||||
@@ -540,21 +540,22 @@ void NativeWindowViews::Unmaximize() {
|
||||
}
|
||||
|
||||
bool NativeWindowViews::IsMaximized() {
|
||||
// For window without WS_THICKFRAME style, we can not call IsMaximized().
|
||||
// This path will be used for transparent windows as well.
|
||||
|
||||
if (widget()->IsMaximized()) {
|
||||
return true;
|
||||
} else {
|
||||
#if defined(OS_WIN)
|
||||
if (!(::GetWindowLong(GetAcceleratedWidget(), GWL_STYLE) & WS_THICKFRAME)) {
|
||||
// Compare the size of the window with the size of the display
|
||||
auto display = display::Screen::GetScreen()->GetDisplayNearestWindow(
|
||||
GetNativeWindow());
|
||||
// Maximized if the window is the same dimensions and placement as the
|
||||
// display
|
||||
return GetBounds() == display.work_area();
|
||||
}
|
||||
if (transparent()) {
|
||||
// Compare the size of the window with the size of the display
|
||||
auto display = display::Screen::GetScreen()->GetDisplayNearestWindow(
|
||||
GetNativeWindow());
|
||||
// Maximized if the window is the same dimensions and placement as the
|
||||
// display
|
||||
return GetBounds() == display.work_area();
|
||||
}
|
||||
#endif
|
||||
|
||||
return widget()->IsMaximized();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void NativeWindowViews::Minimize() {
|
||||
|
||||
@@ -149,8 +149,8 @@ std::set<NativeWindowViews*> NativeWindowViews::forwarding_windows_;
|
||||
HHOOK NativeWindowViews::mouse_hook_ = NULL;
|
||||
|
||||
void NativeWindowViews::Maximize() {
|
||||
// Only use Maximize() when window has WS_THICKFRAME style
|
||||
if (::GetWindowLong(GetAcceleratedWidget(), GWL_STYLE) & WS_THICKFRAME) {
|
||||
// Only use Maximize() when window is NOT transparent style
|
||||
if (!transparent()) {
|
||||
if (IsVisible())
|
||||
widget()->Maximize();
|
||||
else
|
||||
@@ -324,8 +324,31 @@ bool NativeWindowViews::PreHandleMSG(UINT message,
|
||||
GET_Y_LPARAM(l_param), &prevent_default);
|
||||
return prevent_default;
|
||||
}
|
||||
default:
|
||||
case WM_SYSCOMMAND: {
|
||||
// Mask is needed to account for double clicking title bar to maximize
|
||||
WPARAM max_mask = 0xFFF0;
|
||||
if (transparent() && ((w_param & max_mask) == SC_MAXIMIZE)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case WM_INITMENU: {
|
||||
// This is handling the scenario where the menu might get triggered by the
|
||||
// user doing "alt + space" resulting in system maximization and restore
|
||||
// being used on transparent windows when that does not work.
|
||||
if (transparent()) {
|
||||
HMENU menu = GetSystemMenu(GetAcceleratedWidget(), false);
|
||||
EnableMenuItem(menu, SC_MAXIMIZE,
|
||||
MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
|
||||
EnableMenuItem(menu, SC_RESTORE,
|
||||
MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.developer-tools</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.10.0</string>
|
||||
<string>${MACOSX_DEPLOYMENT_TARGET}</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -50,8 +50,8 @@ END
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 14,0,0,20210331
|
||||
PRODUCTVERSION 14,0,0,20210331
|
||||
FILEVERSION 14,0,0,20210406
|
||||
PRODUCTVERSION 14,0,0,20210406
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include <memory>
|
||||
#include <gmodule.h>
|
||||
|
||||
#include "shell/browser/ui/file_dialog.h"
|
||||
#include "shell/browser/ui/gtk_util.h"
|
||||
#include <memory>
|
||||
|
||||
#include "base/callback.h"
|
||||
#include "base/files/file_util.h"
|
||||
#include "base/strings/string_util.h"
|
||||
#include "shell/browser/javascript_environment.h"
|
||||
#include "shell/browser/native_window_views.h"
|
||||
#include "shell/browser/ui/file_dialog.h"
|
||||
#include "shell/browser/ui/gtk_util.h"
|
||||
#include "shell/browser/unresponsive_suppressor.h"
|
||||
#include "shell/common/gin_converters/file_path_converter.h"
|
||||
#include "ui/base/glib/glib_signal.h"
|
||||
@@ -27,6 +28,27 @@
|
||||
|
||||
namespace file_dialog {
|
||||
|
||||
static GModule* gtk_module;
|
||||
static base::Optional<bool> supports_gtk_file_chooser_native;
|
||||
|
||||
using dl_gtk_native_dialog_show_t = void (*)(void*);
|
||||
using dl_gtk_native_dialog_destroy_t = void (*)(void*);
|
||||
using dl_gtk_native_dialog_set_modal_t = void (*)(void*, gboolean);
|
||||
using dl_gtk_native_dialog_run_t = int (*)(void*);
|
||||
using dl_gtk_native_dialog_hide_t = void (*)(void*);
|
||||
using dl_gtk_file_chooser_native_new_t = void* (*)(const char*,
|
||||
GtkWindow*,
|
||||
GtkFileChooserAction,
|
||||
const char*,
|
||||
const char*);
|
||||
|
||||
static dl_gtk_native_dialog_show_t dl_gtk_native_dialog_show;
|
||||
static dl_gtk_native_dialog_destroy_t dl_gtk_native_dialog_destroy;
|
||||
static dl_gtk_native_dialog_set_modal_t dl_gtk_native_dialog_set_modal;
|
||||
static dl_gtk_native_dialog_run_t dl_gtk_native_dialog_run;
|
||||
static dl_gtk_native_dialog_hide_t dl_gtk_native_dialog_hide;
|
||||
static dl_gtk_file_chooser_native_new_t dl_gtk_file_chooser_native_new;
|
||||
|
||||
DialogSettings::DialogSettings() = default;
|
||||
DialogSettings::DialogSettings(const DialogSettings&) = default;
|
||||
DialogSettings::~DialogSettings() = default;
|
||||
@@ -36,19 +58,71 @@ namespace {
|
||||
static const int kPreviewWidth = 256;
|
||||
static const int kPreviewHeight = 512;
|
||||
|
||||
// Makes sure that .jpg also shows .JPG.
|
||||
gboolean FileFilterCaseInsensitive(const GtkFileFilterInfo* file_info,
|
||||
std::string* file_extension) {
|
||||
// Makes .* file extension matches all file types.
|
||||
if (*file_extension == ".*")
|
||||
return true;
|
||||
return base::EndsWith(file_info->filename, *file_extension,
|
||||
base::CompareCase::INSENSITIVE_ASCII);
|
||||
}
|
||||
void InitGtkFileChooserNativeSupport() {
|
||||
// Return early if we have already setup the native functions or we have tried
|
||||
// once before and failed. Avoid running expensive dynamic library operations.
|
||||
if (supports_gtk_file_chooser_native) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Deletes |data| when gtk_file_filter_add_custom() is done with it.
|
||||
void OnFileFilterDataDestroyed(std::string* file_extension) {
|
||||
delete file_extension;
|
||||
// Mark that we have attempted to initialize support at least once
|
||||
supports_gtk_file_chooser_native = false;
|
||||
|
||||
if (!g_module_supported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
gtk_module = g_module_open("libgtk-3.so", G_MODULE_BIND_LAZY);
|
||||
if (!gtk_module) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Will never be unloaded
|
||||
g_module_make_resident(gtk_module);
|
||||
|
||||
bool found = g_module_symbol(
|
||||
gtk_module, "gtk_file_chooser_native_new",
|
||||
reinterpret_cast<void**>(&dl_gtk_file_chooser_native_new));
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
found = g_module_symbol(
|
||||
gtk_module, "gtk_native_dialog_set_modal",
|
||||
reinterpret_cast<void**>(&dl_gtk_native_dialog_set_modal));
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
found =
|
||||
g_module_symbol(gtk_module, "gtk_native_dialog_destroy",
|
||||
reinterpret_cast<void**>(&dl_gtk_native_dialog_destroy));
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
found = g_module_symbol(gtk_module, "gtk_native_dialog_show",
|
||||
reinterpret_cast<void**>(&dl_gtk_native_dialog_show));
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
found = g_module_symbol(gtk_module, "gtk_native_dialog_hide",
|
||||
reinterpret_cast<void**>(&dl_gtk_native_dialog_hide));
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
found = g_module_symbol(gtk_module, "gtk_native_dialog_run",
|
||||
reinterpret_cast<void**>(&dl_gtk_native_dialog_run));
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
supports_gtk_file_chooser_native =
|
||||
dl_gtk_file_chooser_native_new && dl_gtk_native_dialog_set_modal &&
|
||||
dl_gtk_native_dialog_destroy && dl_gtk_native_dialog_run &&
|
||||
dl_gtk_native_dialog_show && dl_gtk_native_dialog_hide;
|
||||
}
|
||||
|
||||
class FileChooserDialog {
|
||||
@@ -66,30 +140,43 @@ class FileChooserDialog {
|
||||
else if (action == GTK_FILE_CHOOSER_ACTION_OPEN)
|
||||
confirm_text = gtk_util::kOpenLabel;
|
||||
|
||||
dialog_ = gtk_file_chooser_dialog_new(
|
||||
settings.title.c_str(), nullptr, action, gtk_util::kCancelLabel,
|
||||
GTK_RESPONSE_CANCEL, confirm_text, GTK_RESPONSE_ACCEPT, NULL);
|
||||
InitGtkFileChooserNativeSupport();
|
||||
|
||||
if (*supports_gtk_file_chooser_native) {
|
||||
dialog_ = GTK_FILE_CHOOSER(
|
||||
dl_gtk_file_chooser_native_new(settings.title.c_str(), NULL, action,
|
||||
confirm_text, gtk_util::kCancelLabel));
|
||||
} else {
|
||||
dialog_ = GTK_FILE_CHOOSER(gtk_file_chooser_dialog_new(
|
||||
settings.title.c_str(), NULL, action, gtk_util::kCancelLabel,
|
||||
GTK_RESPONSE_CANCEL, confirm_text, GTK_RESPONSE_ACCEPT, NULL));
|
||||
}
|
||||
|
||||
if (parent_) {
|
||||
parent_->SetEnabled(false);
|
||||
gtk::SetGtkTransientForAura(dialog_, parent_->GetNativeWindow());
|
||||
gtk_window_set_modal(GTK_WINDOW(dialog_), TRUE);
|
||||
if (*supports_gtk_file_chooser_native) {
|
||||
dl_gtk_native_dialog_set_modal(static_cast<void*>(dialog_), TRUE);
|
||||
} else {
|
||||
gtk::SetGtkTransientForAura(GTK_WIDGET(dialog_),
|
||||
parent_->GetNativeWindow());
|
||||
gtk_window_set_modal(GTK_WINDOW(dialog_), TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
if (action == GTK_FILE_CHOOSER_ACTION_SAVE)
|
||||
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog_),
|
||||
TRUE);
|
||||
gtk_file_chooser_set_do_overwrite_confirmation(dialog_, TRUE);
|
||||
if (action != GTK_FILE_CHOOSER_ACTION_OPEN)
|
||||
gtk_file_chooser_set_create_folders(GTK_FILE_CHOOSER(dialog_), TRUE);
|
||||
gtk_file_chooser_set_create_folders(dialog_, TRUE);
|
||||
|
||||
if (!settings.default_path.empty()) {
|
||||
base::ThreadRestrictions::ScopedAllowIO allow_io;
|
||||
if (base::DirectoryExists(settings.default_path)) {
|
||||
gtk_file_chooser_set_current_folder(
|
||||
GTK_FILE_CHOOSER(dialog_), settings.default_path.value().c_str());
|
||||
dialog_, settings.default_path.value().c_str());
|
||||
} else {
|
||||
if (settings.default_path.IsAbsolute()) {
|
||||
gtk_file_chooser_set_current_folder(
|
||||
GTK_FILE_CHOOSER(dialog_),
|
||||
settings.default_path.DirName().value().c_str());
|
||||
dialog_, settings.default_path.DirName().value().c_str());
|
||||
}
|
||||
|
||||
gtk_file_chooser_set_current_name(
|
||||
@@ -101,14 +188,25 @@ class FileChooserDialog {
|
||||
if (!settings.filters.empty())
|
||||
AddFilters(settings.filters);
|
||||
|
||||
preview_ = gtk_image_new();
|
||||
g_signal_connect(dialog_, "update-preview",
|
||||
G_CALLBACK(OnUpdatePreviewThunk), this);
|
||||
gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog_), preview_);
|
||||
// GtkFileChooserNative does not support preview widgets through the
|
||||
// org.freedesktop.portal.FileChooser portal. In the case of running through
|
||||
// the org.freedesktop.portal.FileChooser portal, anything having to do with
|
||||
// the update-preview signal or the preview widget will just be ignored.
|
||||
if (!*supports_gtk_file_chooser_native) {
|
||||
preview_ = gtk_image_new();
|
||||
g_signal_connect(dialog_, "update-preview",
|
||||
G_CALLBACK(OnUpdatePreviewThunk), this);
|
||||
gtk_file_chooser_set_preview_widget(dialog_, preview_);
|
||||
}
|
||||
}
|
||||
|
||||
~FileChooserDialog() {
|
||||
gtk_widget_destroy(dialog_);
|
||||
if (*supports_gtk_file_chooser_native) {
|
||||
dl_gtk_native_dialog_destroy(static_cast<void*>(dialog_));
|
||||
} else {
|
||||
gtk_widget_destroy(GTK_WIDGET(dialog_));
|
||||
}
|
||||
|
||||
if (parent_)
|
||||
parent_->SetEnabled(true);
|
||||
}
|
||||
@@ -117,7 +215,7 @@ class FileChooserDialog {
|
||||
const auto hasProp = [properties](OpenFileDialogProperty prop) {
|
||||
return gboolean((properties & prop) != 0);
|
||||
};
|
||||
auto* file_chooser = GTK_FILE_CHOOSER(dialog());
|
||||
auto* file_chooser = dialog();
|
||||
gtk_file_chooser_set_select_multiple(file_chooser,
|
||||
hasProp(OPEN_DIALOG_MULTI_SELECTIONS));
|
||||
gtk_file_chooser_set_show_hidden(file_chooser,
|
||||
@@ -128,7 +226,7 @@ class FileChooserDialog {
|
||||
const auto hasProp = [properties](SaveFileDialogProperty prop) {
|
||||
return gboolean((properties & prop) != 0);
|
||||
};
|
||||
auto* file_chooser = GTK_FILE_CHOOSER(dialog());
|
||||
auto* file_chooser = dialog();
|
||||
gtk_file_chooser_set_show_hidden(file_chooser,
|
||||
hasProp(SAVE_DIALOG_SHOW_HIDDEN_FILES));
|
||||
gtk_file_chooser_set_do_overwrite_confirmation(
|
||||
@@ -136,21 +234,23 @@ class FileChooserDialog {
|
||||
}
|
||||
|
||||
void RunAsynchronous() {
|
||||
g_signal_connect(dialog_, "delete-event",
|
||||
G_CALLBACK(gtk_widget_hide_on_delete), NULL);
|
||||
g_signal_connect(dialog_, "response", G_CALLBACK(OnFileDialogResponseThunk),
|
||||
this);
|
||||
gtk_widget_show_all(dialog_);
|
||||
if (*supports_gtk_file_chooser_native) {
|
||||
dl_gtk_native_dialog_show(static_cast<void*>(dialog_));
|
||||
} else {
|
||||
gtk_widget_show_all(GTK_WIDGET(dialog_));
|
||||
|
||||
#if defined(USE_X11)
|
||||
if (!features::IsUsingOzonePlatform()) {
|
||||
// We need to call gtk_window_present after making the widgets visible to
|
||||
// make sure window gets correctly raised and gets focus.
|
||||
x11::Time time = ui::X11EventSource::GetInstance()->GetTimestamp();
|
||||
gtk_window_present_with_time(GTK_WINDOW(dialog_),
|
||||
static_cast<uint32_t>(time));
|
||||
}
|
||||
if (!features::IsUsingOzonePlatform()) {
|
||||
// We need to call gtk_window_present after making the widgets visible
|
||||
// to make sure window gets correctly raised and gets focus.
|
||||
x11::Time time = ui::X11EventSource::GetInstance()->GetTimestamp();
|
||||
gtk_window_present_with_time(GTK_WINDOW(dialog_),
|
||||
static_cast<uint32_t>(time));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void RunSaveAsynchronous(
|
||||
@@ -170,7 +270,7 @@ class FileChooserDialog {
|
||||
}
|
||||
|
||||
base::FilePath GetFileName() const {
|
||||
gchar* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog_));
|
||||
gchar* filename = gtk_file_chooser_get_filename(dialog_);
|
||||
const base::FilePath path(filename);
|
||||
g_free(filename);
|
||||
return path;
|
||||
@@ -194,7 +294,7 @@ class FileChooserDialog {
|
||||
GtkWidget*,
|
||||
int);
|
||||
|
||||
GtkWidget* dialog() const { return dialog_; }
|
||||
GtkFileChooser* dialog() const { return dialog_; }
|
||||
|
||||
private:
|
||||
void AddFilters(const Filters& filters);
|
||||
@@ -202,7 +302,7 @@ class FileChooserDialog {
|
||||
electron::NativeWindowViews* parent_;
|
||||
electron::UnresponsiveSuppressor unresponsive_suppressor_;
|
||||
|
||||
GtkWidget* dialog_;
|
||||
GtkFileChooser* dialog_;
|
||||
GtkWidget* preview_;
|
||||
|
||||
Filters filters_;
|
||||
@@ -210,13 +310,17 @@ class FileChooserDialog {
|
||||
std::unique_ptr<gin_helper::Promise<gin_helper::Dictionary>> open_promise_;
|
||||
|
||||
// Callback for when we update the preview for the selection.
|
||||
CHROMEG_CALLBACK_0(FileChooserDialog, void, OnUpdatePreview, GtkWidget*);
|
||||
CHROMEG_CALLBACK_0(FileChooserDialog, void, OnUpdatePreview, GtkFileChooser*);
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(FileChooserDialog);
|
||||
};
|
||||
|
||||
void FileChooserDialog::OnFileDialogResponse(GtkWidget* widget, int response) {
|
||||
gtk_widget_hide(dialog_);
|
||||
if (*supports_gtk_file_chooser_native) {
|
||||
dl_gtk_native_dialog_hide(static_cast<void*>(dialog_));
|
||||
} else {
|
||||
gtk_widget_hide(GTK_WIDGET(dialog_));
|
||||
}
|
||||
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
if (save_promise_) {
|
||||
@@ -250,35 +354,33 @@ void FileChooserDialog::AddFilters(const Filters& filters) {
|
||||
GtkFileFilter* gtk_filter = gtk_file_filter_new();
|
||||
|
||||
for (const auto& extension : filter.second) {
|
||||
auto file_extension = std::make_unique<std::string>("." + extension);
|
||||
gtk_file_filter_add_custom(
|
||||
gtk_filter, GTK_FILE_FILTER_FILENAME,
|
||||
reinterpret_cast<GtkFileFilterFunc>(FileFilterCaseInsensitive),
|
||||
file_extension.release(),
|
||||
reinterpret_cast<GDestroyNotify>(OnFileFilterDataDestroyed));
|
||||
// guarantee a pure lowercase variant
|
||||
std::string file_extension = base::ToLowerASCII("*." + extension);
|
||||
gtk_file_filter_add_pattern(gtk_filter, file_extension.c_str());
|
||||
// guarantee a pure uppercase variant
|
||||
file_extension = base::ToUpperASCII("*." + extension);
|
||||
gtk_file_filter_add_pattern(gtk_filter, file_extension.c_str());
|
||||
}
|
||||
|
||||
gtk_file_filter_set_name(gtk_filter, filter.first.c_str());
|
||||
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog_), gtk_filter);
|
||||
gtk_file_chooser_add_filter(dialog_, gtk_filter);
|
||||
}
|
||||
}
|
||||
|
||||
void FileChooserDialog::OnUpdatePreview(GtkWidget* chooser) {
|
||||
gchar* filename =
|
||||
gtk_file_chooser_get_preview_filename(GTK_FILE_CHOOSER(chooser));
|
||||
void FileChooserDialog::OnUpdatePreview(GtkFileChooser* chooser) {
|
||||
CHECK(!*supports_gtk_file_chooser_native);
|
||||
gchar* filename = gtk_file_chooser_get_preview_filename(chooser);
|
||||
if (!filename) {
|
||||
gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser),
|
||||
FALSE);
|
||||
gtk_file_chooser_set_preview_widget_active(chooser, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't attempt to open anything which isn't a regular file. If a named pipe,
|
||||
// this may hang. See https://crbug.com/534754.
|
||||
// Don't attempt to open anything which isn't a regular file. If a named
|
||||
// pipe, this may hang. See https://crbug.com/534754.
|
||||
struct stat stat_buf;
|
||||
if (stat(filename, &stat_buf) != 0 || !S_ISREG(stat_buf.st_mode)) {
|
||||
g_free(filename);
|
||||
gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser),
|
||||
FALSE);
|
||||
gtk_file_chooser_set_preview_widget_active(chooser, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -290,12 +392,30 @@ void FileChooserDialog::OnUpdatePreview(GtkWidget* chooser) {
|
||||
gtk_image_set_from_pixbuf(GTK_IMAGE(preview_), pixbuf);
|
||||
g_object_unref(pixbuf);
|
||||
}
|
||||
gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(chooser),
|
||||
pixbuf ? TRUE : FALSE);
|
||||
gtk_file_chooser_set_preview_widget_active(chooser, pixbuf ? TRUE : FALSE);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ShowFileDialog(const FileChooserDialog& dialog) {
|
||||
if (*supports_gtk_file_chooser_native) {
|
||||
dl_gtk_native_dialog_show(static_cast<void*>(dialog.dialog()));
|
||||
} else {
|
||||
gtk_widget_show_all(GTK_WIDGET(dialog.dialog()));
|
||||
}
|
||||
}
|
||||
|
||||
int RunFileDialog(const FileChooserDialog& dialog) {
|
||||
int response = 0;
|
||||
if (*supports_gtk_file_chooser_native) {
|
||||
response = dl_gtk_native_dialog_run(static_cast<void*>(dialog.dialog()));
|
||||
} else {
|
||||
response = gtk_dialog_run(GTK_DIALOG(dialog.dialog()));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
bool ShowOpenDialogSync(const DialogSettings& settings,
|
||||
std::vector<base::FilePath>* paths) {
|
||||
GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN;
|
||||
@@ -304,8 +424,9 @@ bool ShowOpenDialogSync(const DialogSettings& settings,
|
||||
FileChooserDialog open_dialog(action, settings);
|
||||
open_dialog.SetupOpenProperties(settings.properties);
|
||||
|
||||
gtk_widget_show_all(open_dialog.dialog());
|
||||
int response = gtk_dialog_run(GTK_DIALOG(open_dialog.dialog()));
|
||||
ShowFileDialog(open_dialog);
|
||||
|
||||
const int response = RunFileDialog(open_dialog);
|
||||
if (response == GTK_RESPONSE_ACCEPT) {
|
||||
*paths = open_dialog.GetFileNames();
|
||||
return true;
|
||||
@@ -327,8 +448,9 @@ bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) {
|
||||
FileChooserDialog save_dialog(GTK_FILE_CHOOSER_ACTION_SAVE, settings);
|
||||
save_dialog.SetupSaveProperties(settings.properties);
|
||||
|
||||
gtk_widget_show_all(save_dialog.dialog());
|
||||
int response = gtk_dialog_run(GTK_DIALOG(save_dialog.dialog()));
|
||||
ShowFileDialog(save_dialog);
|
||||
|
||||
const int response = RunFileDialog(save_dialog);
|
||||
if (response == GTK_RESPONSE_ACCEPT) {
|
||||
*path = save_dialog.GetFileName();
|
||||
return true;
|
||||
|
||||
@@ -28,6 +28,13 @@ bool AppendTask(const JumpListItem& item, IObjectCollection* collection) {
|
||||
FAILED(link->SetDescription(item.description.c_str())))
|
||||
return false;
|
||||
|
||||
// SetDescription limits the size of the parameter to INFOTIPSIZE (1024),
|
||||
// which suggests rejection when exceeding that limit, but experimentation
|
||||
// has shown that descriptions longer than 260 characters cause a silent
|
||||
// failure, despite SetDescription returning the success code S_OK.
|
||||
if (item.description.size() > 260)
|
||||
return false;
|
||||
|
||||
if (!item.icon_path.empty() &&
|
||||
FAILED(link->SetIconLocation(item.icon_path.value().c_str(),
|
||||
item.icon_index)))
|
||||
|
||||
@@ -112,16 +112,21 @@ describe('webContents.setWindowOpenHandler', () => {
|
||||
const { browserWindowOptions } = testConfig[testName];
|
||||
|
||||
describe(testName, () => {
|
||||
beforeEach((done) => {
|
||||
beforeEach(async () => {
|
||||
browserWindow = new BrowserWindow(browserWindowOptions);
|
||||
browserWindow.loadURL('about:blank');
|
||||
browserWindow.on('ready-to-show', () => { browserWindow.show(); done(); });
|
||||
await browserWindow.loadURL('about:blank');
|
||||
browserWindow.show();
|
||||
});
|
||||
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
it('does not fire window creation events if an override returns action: deny', (done) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
|
||||
it('does not fire window creation events if an override returns action: deny', async () => {
|
||||
const denied = new Promise((resolve) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
setTimeout(resolve);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
});
|
||||
browserWindow.webContents.on('new-window', () => {
|
||||
assert.fail('new-window should not to be called with an overridden window.open');
|
||||
});
|
||||
@@ -132,9 +137,51 @@ describe('webContents.setWindowOpenHandler', () => {
|
||||
|
||||
browserWindow.webContents.executeJavaScript("window.open('about:blank') && true");
|
||||
|
||||
setTimeout(() => {
|
||||
done();
|
||||
}, 500);
|
||||
await denied;
|
||||
});
|
||||
|
||||
it('is called when clicking on a target=_blank link', async () => {
|
||||
const denied = new Promise((resolve) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
setTimeout(resolve);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
});
|
||||
browserWindow.webContents.on('new-window', () => {
|
||||
assert.fail('new-window should not to be called with an overridden window.open');
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not to be called with an overridden window.open');
|
||||
});
|
||||
|
||||
await browserWindow.webContents.loadURL('data:text/html,<a target="_blank" href="http://example.com" style="display: block; width: 100%; height: 100%; position: fixed; left: 0; top: 0;">link</a>');
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1 });
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1 });
|
||||
|
||||
await denied;
|
||||
});
|
||||
|
||||
it('is called when shift-clicking on a link', async () => {
|
||||
const denied = new Promise((resolve) => {
|
||||
browserWindow.webContents.setWindowOpenHandler(() => {
|
||||
setTimeout(resolve);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
});
|
||||
browserWindow.webContents.on('new-window', () => {
|
||||
assert.fail('new-window should not to be called with an overridden window.open');
|
||||
});
|
||||
|
||||
browserWindow.webContents.on('did-create-window', () => {
|
||||
assert.fail('did-create-window should not to be called with an overridden window.open');
|
||||
});
|
||||
|
||||
await browserWindow.webContents.loadURL('data:text/html,<a href="http://example.com" style="display: block; width: 100%; height: 100%; position: fixed; left: 0; top: 0;">link</a>');
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseDown', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
|
||||
browserWindow.webContents.sendInputEvent({ type: 'mouseUp', x: 10, y: 10, button: 'left', clickCount: 1, modifiers: ['shift'] });
|
||||
|
||||
await denied;
|
||||
});
|
||||
|
||||
it('fires handler with correct params', (done) => {
|
||||
|
||||
@@ -1047,9 +1047,9 @@ xmlbuilder@~9.0.1:
|
||||
integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
|
||||
|
||||
y18n@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.1.tgz#1ad2a7eddfa8bce7caa2e1f6b5da96c39d99d571"
|
||||
integrity sha512-/jJ831jEs4vGDbYPQp4yGKDYPSCCEQ45uZWJHE1AoYBzqdZi8+LDWas0z4HrmJXmKdpFsTiowSHXdxyFhpmdMg==
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18"
|
||||
integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==
|
||||
|
||||
yargs-parser@^20.0.0:
|
||||
version "20.0.0"
|
||||
|
||||
28
typings/internal-ambient.d.ts
vendored
28
typings/internal-ambient.d.ts
vendored
@@ -101,6 +101,33 @@ declare namespace NodeJS {
|
||||
removeGuest(embedder: Electron.WebContents, guestInstanceId: number): void;
|
||||
}
|
||||
|
||||
interface InternalWebPreferences {
|
||||
contextIsolation: boolean;
|
||||
disableElectronSiteInstanceOverrides: boolean;
|
||||
guestInstanceId: number;
|
||||
hiddenPage: boolean;
|
||||
nativeWindowOpen: boolean;
|
||||
nodeIntegration: boolean;
|
||||
openerId: number;
|
||||
preload: string
|
||||
preloadScripts: string[];
|
||||
webviewTag: boolean;
|
||||
worldSafeExecuteJavaScript: boolean;
|
||||
}
|
||||
|
||||
interface WebFrameBinding {
|
||||
_findFrameByRoutingId(window: Window, routingId: number): Window;
|
||||
_getFrameForSelector(window: Window, selector: string): Window;
|
||||
_findFrameByName(window: Window, name: string): Window;
|
||||
_getOpener(window: Window): Window;
|
||||
_getParent(window: Window): Window;
|
||||
_getTop(window: Window): Window;
|
||||
_getFirstChild(window: Window): Window;
|
||||
_getNextSibling(window: Window): Window;
|
||||
_getRoutingId(window: Window): number;
|
||||
getWebPreference<K extends keyof InternalWebPreferences>(window: Window, name: K): InternalWebPreferences[K];
|
||||
}
|
||||
|
||||
type DataPipe = {
|
||||
write: (buf: Uint8Array) => Promise<void>;
|
||||
done: () => void;
|
||||
@@ -218,6 +245,7 @@ declare namespace NodeJS {
|
||||
}
|
||||
_linkedBinding(name: 'electron_renderer_crash_reporter'): Electron.CrashReporter;
|
||||
_linkedBinding(name: 'electron_renderer_ipc'): { ipc: IpcRendererBinding };
|
||||
_linkedBinding(name: 'electron_renderer_web_frame'): WebFrameBinding;
|
||||
log: NodeJS.WriteStream['write'];
|
||||
activateUvLoop(): void;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user