mirror of
https://github.com/hyprwm/Hyprland.git
synced 2025-05-19 08:30:22 -07:00
async: add Promise and use it for AsyncDialogBox
This commit is contained in:
parent
4f868a1f3c
commit
0302bfdc22
@ -2906,6 +2906,8 @@ std::optional<std::string> CConfigManager::handlePermission(const std::string& c
|
|||||||
|
|
||||||
if (data[1] == "screencopy")
|
if (data[1] == "screencopy")
|
||||||
type = PERMISSION_TYPE_SCREENCOPY;
|
type = PERMISSION_TYPE_SCREENCOPY;
|
||||||
|
else if (data[1] == "plugin")
|
||||||
|
type = PERMISSION_TYPE_PLUGIN;
|
||||||
|
|
||||||
if (data[2] == "ask")
|
if (data[2] == "ask")
|
||||||
mode = PERMISSION_RULE_ALLOW_MODE_ASK;
|
mode = PERMISSION_RULE_ALLOW_MODE_ASK;
|
||||||
|
@ -59,8 +59,7 @@ void CAsyncDialogBox::onWrite(int fd, uint32_t mask) {
|
|||||||
if (mask & (WL_EVENT_HANGUP | WL_EVENT_ERROR)) {
|
if (mask & (WL_EVENT_HANGUP | WL_EVENT_ERROR)) {
|
||||||
Debug::log(LOG, "CAsyncDialogBox: dialog {:x} hung up, closed.");
|
Debug::log(LOG, "CAsyncDialogBox: dialog {:x} hung up, closed.");
|
||||||
|
|
||||||
if (m_onResolution)
|
m_promiseResolver->resolve(m_stdout);
|
||||||
m_onResolution(m_stdout);
|
|
||||||
|
|
||||||
wl_event_source_remove(m_readEventSource);
|
wl_event_source_remove(m_readEventSource);
|
||||||
m_selfReference.reset();
|
m_selfReference.reset();
|
||||||
@ -68,9 +67,7 @@ void CAsyncDialogBox::onWrite(int fd, uint32_t mask) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CAsyncDialogBox::open(std::function<void(std::string)> onResolution) {
|
SP<CPromise<std::string>> CAsyncDialogBox::open() {
|
||||||
m_onResolution = onResolution;
|
|
||||||
|
|
||||||
std::string buttonsString = "";
|
std::string buttonsString = "";
|
||||||
for (auto& b : m_buttons) {
|
for (auto& b : m_buttons) {
|
||||||
buttonsString += b + ";";
|
buttonsString += b + ";";
|
||||||
@ -83,7 +80,7 @@ void CAsyncDialogBox::open(std::function<void(std::string)> onResolution) {
|
|||||||
int outPipe[2];
|
int outPipe[2];
|
||||||
if (pipe(outPipe)) {
|
if (pipe(outPipe)) {
|
||||||
Debug::log(ERR, "CAsyncDialogBox::open: failed to pipe()");
|
Debug::log(ERR, "CAsyncDialogBox::open: failed to pipe()");
|
||||||
return;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_pipeReadFd = CFileDescriptor(outPipe[0]);
|
m_pipeReadFd = CFileDescriptor(outPipe[0]);
|
||||||
@ -94,7 +91,7 @@ void CAsyncDialogBox::open(std::function<void(std::string)> onResolution) {
|
|||||||
|
|
||||||
if (!m_readEventSource) {
|
if (!m_readEventSource) {
|
||||||
Debug::log(ERR, "CAsyncDialogBox::open: failed to add read fd to loop");
|
Debug::log(ERR, "CAsyncDialogBox::open: failed to add read fd to loop");
|
||||||
return;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_selfReference = m_selfWeakReference.lock();
|
m_selfReference = m_selfWeakReference.lock();
|
||||||
@ -102,13 +99,17 @@ void CAsyncDialogBox::open(std::function<void(std::string)> onResolution) {
|
|||||||
if (!proc.runAsync()) {
|
if (!proc.runAsync()) {
|
||||||
Debug::log(ERR, "CAsyncDialogBox::open: failed to run async");
|
Debug::log(ERR, "CAsyncDialogBox::open: failed to run async");
|
||||||
wl_event_source_remove(m_readEventSource);
|
wl_event_source_remove(m_readEventSource);
|
||||||
return;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_dialogPid = proc.pid();
|
m_dialogPid = proc.pid();
|
||||||
|
|
||||||
// close the write fd, only the dialog owns it now
|
// close the write fd, only the dialog owns it now
|
||||||
close(outPipe[1]);
|
close(outPipe[1]);
|
||||||
|
|
||||||
|
auto promise = CPromise<std::string>::make([this](SP<CPromiseResolver<std::string>> r) { m_promiseResolver = r; });
|
||||||
|
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CAsyncDialogBox::kill() {
|
void CAsyncDialogBox::kill() {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "../macros.hpp"
|
#include "../macros.hpp"
|
||||||
#include "./memory/Memory.hpp"
|
#include "./memory/Memory.hpp"
|
||||||
|
#include "./defer/Promise.hpp"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@ -15,29 +16,30 @@ class CAsyncDialogBox {
|
|||||||
public:
|
public:
|
||||||
static SP<CAsyncDialogBox> create(const std::string& title, const std::string& description, std::vector<std::string> buttons);
|
static SP<CAsyncDialogBox> create(const std::string& title, const std::string& description, std::vector<std::string> buttons);
|
||||||
|
|
||||||
CAsyncDialogBox(const CAsyncDialogBox&) = delete;
|
CAsyncDialogBox(const CAsyncDialogBox&) = delete;
|
||||||
CAsyncDialogBox(CAsyncDialogBox&&) = delete;
|
CAsyncDialogBox(CAsyncDialogBox&&) = delete;
|
||||||
CAsyncDialogBox& operator=(const CAsyncDialogBox&) = delete;
|
CAsyncDialogBox& operator=(const CAsyncDialogBox&) = delete;
|
||||||
CAsyncDialogBox& operator=(CAsyncDialogBox&&) = delete;
|
CAsyncDialogBox& operator=(CAsyncDialogBox&&) = delete;
|
||||||
|
|
||||||
void open(std::function<void(std::string)> onResolution);
|
SP<CPromise<std::string>> open();
|
||||||
void kill();
|
void kill();
|
||||||
bool isRunning() const;
|
bool isRunning() const;
|
||||||
|
|
||||||
void onWrite(int fd, uint32_t mask);
|
void onWrite(int fd, uint32_t mask);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
CAsyncDialogBox(const std::string& title, const std::string& description, std::vector<std::string> buttons);
|
CAsyncDialogBox(const std::string& title, const std::string& description, std::vector<std::string> buttons);
|
||||||
|
|
||||||
pid_t m_dialogPid = 0;
|
pid_t m_dialogPid = 0;
|
||||||
wl_event_source* m_readEventSource = nullptr;
|
wl_event_source* m_readEventSource = nullptr;
|
||||||
std::function<void(std::string)> m_onResolution;
|
Hyprutils::OS::CFileDescriptor m_pipeReadFd;
|
||||||
Hyprutils::OS::CFileDescriptor m_pipeReadFd;
|
std::string m_stdout = "";
|
||||||
std::string m_stdout = "";
|
|
||||||
|
|
||||||
const std::string m_title;
|
const std::string m_title;
|
||||||
const std::string m_description;
|
const std::string m_description;
|
||||||
const std::vector<std::string> m_buttons;
|
const std::vector<std::string> m_buttons;
|
||||||
|
|
||||||
|
SP<CPromiseResolver<std::string>> m_promiseResolver;
|
||||||
|
|
||||||
// WARNING: cyclic reference. This will be removed once the event source is removed to avoid dangling pointers
|
// WARNING: cyclic reference. This will be removed once the event source is removed to avoid dangling pointers
|
||||||
SP<CAsyncDialogBox> m_selfReference;
|
SP<CAsyncDialogBox> m_selfReference;
|
||||||
|
119
src/helpers/defer/Promise.hpp
Normal file
119
src/helpers/defer/Promise.hpp
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include "../memory/Memory.hpp"
|
||||||
|
|
||||||
|
// TODO: move into hyprutils
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class CPromise;
|
||||||
|
template <typename T>
|
||||||
|
class CPromiseResult;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class CPromiseResolver {
|
||||||
|
public:
|
||||||
|
CPromiseResolver(const CPromiseResolver&) = delete;
|
||||||
|
CPromiseResolver(CPromiseResolver&&) = delete;
|
||||||
|
CPromiseResolver& operator=(const CPromiseResolver&) = delete;
|
||||||
|
CPromiseResolver& operator=(CPromiseResolver&&) = delete;
|
||||||
|
|
||||||
|
void resolve(T value) {
|
||||||
|
if (m_promise->m_result)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_promise->m_result = CPromiseResult<T>::result(value);
|
||||||
|
|
||||||
|
if (!m_promise->m_then)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_promise->m_then(m_promise->m_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reject(const std::string& reason) {
|
||||||
|
if (m_promise->m_result)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_promise->m_result = CPromiseResult<T>::err(reason);
|
||||||
|
|
||||||
|
if (!m_promise->m_then)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_promise->m_then(m_promise->m_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
CPromiseResolver(SP<CPromise<T>> promise) : m_promise(promise) {}
|
||||||
|
|
||||||
|
SP<CPromise<T>> m_promise;
|
||||||
|
|
||||||
|
friend class CPromise<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class CPromiseResult {
|
||||||
|
public:
|
||||||
|
bool hasError() {
|
||||||
|
return m_hasError;
|
||||||
|
}
|
||||||
|
|
||||||
|
T result() {
|
||||||
|
return m_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string error() {
|
||||||
|
return m_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static SP<CPromiseResult<T>> result(T result) {
|
||||||
|
auto p = SP<CPromiseResult<T>>(new CPromiseResult<T>());
|
||||||
|
p->m_result = result;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SP<CPromiseResult<T>> err(std::string reason) {
|
||||||
|
auto p = SP<CPromiseResult<T>>(new CPromiseResult<T>());
|
||||||
|
p->m_error = reason;
|
||||||
|
p->m_hasError = true;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
T m_result = {};
|
||||||
|
std::string m_error = {};
|
||||||
|
bool m_hasError = false;
|
||||||
|
|
||||||
|
friend class CPromiseResolver<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class CPromise {
|
||||||
|
public:
|
||||||
|
CPromise(const CPromise&) = delete;
|
||||||
|
CPromise(CPromise&&) = delete;
|
||||||
|
CPromise& operator=(const CPromise&) = delete;
|
||||||
|
CPromise& operator=(CPromise&&) = delete;
|
||||||
|
|
||||||
|
static SP<CPromise> make(const std::function<void(SP<CPromiseResolver<T>>)>& fn) {
|
||||||
|
auto sp = SP<CPromise<T>>(new CPromise<T>());
|
||||||
|
fn(SP<CPromiseResolver<T>>(new CPromiseResolver<T>(sp)));
|
||||||
|
return sp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void then(std::function<void(SP<CPromiseResult<T>>)>&& fn) {
|
||||||
|
m_then = std::move(fn);
|
||||||
|
if (m_result)
|
||||||
|
m_then(m_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
CPromise() = default;
|
||||||
|
|
||||||
|
const std::function<void(SP<CPromiseResult<T>>)> m_fn;
|
||||||
|
std::function<void(SP<CPromiseResult<T>>)> m_then;
|
||||||
|
SP<CPromiseResult<T>> m_result;
|
||||||
|
|
||||||
|
friend class CPromiseResult<T>;
|
||||||
|
friend class CPromiseResolver<T>;
|
||||||
|
};
|
@ -169,7 +169,14 @@ void CANRManager::SANRData::runDialog(const std::string& title, const std::strin
|
|||||||
appClass.empty() ? "unknown" : appClass),
|
appClass.empty() ? "unknown" : appClass),
|
||||||
std::vector<std::string>{"Terminate", "Wait"});
|
std::vector<std::string>{"Terminate", "Wait"});
|
||||||
|
|
||||||
dialogBox->open([dialogWmPID, this](std::string result) {
|
dialogBox->open()->then([dialogWmPID, this](SP<CPromiseResult<std::string>> r) {
|
||||||
|
if (r->hasError()) {
|
||||||
|
Debug::log(ERR, "CANRManager::SANRData::runDialog: error spawning dialog");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& result = r->result();
|
||||||
|
|
||||||
if (result.starts_with("Terminate"))
|
if (result.starts_with("Terminate"))
|
||||||
::kill(dialogWmPID, SIGKILL);
|
::kill(dialogWmPID, SIGKILL);
|
||||||
else if (result.starts_with("Wait"))
|
else if (result.starts_with("Wait"))
|
||||||
|
@ -48,6 +48,7 @@ static const char* permissionToString(eDynamicPermissionType type) {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case PERMISSION_TYPE_UNKNOWN: return "PERMISSION_TYPE_UNKNOWN";
|
case PERMISSION_TYPE_UNKNOWN: return "PERMISSION_TYPE_UNKNOWN";
|
||||||
case PERMISSION_TYPE_SCREENCOPY: return "PERMISSION_TYPE_SCREENCOPY";
|
case PERMISSION_TYPE_SCREENCOPY: return "PERMISSION_TYPE_SCREENCOPY";
|
||||||
|
case PERMISSION_TYPE_PLUGIN: return "PERMISSION_TYPE_PLUGIN";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "error";
|
return "error";
|
||||||
@ -57,6 +58,7 @@ static const char* permissionToHumanString(eDynamicPermissionType type) {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case PERMISSION_TYPE_UNKNOWN: return "requesting an unknown permission";
|
case PERMISSION_TYPE_UNKNOWN: return "requesting an unknown permission";
|
||||||
case PERMISSION_TYPE_SCREENCOPY: return "trying to capture your screen";
|
case PERMISSION_TYPE_SCREENCOPY: return "trying to capture your screen";
|
||||||
|
case PERMISSION_TYPE_PLUGIN: return "trying to load a plugin";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "error";
|
return "error";
|
||||||
@ -210,10 +212,22 @@ void CDynamicPermissionManager::askForPermission(wl_client* client, const std::s
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rule->m_dialogBox->open([r = WP<CDynamicPermissionRule>(rule), binaryPath](std::string result) {
|
rule->m_promise = rule->m_dialogBox->open();
|
||||||
|
rule->m_promise->then([r = WP<CDynamicPermissionRule>(rule), binaryPath](SP<CPromiseResult<std::string>> pr) {
|
||||||
if (!r)
|
if (!r)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (pr->hasError()) {
|
||||||
|
// not reachable for now
|
||||||
|
Debug::log(TRACE, "CDynamicPermissionRule: error spawning dialog box");
|
||||||
|
if (r->m_promiseResolverForExternal)
|
||||||
|
r->m_promiseResolverForExternal->reject("error spawning dialog box");
|
||||||
|
r->m_promiseResolverForExternal.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& result = pr->result();
|
||||||
|
|
||||||
Debug::log(TRACE, "CDynamicPermissionRule: user returned {}", result);
|
Debug::log(TRACE, "CDynamicPermissionRule: user returned {}", result);
|
||||||
|
|
||||||
if (result.starts_with("Allow once"))
|
if (result.starts_with("Allow once"))
|
||||||
@ -226,9 +240,29 @@ void CDynamicPermissionManager::askForPermission(wl_client* client, const std::s
|
|||||||
r->m_binaryPath = binaryPath;
|
r->m_binaryPath = binaryPath;
|
||||||
} else if (result.starts_with("Allow"))
|
} else if (result.starts_with("Allow"))
|
||||||
r->m_allowMode = PERMISSION_RULE_ALLOW_MODE_ALLOW;
|
r->m_allowMode = PERMISSION_RULE_ALLOW_MODE_ALLOW;
|
||||||
|
|
||||||
|
if (r->m_promiseResolverForExternal)
|
||||||
|
r->m_promiseResolverForExternal->resolve(r->m_allowMode);
|
||||||
|
|
||||||
|
r->m_promise.reset();
|
||||||
|
r->m_promiseResolverForExternal.reset();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SP<CPromise<eDynamicPermissionAllowMode>> CDynamicPermissionManager::promiseFor(wl_client* client, eDynamicPermissionType permission) {
|
||||||
|
auto rule = std::ranges::find_if(m_rules, [client, permission](const auto& e) { return e->m_client == client && e->m_type == permission; });
|
||||||
|
if (rule == m_rules.end())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if (!(*rule)->m_promise)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if ((*rule)->m_promiseResolverForExternal)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
return CPromise<eDynamicPermissionAllowMode>::make([rule](SP<CPromiseResolver<eDynamicPermissionAllowMode>> r) { (*rule)->m_promiseResolverForExternal = r; });
|
||||||
|
}
|
||||||
|
|
||||||
void CDynamicPermissionManager::removeRulesForClient(wl_client* client) {
|
void CDynamicPermissionManager::removeRulesForClient(wl_client* client) {
|
||||||
std::erase_if(m_rules, [client](const auto& e) { return e->m_client == client; });
|
std::erase_if(m_rules, [client](const auto& e) { return e->m_client == client; });
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
#include "../../helpers/AsyncDialogBox.hpp"
|
#include "../../helpers/AsyncDialogBox.hpp"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <wayland-server-core.h>
|
#include <wayland-server-core.h>
|
||||||
#include <optional>
|
#include "../../helpers/defer/Promise.hpp"
|
||||||
|
|
||||||
// NOLINTNEXTLINE
|
// NOLINTNEXTLINE
|
||||||
namespace re2 {
|
namespace re2 {
|
||||||
@ -15,6 +15,7 @@ namespace re2 {
|
|||||||
enum eDynamicPermissionType : uint8_t {
|
enum eDynamicPermissionType : uint8_t {
|
||||||
PERMISSION_TYPE_UNKNOWN = 0,
|
PERMISSION_TYPE_UNKNOWN = 0,
|
||||||
PERMISSION_TYPE_SCREENCOPY,
|
PERMISSION_TYPE_SCREENCOPY,
|
||||||
|
PERMISSION_TYPE_PLUGIN,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum eDynamicPermissionRuleSource : uint8_t {
|
enum eDynamicPermissionRuleSource : uint8_t {
|
||||||
@ -50,16 +51,18 @@ class CDynamicPermissionRule {
|
|||||||
// user rule
|
// user rule
|
||||||
CDynamicPermissionRule(wl_client* const client, eDynamicPermissionType type, eDynamicPermissionAllowMode defaultAllowMode = PERMISSION_RULE_ALLOW_MODE_ASK);
|
CDynamicPermissionRule(wl_client* const client, eDynamicPermissionType type, eDynamicPermissionAllowMode defaultAllowMode = PERMISSION_RULE_ALLOW_MODE_ASK);
|
||||||
|
|
||||||
const eDynamicPermissionType m_type = PERMISSION_TYPE_UNKNOWN;
|
const eDynamicPermissionType m_type = PERMISSION_TYPE_UNKNOWN;
|
||||||
const eDynamicPermissionRuleSource m_source = PERMISSION_RULE_SOURCE_UNKNOWN;
|
const eDynamicPermissionRuleSource m_source = PERMISSION_RULE_SOURCE_UNKNOWN;
|
||||||
wl_client* const m_client = nullptr;
|
wl_client* const m_client = nullptr;
|
||||||
std::string m_binaryPath = "";
|
std::string m_binaryPath = "";
|
||||||
UP<re2::RE2> m_binaryRegex;
|
UP<re2::RE2> m_binaryRegex;
|
||||||
|
|
||||||
eDynamicPermissionAllowMode m_allowMode = PERMISSION_RULE_ALLOW_MODE_ASK;
|
eDynamicPermissionAllowMode m_allowMode = PERMISSION_RULE_ALLOW_MODE_ASK;
|
||||||
SP<CAsyncDialogBox> m_dialogBox; // for pending
|
SP<CAsyncDialogBox> m_dialogBox; // for pending
|
||||||
|
SP<CPromise<std::string>> m_promise; // for pending
|
||||||
|
SP<CPromiseResolver<eDynamicPermissionAllowMode>> m_promiseResolverForExternal; // for external promise
|
||||||
|
|
||||||
SDynamicPermissionRuleDestroyWrapper m_destroyWrapper;
|
SDynamicPermissionRuleDestroyWrapper m_destroyWrapper;
|
||||||
|
|
||||||
friend class CDynamicPermissionManager;
|
friend class CDynamicPermissionManager;
|
||||||
};
|
};
|
||||||
@ -73,7 +76,11 @@ class CDynamicPermissionManager {
|
|||||||
// (will continue returning false if the user does not agree, of course.)
|
// (will continue returning false if the user does not agree, of course.)
|
||||||
eDynamicPermissionAllowMode clientPermissionMode(wl_client* client, eDynamicPermissionType permission);
|
eDynamicPermissionAllowMode clientPermissionMode(wl_client* client, eDynamicPermissionType permission);
|
||||||
|
|
||||||
void removeRulesForClient(wl_client* client);
|
// get a promise for the result. Returns null if there already was one requested for the client.
|
||||||
|
// Returns null if state is not pending
|
||||||
|
SP<CPromise<eDynamicPermissionAllowMode>> promiseFor(wl_client* client, eDynamicPermissionType permission);
|
||||||
|
|
||||||
|
void removeRulesForClient(wl_client* client);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void askForPermission(wl_client* client, const std::string& binaryName, eDynamicPermissionType type);
|
void askForPermission(wl_client* client, const std::string& binaryName, eDynamicPermissionType type);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user