Add window capture
Now that we have the priorties window in we can finally be able to select windows for capture source such as window capture. Only took about an hour or two to write. Also, fixed some depednency issues on winmm.lib with obs-outputs
This commit is contained in:
parent
e78dc5aa51
commit
906535022f
@ -2,7 +2,8 @@ project(obs-outputs)
|
||||
|
||||
if(WIN32)
|
||||
set(obs-outputs_PLATFORM_DEPS
|
||||
ws2_32.lib)
|
||||
ws2_32.lib
|
||||
winmm.lib)
|
||||
endif()
|
||||
|
||||
set(obs-outputs_librtmp_HEADERS
|
||||
|
@ -6,13 +6,15 @@ set(win-capture_HEADERS
|
||||
set(win-capture_SOURCES
|
||||
dc-capture.c
|
||||
monitor-capture.c
|
||||
window-capture.c
|
||||
plugin-main.c)
|
||||
|
||||
add_library(win-capture MODULE
|
||||
${win-capture_SOURCES}
|
||||
${win-capture_HEADERS})
|
||||
target_link_libraries(win-capture
|
||||
libobs)
|
||||
libobs
|
||||
psapi.lib)
|
||||
|
||||
install_obs_plugin(win-capture)
|
||||
install_obs_plugin_data(win-capture ../../build/data/obs-plugins/win-capture)
|
||||
|
@ -130,6 +130,8 @@ static void monitor_capture_tick(void *data, float seconds)
|
||||
gs_entercontext(obs_graphics());
|
||||
dc_capture_capture(&capture->data, NULL);
|
||||
gs_leavecontext();
|
||||
|
||||
UNUSED_PARAMETER(seconds);
|
||||
}
|
||||
|
||||
static void monitor_capture_render(void *data, effect_t effect)
|
||||
|
@ -3,10 +3,12 @@
|
||||
OBS_DECLARE_MODULE()
|
||||
|
||||
extern struct obs_source_info monitor_capture_info;
|
||||
extern struct obs_source_info window_capture_info;
|
||||
|
||||
bool obs_module_load(uint32_t libobs_ver)
|
||||
{
|
||||
obs_register_source(&monitor_capture_info);
|
||||
obs_register_source(&window_capture_info);
|
||||
|
||||
UNUSED_PARAMETER(libobs_ver);
|
||||
return true;
|
||||
|
459
plugins/win-capture/window-capture.c
Normal file
459
plugins/win-capture/window-capture.c
Normal file
@ -0,0 +1,459 @@
|
||||
#include <stdlib.h>
|
||||
#include <util/dstr.h>
|
||||
#include "dc-capture.h"
|
||||
#include <psapi.h>
|
||||
|
||||
enum window_priority {
|
||||
WINDOW_PRIORITY_CLASS,
|
||||
WINDOW_PRIORITY_TITLE,
|
||||
WINDOW_PRIORITY_EXE,
|
||||
};
|
||||
|
||||
struct window_capture {
|
||||
obs_source_t source;
|
||||
|
||||
char *title;
|
||||
char *class;
|
||||
char *executable;
|
||||
enum window_priority priority;
|
||||
bool cursor;
|
||||
bool compatibility;
|
||||
bool use_wildcards; /* TODO */
|
||||
|
||||
struct dc_capture capture;
|
||||
|
||||
float resize_timer;
|
||||
|
||||
effect_t opaque_effect;
|
||||
|
||||
HWND window;
|
||||
RECT last_rect;
|
||||
};
|
||||
|
||||
void encode_dstr(struct dstr *str)
|
||||
{
|
||||
dstr_replace(str, "#", "#22");
|
||||
dstr_replace(str, ":", "#3A");
|
||||
}
|
||||
|
||||
char *decode_str(const char *src)
|
||||
{
|
||||
struct dstr str = {0};
|
||||
dstr_copy(&str, src);
|
||||
dstr_replace(&str, "#3A", ":");
|
||||
dstr_replace(&str, "#22", "#");
|
||||
return str.array;
|
||||
}
|
||||
|
||||
static void update_settings(struct window_capture *wc, obs_data_t s)
|
||||
{
|
||||
const char *window = obs_data_getstring(s, "window");
|
||||
int priority = (int)obs_data_getint(s, "priority");
|
||||
|
||||
bfree(wc->title);
|
||||
bfree(wc->class);
|
||||
bfree(wc->executable);
|
||||
wc->title = NULL;
|
||||
wc->class = NULL;
|
||||
wc->executable = NULL;
|
||||
|
||||
if (window) {
|
||||
char **strlist = strlist_split(window, ':', true);
|
||||
|
||||
if (strlist[0] && strlist[1] && strlist[2]) {
|
||||
wc->title = decode_str(strlist[0]);
|
||||
wc->class = decode_str(strlist[1]);
|
||||
wc->executable = decode_str(strlist[2]);
|
||||
}
|
||||
|
||||
strlist_free(strlist);
|
||||
}
|
||||
|
||||
wc->priority = (enum window_priority)priority;
|
||||
wc->cursor = obs_data_getbool(s, "cursor");
|
||||
wc->use_wildcards = obs_data_getbool(s, "use_wildcards");
|
||||
}
|
||||
|
||||
static bool get_exe_name(struct dstr *name, HWND window)
|
||||
{
|
||||
wchar_t wname[MAX_PATH];
|
||||
struct dstr temp = {0};
|
||||
bool success = false;
|
||||
HANDLE process = NULL;
|
||||
char *slash;
|
||||
DWORD id;
|
||||
|
||||
GetWindowThreadProcessId(window, &id);
|
||||
if (id == GetCurrentProcessId())
|
||||
return false;
|
||||
|
||||
process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, id);
|
||||
if (!process)
|
||||
goto fail;
|
||||
|
||||
if (!GetProcessImageFileNameW(process, wname, MAX_PATH))
|
||||
goto fail;
|
||||
|
||||
dstr_from_wcs(&temp, wname);
|
||||
slash = strrchr(temp.array, '\\');
|
||||
if (!slash)
|
||||
goto fail;
|
||||
|
||||
dstr_copy(name, slash+1);
|
||||
success = true;
|
||||
|
||||
fail:
|
||||
if (!success)
|
||||
dstr_copy(name, "unknown");
|
||||
|
||||
dstr_free(&temp);
|
||||
CloseHandle(process);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void get_window_title(struct dstr *name, HWND hwnd)
|
||||
{
|
||||
wchar_t *temp;
|
||||
int len;
|
||||
|
||||
len = GetWindowTextLengthW(hwnd);
|
||||
if (!len)
|
||||
return;
|
||||
|
||||
temp = malloc(sizeof(wchar_t) * (len+1));
|
||||
GetWindowTextW(hwnd, temp, len+1);
|
||||
dstr_from_wcs(name, temp);
|
||||
free(temp);
|
||||
}
|
||||
|
||||
static void get_window_class(struct dstr *class, HWND hwnd)
|
||||
{
|
||||
wchar_t temp[256];
|
||||
|
||||
temp[0] = 0;
|
||||
GetClassNameW(hwnd, temp, sizeof(temp));
|
||||
dstr_from_wcs(class, temp);
|
||||
}
|
||||
|
||||
static void add_window(obs_property_t p, HWND hwnd,
|
||||
struct dstr *title,
|
||||
struct dstr *class,
|
||||
struct dstr *executable)
|
||||
{
|
||||
struct dstr encoded = {0};
|
||||
struct dstr desc = {0};
|
||||
|
||||
if (!get_exe_name(executable, hwnd))
|
||||
return;
|
||||
get_window_title(title, hwnd);
|
||||
get_window_class(class, hwnd);
|
||||
|
||||
dstr_printf(&desc, "[%s]: %s", executable->array, title->array);
|
||||
|
||||
encode_dstr(title);
|
||||
encode_dstr(class);
|
||||
encode_dstr(executable);
|
||||
|
||||
dstr_cat_dstr(&encoded, title);
|
||||
dstr_cat(&encoded, ":");
|
||||
dstr_cat_dstr(&encoded, class);
|
||||
dstr_cat(&encoded, ":");
|
||||
dstr_cat_dstr(&encoded, executable);
|
||||
|
||||
obs_property_list_add_string(p, desc.array, encoded.array);
|
||||
|
||||
dstr_free(&encoded);
|
||||
dstr_free(&desc);
|
||||
}
|
||||
|
||||
static bool check_window_valid(HWND window,
|
||||
struct dstr *title,
|
||||
struct dstr *class,
|
||||
struct dstr *executable)
|
||||
{
|
||||
DWORD styles, ex_styles;
|
||||
RECT rect;
|
||||
|
||||
if (!IsWindowVisible(window) || IsIconic(window))
|
||||
return false;
|
||||
|
||||
GetClientRect(window, &rect);
|
||||
styles = (DWORD)GetWindowLongPtr(window, GWL_STYLE);
|
||||
ex_styles = (DWORD)GetWindowLongPtr(window, GWL_EXSTYLE);
|
||||
|
||||
if (ex_styles & WS_EX_TOOLWINDOW)
|
||||
return false;
|
||||
if (styles & WS_CHILD)
|
||||
return false;
|
||||
if (rect.bottom == 0 || rect.right == 0)
|
||||
return false;
|
||||
|
||||
if (!get_exe_name(executable, window))
|
||||
return false;
|
||||
get_window_title(title, window);
|
||||
get_window_class(class, window);
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline HWND next_window(HWND window,
|
||||
struct dstr *title,
|
||||
struct dstr *class,
|
||||
struct dstr *exe)
|
||||
{
|
||||
while (true) {
|
||||
window = GetNextWindow(window, GW_HWNDNEXT);
|
||||
if (!window || check_window_valid(window, title, class, exe))
|
||||
break;
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
static inline HWND first_window(
|
||||
struct dstr *title,
|
||||
struct dstr *class,
|
||||
struct dstr *executable)
|
||||
{
|
||||
HWND window = GetWindow(GetDesktopWindow(), GW_CHILD);
|
||||
if (!check_window_valid(window, title, class, executable))
|
||||
window = next_window(window, title, class, executable);
|
||||
return window;
|
||||
}
|
||||
|
||||
static void fill_window_list(obs_property_t p)
|
||||
{
|
||||
struct dstr title = {0};
|
||||
struct dstr class = {0};
|
||||
struct dstr executable = {0};
|
||||
|
||||
HWND window = first_window(&title, &class, &executable);
|
||||
|
||||
while (window) {
|
||||
add_window(p, window, &title, &class, &executable);
|
||||
window = next_window(window, &title, &class, &executable);
|
||||
}
|
||||
|
||||
dstr_free(&title);
|
||||
dstr_free(&class);
|
||||
dstr_free(&executable);
|
||||
}
|
||||
|
||||
static int window_rating(struct window_capture *wc,
|
||||
struct dstr *title,
|
||||
struct dstr *class,
|
||||
struct dstr *executable)
|
||||
{
|
||||
int class_val = 1;
|
||||
int title_val = 1;
|
||||
int exe_val = 0;
|
||||
int total = 0;
|
||||
|
||||
if (wc->priority == WINDOW_PRIORITY_CLASS)
|
||||
class_val += 3;
|
||||
else if (wc->priority == WINDOW_PRIORITY_TITLE)
|
||||
title_val += 3;
|
||||
else
|
||||
exe_val += 3;
|
||||
|
||||
if (dstr_cmpi(class, wc->class) == 0)
|
||||
total += class_val;
|
||||
if (dstr_cmpi(title, wc->title) == 0)
|
||||
total += title_val;
|
||||
if (dstr_cmpi(executable, wc->executable) == 0)
|
||||
total += exe_val;
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
static HWND find_window(struct window_capture *wc)
|
||||
{
|
||||
struct dstr title = {0};
|
||||
struct dstr class = {0};
|
||||
struct dstr exe = {0};
|
||||
|
||||
HWND window = first_window(&title, &class, &exe);
|
||||
HWND best_window = NULL;
|
||||
int best_rating = 0;
|
||||
|
||||
while (window) {
|
||||
int rating = window_rating(wc, &title, &class, &exe);
|
||||
if (rating > best_rating) {
|
||||
best_rating = rating;
|
||||
best_window = window;
|
||||
}
|
||||
|
||||
window = next_window(window, &title, &class, &exe);
|
||||
}
|
||||
|
||||
dstr_free(&title);
|
||||
dstr_free(&class);
|
||||
dstr_free(&exe);
|
||||
|
||||
return best_window;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
static const char *wc_getname(const char *locale)
|
||||
{
|
||||
/* TODO: locale */
|
||||
UNUSED_PARAMETER(locale);
|
||||
return "Window capture";
|
||||
}
|
||||
|
||||
static void *wc_create(obs_data_t settings, obs_source_t source)
|
||||
{
|
||||
struct window_capture *wc;
|
||||
effect_t opaque_effect = create_opaque_effect();
|
||||
if (!opaque_effect)
|
||||
return NULL;
|
||||
|
||||
wc = bzalloc(sizeof(struct window_capture));
|
||||
wc->source = source;
|
||||
wc->opaque_effect = opaque_effect;
|
||||
|
||||
update_settings(wc, settings);
|
||||
return wc;
|
||||
}
|
||||
|
||||
static void wc_destroy(void *data)
|
||||
{
|
||||
struct window_capture *wc = data;
|
||||
|
||||
if (wc) {
|
||||
dc_capture_free(&wc->capture);
|
||||
|
||||
bfree(wc->title);
|
||||
bfree(wc->class);
|
||||
bfree(wc->executable);
|
||||
|
||||
gs_entercontext(obs_graphics());
|
||||
effect_destroy(wc->opaque_effect);
|
||||
gs_leavecontext();
|
||||
|
||||
bfree(wc);
|
||||
}
|
||||
}
|
||||
|
||||
static void wc_update(void *data, obs_data_t settings)
|
||||
{
|
||||
struct window_capture *wc = data;
|
||||
update_settings(wc, settings);
|
||||
|
||||
/* forces a reset */
|
||||
wc->window = NULL;
|
||||
}
|
||||
|
||||
static uint32_t wc_width(void *data)
|
||||
{
|
||||
struct window_capture *wc = data;
|
||||
return wc->capture.width;
|
||||
}
|
||||
|
||||
static uint32_t wc_height(void *data)
|
||||
{
|
||||
struct window_capture *wc = data;
|
||||
return wc->capture.height;
|
||||
}
|
||||
|
||||
static void wc_defaults(obs_data_t defaults)
|
||||
{
|
||||
obs_data_setbool(defaults, "cursor", true);
|
||||
obs_data_setbool(defaults, "compatibility", false);
|
||||
}
|
||||
|
||||
static obs_properties_t wc_properties(const char *locale)
|
||||
{
|
||||
obs_properties_t ppts = obs_properties_create(locale);
|
||||
obs_property_t p;
|
||||
|
||||
/* TODO: locale */
|
||||
p = obs_properties_add_list(ppts, "window", "Window",
|
||||
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
|
||||
fill_window_list(p);
|
||||
|
||||
p = obs_properties_add_list(ppts, "priority", "Window Match Priority",
|
||||
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
|
||||
obs_property_list_add_int(p, "Window Title", WINDOW_PRIORITY_TITLE);
|
||||
obs_property_list_add_int(p, "Window Class", WINDOW_PRIORITY_CLASS);
|
||||
obs_property_list_add_int(p, "Executable", WINDOW_PRIORITY_EXE);
|
||||
|
||||
obs_properties_add_bool(ppts, "cursor", "Capture Cursor");
|
||||
|
||||
obs_properties_add_bool(ppts, "compatibility",
|
||||
"Laptop Compatibility Mode");
|
||||
|
||||
return ppts;
|
||||
}
|
||||
|
||||
#define RESIZE_CHECK_TIME 0.2f
|
||||
|
||||
static void wc_tick(void *data, float seconds)
|
||||
{
|
||||
struct window_capture *wc = data;
|
||||
RECT rect;
|
||||
bool reset_capture = false;
|
||||
|
||||
if (!wc->window || !IsWindow(wc->window)) {
|
||||
if (!wc->title && !wc->class)
|
||||
return;
|
||||
|
||||
wc->window = find_window(wc);
|
||||
if (!wc->window)
|
||||
return;
|
||||
|
||||
reset_capture = true;
|
||||
|
||||
} else if (IsIconic(wc->window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
gs_entercontext(obs_graphics());
|
||||
|
||||
GetClientRect(wc->window, &rect);
|
||||
|
||||
if (!reset_capture) {
|
||||
wc->resize_timer += seconds;
|
||||
|
||||
if (wc->resize_timer >= RESIZE_CHECK_TIME) {
|
||||
if (rect.bottom != wc->last_rect.bottom ||
|
||||
rect.right != wc->last_rect.right)
|
||||
reset_capture = true;
|
||||
|
||||
wc->resize_timer = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
if (reset_capture) {
|
||||
wc->resize_timer = 0.0f;
|
||||
wc->last_rect = rect;
|
||||
dc_capture_free(&wc->capture);
|
||||
dc_capture_init(&wc->capture, 0, 0, rect.right, rect.bottom,
|
||||
wc->cursor, wc->compatibility);
|
||||
}
|
||||
|
||||
dc_capture_capture(&wc->capture, wc->window);
|
||||
gs_leavecontext();
|
||||
}
|
||||
|
||||
static void wc_render(void *data, effect_t effect)
|
||||
{
|
||||
struct window_capture *wc = data;
|
||||
dc_capture_render(&wc->capture, wc->opaque_effect);
|
||||
}
|
||||
|
||||
struct obs_source_info window_capture_info = {
|
||||
.id = "window_capture",
|
||||
.type = OBS_SOURCE_TYPE_INPUT,
|
||||
.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW,
|
||||
.getname = wc_getname,
|
||||
.create = wc_create,
|
||||
.destroy = wc_destroy,
|
||||
.update = wc_update,
|
||||
.getwidth = wc_width,
|
||||
.getheight = wc_height,
|
||||
.defaults = wc_defaults,
|
||||
.properties = wc_properties,
|
||||
.video_render = wc_render,
|
||||
.video_tick = wc_tick
|
||||
};
|
@ -132,7 +132,7 @@
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>pthreads.lib;libobs.lib;ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>winmm.lib;pthreads.lib;libobs.lib;ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(OutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
@ -155,7 +155,7 @@
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>pthreads.lib;libobs.lib;ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>winmm.lib;pthreads.lib;libobs.lib;ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(OutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
|
@ -91,7 +91,7 @@
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalLibraryDirectories>$(OutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>libobs.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>psapi.lib;libobs.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>copy "$(OutDir)$(TargetName)$(TargetExt)" "../../../build/obs-plugins/32bit/$(TargetName)$(TargetExt)"</Command>
|
||||
@ -110,7 +110,7 @@
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalLibraryDirectories>$(OutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>libobs.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>psapi.lib;libobs.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>copy "$(OutDir)$(TargetName)$(TargetExt)" "../../../build/obs-plugins/64bit/$(TargetName)$(TargetExt)"</Command>
|
||||
@ -133,7 +133,7 @@
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalLibraryDirectories>$(OutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>libobs.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>psapi.lib;libobs.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>copy "$(OutDir)$(TargetName)$(TargetExt)" "../../../build/obs-plugins/32bit/$(TargetName)$(TargetExt)"</Command>
|
||||
@ -156,7 +156,7 @@
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalLibraryDirectories>$(OutDir);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>libobs.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>psapi.lib;libobs.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>copy "$(OutDir)$(TargetName)$(TargetExt)" "../../../build/obs-plugins/64bit/$(TargetName)$(TargetExt)"</Command>
|
||||
@ -166,6 +166,7 @@
|
||||
<ClCompile Include="..\..\..\plugins\win-capture\dc-capture.c" />
|
||||
<ClCompile Include="..\..\..\plugins\win-capture\monitor-capture.c" />
|
||||
<ClCompile Include="..\..\..\plugins\win-capture\plugin-main.c" />
|
||||
<ClCompile Include="..\..\..\plugins\win-capture\window-capture.c" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\..\..\plugins\win-capture\dc-capture.h" />
|
||||
|
@ -24,6 +24,9 @@
|
||||
<ClCompile Include="..\..\..\plugins\win-capture\dc-capture.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\..\plugins\win-capture\window-capture.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\..\..\plugins\win-capture\dc-capture.h">
|
||||
|
Loading…
x
Reference in New Issue
Block a user