mirror of
https://github.com/electron/electron.git
synced 2026-02-26 03:01:17 -05:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e1099a8e4 | ||
|
|
438347d891 | ||
|
|
c005d4ff56 | ||
|
|
729b0f5508 | ||
|
|
ea21d940ec | ||
|
|
9df18f3fcf | ||
|
|
f724a9ca2f | ||
|
|
f3f1171a09 | ||
|
|
b7f68027a7 |
@@ -1 +1 @@
|
||||
19.0.9
|
||||
19.0.10
|
||||
@@ -24,7 +24,11 @@ template("extract_symbols") {
|
||||
assert(defined(invoker.binary), "Need binary to dump")
|
||||
assert(defined(invoker.symbol_dir), "Need directory for symbol output")
|
||||
|
||||
dump_syms_label = "//third_party/breakpad:dump_syms($host_toolchain)"
|
||||
if (host_os == "win" && target_cpu == "x86") {
|
||||
dump_syms_label = "//third_party/breakpad:dump_syms(//build/toolchain/win:win_clang_x64)"
|
||||
} else {
|
||||
dump_syms_label = "//third_party/breakpad:dump_syms($host_toolchain)"
|
||||
}
|
||||
dump_syms_binary = get_label_info(dump_syms_label, "root_out_dir") +
|
||||
"/dump_syms$_host_executable_suffix"
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ Are you getting stuck anywhere? Here are a few links to places to look:
|
||||
|
||||
<!-- Links -->
|
||||
|
||||
[tutorial]: tutorial-1-prerequisites.md
|
||||
[api documentation]: ../api/app.md
|
||||
[chromium]: https://www.chromium.org/
|
||||
[discord]: https://discord.com/invite/APGC3k5yaH
|
||||
|
||||
@@ -23,6 +23,7 @@ filenames = {
|
||||
|
||||
lib_sources_linux = [
|
||||
"shell/browser/browser_linux.cc",
|
||||
"shell/browser/electron_browser_main_parts_linux.cc",
|
||||
"shell/browser/lib/power_observer_linux.cc",
|
||||
"shell/browser/lib/power_observer_linux.h",
|
||||
"shell/browser/linux/unity_service.cc",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "electron",
|
||||
"version": "19.0.9",
|
||||
"version": "19.0.10",
|
||||
"repository": "https://github.com/electron/electron",
|
||||
"description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -121,3 +121,4 @@ custom_protocols_plzserviceworker.patch
|
||||
posix_replace_doubleforkandexec_with_forkandspawn.patch
|
||||
cherry-pick-22c61cfae5d1.patch
|
||||
remove_default_window_title.patch
|
||||
keep_handling_scroll_update_if_you_can.patch
|
||||
|
||||
164
patches/chromium/keep_handling_scroll_update_if_you_can.patch
Normal file
164
patches/chromium/keep_handling_scroll_update_if_you_can.patch
Normal file
@@ -0,0 +1,164 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Mehdi Kazemi <mehdika@chromium.org>
|
||||
Date: Tue, 7 Jun 2022 17:32:20 +0000
|
||||
Subject: Keep handling scroll update if you can!
|
||||
|
||||
If scroll_gesture_handling_node_ doesn't have a layout object, but you can still handle GSU, do that!
|
||||
|
||||
Change-Id: Ib82dad96d319b186a5cc2f2eb07495ca5ae994a3
|
||||
Bug: 1330045
|
||||
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3681882
|
||||
Reviewed-by: Steve Kobes <skobes@chromium.org>
|
||||
Commit-Queue: Mehdi Kazemi <mehdika@chromium.org>
|
||||
Cr-Commit-Position: refs/heads/main@{#1011539}
|
||||
|
||||
diff --git a/third_party/blink/renderer/core/input/scroll_manager.cc b/third_party/blink/renderer/core/input/scroll_manager.cc
|
||||
index 28b6092d264219802f718bfaf58f2da4c6054e43..604cad242a9e5626c7d6bac305cb2e323e1f1fca 100644
|
||||
--- a/third_party/blink/renderer/core/input/scroll_manager.cc
|
||||
+++ b/third_party/blink/renderer/core/input/scroll_manager.cc
|
||||
@@ -589,12 +589,6 @@ WebInputEventResult ScrollManager::HandleGestureScrollUpdate(
|
||||
return WebInputEventResult::kNotHandled;
|
||||
}
|
||||
|
||||
- Node* node = scroll_gesture_handling_node_.Get();
|
||||
- if (!node || !node->GetLayoutObject()) {
|
||||
- TRACE_EVENT_INSTANT0("input", "Lost scroll_gesture_handling_node",
|
||||
- TRACE_EVENT_SCOPE_THREAD);
|
||||
- return WebInputEventResult::kNotHandled;
|
||||
- }
|
||||
if (snap_fling_controller_) {
|
||||
if (snap_fling_controller_->HandleGestureScrollUpdate(
|
||||
GetGestureScrollUpdateInfo(gesture_event))) {
|
||||
@@ -615,7 +609,10 @@ WebInputEventResult ScrollManager::HandleGestureScrollUpdate(
|
||||
if (delta.IsZero())
|
||||
return WebInputEventResult::kNotHandled;
|
||||
|
||||
- LayoutObject* layout_object = node->GetLayoutObject();
|
||||
+ LayoutObject* layout_object =
|
||||
+ scroll_gesture_handling_node_
|
||||
+ ? scroll_gesture_handling_node_->GetLayoutObject()
|
||||
+ : nullptr;
|
||||
|
||||
// Try to send the event to the correct view.
|
||||
WebInputEventResult result =
|
||||
diff --git a/third_party/blink/web_tests/fast/events/touch/touch-latched-scroll-node-removed.html b/third_party/blink/web_tests/fast/events/touch/touch-latched-scroll-node-removed.html
|
||||
index 90265f908ad0ce93d7bd401df2aa6657cf25e6fb..b9b34c24496868355c6eff06f738a95832956481 100644
|
||||
--- a/third_party/blink/web_tests/fast/events/touch/touch-latched-scroll-node-removed.html
|
||||
+++ b/third_party/blink/web_tests/fast/events/touch/touch-latched-scroll-node-removed.html
|
||||
@@ -62,6 +62,7 @@ childDiv.addEventListener('scroll', () => {
|
||||
|
||||
promise_test( async () => {
|
||||
setUpForTest();
|
||||
+ await waitForCompositorCommit();
|
||||
// Start scrolling on the child div and remove the div in the middle of
|
||||
// scrolling, then check that parentDiv have not scrolled.
|
||||
var x = (rect.left + rect.right) / 2;
|
||||
@@ -69,6 +70,7 @@ promise_test( async () => {
|
||||
// Slow scrolling gives enough time to switch from cc to main.
|
||||
var pixels_per_sec = 100;
|
||||
await smoothScroll(400, x, y, GestureSourceType.TOUCH_INPUT, 'down', pixels_per_sec);
|
||||
- await waitFor( () => {return parentDiv.scrollTop === 0});
|
||||
-}, "New node must start wheel scrolling when the latched node is removed.");
|
||||
+ await conditionHolds( () => { return parentDiv.scrollTop === 0; },
|
||||
+ "parentDiv has scrolled, which should not have!" );
|
||||
+}, "New node must NOT start wheel scrolling when the latched node is removed.");
|
||||
</script>
|
||||
diff --git a/third_party/blink/web_tests/fast/events/wheel/wheel-latched-scroll-node-removed.html b/third_party/blink/web_tests/fast/events/wheel/wheel-latched-scroll-node-removed.html
|
||||
index 966a887b2129fa26cd990b367a7df1fc9135a207..493f13d9c519422b00fe0e11874032fdf25130db 100644
|
||||
--- a/third_party/blink/web_tests/fast/events/wheel/wheel-latched-scroll-node-removed.html
|
||||
+++ b/third_party/blink/web_tests/fast/events/wheel/wheel-latched-scroll-node-removed.html
|
||||
@@ -62,6 +62,7 @@ childDiv.addEventListener('scroll', () => {
|
||||
|
||||
promise_test( async () => {
|
||||
setUpForTest();
|
||||
+ await waitForCompositorCommit();
|
||||
// Start scrolling on the child div and remove the div in the middle of
|
||||
// scrolling, then check that parentDiv have not scrolled.
|
||||
var x = (rect.left + rect.right) / 2;
|
||||
@@ -69,6 +70,7 @@ promise_test( async () => {
|
||||
// Slow scrolling gives enough time to switch from cc to main.
|
||||
var pixels_per_sec = 100;
|
||||
await smoothScroll(400, x, y, GestureSourceType.MOUSE_INPUT, 'down', pixels_per_sec);
|
||||
- await waitFor( () => {return parentDiv.scrollTop === 0});
|
||||
-}, "New node must start wheel scrolling when the latched node is removed.");
|
||||
+ await conditionHolds( () => { return parentDiv.scrollTop === 0; },
|
||||
+ "parentDiv has scrolled, which should not have!" );
|
||||
+}, "New node must NOT start wheel scrolling when the latched node is removed.");
|
||||
</script>
|
||||
diff --git a/third_party/blink/web_tests/fast/scrolling/inertial-scrolling-with-pointer-events-none-overlay.html b/third_party/blink/web_tests/fast/scrolling/inertial-scrolling-with-pointer-events-none-overlay.html
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..47291b70316beaac16adaa6ddd0035ebeb9ec84f
|
||||
--- /dev/null
|
||||
+++ b/third_party/blink/web_tests/fast/scrolling/inertial-scrolling-with-pointer-events-none-overlay.html
|
||||
@@ -0,0 +1,71 @@
|
||||
+<!DOCTYPE HTML>
|
||||
+<script src="../../resources/testharness.js"></script>
|
||||
+<script src="../../resources/testharnessreport.js"></script>
|
||||
+<script src="../../resources/testdriver.js"></script>
|
||||
+<script src="../../resources/testdriver-actions.js"></script>
|
||||
+<script src="../../resources/testdriver-vendor.js"></script>
|
||||
+<script src="../../resources/gesture-util.js"></script>
|
||||
+
|
||||
+<style>
|
||||
+#container {
|
||||
+ width: 400px;
|
||||
+ height: 400px;
|
||||
+ overflow: auto;
|
||||
+}
|
||||
+
|
||||
+#inner {
|
||||
+ height: 3000px;
|
||||
+ background-color: #eee;
|
||||
+}
|
||||
+
|
||||
+#overlay {
|
||||
+ position: absolute;
|
||||
+ left: 0;
|
||||
+ top: 0;
|
||||
+ width: 100%;
|
||||
+ height: 100%;
|
||||
+ pointer-events: none;
|
||||
+}
|
||||
+
|
||||
+p {
|
||||
+ margin: 0;
|
||||
+ padding: 1000px 0;
|
||||
+}
|
||||
+</style>
|
||||
+
|
||||
+<body style="margin:0" onload=runTest()>
|
||||
+ <div id="container">
|
||||
+ <div id="inner"></div>
|
||||
+ </div>
|
||||
+ <div id="overlay"></div>
|
||||
+</body>
|
||||
+
|
||||
+<script>
|
||||
+const container = document.getElementById('container');
|
||||
+const inner = document.getElementById('inner');
|
||||
+
|
||||
+const update = () => inner.innerHTML = '<p>Content</p>';
|
||||
+setInterval(update, 200);
|
||||
+
|
||||
+function runTest() {
|
||||
+ promise_test (async (t) => {
|
||||
+ const pixels_to_scroll = 100;
|
||||
+ const start_x = 200;
|
||||
+ const start_y = 200;
|
||||
+ const speed_in_pixels_s = 900;
|
||||
+
|
||||
+ await waitForCompositorCommit();
|
||||
+ await swipe(pixels_to_scroll, start_x, start_y, 'up', speed_in_pixels_s);
|
||||
+ await waitForAnimationEndTimeBased(() => { return container.scrollTop; });
|
||||
+ assert_greater_than(container.scrollTop, pixels_to_scroll,
|
||||
+ "container should scroll at least 100 pixels, which is the length of the swipe.");
|
||||
+
|
||||
+ const scroll_top_previous_value = container.scrollTop;
|
||||
+
|
||||
+ await waitForCompositorCommit();
|
||||
+ await swipe(pixels_to_scroll, start_x, start_y, 'up', speed_in_pixels_s);
|
||||
+ await waitForAnimationEndTimeBased(() => { return container.scrollTop; });
|
||||
+ assert_greater_than(container.scrollTop, scroll_top_previous_value + pixels_to_scroll);
|
||||
+ }, "Make sure inertial scrolling is not broken with pointer-events:none overlay.");
|
||||
+}
|
||||
+</script>
|
||||
@@ -3524,7 +3524,12 @@ void WebContents::EnumerateDirectory(
|
||||
|
||||
bool WebContents::IsFullscreenForTabOrPending(
|
||||
const content::WebContents* source) {
|
||||
return html_fullscreen_;
|
||||
bool transition_fs = owner_window()
|
||||
? owner_window()->fullscreen_transition_state() !=
|
||||
NativeWindow::FullScreenTransitionState::NONE
|
||||
: false;
|
||||
|
||||
return html_fullscreen_ || transition_fs;
|
||||
}
|
||||
|
||||
bool WebContents::TakeFocus(content::WebContents* source, bool reverse) {
|
||||
@@ -3846,9 +3851,8 @@ void WebContents::SetHtmlApiFullscreen(bool enter_fullscreen) {
|
||||
? !web_preferences->ShouldDisableHtmlFullscreenWindowResize()
|
||||
: true;
|
||||
|
||||
if (html_fullscreenable) {
|
||||
if (html_fullscreenable)
|
||||
owner_window_->SetFullScreen(enter_fullscreen);
|
||||
}
|
||||
|
||||
UpdateHtmlApiFullscreen(enter_fullscreen);
|
||||
native_fullscreen_ = false;
|
||||
|
||||
@@ -218,6 +218,7 @@ int ElectronBrowserMainParts::PreEarlyInitialization() {
|
||||
HandleSIGCHLD();
|
||||
#endif
|
||||
#if BUILDFLAG(IS_LINUX)
|
||||
DetectOzonePlatform();
|
||||
ui::OzonePlatform::PreEarlyInitialization();
|
||||
#endif
|
||||
|
||||
|
||||
@@ -122,6 +122,10 @@ class ElectronBrowserMainParts : public content::BrowserMainParts {
|
||||
const scoped_refptr<base::SingleThreadTaskRunner>& task_runner);
|
||||
#endif
|
||||
|
||||
#if BUILDFLAG(IS_LINUX)
|
||||
void DetectOzonePlatform();
|
||||
#endif
|
||||
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
void FreeAppDelegate();
|
||||
void RegisterURLHandler();
|
||||
|
||||
135
shell/browser/electron_browser_main_parts_linux.cc
Normal file
135
shell/browser/electron_browser_main_parts_linux.cc
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2022 GitHub, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/electron_browser_main_parts.h"
|
||||
|
||||
#include "base/command_line.h"
|
||||
#include "base/environment.h"
|
||||
#include "ui/ozone/buildflags.h"
|
||||
#include "ui/ozone/public/ozone_switches.h"
|
||||
|
||||
#if BUILDFLAG(OZONE_PLATFORM_WAYLAND)
|
||||
#include "base/files/file_path.h"
|
||||
#include "base/files/file_util.h"
|
||||
#include "base/nix/xdg_util.h"
|
||||
#include "base/threading/thread_restrictions.h"
|
||||
#endif
|
||||
|
||||
#if BUILDFLAG(OZONE_PLATFORM_WAYLAND)
|
||||
|
||||
constexpr char kPlatformWayland[] = "wayland";
|
||||
|
||||
bool HasWaylandDisplay(base::Environment* env) {
|
||||
std::string wayland_display;
|
||||
const bool has_wayland_display =
|
||||
env->GetVar("WAYLAND_DISPLAY", &wayland_display) &&
|
||||
!wayland_display.empty();
|
||||
if (has_wayland_display)
|
||||
return true;
|
||||
|
||||
std::string xdg_runtime_dir;
|
||||
const bool has_xdg_runtime_dir =
|
||||
env->GetVar("XDG_RUNTIME_DIR", &xdg_runtime_dir) &&
|
||||
!xdg_runtime_dir.empty();
|
||||
if (has_xdg_runtime_dir) {
|
||||
auto wayland_server_pipe =
|
||||
base::FilePath(xdg_runtime_dir).Append("wayland-0");
|
||||
// Normally, this should happen exactly once, at the startup of the main
|
||||
// process.
|
||||
base::ScopedAllowBlocking allow_blocking;
|
||||
return base::PathExists(wayland_server_pipe);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif // BUILDFLAG(OZONE_PLATFORM_WAYLAND)
|
||||
|
||||
#if BUILDFLAG(OZONE_PLATFORM_X11)
|
||||
constexpr char kPlatformX11[] = "x11";
|
||||
#endif
|
||||
|
||||
namespace electron {
|
||||
|
||||
namespace {
|
||||
|
||||
// Evaluates the environment and returns the effective platform name for the
|
||||
// given |ozone_platform_hint|.
|
||||
// For the "auto" value, returns "wayland" if the XDG session type is "wayland",
|
||||
// "x11" otherwise.
|
||||
// For the "wayland" value, checks if the Wayland server is available, and
|
||||
// returns "x11" if it is not.
|
||||
// See https://crbug.com/1246928.
|
||||
std::string MaybeFixPlatformName(const std::string& ozone_platform_hint) {
|
||||
#if BUILDFLAG(OZONE_PLATFORM_WAYLAND)
|
||||
// Wayland is selected if both conditions below are true:
|
||||
// 1. The user selected either 'wayland' or 'auto'.
|
||||
// 2. The XDG session type is 'wayland', OR the user has selected 'wayland'
|
||||
// explicitly and a Wayland server is running.
|
||||
// Otherwise, fall back to X11.
|
||||
if (ozone_platform_hint == kPlatformWayland ||
|
||||
ozone_platform_hint == "auto") {
|
||||
auto env(base::Environment::Create());
|
||||
|
||||
std::string xdg_session_type;
|
||||
const bool has_xdg_session_type =
|
||||
env->GetVar(base::nix::kXdgSessionTypeEnvVar, &xdg_session_type) &&
|
||||
!xdg_session_type.empty();
|
||||
|
||||
if ((has_xdg_session_type && xdg_session_type == "wayland") ||
|
||||
(ozone_platform_hint == kPlatformWayland &&
|
||||
HasWaylandDisplay(env.get()))) {
|
||||
return kPlatformWayland;
|
||||
}
|
||||
}
|
||||
#endif // BUILDFLAG(OZONE_PLATFORM_WAYLAND)
|
||||
|
||||
#if BUILDFLAG(OZONE_PLATFORM_X11)
|
||||
if (ozone_platform_hint == kPlatformX11) {
|
||||
return kPlatformX11;
|
||||
}
|
||||
#if BUILDFLAG(OZONE_PLATFORM_WAYLAND)
|
||||
if (ozone_platform_hint == kPlatformWayland ||
|
||||
ozone_platform_hint == "auto") {
|
||||
// We are here if:
|
||||
// - The binary has both X11 and Wayland backends.
|
||||
// - The user wanted Wayland but that did not work, otherwise it would have
|
||||
// been returned above.
|
||||
if (ozone_platform_hint == kPlatformWayland) {
|
||||
LOG(WARNING) << "No Wayland server is available. Falling back to X11.";
|
||||
} else {
|
||||
LOG(WARNING) << "This is not a Wayland session. Falling back to X11. "
|
||||
"If you need to run Chrome on Wayland using some "
|
||||
"embedded compositor, e. g., Weston, please specify "
|
||||
"Wayland as your preferred Ozone platform, or use "
|
||||
"--ozone-platform=wayland.";
|
||||
}
|
||||
return kPlatformX11;
|
||||
}
|
||||
#endif // BUILDFLAG(OZONE_PLATFORM_WAYLAND)
|
||||
#endif // BUILDFLAG(OZONE_PLATFORM_X11)
|
||||
|
||||
return ozone_platform_hint;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ElectronBrowserMainParts::DetectOzonePlatform() {
|
||||
auto* const command_line = base::CommandLine::ForCurrentProcess();
|
||||
if (!command_line->HasSwitch(switches::kOzonePlatform)) {
|
||||
const auto ozone_platform_hint =
|
||||
command_line->GetSwitchValueASCII(switches::kOzonePlatformHint);
|
||||
if (!ozone_platform_hint.empty()) {
|
||||
command_line->AppendSwitchASCII(
|
||||
switches::kOzonePlatform, MaybeFixPlatformName(ozone_platform_hint));
|
||||
}
|
||||
}
|
||||
|
||||
auto env = base::Environment::Create();
|
||||
std::string desktop_startup_id;
|
||||
if (env->GetVar("DESKTOP_STARTUP_ID", &desktop_startup_id))
|
||||
command_line->AppendSwitchASCII("desktop-startup-id", desktop_startup_id);
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
@@ -718,6 +718,15 @@ std::string NativeWindow::GetAccessibleTitle() {
|
||||
return base::UTF16ToUTF8(accessible_title_);
|
||||
}
|
||||
|
||||
void NativeWindow::HandlePendingFullscreenTransitions() {
|
||||
if (pending_transitions_.empty())
|
||||
return;
|
||||
|
||||
bool next_transition = pending_transitions_.front();
|
||||
pending_transitions_.pop();
|
||||
SetFullScreen(next_transition);
|
||||
}
|
||||
|
||||
// static
|
||||
int32_t NativeWindow::next_id_ = 0;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -317,6 +318,17 @@ class NativeWindow : public base::SupportsUserData,
|
||||
observers_.RemoveObserver(obs);
|
||||
}
|
||||
|
||||
enum class FullScreenTransitionState { ENTERING, EXITING, NONE };
|
||||
|
||||
// Handle fullscreen transitions.
|
||||
void HandlePendingFullscreenTransitions();
|
||||
void set_fullscreen_transition_state(FullScreenTransitionState state) {
|
||||
fullscreen_transition_state_ = state;
|
||||
}
|
||||
FullScreenTransitionState fullscreen_transition_state() const {
|
||||
return fullscreen_transition_state_;
|
||||
}
|
||||
|
||||
views::Widget* widget() const { return widget_.get(); }
|
||||
views::View* content_view() const { return content_view_; }
|
||||
|
||||
@@ -375,6 +387,10 @@ class NativeWindow : public base::SupportsUserData,
|
||||
// The "titleBarStyle" option.
|
||||
TitleBarStyle title_bar_style_ = TitleBarStyle::kNormal;
|
||||
|
||||
std::queue<bool> pending_transitions_;
|
||||
FullScreenTransitionState fullscreen_transition_state_ =
|
||||
FullScreenTransitionState::NONE;
|
||||
|
||||
private:
|
||||
std::unique_ptr<views::Widget> widget_;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -174,11 +173,6 @@ class NativeWindowMac : public NativeWindow,
|
||||
void SetCollectionBehavior(bool on, NSUInteger flag);
|
||||
void SetWindowLevel(int level);
|
||||
|
||||
enum class FullScreenTransitionState { ENTERING, EXITING, NONE };
|
||||
|
||||
// Handle fullscreen transitions.
|
||||
void SetFullScreenTransitionState(FullScreenTransitionState state);
|
||||
void HandlePendingFullscreenTransitions();
|
||||
bool HandleDeferredClose();
|
||||
void SetHasDeferredWindowClose(bool defer_close) {
|
||||
has_deferred_window_close_ = defer_close;
|
||||
@@ -249,13 +243,6 @@ class NativeWindowMac : public NativeWindow,
|
||||
bool zoom_to_page_width_ = false;
|
||||
absl::optional<gfx::Point> traffic_light_position_;
|
||||
|
||||
std::queue<bool> pending_transitions_;
|
||||
FullScreenTransitionState fullscreen_transition_state() const {
|
||||
return fullscreen_transition_state_;
|
||||
}
|
||||
FullScreenTransitionState fullscreen_transition_state_ =
|
||||
FullScreenTransitionState::NONE;
|
||||
|
||||
// Trying to close an NSWindow during a fullscreen transition will cause the
|
||||
// window to lock up. Use this to track if CloseWindow was called during a
|
||||
// fullscreen transition, to defer the -[NSWindow close] call until the
|
||||
|
||||
@@ -483,7 +483,8 @@ void NativeWindowMac::Close() {
|
||||
// [window_ performClose:nil], the window won't close properly
|
||||
// even after the user has ended the sheet.
|
||||
// Ensure it's closed before calling [window_ performClose:nil].
|
||||
SetEnabled(true);
|
||||
if ([window_ attachedSheet])
|
||||
[window_ endSheet:[window_ attachedSheet]];
|
||||
|
||||
[window_ performClose:nil];
|
||||
|
||||
@@ -553,7 +554,8 @@ void NativeWindowMac::Hide() {
|
||||
// If a sheet is attached to the window when we call [window_ orderOut:nil],
|
||||
// the sheet won't be able to show again on the same window.
|
||||
// Ensure it's closed before calling [window_ orderOut:nil].
|
||||
SetEnabled(true);
|
||||
if ([window_ attachedSheet])
|
||||
[window_ endSheet:[window_ attachedSheet]];
|
||||
|
||||
if (is_modal() && parent()) {
|
||||
[window_ orderOut:nil];
|
||||
@@ -586,20 +588,24 @@ bool NativeWindowMac::IsVisible() {
|
||||
return [window_ isVisible] && !occluded && !IsMinimized();
|
||||
}
|
||||
|
||||
void NativeWindowMac::SetFullScreenTransitionState(
|
||||
FullScreenTransitionState state) {
|
||||
fullscreen_transition_state_ = state;
|
||||
}
|
||||
|
||||
bool NativeWindowMac::IsEnabled() {
|
||||
return [window_ attachedSheet] == nil;
|
||||
}
|
||||
|
||||
void NativeWindowMac::SetEnabled(bool enable) {
|
||||
if (!enable) {
|
||||
[window_ beginSheet:window_
|
||||
NSRect frame = [window_ frame];
|
||||
NSWindow* window =
|
||||
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, frame.size.width,
|
||||
frame.size.height)
|
||||
styleMask:NSWindowStyleMaskTitled
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
[window setAlphaValue:0.5];
|
||||
|
||||
[window_ beginSheet:window
|
||||
completionHandler:^(NSModalResponse returnCode) {
|
||||
NSLog(@"modal enabled");
|
||||
NSLog(@"main window disabled");
|
||||
return;
|
||||
}];
|
||||
} else if ([window_ attachedSheet]) {
|
||||
@@ -674,15 +680,6 @@ bool NativeWindowMac::IsMinimized() {
|
||||
return [window_ isMiniaturized];
|
||||
}
|
||||
|
||||
void NativeWindowMac::HandlePendingFullscreenTransitions() {
|
||||
if (pending_transitions_.empty())
|
||||
return;
|
||||
|
||||
bool next_transition = pending_transitions_.front();
|
||||
pending_transitions_.pop();
|
||||
SetFullScreen(next_transition);
|
||||
}
|
||||
|
||||
bool NativeWindowMac::HandleDeferredClose() {
|
||||
if (has_deferred_window_close_) {
|
||||
SetHasDeferredWindowClose(false);
|
||||
|
||||
@@ -729,7 +729,6 @@ void NativeWindowViews::SetBounds(const gfx::Rect& bounds, bool animate) {
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
if (is_moving_ || is_resizing_) {
|
||||
pending_bounds_change_ = bounds;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -225,6 +225,7 @@ class NativeWindowViews : public NativeWindow,
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
void HandleSizeEvent(WPARAM w_param, LPARAM l_param);
|
||||
void ResetWindowControls();
|
||||
void SetForwardMouseMessages(bool forward);
|
||||
static LRESULT CALLBACK SubclassProc(HWND hwnd,
|
||||
UINT msg,
|
||||
|
||||
@@ -415,6 +415,7 @@ void NativeWindowViews::HandleSizeEvent(WPARAM w_param, LPARAM l_param) {
|
||||
last_window_state_ != ui::SHOW_STATE_MAXIMIZED) {
|
||||
last_window_state_ = ui::SHOW_STATE_MAXIMIZED;
|
||||
NotifyWindowMaximize();
|
||||
ResetWindowControls();
|
||||
} else if (w_param == SIZE_MINIMIZED &&
|
||||
last_window_state_ != ui::SHOW_STATE_MINIMIZED) {
|
||||
last_window_state_ = ui::SHOW_STATE_MINIMIZED;
|
||||
@@ -440,18 +441,23 @@ void NativeWindowViews::HandleSizeEvent(WPARAM w_param, LPARAM l_param) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// If a given window was minimized/maximized and has since been
|
||||
// restored, ensure the WCO buttons are set to normal state.
|
||||
auto* ncv = widget()->non_client_view();
|
||||
if (IsWindowControlsOverlayEnabled() && ncv) {
|
||||
auto* frame_view = static_cast<WinFrameView*>(ncv->frame_view());
|
||||
frame_view->caption_button_container()->ResetWindowControls();
|
||||
}
|
||||
ResetWindowControls();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NativeWindowViews::ResetWindowControls() {
|
||||
// If a given window was minimized and has since been
|
||||
// unminimized (restored/maximized), ensure the WCO buttons
|
||||
// are reset to their default unpressed state.
|
||||
auto* ncv = widget()->non_client_view();
|
||||
if (IsWindowControlsOverlayEnabled() && ncv) {
|
||||
auto* frame_view = static_cast<WinFrameView*>(ncv->frame_view());
|
||||
frame_view->caption_button_container()->ResetWindowControls();
|
||||
}
|
||||
}
|
||||
|
||||
void NativeWindowViews::SetForwardMouseMessages(bool forward) {
|
||||
if (forward && !forwarding_mouse_messages_) {
|
||||
forwarding_mouse_messages_ = true;
|
||||
|
||||
@@ -50,8 +50,8 @@ END
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 19,0,9,0
|
||||
PRODUCTVERSION 19,0,9,0
|
||||
FILEVERSION 19,0,10,0
|
||||
PRODUCTVERSION 19,0,10,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -68,12 +68,12 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "GitHub, Inc."
|
||||
VALUE "FileDescription", "Electron"
|
||||
VALUE "FileVersion", "19.0.9"
|
||||
VALUE "FileVersion", "19.0.10"
|
||||
VALUE "InternalName", "electron.exe"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2015 GitHub, Inc. All rights reserved."
|
||||
VALUE "OriginalFilename", "electron.exe"
|
||||
VALUE "ProductName", "Electron"
|
||||
VALUE "ProductVersion", "19.0.9"
|
||||
VALUE "ProductVersion", "19.0.10"
|
||||
VALUE "SquirrelAwareVersion", "1"
|
||||
END
|
||||
END
|
||||
|
||||
@@ -238,7 +238,7 @@ using FullScreenTransitionState =
|
||||
// Store resizable mask so it can be restored after exiting fullscreen.
|
||||
is_resizable_ = shell_->HasStyleMask(NSWindowStyleMaskResizable);
|
||||
|
||||
shell_->SetFullScreenTransitionState(FullScreenTransitionState::ENTERING);
|
||||
shell_->set_fullscreen_transition_state(FullScreenTransitionState::ENTERING);
|
||||
|
||||
shell_->NotifyWindowWillEnterFullScreen();
|
||||
|
||||
@@ -247,7 +247,7 @@ using FullScreenTransitionState =
|
||||
}
|
||||
|
||||
- (void)windowDidEnterFullScreen:(NSNotification*)notification {
|
||||
shell_->SetFullScreenTransitionState(FullScreenTransitionState::NONE);
|
||||
shell_->set_fullscreen_transition_state(FullScreenTransitionState::NONE);
|
||||
|
||||
shell_->NotifyWindowEnterFullScreen();
|
||||
|
||||
@@ -258,13 +258,13 @@ using FullScreenTransitionState =
|
||||
}
|
||||
|
||||
- (void)windowWillExitFullScreen:(NSNotification*)notification {
|
||||
shell_->SetFullScreenTransitionState(FullScreenTransitionState::EXITING);
|
||||
shell_->set_fullscreen_transition_state(FullScreenTransitionState::EXITING);
|
||||
|
||||
shell_->NotifyWindowWillLeaveFullScreen();
|
||||
}
|
||||
|
||||
- (void)windowDidExitFullScreen:(NSNotification*)notification {
|
||||
shell_->SetFullScreenTransitionState(FullScreenTransitionState::NONE);
|
||||
shell_->set_fullscreen_transition_state(FullScreenTransitionState::NONE);
|
||||
|
||||
shell_->SetResizable(is_resizable_);
|
||||
shell_->NotifyWindowLeaveFullScreen();
|
||||
|
||||
@@ -4323,6 +4323,14 @@ describe('BrowserWindow module', () => {
|
||||
await createTwo();
|
||||
});
|
||||
|
||||
ifit(process.platform !== 'darwin')('can disable and enable a window', () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
w.setEnabled(false);
|
||||
expect(w.isEnabled()).to.be.false('w.isEnabled()');
|
||||
w.setEnabled(true);
|
||||
expect(w.isEnabled()).to.be.true('!w.isEnabled()');
|
||||
});
|
||||
|
||||
ifit(process.platform !== 'darwin')('disables parent window', () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
const c = new BrowserWindow({ show: false, parent: w, modal: true });
|
||||
@@ -4973,19 +4981,80 @@ describe('BrowserWindow module', () => {
|
||||
expect(w.isFullScreen()).to.be.false('is fullscreen');
|
||||
});
|
||||
|
||||
it('handles several HTML fullscreen transitions', async () => {
|
||||
const w = new BrowserWindow();
|
||||
await w.loadFile(path.join(fixtures, 'pages', 'a.html'));
|
||||
|
||||
expect(w.isFullScreen()).to.be.false('is fullscreen');
|
||||
|
||||
const enterFullScreen = emittedOnce(w, 'enter-full-screen');
|
||||
const leaveFullScreen = emittedOnce(w, 'leave-full-screen');
|
||||
|
||||
await w.webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
|
||||
await enterFullScreen;
|
||||
await w.webContents.executeJavaScript('document.exitFullscreen()', true);
|
||||
await leaveFullScreen;
|
||||
|
||||
expect(w.isFullScreen()).to.be.false('is fullscreen');
|
||||
|
||||
await delay();
|
||||
|
||||
await w.webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
|
||||
await enterFullScreen;
|
||||
await w.webContents.executeJavaScript('document.exitFullscreen()', true);
|
||||
await leaveFullScreen;
|
||||
|
||||
expect(w.isFullScreen()).to.be.false('is fullscreen');
|
||||
});
|
||||
|
||||
it('handles several transitions in close proximity', async () => {
|
||||
const w = new BrowserWindow();
|
||||
|
||||
expect(w.isFullScreen()).to.be.false('is fullscreen');
|
||||
|
||||
const enterFS = emittedNTimes(w, 'enter-full-screen', 2);
|
||||
const leaveFS = emittedNTimes(w, 'leave-full-screen', 2);
|
||||
|
||||
w.setFullScreen(true);
|
||||
w.setFullScreen(false);
|
||||
w.setFullScreen(true);
|
||||
w.setFullScreen(false);
|
||||
|
||||
const enterFullScreen = emittedNTimes(w, 'enter-full-screen', 2);
|
||||
await enterFullScreen;
|
||||
await Promise.all([enterFS, leaveFS]);
|
||||
|
||||
expect(w.isFullScreen()).to.be.true('not fullscreen');
|
||||
expect(w.isFullScreen()).to.be.false('not fullscreen');
|
||||
});
|
||||
|
||||
it('handles several chromium-initiated transitions in close proximity', async () => {
|
||||
const w = new BrowserWindow();
|
||||
await w.loadFile(path.join(fixtures, 'pages', 'a.html'));
|
||||
|
||||
expect(w.isFullScreen()).to.be.false('is fullscreen');
|
||||
|
||||
let enterCount = 0;
|
||||
let exitCount = 0;
|
||||
|
||||
const done = new Promise<void>(resolve => {
|
||||
const checkDone = () => {
|
||||
if (enterCount === 2 && exitCount === 2) resolve();
|
||||
};
|
||||
|
||||
w.webContents.on('enter-html-full-screen', () => {
|
||||
enterCount++;
|
||||
checkDone();
|
||||
});
|
||||
|
||||
w.webContents.on('leave-html-full-screen', () => {
|
||||
exitCount++;
|
||||
checkDone();
|
||||
});
|
||||
});
|
||||
|
||||
await w.webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
|
||||
await w.webContents.executeJavaScript('document.exitFullscreen()');
|
||||
await w.webContents.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
|
||||
await w.webContents.executeJavaScript('document.exitFullscreen()');
|
||||
await done;
|
||||
});
|
||||
|
||||
it('does not crash when exiting simpleFullScreen (properties)', async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as url from 'url';
|
||||
import * as ChildProcess from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
import { promisify } from 'util';
|
||||
import { ifit, ifdescribe, delay, defer } from './spec-helpers';
|
||||
import { ifit, ifdescribe, defer, delay } from './spec-helpers';
|
||||
import { AddressInfo } from 'net';
|
||||
import { PipeTransport } from './pipe-transport';
|
||||
|
||||
@@ -1653,7 +1653,7 @@ describe('iframe using HTML fullscreen API while window is OS-fullscreened', ()
|
||||
server.close();
|
||||
});
|
||||
|
||||
it('can fullscreen from out-of-process iframes (OOPIFs)', async () => {
|
||||
ifit(process.platform !== 'darwin')('can fullscreen from out-of-process iframes (non-macOS)', async () => {
|
||||
const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange');
|
||||
const html =
|
||||
'<iframe style="width: 0" frameborder=0 src="http://localhost:8989" allowfullscreen></iframe>';
|
||||
@@ -1677,8 +1677,37 @@ describe('iframe using HTML fullscreen API while window is OS-fullscreened', ()
|
||||
expect(width).to.equal(0);
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('can fullscreen from out-of-process iframes (macOS)', async () => {
|
||||
await emittedOnce(w, 'enter-full-screen');
|
||||
const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange');
|
||||
const html =
|
||||
'<iframe style="width: 0" frameborder=0 src="http://localhost:8989" allowfullscreen></iframe>';
|
||||
w.loadURL(`data:text/html,${html}`);
|
||||
await fullscreenChange;
|
||||
|
||||
const fullscreenWidth = await w.webContents.executeJavaScript(
|
||||
"document.querySelector('iframe').offsetWidth"
|
||||
);
|
||||
expect(fullscreenWidth > 0).to.be.true();
|
||||
|
||||
await w.webContents.executeJavaScript(
|
||||
"document.querySelector('iframe').contentWindow.postMessage('exitFullscreen', '*')"
|
||||
);
|
||||
await emittedOnce(w.webContents, 'leave-html-full-screen');
|
||||
|
||||
const width = await w.webContents.executeJavaScript(
|
||||
"document.querySelector('iframe').offsetWidth"
|
||||
);
|
||||
expect(width).to.equal(0);
|
||||
|
||||
w.setFullScreen(false);
|
||||
await emittedOnce(w, 'leave-full-screen');
|
||||
});
|
||||
|
||||
// TODO(jkleinsc) fix this flaky test on WOA
|
||||
ifit(process.platform !== 'win32' || process.arch !== 'arm64')('can fullscreen from in-process iframes', async () => {
|
||||
if (process.platform === 'darwin') await emittedOnce(w, 'enter-full-screen');
|
||||
|
||||
const fullscreenChange = emittedOnce(ipcMain, 'fullscreenChange');
|
||||
w.loadFile(path.join(fixturesPath, 'pages', 'fullscreen-ipif.html'));
|
||||
await fullscreenChange;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div id="div">
|
||||
WebView
|
||||
</div>
|
||||
<video></video>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
const {ipcRenderer} = require('electron')
|
||||
ipcRenderer.send('webview-ready')
|
||||
|
||||
@@ -426,11 +426,16 @@ describe('<webview> tag', function () {
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
|
||||
const attachPromise = emittedOnce(w.webContents, 'did-attach-webview');
|
||||
const loadPromise = emittedOnce(w.webContents, 'did-finish-load');
|
||||
const readyPromise = emittedOnce(ipcMain, 'webview-ready');
|
||||
|
||||
w.loadFile(path.join(__dirname, 'fixtures', 'webview', 'fullscreen', 'main.html'));
|
||||
|
||||
const [, webview] = await attachPromise;
|
||||
await readyPromise;
|
||||
await Promise.all([readyPromise, loadPromise]);
|
||||
|
||||
return [w, webview];
|
||||
};
|
||||
|
||||
@@ -442,17 +447,38 @@ describe('<webview> tag', function () {
|
||||
closeAllWindows();
|
||||
});
|
||||
|
||||
it('should make parent frame element fullscreen too', async () => {
|
||||
ifit(process.platform !== 'darwin')('should make parent frame element fullscreen too (non-macOS)', async () => {
|
||||
const [w, webview] = await loadWebViewWindow();
|
||||
expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.false();
|
||||
|
||||
const parentFullscreen = emittedOnce(ipcMain, 'fullscreenchange');
|
||||
await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
|
||||
await parentFullscreen;
|
||||
|
||||
expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.true();
|
||||
|
||||
const close = emittedOnce(w, 'closed');
|
||||
w.close();
|
||||
await emittedOnce(w, 'closed');
|
||||
await close;
|
||||
});
|
||||
|
||||
ifit(process.platform === 'darwin')('should make parent frame element fullscreen too (macOS)', async () => {
|
||||
const [w, webview] = await loadWebViewWindow();
|
||||
expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.false();
|
||||
|
||||
const parentFullscreen = emittedOnce(ipcMain, 'fullscreenchange');
|
||||
const enterHTMLFS = emittedOnce(w.webContents, 'enter-html-full-screen');
|
||||
const leaveHTMLFS = emittedOnce(w.webContents, 'leave-html-full-screen');
|
||||
|
||||
await webview.executeJavaScript('document.getElementById("div").requestFullscreen()', true);
|
||||
expect(await w.webContents.executeJavaScript('isIframeFullscreen()')).to.be.true();
|
||||
|
||||
await webview.executeJavaScript('document.exitFullscreen()');
|
||||
await Promise.all([enterHTMLFS, leaveHTMLFS, parentFullscreen]);
|
||||
|
||||
const close = emittedOnce(w, 'closed');
|
||||
w.close();
|
||||
await close;
|
||||
});
|
||||
|
||||
// FIXME(zcbenz): Fullscreen events do not work on Linux.
|
||||
@@ -468,8 +494,9 @@ describe('<webview> tag', function () {
|
||||
await delay(0);
|
||||
expect(w.isFullScreen()).to.be.false();
|
||||
|
||||
const close = emittedOnce(w, 'closed');
|
||||
w.close();
|
||||
await emittedOnce(w, 'closed');
|
||||
await close;
|
||||
});
|
||||
|
||||
// Sending ESC via sendInputEvent only works on Windows.
|
||||
@@ -485,8 +512,9 @@ describe('<webview> tag', function () {
|
||||
await delay(0);
|
||||
expect(w.isFullScreen()).to.be.false();
|
||||
|
||||
const close = emittedOnce(w, 'closed');
|
||||
w.close();
|
||||
await emittedOnce(w, 'closed');
|
||||
await close;
|
||||
});
|
||||
|
||||
it('pressing ESC should emit the leave-html-full-screen event', async () => {
|
||||
@@ -513,11 +541,27 @@ describe('<webview> tag', function () {
|
||||
const leaveFSWindow = emittedOnce(w, 'leave-html-full-screen');
|
||||
const leaveFSWebview = emittedOnce(webContents, 'leave-html-full-screen');
|
||||
webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Escape' });
|
||||
await leaveFSWindow;
|
||||
await leaveFSWebview;
|
||||
await leaveFSWindow;
|
||||
|
||||
const close = emittedOnce(w, 'closed');
|
||||
w.close();
|
||||
await emittedOnce(w, 'closed');
|
||||
await close;
|
||||
});
|
||||
|
||||
it('should support user gesture', async () => {
|
||||
const [w, webview] = await loadWebViewWindow();
|
||||
|
||||
const waitForEnterHtmlFullScreen = emittedOnce(webview, 'enter-html-full-screen');
|
||||
|
||||
const jsScript = "document.querySelector('video').webkitRequestFullscreen()";
|
||||
webview.executeJavaScript(jsScript, true);
|
||||
|
||||
await waitForEnterHtmlFullScreen;
|
||||
|
||||
const close = emittedOnce(w, 'closed');
|
||||
w.close();
|
||||
await close;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
2
spec/fixtures/pages/fullscreen.html
vendored
2
spec/fixtures/pages/fullscreen.html
vendored
@@ -1 +1 @@
|
||||
<video></video>
|
||||
<video></video>
|
||||
@@ -922,20 +922,6 @@ describe('<webview> tag', function () {
|
||||
});
|
||||
|
||||
describe('executeJavaScript', () => {
|
||||
it('should support user gesture', async () => {
|
||||
await loadWebView(webview, {
|
||||
src: `file://${fixtures}/pages/fullscreen.html`
|
||||
});
|
||||
|
||||
// Event handler has to be added before js execution.
|
||||
const waitForEnterHtmlFullScreen = waitForEvent(webview, 'enter-html-full-screen');
|
||||
|
||||
const jsScript = "document.querySelector('video').webkitRequestFullscreen()";
|
||||
webview.executeJavaScript(jsScript, true);
|
||||
|
||||
return waitForEnterHtmlFullScreen;
|
||||
});
|
||||
|
||||
it('can return the result of the executed script', async () => {
|
||||
await loadWebView(webview, {
|
||||
src: 'about:blank'
|
||||
|
||||
Reference in New Issue
Block a user