image module reworked and added multiple image rendering capability
This commit is contained in:
@@ -14,6 +14,64 @@
|
|||||||
|
|
||||||
namespace waybar::modules {
|
namespace waybar::modules {
|
||||||
|
|
||||||
|
namespace image {
|
||||||
|
|
||||||
|
class IStrategy {
|
||||||
|
public:
|
||||||
|
virtual ~IStrategy() = default;
|
||||||
|
virtual void update() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SingleImageStrategy : public IStrategy {
|
||||||
|
public:
|
||||||
|
SingleImageStrategy(const std::string &, const Json::Value &, const std::string &,
|
||||||
|
Gtk::EventBox &, bool);
|
||||||
|
~SingleImageStrategy() override = default;
|
||||||
|
void update() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void parseOutputRaw();
|
||||||
|
|
||||||
|
util::command::res output_;
|
||||||
|
Json::Value config_;
|
||||||
|
Gtk::Image image_;
|
||||||
|
std::string path_;
|
||||||
|
std::string tooltip_;
|
||||||
|
int size_;
|
||||||
|
Gtk::Box box_;
|
||||||
|
bool hasTooltip_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MultipleImageStrategy : public IStrategy {
|
||||||
|
public:
|
||||||
|
MultipleImageStrategy(const std::string &, const Json::Value &, const std::string &,
|
||||||
|
Gtk::EventBox &);
|
||||||
|
~MultipleImageStrategy() override = default;
|
||||||
|
void update() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ImageData {
|
||||||
|
std::string path;
|
||||||
|
std::string marker;
|
||||||
|
std::string tooltip;
|
||||||
|
std::string on_click;
|
||||||
|
std::shared_ptr<Gtk::Image> img;
|
||||||
|
std::shared_ptr<Gtk::Button> btn;
|
||||||
|
};
|
||||||
|
|
||||||
|
void setImagesData(const Json::Value &);
|
||||||
|
void setupAndDraw();
|
||||||
|
void resetBoxAndMemory();
|
||||||
|
void handleClick(const Glib::ustring &data);
|
||||||
|
|
||||||
|
Json::Value config_;
|
||||||
|
int size_;
|
||||||
|
Gtk::Box box_;
|
||||||
|
std::vector<ImageData> images_data_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace image
|
||||||
|
|
||||||
class Image : public AModule {
|
class Image : public AModule {
|
||||||
public:
|
public:
|
||||||
Image(const std::string &, const Json::Value &);
|
Image(const std::string &, const Json::Value &);
|
||||||
@@ -24,16 +82,11 @@ class Image : public AModule {
|
|||||||
private:
|
private:
|
||||||
void delayWorker();
|
void delayWorker();
|
||||||
void handleEvent();
|
void handleEvent();
|
||||||
void parseOutputRaw();
|
static std::unique_ptr<image::IStrategy> getStrategy(const std::string &, const Json::Value &,
|
||||||
|
const std::string &, Gtk::EventBox &, bool);
|
||||||
|
|
||||||
Gtk::Box box_;
|
|
||||||
Gtk::Image image_;
|
|
||||||
std::string path_;
|
|
||||||
std::string tooltip_;
|
|
||||||
int size_;
|
|
||||||
int interval_;
|
int interval_;
|
||||||
util::command::res output_;
|
std::unique_ptr<image::IStrategy> strategy_;
|
||||||
|
|
||||||
util::SleeperThread thread_;
|
util::SleeperThread thread_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+156
-1
@@ -6,10 +6,19 @@ waybar - image module
|
|||||||
|
|
||||||
# DESCRIPTION
|
# DESCRIPTION
|
||||||
|
|
||||||
The *image* module displays an image from a path.
|
The *image* module displays container of images(or image) from a provided paths
|
||||||
|
|
||||||
|
# REMARK
|
||||||
|
|
||||||
|
This module has been rewritten to add multiple image rendering functionality.
|
||||||
|
To avoid users inconvenience of breaking changes, everything related to old(only
|
||||||
|
one *image*) implementation has been left untouched including configuration part. For this
|
||||||
|
reason this wiki page is split into two parts
|
||||||
|
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
|
|
||||||
|
For single *image*
|
||||||
|
|
||||||
*path*: ++
|
*path*: ++
|
||||||
typeof: string ++
|
typeof: string ++
|
||||||
The path to the image.
|
The path to the image.
|
||||||
@@ -88,3 +97,149 @@ $path\\n$tooltip
|
|||||||
|
|
||||||
- *#image*
|
- *#image*
|
||||||
- *#image.empty*
|
- *#image.empty*
|
||||||
|
|
||||||
|
# CONFIGURATION
|
||||||
|
|
||||||
|
For multiple *image*
|
||||||
|
|
||||||
|
*size*: ++
|
||||||
|
typeof: string ++
|
||||||
|
default: 16 ++
|
||||||
|
Minumum size of the rendered image in pixels.
|
||||||
|
|
||||||
|
*interval*: ++
|
||||||
|
typeof: interger ++
|
||||||
|
default: INT_MAX ++
|
||||||
|
The interval in seconds to redraw module ++
|
||||||
|
Default value is max value of int and changing it is only recommended ++
|
||||||
|
if image path or other property is being changed overtime ++
|
||||||
|
If no *interval* is provided, the module will only be rendered once.
|
||||||
|
|
||||||
|
*multiple*: ++
|
||||||
|
typeof: bool ++
|
||||||
|
default: false ++
|
||||||
|
this parameter is used to decide if old *image* implementation should be ++
|
||||||
|
used(therefore render only one image) or to pick a new implementation to ++
|
||||||
|
rended multiple images provided from this config. default value of this ++
|
||||||
|
parameter is false.
|
||||||
|
|
||||||
|
*signal*: ++
|
||||||
|
typeof: interger ++
|
||||||
|
default: 0 ++
|
||||||
|
The signal number is used to redraw the module. ++
|
||||||
|
This is used if *interval* is not provided and module should change ++
|
||||||
|
regularly depended on external events, e.g when clicking on it. ++
|
||||||
|
The provided value is valid between 1 and N, where SIGRTMIN+N <= SIGRTMAX. ++
|
||||||
|
e.g if *signal* = 7, 34 + 7 = 41, which means waybar process should ++
|
||||||
|
receive signal 41 to redraw this module, since SIGRTMIN = 34 (see 'kill -l').
|
||||||
|
|
||||||
|
*entries*: ++
|
||||||
|
typeof: array ++
|
||||||
|
default: empty array ++
|
||||||
|
Json array of objects consisting of several fields ++
|
||||||
|
this is a main part of configuration where per image properties are defined
|
||||||
|
|
||||||
|
*path*: ++
|
||||||
|
typeof: string ++
|
||||||
|
default: empty string ++
|
||||||
|
file path of picture to draw
|
||||||
|
|
||||||
|
*marker*: ++
|
||||||
|
typeof: string ++
|
||||||
|
default: empty string ++
|
||||||
|
user defined per image keyword(marker) to identify them, e.g in styles.css ++
|
||||||
|
e.g if you want to draw 3 pictures and one of them dimmed, you can give ++
|
||||||
|
*marker*: "dimmed" value and in styles.css write something like this ++
|
||||||
|
#image .dimmed { opacity: 0.3 }
|
||||||
|
|
||||||
|
*tooltip*: ++
|
||||||
|
typeof: string ++
|
||||||
|
default: empty string ++
|
||||||
|
description of the image displayed on hover
|
||||||
|
|
||||||
|
*on-click*: ++
|
||||||
|
typeof: string ++
|
||||||
|
default: empty string ++
|
||||||
|
action to perform when clicking on the image. ++
|
||||||
|
The action can be any system or custom binary/script ++
|
||||||
|
e.g if { "on-click": "notify-send \"hello world\"" } ++
|
||||||
|
"hello world" will appear as notification.
|
||||||
|
|
||||||
|
*exec*: ++
|
||||||
|
typeof: string ++
|
||||||
|
default: empty string ++
|
||||||
|
same as *entries* but provided from external script which should dynamically ++
|
||||||
|
return json array of objects consisting of same keys as *entries*. ++
|
||||||
|
This option is specifically useful in combination with *signal* option without *interval* provided.
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
With entries
|
||||||
|
```
|
||||||
|
"image#wentr": {
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"path": "/home/user/Pictures/idk1.png",
|
||||||
|
"marker": "dimmed",
|
||||||
|
"tooltip": "dimmed image",
|
||||||
|
"on-click": "notify-send \"hello world\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/home/user/Pictures/idk2.png",
|
||||||
|
"marker": "normal",
|
||||||
|
"on-click": "myCustomScript.py"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"size": 32,
|
||||||
|
"interval": 1 // will redraw every 5 seconds
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With exec
|
||||||
|
```
|
||||||
|
"image#wexec": {
|
||||||
|
"exec": "imageProvider.py",
|
||||||
|
"size": 32,
|
||||||
|
"signal": 7 // e.g 'pkill -n waybar --signal 41' will trigger redraw
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
where imageProvider.py looks like this
|
||||||
|
```
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
dir = "/home/user/Pictures/"
|
||||||
|
out = [
|
||||||
|
{"path": dir + "idk1.png", "marker": "clickable", "on-click": "echo 'hello world'"},
|
||||||
|
{"path": dir + "idk2.png", "marker": "normal", "tooltip": "sample tooltip"},
|
||||||
|
{"path": dir + "idk3.png", "marker": "important"},
|
||||||
|
]
|
||||||
|
res = json.dumps(out)
|
||||||
|
print(res)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example styles
|
||||||
|
```
|
||||||
|
#image .important {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image button.important {
|
||||||
|
opacity: 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
#image .normal {
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# STYLE
|
||||||
|
|
||||||
|
- *#image*
|
||||||
|
- *#image*.empty /\*when image could not be rendered\*/
|
||||||
|
- *#image*.button /\*when on-click is provided\*/
|
||||||
|
- *#image*.*marker* /\*where *marker* is parameter from config\*/
|
||||||
|
- *#image*.button.*marker*
|
||||||
|
|
||||||
|
|||||||
+196
-20
@@ -1,25 +1,13 @@
|
|||||||
#include "modules/image.hpp"
|
#include "modules/image.hpp"
|
||||||
|
|
||||||
|
#include <json/value.h>
|
||||||
|
|
||||||
waybar::modules::Image::Image(const std::string& id, const Json::Value& config)
|
waybar::modules::Image::Image(const std::string& id, const Json::Value& config)
|
||||||
: AModule(config, "image", id), box_(Gtk::ORIENTATION_HORIZONTAL, 0) {
|
: AModule(config, "image", id) {
|
||||||
box_.pack_start(image_);
|
strategy_ = getStrategy(id, config, MODULE_CLASS, event_box_, tooltipEnabled());
|
||||||
box_.set_name("image");
|
|
||||||
if (!id.empty()) {
|
|
||||||
box_.get_style_context()->add_class(id);
|
|
||||||
}
|
|
||||||
box_.get_style_context()->add_class(MODULE_CLASS);
|
|
||||||
event_box_.add(box_);
|
|
||||||
|
|
||||||
dp.emit();
|
|
||||||
|
|
||||||
size_ = config["size"].asInt();
|
|
||||||
|
|
||||||
interval_ = config_["interval"].asInt();
|
interval_ = config_["interval"].asInt();
|
||||||
|
|
||||||
if (size_ == 0) {
|
|
||||||
size_ = 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interval_ == 0) {
|
if (interval_ == 0) {
|
||||||
interval_ = INT_MAX;
|
interval_ = INT_MAX;
|
||||||
}
|
}
|
||||||
@@ -27,6 +15,20 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config)
|
|||||||
delayWorker();
|
delayWorker();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto waybar::modules::Image::getStrategy(
|
||||||
|
const std::string& id, const Json::Value& cfg, const std::string& module, Gtk::EventBox& evbox,
|
||||||
|
bool hasTooltip) -> std::unique_ptr<waybar::modules::image::IStrategy> {
|
||||||
|
std::unique_ptr<waybar::modules::image::IStrategy> strat;
|
||||||
|
if (!cfg["multiple"].empty() && cfg["multiple"].asBool()) {
|
||||||
|
strat = std::make_unique<waybar::modules::image::MultipleImageStrategy>(id, cfg, module, evbox);
|
||||||
|
} else {
|
||||||
|
strat = std::make_unique<waybar::modules::image::SingleImageStrategy>(id, cfg, module, evbox,
|
||||||
|
hasTooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return strat;
|
||||||
|
}
|
||||||
|
|
||||||
void waybar::modules::Image::delayWorker() {
|
void waybar::modules::Image::delayWorker() {
|
||||||
thread_ = [this] {
|
thread_ = [this] {
|
||||||
dp.emit();
|
dp.emit();
|
||||||
@@ -42,6 +44,180 @@ void waybar::modules::Image::refresh(int sig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto waybar::modules::Image::update() -> void {
|
auto waybar::modules::Image::update() -> void {
|
||||||
|
strategy_->update();
|
||||||
|
|
||||||
|
AModule::update();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace waybar::modules::image {
|
||||||
|
|
||||||
|
MultipleImageStrategy::MultipleImageStrategy(const std::string& id, const Json::Value& config,
|
||||||
|
const std::string& module, Gtk::EventBox& evbox)
|
||||||
|
: IStrategy(), box_(Gtk::ORIENTATION_HORIZONTAL, 0) {
|
||||||
|
config_ = config;
|
||||||
|
|
||||||
|
box_.set_name("image");
|
||||||
|
box_.get_style_context()->add_class(id);
|
||||||
|
box_.get_style_context()->add_class(module);
|
||||||
|
evbox.add(box_);
|
||||||
|
|
||||||
|
size_ = config["size"].asInt();
|
||||||
|
if (size_ == 0) {
|
||||||
|
size_ = 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultipleImageStrategy::update() {
|
||||||
|
// spdlog::info("update function run");
|
||||||
|
|
||||||
|
// clear box_, previous css classes and memory
|
||||||
|
if (box_.get_children().size() > 0) {
|
||||||
|
resetBoxAndMemory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// set new images from config script
|
||||||
|
if (!config_["entries"].empty()) {
|
||||||
|
setImagesData(config_["entries"]);
|
||||||
|
} else if (!config_["exec"].empty()) {
|
||||||
|
auto exec = util::command::exec(config_["exec"].asString(), "");
|
||||||
|
Json::Value as_json;
|
||||||
|
Json::Reader reader;
|
||||||
|
|
||||||
|
if (!reader.parse(exec.out, as_json)) {
|
||||||
|
spdlog::error("invalid json from exec {}", exec.out);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setImagesData(as_json);
|
||||||
|
} else {
|
||||||
|
spdlog::error("no image files provded in config");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAndDraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultipleImageStrategy::setupAndDraw() {
|
||||||
|
for (unsigned int i = 0; i < images_data_.size(); i++) {
|
||||||
|
images_data_[i].img = std::make_shared<Gtk::Image>();
|
||||||
|
images_data_[i].btn = std::make_shared<Gtk::Button>();
|
||||||
|
|
||||||
|
auto img = images_data_[i].img;
|
||||||
|
auto data = images_data_[i];
|
||||||
|
|
||||||
|
auto path = data.path;
|
||||||
|
auto marker = data.marker;
|
||||||
|
auto tooltip = data.tooltip;
|
||||||
|
bool has_onclick = !data.on_click.empty();
|
||||||
|
|
||||||
|
Glib::RefPtr<Gdk::Pixbuf> pixbuf;
|
||||||
|
pixbuf = Gdk::Pixbuf::create_from_file(path, size_, size_);
|
||||||
|
|
||||||
|
if (has_onclick) {
|
||||||
|
auto btn = images_data_[i].btn;
|
||||||
|
btn->set_name("button_" + path);
|
||||||
|
btn->get_style_context()->add_class(marker);
|
||||||
|
btn->set_tooltip_text(tooltip);
|
||||||
|
btn->set_image(*img);
|
||||||
|
box_.pack_start(*btn);
|
||||||
|
|
||||||
|
btn->add_events(Gdk::BUTTON_PRESS_MASK);
|
||||||
|
btn->signal_clicked().connect(
|
||||||
|
sigc::bind(sigc::mem_fun(*this, &MultipleImageStrategy::handleClick), data.on_click));
|
||||||
|
|
||||||
|
if (pixbuf) {
|
||||||
|
btn->show_all();
|
||||||
|
img->set(pixbuf);
|
||||||
|
box_.get_style_context()->remove_class("empty");
|
||||||
|
} else {
|
||||||
|
btn->hide();
|
||||||
|
img->clear();
|
||||||
|
img->hide();
|
||||||
|
box_.get_style_context()->add_class("empty");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
img->set_name(path);
|
||||||
|
img->get_style_context()->add_class(marker);
|
||||||
|
img->set_tooltip_text(tooltip);
|
||||||
|
box_.pack_start(*img);
|
||||||
|
// spdlog::info("added image -> {}:{}", marker, path);
|
||||||
|
|
||||||
|
if (pixbuf) {
|
||||||
|
img->set(pixbuf);
|
||||||
|
img->show();
|
||||||
|
box_.get_style_context()->remove_class("empty");
|
||||||
|
} else {
|
||||||
|
img->clear();
|
||||||
|
img->hide();
|
||||||
|
box_.get_style_context()->add_class("empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultipleImageStrategy::setImagesData(const Json::Value& entries) {
|
||||||
|
for (unsigned int i = 0; i < entries.size(); i++) {
|
||||||
|
auto path = entries[i]["path"];
|
||||||
|
auto marker = entries[i]["marker"];
|
||||||
|
auto tooltip = entries[i]["tooltip"];
|
||||||
|
auto onclick = entries[i]["on-click"];
|
||||||
|
|
||||||
|
bool has_tooltip_err = !tooltip.empty() && !tooltip.isString();
|
||||||
|
bool has_onclick_err = !onclick.empty() && !onclick.isString();
|
||||||
|
|
||||||
|
if (!path.isString() || !marker.isString() || has_tooltip_err || has_onclick_err ||
|
||||||
|
!Glib::file_test(path.asString(), Glib::FILE_TEST_EXISTS)) {
|
||||||
|
spdlog::error("invalid input in images config -> {}", entries[i]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ImageData data;
|
||||||
|
data.path = path.asString();
|
||||||
|
data.marker = marker.asString();
|
||||||
|
data.tooltip = !tooltip.empty() ? tooltip.asString() : "";
|
||||||
|
data.on_click = onclick.asString();
|
||||||
|
|
||||||
|
images_data_.push_back(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultipleImageStrategy::resetBoxAndMemory() {
|
||||||
|
auto children = box_.get_children();
|
||||||
|
for (auto child : children) {
|
||||||
|
box_.remove(*child);
|
||||||
|
// spdlog::info("child removed with name -> {}", std::string(child->get_name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
images_data_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultipleImageStrategy::handleClick(const Glib::ustring& data) {
|
||||||
|
auto msg = std::string(data);
|
||||||
|
|
||||||
|
auto exec = util::command::exec(data, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
SingleImageStrategy::SingleImageStrategy(const std::string& id, const Json::Value& config,
|
||||||
|
const std::string& module, Gtk::EventBox& evbox,
|
||||||
|
bool tooltipEnabled)
|
||||||
|
: IStrategy(), box_(Gtk::ORIENTATION_HORIZONTAL, 0) {
|
||||||
|
config_ = config;
|
||||||
|
hasTooltip_ = tooltipEnabled;
|
||||||
|
|
||||||
|
box_.pack_start(image_);
|
||||||
|
box_.set_name("image");
|
||||||
|
if (!id.empty()) {
|
||||||
|
box_.get_style_context()->add_class(id);
|
||||||
|
}
|
||||||
|
box_.get_style_context()->add_class(module);
|
||||||
|
evbox.add(box_);
|
||||||
|
|
||||||
|
size_ = config["size"].asInt();
|
||||||
|
if (size_ == 0) {
|
||||||
|
size_ = 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SingleImageStrategy::update() {
|
||||||
Glib::RefPtr<Gdk::Pixbuf> pixbuf;
|
Glib::RefPtr<Gdk::Pixbuf> pixbuf;
|
||||||
if (config_["path"].isString()) {
|
if (config_["path"].isString()) {
|
||||||
path_ = config_["path"].asString();
|
path_ = config_["path"].asString();
|
||||||
@@ -57,7 +233,7 @@ auto waybar::modules::Image::update() -> void {
|
|||||||
pixbuf = {};
|
pixbuf = {};
|
||||||
|
|
||||||
if (pixbuf) {
|
if (pixbuf) {
|
||||||
if (tooltipEnabled() && !tooltip_.empty()) {
|
if (hasTooltip_ && !tooltip_.empty()) {
|
||||||
if (box_.get_tooltip_markup() != tooltip_) {
|
if (box_.get_tooltip_markup() != tooltip_) {
|
||||||
box_.set_tooltip_markup(tooltip_);
|
box_.set_tooltip_markup(tooltip_);
|
||||||
}
|
}
|
||||||
@@ -70,11 +246,9 @@ auto waybar::modules::Image::update() -> void {
|
|||||||
image_.hide();
|
image_.hide();
|
||||||
box_.get_style_context()->add_class("empty");
|
box_.get_style_context()->add_class("empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
AModule::update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void waybar::modules::Image::parseOutputRaw() {
|
void SingleImageStrategy::parseOutputRaw() {
|
||||||
std::istringstream output(output_.out);
|
std::istringstream output(output_.out);
|
||||||
std::string line;
|
std::string line;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
@@ -89,3 +263,5 @@ void waybar::modules::Image::parseOutputRaw() {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace waybar::modules::image
|
||||||
|
|||||||
Reference in New Issue
Block a user