mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 20:54:43 +00:00
Merge pull request #1629 from lightpanda-io/script_event_dispatch
Use EventManager.dispatch for Script events
This commit is contained in:
@@ -61,6 +61,7 @@ arena: Allocator,
|
|||||||
// 'load' event (e.g. amazon product page has no listener and ~350 resources)
|
// 'load' event (e.g. amazon product page has no listener and ~350 resources)
|
||||||
has_dom_load_listener: bool,
|
has_dom_load_listener: bool,
|
||||||
listener_pool: std.heap.MemoryPool(Listener),
|
listener_pool: std.heap.MemoryPool(Listener),
|
||||||
|
ignore_list: std.ArrayList(*Listener),
|
||||||
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
||||||
lookup: std.HashMapUnmanaged(
|
lookup: std.HashMapUnmanaged(
|
||||||
EventKey,
|
EventKey,
|
||||||
@@ -76,6 +77,7 @@ pub fn init(arena: Allocator, page: *Page) EventManager {
|
|||||||
.page = page,
|
.page = page,
|
||||||
.lookup = .{},
|
.lookup = .{},
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
|
.ignore_list = .{},
|
||||||
.list_pool = .init(arena),
|
.list_pool = .init(arena),
|
||||||
.listener_pool = .init(arena),
|
.listener_pool = .init(arena),
|
||||||
.dispatch_depth = 0,
|
.dispatch_depth = 0,
|
||||||
@@ -155,6 +157,11 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
|
|||||||
};
|
};
|
||||||
// append the listener to the list of listeners for this target
|
// append the listener to the list of listeners for this target
|
||||||
gop.value_ptr.*.append(&listener.node);
|
gop.value_ptr.*.append(&listener.node);
|
||||||
|
|
||||||
|
// Track load listeners for script execution ignore list
|
||||||
|
if (type_string.eql(comptime .wrap("load"))) {
|
||||||
|
try self.ignore_list.append(self.arena, listener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
||||||
@@ -167,6 +174,10 @@ pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callba
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clearIgnoreList(self: *EventManager) void {
|
||||||
|
self.ignore_list.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
// Dispatching can be recursive from the compiler's point of view, so we need to
|
// Dispatching can be recursive from the compiler's point of view, so we need to
|
||||||
// give it an explicit error set so that other parts of the code can use and
|
// give it an explicit error set so that other parts of the code can use and
|
||||||
// inferred error.
|
// inferred error.
|
||||||
@@ -178,7 +189,21 @@ const DispatchError = error{
|
|||||||
ExecutionError,
|
ExecutionError,
|
||||||
JsException,
|
JsException,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const DispatchOpts = struct {
|
||||||
|
// A "load" event triggered by a script (in ScriptManager) should not trigger
|
||||||
|
// a "load" listener added within that script. Therefore, any "load" listener
|
||||||
|
// that we add go into an ignore list until after the script finishes executing.
|
||||||
|
// The ignore list is only checked when apply_ignore == true, which is only
|
||||||
|
// set by the ScriptManager when raising the script's "load" event.
|
||||||
|
apply_ignore: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {
|
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {
|
||||||
|
return self.dispatchOpts(target, event, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {
|
||||||
defer if (!event._v8_handoff) event.deinit(false, self.page);
|
defer if (!event._v8_handoff) event.deinit(false, self.page);
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -197,7 +222,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
|
|||||||
};
|
};
|
||||||
|
|
||||||
switch (target._type) {
|
switch (target._type) {
|
||||||
.node => |node| try self.dispatchNode(node, event, &was_handled),
|
.node => |node| try self.dispatchNode(node, event, &was_handled, opts),
|
||||||
.xhr,
|
.xhr,
|
||||||
.window,
|
.window,
|
||||||
.abort_signal,
|
.abort_signal,
|
||||||
@@ -214,7 +239,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
|
|||||||
.event_target = @intFromPtr(target),
|
.event_target = @intFromPtr(target),
|
||||||
.type_string = event._type_string,
|
.type_string = event._type_string,
|
||||||
}) orelse return;
|
}) orelse return;
|
||||||
try self.dispatchAll(list, target, event, &was_handled);
|
try self.dispatchAll(list, target, event, &was_handled, opts);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,10 +287,10 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
|||||||
.event_target = @intFromPtr(target),
|
.event_target = @intFromPtr(target),
|
||||||
.type_string = event._type_string,
|
.type_string = event._type_string,
|
||||||
}) orelse return;
|
}) orelse return;
|
||||||
try self.dispatchAll(list, target, event, &was_dispatched);
|
try self.dispatchAll(list, target, event, &was_dispatched, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
|
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool, comptime opts: DispatchOpts) !void {
|
||||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||||
|
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
@@ -346,7 +371,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
.event_target = @intFromPtr(current_target),
|
.event_target = @intFromPtr(current_target),
|
||||||
.type_string = event._type_string,
|
.type_string = event._type_string,
|
||||||
})) |list| {
|
})) |list| {
|
||||||
try self.dispatchPhase(list, current_target, event, was_handled, true);
|
try self.dispatchPhase(list, current_target, event, was_handled, comptime .init(true, opts));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +405,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
.type_string = event._type_string,
|
.type_string = event._type_string,
|
||||||
.event_target = @intFromPtr(target_et),
|
.event_target = @intFromPtr(target_et),
|
||||||
})) |list| {
|
})) |list| {
|
||||||
try self.dispatchPhase(list, target_et, event, was_handled, null);
|
try self.dispatchPhase(list, target_et, event, was_handled, comptime .init(null, opts));
|
||||||
if (event._stop_propagation) {
|
if (event._stop_propagation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -397,13 +422,25 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
|||||||
.type_string = event._type_string,
|
.type_string = event._type_string,
|
||||||
.event_target = @intFromPtr(current_target),
|
.event_target = @intFromPtr(current_target),
|
||||||
})) |list| {
|
})) |list| {
|
||||||
try self.dispatchPhase(list, current_target, event, was_handled, false);
|
try self.dispatchPhase(list, current_target, event, was_handled, comptime .init(false, opts));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime capture_only: ?bool) !void {
|
const DispatchPhaseOpts = struct {
|
||||||
|
capture_only: ?bool = null,
|
||||||
|
apply_ignore: bool = false,
|
||||||
|
|
||||||
|
fn init(capture_only: ?bool, opts: DispatchOpts) DispatchPhaseOpts {
|
||||||
|
return .{
|
||||||
|
.capture_only = capture_only,
|
||||||
|
.apply_ignore = opts.apply_ignore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime opts: DispatchPhaseOpts) !void {
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
|
|
||||||
// Track dispatch depth for deferred removal
|
// Track dispatch depth for deferred removal
|
||||||
@@ -429,7 +466,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
// Iterate through the list, stopping after we've encountered the last_listener
|
// Iterate through the list, stopping after we've encountered the last_listener
|
||||||
var node = list.first;
|
var node = list.first;
|
||||||
var is_done = false;
|
var is_done = false;
|
||||||
while (node) |n| {
|
node_loop: while (node) |n| {
|
||||||
if (is_done) {
|
if (is_done) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -439,7 +476,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
node = n.next;
|
node = n.next;
|
||||||
|
|
||||||
// Skip non-matching listeners
|
// Skip non-matching listeners
|
||||||
if (comptime capture_only) |capture| {
|
if (comptime opts.capture_only) |capture| {
|
||||||
if (listener.capture != capture) {
|
if (listener.capture != capture) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -458,6 +495,14 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (comptime opts.apply_ignore) {
|
||||||
|
for (self.ignore_list.items) |ignored| {
|
||||||
|
if (ignored == listener) {
|
||||||
|
continue :node_loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
|
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
|
||||||
if (listener.once) {
|
if (listener.once) {
|
||||||
self.removeListener(list, listener);
|
self.removeListener(list, listener);
|
||||||
@@ -502,8 +547,8 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Non-Node dispatching (XHR, Window without propagation)
|
// Non-Node dispatching (XHR, Window without propagation)
|
||||||
fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool) !void {
|
fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime opts: DispatchOpts) !void {
|
||||||
return self.dispatchPhase(list, current_target, event, was_handled, null);
|
return self.dispatchPhase(list, current_target, event, was_handled, comptime .init(null, opts));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global {
|
fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global {
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ const std = @import("std");
|
|||||||
const lp = @import("lightpanda");
|
const lp = @import("lightpanda");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const js = @import("js/js.zig");
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
|
const Http = @import("../http/Http.zig");
|
||||||
|
const String = @import("../string.zig").String;
|
||||||
|
|
||||||
|
const js = @import("js/js.zig");
|
||||||
const URL = @import("URL.zig");
|
const URL = @import("URL.zig");
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
const Browser = @import("Browser.zig");
|
const Browser = @import("Browser.zig");
|
||||||
const Http = @import("../http/Http.zig");
|
|
||||||
|
|
||||||
const Element = @import("webapi/Element.zig");
|
const Element = @import("webapi/Element.zig");
|
||||||
|
|
||||||
@@ -830,13 +831,15 @@ pub const Script = struct {
|
|||||||
.kind = self.kind,
|
.kind = self.kind,
|
||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
self.executeCallback("error", local.toLocal(script_element._on_error), page);
|
self.executeCallback(comptime .wrap("error"), page);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.executeCallback("load", local.toLocal(script_element._on_load), page);
|
self.executeCallback(comptime .wrap("load"), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer page._event_manager.clearIgnoreList();
|
||||||
|
|
||||||
var try_catch: js.TryCatch = undefined;
|
var try_catch: js.TryCatch = undefined;
|
||||||
try_catch.init(local);
|
try_catch.init(local);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
@@ -855,7 +858,7 @@ pub const Script = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.browser, "executed script", .{ .src = url, .success = success, .on_load = script_element._on_load != null });
|
log.debug(.browser, "executed script", .{ .src = url, .success = success });
|
||||||
}
|
}
|
||||||
|
|
||||||
defer {
|
defer {
|
||||||
@@ -867,7 +870,7 @@ pub const Script = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
self.executeCallback("load", local.toLocal(script_element._on_load), page);
|
self.executeCallback(comptime .wrap("load"), page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,14 +881,12 @@ pub const Script = struct {
|
|||||||
.cacheable = cacheable,
|
.cacheable = cacheable,
|
||||||
});
|
});
|
||||||
|
|
||||||
self.executeCallback("error", local.toLocal(script_element._on_error), page);
|
self.executeCallback(comptime .wrap("error"), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void {
|
fn executeCallback(self: *const Script, typ: String, page: *Page) void {
|
||||||
const cb = cb_ orelse return;
|
|
||||||
|
|
||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const event = Event.initTrusted(comptime .wrap(typ), .{}, page) catch |err| {
|
const event = Event.initTrusted(typ, .{}, page) catch |err| {
|
||||||
log.warn(.js, "script internal callback", .{
|
log.warn(.js, "script internal callback", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
.type = typ,
|
.type = typ,
|
||||||
@@ -893,14 +894,11 @@ pub const Script = struct {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer if (!event._v8_handoff) event.deinit(false, self.manager.page);
|
page._event_manager.dispatchOpts(self.script_element.?.asNode().asEventTarget(), event, .{ .apply_ignore = true }) catch |err| {
|
||||||
|
|
||||||
var caught: js.TryCatch.Caught = undefined;
|
|
||||||
cb.tryCall(void, .{event}, &caught) catch {
|
|
||||||
log.warn(.js, "script callback", .{
|
log.warn(.js, "script callback", .{
|
||||||
.url = self.url,
|
.url = self.url,
|
||||||
.type = typ,
|
.type = typ,
|
||||||
.caught = caught,
|
.err = err,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/browser/tests/element/html/script/empty.js
Normal file
0
src/browser/tests/element/html/script/empty.js
Normal file
@@ -2,11 +2,28 @@
|
|||||||
<script src="../../../testing.js"></script>
|
<script src="../../../testing.js"></script>
|
||||||
|
|
||||||
<script id="script">
|
<script id="script">
|
||||||
{
|
{
|
||||||
let s = document.createElement('script');
|
let dom_load = false;
|
||||||
testing.expectEqual('', s.src);
|
let attribute_load = false;
|
||||||
|
|
||||||
s.src = 'over.9000.js';
|
let s = document.createElement('script');
|
||||||
testing.expectEqual(testing.BASE_URL + 'element/html/script/over.9000.js', s.src);
|
document.documentElement.addEventListener('load', (e) => {
|
||||||
|
testing.expectEqual(s, e.target);
|
||||||
|
dom_load = true;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
testing.expectEqual('', s.src);
|
||||||
|
s.onload = function(e) {
|
||||||
|
testing.expectEqual(s, e.target);
|
||||||
|
attribute_load = true;
|
||||||
}
|
}
|
||||||
|
s.src = 'empty.js';
|
||||||
|
testing.expectEqual(testing.BASE_URL + 'element/html/script/empty.js', s.src);
|
||||||
|
document.head.appendChild(s);
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual(true, dom_load);
|
||||||
|
testing.expectEqual(true, attribute_load);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ const Script = @This();
|
|||||||
|
|
||||||
_proto: *HtmlElement,
|
_proto: *HtmlElement,
|
||||||
_src: []const u8 = "",
|
_src: []const u8 = "",
|
||||||
_on_load: ?js.Function.Global = null,
|
|
||||||
_on_error: ?js.Function.Global = null,
|
|
||||||
_executed: bool = false,
|
_executed: bool = false,
|
||||||
|
|
||||||
pub fn asElement(self: *Script) *Element {
|
pub fn asElement(self: *Script) *Element {
|
||||||
@@ -108,22 +106,6 @@ pub fn setDefer(self: *Script, value: bool, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getOnLoad(self: *const Script) ?js.Function.Global {
|
|
||||||
return self._on_load;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setOnLoad(self: *Script, cb: ?js.Function.Global) void {
|
|
||||||
self._on_load = cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getOnError(self: *const Script) ?js.Function.Global {
|
|
||||||
return self._on_error;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setOnError(self: *Script, cb: ?js.Function.Global) void {
|
|
||||||
self._on_error = cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getNoModule(self: *const Script) bool {
|
pub fn getNoModule(self: *const Script) bool {
|
||||||
return self.asConstElement().getAttributeSafe(comptime .wrap("nomodule")) != null;
|
return self.asConstElement().getAttributeSafe(comptime .wrap("nomodule")) != null;
|
||||||
}
|
}
|
||||||
@@ -147,8 +129,6 @@ pub const JsApi = struct {
|
|||||||
pub const @"type" = bridge.accessor(Script.getType, Script.setType, .{});
|
pub const @"type" = bridge.accessor(Script.getType, Script.setType, .{});
|
||||||
pub const nonce = bridge.accessor(Script.getNonce, Script.setNonce, .{});
|
pub const nonce = bridge.accessor(Script.getNonce, Script.setNonce, .{});
|
||||||
pub const charset = bridge.accessor(Script.getCharset, Script.setCharset, .{});
|
pub const charset = bridge.accessor(Script.getCharset, Script.setCharset, .{});
|
||||||
pub const onload = bridge.accessor(Script.getOnLoad, Script.setOnLoad, .{});
|
|
||||||
pub const onerror = bridge.accessor(Script.getOnError, Script.setOnError, .{});
|
|
||||||
pub const noModule = bridge.accessor(Script.getNoModule, null, .{});
|
pub const noModule = bridge.accessor(Script.getNoModule, null, .{});
|
||||||
pub const innerText = bridge.accessor(_innerText, Script.setInnerText, .{});
|
pub const innerText = bridge.accessor(_innerText, Script.setInnerText, .{});
|
||||||
fn _innerText(self: *Script, page: *const Page) ![]const u8 {
|
fn _innerText(self: *Script, page: *const Page) ![]const u8 {
|
||||||
@@ -160,26 +140,10 @@ pub const JsApi = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Build = struct {
|
pub const Build = struct {
|
||||||
pub fn complete(node: *Node, page: *Page) !void {
|
pub fn complete(node: *Node, _: *Page) !void {
|
||||||
const self = node.as(Script);
|
const self = node.as(Script);
|
||||||
const element = self.asElement();
|
const element = self.asElement();
|
||||||
self._src = element.getAttributeSafe(comptime .wrap("src")) orelse "";
|
self._src = element.getAttributeSafe(comptime .wrap("src")) orelse "";
|
||||||
|
|
||||||
if (element.getAttributeSafe(comptime .wrap("onload"))) |on_load| {
|
|
||||||
if (page.js.stringToPersistedFunction(on_load)) |func| {
|
|
||||||
self._on_load = func;
|
|
||||||
} else |err| {
|
|
||||||
log.err(.js, "script.onload", .{ .err = err, .str = on_load });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.getAttributeSafe(comptime .wrap("onerror"))) |on_error| {
|
|
||||||
if (page.js.stringToPersistedFunction(on_error)) |func| {
|
|
||||||
self._on_error = func;
|
|
||||||
} else |err| {
|
|
||||||
log.err(.js, "script.onerror", .{ .err = err, .str = on_error });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user