mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-10 19:13:29 +00:00
Fix loading of toolbox text/tooltips of the current language
This commit is contained in:
parent
8d9c3c7c11
commit
473542542e
@ -89,10 +89,21 @@ public:
|
|||||||
Preferences m_preferences;
|
Preferences m_preferences;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class App::LoadLanguage {
|
||||||
|
public:
|
||||||
|
LoadLanguage(Preferences& pref,
|
||||||
|
Extensions& exts) {
|
||||||
|
Strings::createInstance(pref, exts);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class App::Modules {
|
class App::Modules {
|
||||||
public:
|
public:
|
||||||
LoggerModule m_loggerModule;
|
LoggerModule m_loggerModule;
|
||||||
FileSystemModule m_file_system_module;
|
FileSystemModule m_file_system_module;
|
||||||
|
Extensions m_extensions;
|
||||||
|
// Load main language (after loading the extensions)
|
||||||
|
LoadLanguage m_loadLanguage;
|
||||||
tools::ToolBox m_toolbox;
|
tools::ToolBox m_toolbox;
|
||||||
tools::ActiveToolManager m_activeToolManager;
|
tools::ActiveToolManager m_activeToolManager;
|
||||||
Commands m_commands;
|
Commands m_commands;
|
||||||
@ -100,12 +111,13 @@ public:
|
|||||||
RecentFiles m_recent_files;
|
RecentFiles m_recent_files;
|
||||||
InputChain m_inputChain;
|
InputChain m_inputChain;
|
||||||
clipboard::ClipboardManager m_clipboardManager;
|
clipboard::ClipboardManager m_clipboardManager;
|
||||||
Extensions m_extensions;
|
|
||||||
// This is a raw pointer because we want to delete this explicitly.
|
// This is a raw pointer because we want to delete this explicitly.
|
||||||
app::crash::DataRecovery* m_recovery;
|
app::crash::DataRecovery* m_recovery;
|
||||||
|
|
||||||
Modules(bool createLogInDesktop, Preferences& pref)
|
Modules(const bool createLogInDesktop,
|
||||||
|
Preferences& pref)
|
||||||
: m_loggerModule(createLogInDesktop)
|
: m_loggerModule(createLogInDesktop)
|
||||||
|
, m_loadLanguage(pref, m_extensions)
|
||||||
, m_activeToolManager(&m_toolbox)
|
, m_activeToolManager(&m_toolbox)
|
||||||
, m_recent_files(pref.general.recentItems())
|
, m_recent_files(pref.general.recentItems())
|
||||||
, m_recovery(nullptr) {
|
, m_recovery(nullptr) {
|
||||||
@ -175,6 +187,7 @@ void App::initialize(const AppOptions& options)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load modules
|
||||||
m_modules = new Modules(createLogInDesktop, preferences());
|
m_modules = new Modules(createLogInDesktop, preferences());
|
||||||
m_legacy = new LegacyModules(isGui() ? REQUIRE_INTERFACE: 0);
|
m_legacy = new LegacyModules(isGui() ? REQUIRE_INTERFACE: 0);
|
||||||
m_brushes.reset(new AppBrushes);
|
m_brushes.reset(new AppBrushes);
|
||||||
@ -194,9 +207,6 @@ void App::initialize(const AppOptions& options)
|
|||||||
if (isGui()) {
|
if (isGui()) {
|
||||||
LOG("APP: GUI mode\n");
|
LOG("APP: GUI mode\n");
|
||||||
|
|
||||||
// Load main language (which might be in an extension)
|
|
||||||
Strings::instance()->loadCurrentLanguage();
|
|
||||||
|
|
||||||
// Setup the GUI cursor and redraw screen
|
// Setup the GUI cursor and redraw screen
|
||||||
ui::set_use_native_cursors(preferences().cursor.useNativeCursor());
|
ui::set_use_native_cursors(preferences().cursor.useNativeCursor());
|
||||||
ui::set_mouse_cursor_scale(preferences().cursor.cursorScale());
|
ui::set_mouse_cursor_scale(preferences().cursor.cursorScale());
|
||||||
|
@ -104,6 +104,7 @@ namespace app {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
class CoreModules;
|
class CoreModules;
|
||||||
|
class LoadLanguage;
|
||||||
class Modules;
|
class Modules;
|
||||||
|
|
||||||
static App* m_instance;
|
static App* m_instance;
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
#include "app/extensions.h"
|
#include "app/extensions.h"
|
||||||
#include "app/pref/preferences.h"
|
#include "app/pref/preferences.h"
|
||||||
#include "app/resource_finder.h"
|
#include "app/resource_finder.h"
|
||||||
#include "app/ui/main_window.h"
|
|
||||||
#include "app/xml_document.h"
|
#include "app/xml_document.h"
|
||||||
#include "app/xml_exception.h"
|
#include "app/xml_exception.h"
|
||||||
#include "base/fs.h"
|
#include "base/fs.h"
|
||||||
@ -22,20 +21,29 @@
|
|||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
|
static Strings* singleton = nullptr;
|
||||||
static const char* kDefLanguage = "en";
|
static const char* kDefLanguage = "en";
|
||||||
|
|
||||||
|
// static
|
||||||
|
void Strings::createInstance(Preferences& pref,
|
||||||
|
Extensions& exts)
|
||||||
|
{
|
||||||
|
ASSERT(!singleton);
|
||||||
|
singleton = new Strings(pref, exts);
|
||||||
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
Strings* Strings::instance()
|
Strings* Strings::instance()
|
||||||
{
|
{
|
||||||
static Strings* singleton = nullptr;
|
|
||||||
if (!singleton)
|
|
||||||
singleton = new Strings();
|
|
||||||
return singleton;
|
return singleton;
|
||||||
}
|
}
|
||||||
|
|
||||||
Strings::Strings()
|
Strings::Strings(Preferences& pref,
|
||||||
|
Extensions& exts)
|
||||||
|
: m_pref(pref)
|
||||||
|
, m_exts(exts)
|
||||||
{
|
{
|
||||||
loadLanguage(kDefLanguage);
|
loadLanguage(currentLanguage());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::set<std::string> Strings::availableLanguages() const
|
std::set<std::string> Strings::availableLanguages() const
|
||||||
@ -56,7 +64,7 @@ std::set<std::string> Strings::availableLanguages() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add languages in extensions
|
// Add languages in extensions
|
||||||
for (const auto& ext : App::instance()->extensions()) {
|
for (const auto& ext : m_exts) {
|
||||||
if (ext->isEnabled() &&
|
if (ext->isEnabled() &&
|
||||||
ext->hasLanguages()) {
|
ext->hasLanguages()) {
|
||||||
for (const auto& langId : ext->languages())
|
for (const auto& langId : ext->languages())
|
||||||
@ -70,7 +78,7 @@ std::set<std::string> Strings::availableLanguages() const
|
|||||||
|
|
||||||
std::string Strings::currentLanguage() const
|
std::string Strings::currentLanguage() const
|
||||||
{
|
{
|
||||||
return Preferences::instance().general.language();
|
return m_pref.general.language();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Strings::setCurrentLanguage(const std::string& langId)
|
void Strings::setCurrentLanguage(const std::string& langId)
|
||||||
@ -79,19 +87,10 @@ void Strings::setCurrentLanguage(const std::string& langId)
|
|||||||
if (currentLanguage() == langId)
|
if (currentLanguage() == langId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Preferences::instance().general.language(langId);
|
m_pref.general.language(langId);
|
||||||
loadLanguage(langId);
|
loadLanguage(langId);
|
||||||
|
|
||||||
// Reload menus
|
LanguageChange();
|
||||||
App::instance()->mainWindow()->reloadMenus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when extensions are available
|
|
||||||
void Strings::loadCurrentLanguage()
|
|
||||||
{
|
|
||||||
std::string langId = currentLanguage();
|
|
||||||
if (langId != kDefLanguage)
|
|
||||||
loadLanguage(langId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Strings::loadLanguage(const std::string& langId)
|
void Strings::loadLanguage(const std::string& langId)
|
||||||
@ -120,8 +119,7 @@ void Strings::loadStringsFromDataDir(const std::string& langId)
|
|||||||
|
|
||||||
void Strings::loadStringsFromExtension(const std::string& langId)
|
void Strings::loadStringsFromExtension(const std::string& langId)
|
||||||
{
|
{
|
||||||
Extensions& exts = App::instance()->extensions();
|
std::string fn = m_exts.languagePath(langId);
|
||||||
std::string fn = exts.languagePath(langId);
|
|
||||||
if (!fn.empty() && base::is_file(fn))
|
if (!fn.empty() && base::is_file(fn))
|
||||||
loadStringsFromFile(fn);
|
loadStringsFromFile(fn);
|
||||||
}
|
}
|
||||||
|
@ -12,30 +12,41 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "obs/signal.h"
|
||||||
|
|
||||||
#include "strings.ini.h"
|
#include "strings.ini.h"
|
||||||
|
|
||||||
namespace app {
|
namespace app {
|
||||||
|
|
||||||
|
class Preferences;
|
||||||
|
class Extensions;
|
||||||
|
|
||||||
// Singleton class to load and access "strings/en.ini" file.
|
// Singleton class to load and access "strings/en.ini" file.
|
||||||
class Strings : public app::gen::Strings<app::Strings> {
|
class Strings : public app::gen::Strings<app::Strings> {
|
||||||
public:
|
public:
|
||||||
|
static void createInstance(Preferences& pref,
|
||||||
|
Extensions& exts);
|
||||||
static Strings* instance();
|
static Strings* instance();
|
||||||
|
|
||||||
const std::string& translate(const char* id) const;
|
const std::string& translate(const char* id) const;
|
||||||
|
|
||||||
void loadCurrentLanguage();
|
|
||||||
std::set<std::string> availableLanguages() const;
|
std::set<std::string> availableLanguages() const;
|
||||||
std::string currentLanguage() const;
|
std::string currentLanguage() const;
|
||||||
void setCurrentLanguage(const std::string& langId);
|
void setCurrentLanguage(const std::string& langId);
|
||||||
|
|
||||||
|
obs::signal<void()> LanguageChange;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Strings();
|
Strings(Preferences& pref,
|
||||||
|
Extensions& exts);
|
||||||
|
|
||||||
void loadLanguage(const std::string& langId);
|
void loadLanguage(const std::string& langId);
|
||||||
void loadStringsFromDataDir(const std::string& langId);
|
void loadStringsFromDataDir(const std::string& langId);
|
||||||
void loadStringsFromExtension(const std::string& langId);
|
void loadStringsFromExtension(const std::string& langId);
|
||||||
void loadStringsFromFile(const std::string& fn);
|
void loadStringsFromFile(const std::string& fn);
|
||||||
|
|
||||||
|
Preferences& m_pref;
|
||||||
|
Extensions& m_exts;
|
||||||
mutable std::unordered_map<std::string, std::string> m_strings;
|
mutable std::unordered_map<std::string, std::string> m_strings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2001-2015 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
// the End-User License Agreement for Aseprite.
|
// the End-User License Agreement for Aseprite.
|
||||||
@ -26,16 +26,10 @@ namespace app {
|
|||||||
class Tool {
|
class Tool {
|
||||||
public:
|
public:
|
||||||
|
|
||||||
Tool(ToolGroup* group,
|
Tool(ToolGroup* group, const std::string& id)
|
||||||
const std::string& id,
|
|
||||||
const std::string& text,
|
|
||||||
const std::string& tips,
|
|
||||||
int default_brush_size)
|
|
||||||
: m_group(group)
|
: m_group(group)
|
||||||
, m_id(id)
|
, m_id(id)
|
||||||
, m_text(text)
|
, m_default_brush_size(1)
|
||||||
, m_tips(tips)
|
|
||||||
, m_default_brush_size(default_brush_size)
|
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
virtual ~Tool()
|
virtual ~Tool()
|
||||||
@ -47,6 +41,12 @@ namespace app {
|
|||||||
const std::string& getTips() const { return m_tips; }
|
const std::string& getTips() const { return m_tips; }
|
||||||
int getDefaultBrushSize() const { return m_default_brush_size; }
|
int getDefaultBrushSize() const { return m_default_brush_size; }
|
||||||
|
|
||||||
|
void setText(const std::string& text) { m_text = text; }
|
||||||
|
void setTips(const std::string& tips) { m_tips = tips; }
|
||||||
|
void setDefaultBrushSize(const int default_brush_size) {
|
||||||
|
m_default_brush_size = default_brush_size;
|
||||||
|
}
|
||||||
|
|
||||||
Fill getFill(int button) { return m_button[button].m_fill; }
|
Fill getFill(int button) { return m_button[button].m_fill; }
|
||||||
Ink* getInk(int button) { return m_button[button].m_ink; }
|
Ink* getInk(int button) { return m_button[button].m_ink; }
|
||||||
Controller* getController(int button) { return m_button[button].m_controller; }
|
Controller* getController(int button) { return m_button[button].m_controller; }
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
// the End-User License Agreement for Aseprite.
|
// the End-User License Agreement for Aseprite.
|
||||||
@ -31,6 +31,7 @@
|
|||||||
#include "fixmath/fixmath.h"
|
#include "fixmath/fixmath.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
#include "app/tools/controllers.h"
|
#include "app/tools/controllers.h"
|
||||||
#include "app/tools/inks.h"
|
#include "app/tools/inks.h"
|
||||||
@ -91,6 +92,18 @@ const char* WellKnownPointShapes::Brush = "brush";
|
|||||||
const char* WellKnownPointShapes::FloodFill = "floodfill";
|
const char* WellKnownPointShapes::FloodFill = "floodfill";
|
||||||
const char* WellKnownPointShapes::Spray = "spray";
|
const char* WellKnownPointShapes::Spray = "spray";
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct deleter {
|
||||||
|
template<typename T>
|
||||||
|
void operator()(T* p) { delete p; }
|
||||||
|
|
||||||
|
template<typename A, typename B>
|
||||||
|
void operator()(std::pair<A,B>& p) { delete p.second; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
ToolBox::ToolBox()
|
ToolBox::ToolBox()
|
||||||
{
|
{
|
||||||
m_xmlTranslator.setStringIdPrefix("tools");
|
m_xmlTranslator.setStringIdPrefix("tools");
|
||||||
@ -137,16 +150,12 @@ ToolBox::ToolBox()
|
|||||||
m_intertwiners[WellKnownIntertwiners::AsPixelPerfect] = new IntertwineAsPixelPerfect();
|
m_intertwiners[WellKnownIntertwiners::AsPixelPerfect] = new IntertwineAsPixelPerfect();
|
||||||
|
|
||||||
loadTools();
|
loadTools();
|
||||||
|
|
||||||
|
// When the language is change, we reload the toolbox stirngs/tooltips.
|
||||||
|
Strings::instance()->LanguageChange.connect(
|
||||||
|
[this]{ loadTools(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
struct deleter {
|
|
||||||
template<typename T>
|
|
||||||
void operator()(T* p) { delete p; }
|
|
||||||
|
|
||||||
template<typename A, typename B>
|
|
||||||
void operator()(std::pair<A,B>& p) { delete p.second; }
|
|
||||||
};
|
|
||||||
|
|
||||||
ToolBox::~ToolBox()
|
ToolBox::~ToolBox()
|
||||||
{
|
{
|
||||||
std::for_each(m_tools.begin(), m_tools.end(), deleter());
|
std::for_each(m_tools.begin(), m_tools.end(), deleter());
|
||||||
@ -203,7 +212,19 @@ void ToolBox::loadTools()
|
|||||||
|
|
||||||
LOG(VERBOSE) << "TOOL: Group " << groupId << "\n";
|
LOG(VERBOSE) << "TOOL: Group " << groupId << "\n";
|
||||||
|
|
||||||
ToolGroup* toolGroup = new ToolGroup(groupId);
|
// Find an existent ToolGroup (this is useful in case we are
|
||||||
|
// reloading tool text/tooltips).
|
||||||
|
ToolGroup* toolGroup = nullptr;
|
||||||
|
for (auto g : m_groups) {
|
||||||
|
if (g->id() == groupId) {
|
||||||
|
toolGroup = g;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toolGroup == nullptr) {
|
||||||
|
toolGroup = new ToolGroup(groupId);
|
||||||
|
m_groups.push_back(toolGroup);
|
||||||
|
}
|
||||||
|
|
||||||
// For each tool
|
// For each tool
|
||||||
TiXmlNode* xmlToolNode = xmlGroup->FirstChild("tool");
|
TiXmlNode* xmlToolNode = xmlGroup->FirstChild("tool");
|
||||||
@ -214,20 +235,31 @@ void ToolBox::loadTools()
|
|||||||
std::string toolTips = m_xmlTranslator(xmlTool, "tooltip");
|
std::string toolTips = m_xmlTranslator(xmlTool, "tooltip");
|
||||||
const char* defaultBrushSize = xmlTool->Attribute("default_brush_size");
|
const char* defaultBrushSize = xmlTool->Attribute("default_brush_size");
|
||||||
|
|
||||||
Tool* tool = new Tool(toolGroup, toolId, toolText, toolTips,
|
Tool* tool = nullptr;
|
||||||
defaultBrushSize ? strtol(defaultBrushSize, NULL, 10): 1);
|
for (auto t : m_tools) {
|
||||||
|
if (t->getId() == toolId) {
|
||||||
|
tool = t;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tool == nullptr) {
|
||||||
|
tool = new Tool(toolGroup, toolId);
|
||||||
|
m_tools.push_back(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
tool->setText(toolText);
|
||||||
|
tool->setTips(toolTips);
|
||||||
|
tool->setDefaultBrushSize(
|
||||||
|
defaultBrushSize ? std::strtol(defaultBrushSize, nullptr, 10): 1);
|
||||||
|
|
||||||
LOG(VERBOSE) << "TOOL: Tool " << toolId << " in group " << groupId << " found\n";
|
LOG(VERBOSE) << "TOOL: Tool " << toolId << " in group " << groupId << " found\n";
|
||||||
|
|
||||||
loadToolProperties(xmlTool, tool, 0, "left");
|
loadToolProperties(xmlTool, tool, 0, "left");
|
||||||
loadToolProperties(xmlTool, tool, 1, "right");
|
loadToolProperties(xmlTool, tool, 1, "right");
|
||||||
|
|
||||||
m_tools.push_back(tool);
|
|
||||||
|
|
||||||
xmlTool = xmlTool->NextSiblingElement();
|
xmlTool = xmlTool->NextSiblingElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_groups.push_back(toolGroup);
|
|
||||||
xmlGroup = xmlGroup->NextSiblingElement();
|
xmlGroup = xmlGroup->NextSiblingElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
// the End-User License Agreement for Aseprite.
|
// the End-User License Agreement for Aseprite.
|
||||||
@ -13,6 +13,7 @@
|
|||||||
#include "app/app.h"
|
#include "app/app.h"
|
||||||
#include "app/app_menus.h"
|
#include "app/app_menus.h"
|
||||||
#include "app/commands/commands.h"
|
#include "app/commands/commands.h"
|
||||||
|
#include "app/i18n/strings.h"
|
||||||
#include "app/ini_file.h"
|
#include "app/ini_file.h"
|
||||||
#include "app/modules/editors.h"
|
#include "app/modules/editors.h"
|
||||||
#include "app/notification_delegate.h"
|
#include "app/notification_delegate.h"
|
||||||
@ -150,6 +151,15 @@ MainWindow::MainWindow()
|
|||||||
remapWindow();
|
remapWindow();
|
||||||
|
|
||||||
AppMenus::instance()->rebuildRecentList();
|
AppMenus::instance()->rebuildRecentList();
|
||||||
|
|
||||||
|
// When the language is change, we reload the menu bar strings and
|
||||||
|
// relayout the whole main window.
|
||||||
|
Strings::instance()->LanguageChange.connect(
|
||||||
|
[this]{
|
||||||
|
m_menuBar->reload();
|
||||||
|
layout();
|
||||||
|
invalidate();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::~MainWindow()
|
MainWindow::~MainWindow()
|
||||||
@ -207,14 +217,6 @@ CheckUpdateDelegate* MainWindow::getCheckUpdateDelegate()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void MainWindow::reloadMenus()
|
|
||||||
{
|
|
||||||
m_menuBar->reload();
|
|
||||||
|
|
||||||
layout();
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MainWindow::showNotification(INotificationDelegate* del)
|
void MainWindow::showNotification(INotificationDelegate* del)
|
||||||
{
|
{
|
||||||
m_notifications->addLink(del);
|
m_notifications->addLink(del);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Aseprite
|
// Aseprite
|
||||||
// Copyright (C) 2001-2017 David Capello
|
// Copyright (C) 2001-2018 David Capello
|
||||||
//
|
//
|
||||||
// This program is distributed under the terms of
|
// This program is distributed under the terms of
|
||||||
// the End-User License Agreement for Aseprite.
|
// the End-User License Agreement for Aseprite.
|
||||||
@ -65,7 +65,6 @@ namespace app {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
void start();
|
void start();
|
||||||
void reloadMenus();
|
|
||||||
void showNotification(INotificationDelegate* del);
|
void showNotification(INotificationDelegate* del);
|
||||||
void showHomeOnOpen();
|
void showHomeOnOpen();
|
||||||
void showHome();
|
void showHome();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user