Feature/source plugins #1
No files matched your search
@@ -263,6 +263,7 @@ add_executable(udpscope
|
|||||||
src/ui_cursors.cpp
|
src/ui_cursors.cpp
|
||||||
src/ui_math.cpp
|
src/ui_math.cpp
|
||||||
src/ui_layout.cpp
|
src/ui_layout.cpp
|
||||||
|
src/ui_source.cpp
|
||||||
src/ui_trigger.cpp
|
src/ui_trigger.cpp
|
||||||
src/ui_signals.cpp
|
src/ui_signals.cpp
|
||||||
src/ui_context_help.cpp
|
src/ui_context_help.cpp
|
||||||
|
|||||||
@@ -128,6 +128,34 @@ class ProbeKind final : public SourceKind {
|
|||||||
f.on_change = [cb](std::function<void()> fn) { *cb = std::move(fn); };
|
f.on_change = [cb](std::function<void()> fn) { *cb = std::move(fn); };
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<SourcePanel> make_panels(DataSource& source) const override {
|
||||||
|
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||||
|
gtk_widget_set_margin_top(box, 12);
|
||||||
|
gtk_widget_set_margin_bottom(box, 12);
|
||||||
|
gtk_widget_set_margin_start(box, 12);
|
||||||
|
gtk_widget_set_margin_end(box, 12);
|
||||||
|
GtkWidget* label = gtk_label_new("Test panel");
|
||||||
|
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||||
|
gtk_box_append(GTK_BOX(box), label);
|
||||||
|
// The probe finds the button as the box's last child. It reaches the source only through
|
||||||
|
// `post()`: the command runs inside the next `poll()`, on the producer thread.
|
||||||
|
GtkWidget* poke = gtk_button_new_with_label("Poke");
|
||||||
|
gtk_box_append(GTK_BOX(box), poke);
|
||||||
|
DataSource* src = &source;
|
||||||
|
g_signal_connect_data(
|
||||||
|
poke, "clicked",
|
||||||
|
G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||||
|
DataSource* s = static_cast<DataSource*>(user);
|
||||||
|
s->post([s] { static_cast<ProbeSource*>(s)->note_applied(); });
|
||||||
|
}),
|
||||||
|
src, nullptr, static_cast<GConnectFlags>(0));
|
||||||
|
SourcePanel p;
|
||||||
|
p.id = "poke";
|
||||||
|
p.title = "Probe";
|
||||||
|
p.widget = box;
|
||||||
|
return {p};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
+17
-3
@@ -2978,9 +2978,14 @@ std::string icon_report(GtkIconTheme* theme, const char* name, const char* resou
|
|||||||
/// The words the pills do not draw. The pages' `title` is empty on purpose (see the `.ui`), so the
|
/// The words the pills do not draw. The pages' `title` is empty on purpose (see the `.ui`), so the
|
||||||
/// human name of a tab lives here and is put on the pill — its tooltip and its accessible name — by
|
/// human name of a tab lives here and is put on the pill — its tooltip and its accessible name — by
|
||||||
/// `name_side_tabs()`. That keeps an icon-only pill a *named* control for a screen reader.
|
/// `name_side_tabs()`. That keeps an icon-only pill a *named* control for a screen reader.
|
||||||
static const char* side_tab_title(const char* name) {
|
static const char* side_tab_title(const App& app, const char* name) {
|
||||||
if (name != nullptr && std::strcmp(name, "measure") == 0) return "Measure";
|
if (name != nullptr && std::strcmp(name, "measure") == 0) return "Measure";
|
||||||
if (name != nullptr && std::strcmp(name, "signals") == 0) return "Signals";
|
if (name != nullptr && std::strcmp(name, "signals") == 0) return "Signals";
|
||||||
|
if (name != nullptr) {
|
||||||
|
for (const SourcePanelPage& p : app.source_panels) {
|
||||||
|
if (p.name == name) return p.title.c_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3000,7 +3005,7 @@ void name_side_tabs(App& app) {
|
|||||||
for (guint i = 0; pages != nullptr && pill != nullptr && i < g_list_model_get_n_items(pages);
|
for (guint i = 0; pages != nullptr && pill != nullptr && i < g_list_model_get_n_items(pages);
|
||||||
pill = gtk_widget_get_next_sibling(pill), ++i) {
|
pill = gtk_widget_get_next_sibling(pill), ++i) {
|
||||||
AdwViewStackPage* page = ADW_VIEW_STACK_PAGE(g_list_model_get_item(pages, i));
|
AdwViewStackPage* page = ADW_VIEW_STACK_PAGE(g_list_model_get_item(pages, i));
|
||||||
const char* title = side_tab_title(adw_view_stack_page_get_name(page));
|
const char* title = side_tab_title(app, adw_view_stack_page_get_name(page));
|
||||||
g_object_unref(page);
|
g_object_unref(page);
|
||||||
if (title[0] == '\0') continue;
|
if (title[0] == '\0') continue;
|
||||||
gtk_widget_set_tooltip_text(pill, title);
|
gtk_widget_set_tooltip_text(pill, title);
|
||||||
@@ -3457,6 +3462,7 @@ gboolean tick(gpointer user) {
|
|||||||
// no text and no name either.
|
// no text and no name either.
|
||||||
name_side_tabs(*app);
|
name_side_tabs(*app);
|
||||||
note_panel_controls(*app);
|
note_panel_controls(*app);
|
||||||
|
sync_source_panels(*app);
|
||||||
// The tour, re-measured every frame: the target's allocation is 0 on the frame it is first shown
|
// The tour, re-measured every frame: the target's allocation is 0 on the frame it is first shown
|
||||||
// (see the allocation bullets in `docs/claude/invariants-trigger-and-pinning.md`) and the widget
|
// (see the allocation bullets in `docs/claude/invariants-trigger-and-pinning.md`) and the widget
|
||||||
// moves under the spotlight on a window resize, so a one-shot measurement is wrong both ways.
|
// moves under the spotlight on a window resize, so a one-shot measurement is wrong both ways.
|
||||||
@@ -4551,7 +4557,8 @@ gboolean tick(gpointer user) {
|
|||||||
}
|
}
|
||||||
if (app->capture_button != nullptr) {
|
if (app->capture_button != nullptr) {
|
||||||
// Insensitive until connected: the capture's path is keyed on the source.
|
// Insensitive until connected: the capture's path is keyed on the source.
|
||||||
gtk_widget_set_sensitive(app->capture_button, snap.connected ? TRUE : FALSE);
|
gtk_widget_set_sensitive(app->capture_button,
|
||||||
|
snap.connected && app->source_has_datagrams ? TRUE : FALSE);
|
||||||
if (app->capture_want_ui && !app->acq->capture_want()) {
|
if (app->capture_want_ui && !app->acq->capture_want()) {
|
||||||
// The producer refused the open (a full disk, a read-only directory) and cleared
|
// The producer refused the open (a full disk, a read-only directory) and cleared
|
||||||
// the flag; the button follows the flag rather than lying about the state. Going
|
// the flag; the button follows the flag rather than lying about the state. Going
|
||||||
@@ -5282,6 +5289,9 @@ void on_activate(GtkApplication* application, gpointer user) {
|
|||||||
g_signal_connect(app->side_stack, "notify::visible-child-name",
|
g_signal_connect(app->side_stack, "notify::visible-child-name",
|
||||||
G_CALLBACK(on_side_tab_changed), app);
|
G_CALLBACK(on_side_tab_changed), app);
|
||||||
}
|
}
|
||||||
|
// The source kind's own tabs (a generator, digital I/O). Before the first tick: that is when
|
||||||
|
// `name_side_tabs()` puts the words on the pills, and it must see these pages too.
|
||||||
|
build_source_panels(*app);
|
||||||
|
|
||||||
// The left side is the events pane *and* the plot, in one row: the pane is a column of its own
|
// The left side is the events pane *and* the plot, in one row: the pane is a column of its own
|
||||||
// (the item's "collapsable pane on the left"), revealed by trigger mode.
|
// (the item's "collapsable pane on the left"), revealed by trigger mode.
|
||||||
@@ -5926,6 +5936,7 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
} else {
|
} else {
|
||||||
app.source_title = app.source_host + ":" + std::to_string(app.source_port);
|
app.source_title = app.source_host + ":" + std::to_string(app.source_port);
|
||||||
}
|
}
|
||||||
|
if (SourceKind* k = find_kind(cfg.source_kind)) app.source_has_datagrams = k->has_datagrams();
|
||||||
// The event cache is keyed on the source, like the packet capture. A waveform has no source, so a
|
// The event cache is keyed on the source, like the packet capture. A waveform has no source, so a
|
||||||
// file-mode run has no cache either -- there are no latches in a file.
|
// file-mode run has no cache either -- there are no latches in a file.
|
||||||
if (!app.source_host.empty() && app.source_port != 0) {
|
if (!app.source_host.empty() && app.source_port != 0) {
|
||||||
@@ -6098,6 +6109,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_DENSITY_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_DENSITY_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), density_probe, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), density_probe, &app);
|
||||||
}
|
}
|
||||||
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_SOURCE_KINDS")) {
|
||||||
|
g_timeout_add(static_cast<guint>(std::atoi(env)), source_kinds_probe, &app);
|
||||||
|
}
|
||||||
|
|
||||||
// Our own options were consumed by parse_args(); GTK would reject them, so only
|
// Our own options were consumed by parse_args(); GTK would reject them, so only
|
||||||
// the program name is handed to g_application_run().
|
// the program name is handed to g_application_run().
|
||||||
|
|||||||
@@ -109,6 +109,14 @@ struct TourView {
|
|||||||
void* done_user = nullptr;
|
void* done_user = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// One capability panel a source kind contributed to the side stack. `content` is the kind's own widget
|
||||||
|
/// (made insensitive while the source is disconnected); the page wrapping it is named `name`.
|
||||||
|
struct SourcePanelPage {
|
||||||
|
std::string name; ///< stack page name: "source-" + the kind's panel id
|
||||||
|
std::string title; ///< the pill's tooltip and accessible name; the page's own title stays empty
|
||||||
|
GtkWidget* content = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
ScopeModel* model = nullptr;
|
ScopeModel* model = nullptr;
|
||||||
Acquisition* acq = nullptr;
|
Acquisition* acq = nullptr;
|
||||||
@@ -843,6 +851,9 @@ struct App {
|
|||||||
std::string source_kind = "udps";
|
std::string source_kind = "udps";
|
||||||
SourceConfig source_opts;
|
SourceConfig source_opts;
|
||||||
std::string source_title;
|
std::string source_title;
|
||||||
|
/// The panels the source kind added to `side_stack`, in tab order. Empty for UDPS and in file mode.
|
||||||
|
std::vector<SourcePanelPage> source_panels;
|
||||||
|
bool source_has_datagrams = true; ///< the kind reports raw datagrams; false leaves the capture button disabled
|
||||||
guint layout_save_source = 0;
|
guint layout_save_source = 0;
|
||||||
bool layout_loaded = false;
|
bool layout_loaded = false;
|
||||||
unsigned signal_generation = ~0u;
|
unsigned signal_generation = ~0u;
|
||||||
@@ -1558,6 +1569,14 @@ void on_config_save_as(GtkButton*, gpointer user);
|
|||||||
void config_dialog_refresh(ConfigDialog& d);
|
void config_dialog_refresh(ConfigDialog& d);
|
||||||
void open_configurations(App& app);
|
void open_configurations(App& app);
|
||||||
void save_layout_now(App& app);
|
void save_layout_now(App& app);
|
||||||
|
|
||||||
|
/// Capability panels (`src/ui_source.cpp`). `build_source_panels` adds the source kind's panels as
|
||||||
|
/// pages of `side_stack`, once, before the first tick. `apply_source_panels` sets every panel's
|
||||||
|
/// sensitivity from `connected` -- disabled, never hidden: the tab switcher owns visibility.
|
||||||
|
/// `sync_source_panels` is the per-tick call: it applies the model's own connection flag.
|
||||||
|
void build_source_panels(App& app);
|
||||||
|
void apply_source_panels(App& app, bool connected);
|
||||||
|
void sync_source_panels(App& app);
|
||||||
bool save_configuration_as(App& app, const std::string& name, const std::string& comment);
|
bool save_configuration_as(App& app, const std::string& name, const std::string& comment);
|
||||||
gboolean layout_save_tick(gpointer user);
|
gboolean layout_save_tick(gpointer user);
|
||||||
void schedule_layout_save(App& app);
|
void schedule_layout_save(App& app);
|
||||||
@@ -1855,6 +1874,8 @@ gboolean stats_probe_report(gpointer user);
|
|||||||
gboolean stats_probe(gpointer user);
|
gboolean stats_probe(gpointer user);
|
||||||
gboolean side_tab_probe_report(gpointer user);
|
gboolean side_tab_probe_report(gpointer user);
|
||||||
gboolean side_tab_probe(gpointer user);
|
gboolean side_tab_probe(gpointer user);
|
||||||
|
gboolean source_kinds_probe_report(gpointer user);
|
||||||
|
gboolean source_kinds_probe(gpointer user);
|
||||||
gboolean pan_probe(gpointer user);
|
gboolean pan_probe(gpointer user);
|
||||||
gboolean arm_probe(gpointer user);
|
gboolean arm_probe(gpointer user);
|
||||||
gboolean trigrenum_probe_step3(gpointer user);
|
gboolean trigrenum_probe_step3(gpointer user);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
// automated check, and they are code of their own -- a third of what used to be `ui.cpp` -- so they
|
// automated check, and they are code of their own -- a third of what used to be `ui.cpp` -- so they
|
||||||
// live in a translation unit of their own.
|
// live in a translation unit of their own.
|
||||||
|
|
||||||
|
#include "source/probe_source.hpp"
|
||||||
|
#include "source/registry.hpp"
|
||||||
#include "ui_app.hpp"
|
#include "ui_app.hpp"
|
||||||
|
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
@@ -1887,6 +1889,97 @@ gboolean side_tab_probe(gpointer user) {
|
|||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::uint64_t g_source_kinds_before = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase two: the command the Poke button posted must have been run by `poll()` on the producer.
|
||||||
|
gboolean source_kinds_probe_report(gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
const ProbeSource* src = a->acq != nullptr ? dynamic_cast<const ProbeSource*>(a->acq->source())
|
||||||
|
: nullptr;
|
||||||
|
const std::uint64_t after = src != nullptr ? src->commands_applied() : 0;
|
||||||
|
std::fprintf(stderr, "[source-kinds] command: before=%" G_GUINT64_FORMAT " after=%" G_GUINT64_FORMAT
|
||||||
|
" drained=%d\n",
|
||||||
|
g_source_kinds_before, after, after == g_source_kinds_before + 1 ? 1 : 0);
|
||||||
|
std::fflush(stderr);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the registry offers and what a source kind brings: the kinds, each config form's
|
||||||
|
/// round trip, the panel page (insensitive while disconnected, still present), and a posted command.
|
||||||
|
gboolean source_kinds_probe(gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
|
||||||
|
std::string ids;
|
||||||
|
for (const SourceKind* k : kinds()) ids += (ids.empty() ? "" : ",") + k->id();
|
||||||
|
std::fprintf(stderr, "[source-kinds] kinds=%s\n", ids.c_str());
|
||||||
|
|
||||||
|
// Each kind's form must hand back what it was given.
|
||||||
|
const auto sample = [](const std::string& id) {
|
||||||
|
SourceConfig c;
|
||||||
|
if (id == "udps") {
|
||||||
|
c.set("server", "10.0.0.5");
|
||||||
|
c.set("port", "9001");
|
||||||
|
c.set("multicast", "239.1.2.3");
|
||||||
|
c.set("interface", "10.0.0.9");
|
||||||
|
} else if (id == "probe") {
|
||||||
|
c.set("rate", "750");
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
for (const SourceKind* k : kinds()) {
|
||||||
|
ConfigForm f = k->make_config_form();
|
||||||
|
const SourceConfig in = sample(k->id());
|
||||||
|
f.write(in);
|
||||||
|
const SourceConfig out = f.read();
|
||||||
|
bool same = !in.empty();
|
||||||
|
for (const auto& kv : in.items()) same = same && out.get(kv.first) == kv.second;
|
||||||
|
std::fprintf(stderr, "[source-kinds] form %s: round_trip=%d valid=%d\n", k->id().c_str(),
|
||||||
|
same ? 1 : 0, f.valid() ? 1 : 0);
|
||||||
|
// The rows were never parented; sink and release them so the form leaves nothing behind.
|
||||||
|
for (GtkWidget* row : f.rows) {
|
||||||
|
g_object_ref_sink(row);
|
||||||
|
g_object_unref(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The panel page: built, insensitive while disconnected, present either way.
|
||||||
|
const SourcePanelPage* page = a->source_panels.empty() ? nullptr : &a->source_panels.front();
|
||||||
|
const auto report = [&](const char* state) {
|
||||||
|
const bool present =
|
||||||
|
page != nullptr && a->side_stack != nullptr &&
|
||||||
|
adw_view_stack_get_child_by_name(ADW_VIEW_STACK(a->side_stack), page->name.c_str()) !=
|
||||||
|
nullptr;
|
||||||
|
std::fprintf(stderr, "[source-kinds] panel %s: panels=%zu present=%d sensitive=%d\n", state,
|
||||||
|
a->source_panels.size(), present ? 1 : 0,
|
||||||
|
page != nullptr && gtk_widget_get_sensitive(page->content) ? 1 : 0);
|
||||||
|
};
|
||||||
|
apply_source_panels(*a, false);
|
||||||
|
report("disconnected");
|
||||||
|
apply_source_panels(*a, true);
|
||||||
|
report("connected");
|
||||||
|
|
||||||
|
// The probe kind has no datagrams, so the packet-capture button stays disabled, and is still there.
|
||||||
|
std::fprintf(stderr, "[source-kinds] capture button: present=%d sensitive=%d\n",
|
||||||
|
a->capture_button != nullptr ? 1 : 0,
|
||||||
|
a->capture_button != nullptr && gtk_widget_get_sensitive(a->capture_button) ? 1 : 0);
|
||||||
|
|
||||||
|
// Click the panel's button: it posts a command, and a later `poll()` runs it.
|
||||||
|
ProbeSource* src = a->acq != nullptr ? dynamic_cast<ProbeSource*>(a->acq->source()) : nullptr;
|
||||||
|
g_source_kinds_before = src != nullptr ? src->commands_applied() : 0;
|
||||||
|
if (page != nullptr) {
|
||||||
|
if (GtkWidget* poke = gtk_widget_get_last_child(page->content)) {
|
||||||
|
g_signal_emit_by_name(poke, "clicked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fflush(stderr);
|
||||||
|
g_timeout_add(500, source_kinds_probe_report, a);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
gboolean pan_probe(gpointer user) {
|
gboolean pan_probe(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE;
|
if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE;
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
|
|
||||||
|
#include "source/registry.hpp"
|
||||||
|
#include "ui_app.hpp"
|
||||||
|
|
||||||
|
#include <adwaita.h>
|
||||||
|
|
||||||
|
namespace udpscope {
|
||||||
|
namespace ui_impl {
|
||||||
|
|
||||||
|
void build_source_panels(App& app) {
|
||||||
|
if (app.side_stack == nullptr || app.acq == nullptr || app.acq->source() == nullptr) return;
|
||||||
|
SourceKind* kind = find_kind(app.source_kind);
|
||||||
|
if (kind == nullptr) return;
|
||||||
|
for (const SourcePanel& p : kind->make_panels(*app.acq->source())) {
|
||||||
|
if (p.widget == nullptr || p.id.empty()) continue;
|
||||||
|
// Each page scrolls on its own, like Measure and Signals: panels differ in height.
|
||||||
|
GtkWidget* scroller = gtk_scrolled_window_new();
|
||||||
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_NEVER,
|
||||||
|
GTK_POLICY_AUTOMATIC);
|
||||||
|
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), p.widget);
|
||||||
|
const std::string name = "source-" + p.id;
|
||||||
|
// An empty title on purpose, as for the built-in tabs: the pill is an icon, and
|
||||||
|
// `name_side_tabs()` puts `title` on it as tooltip and accessible name.
|
||||||
|
adw_view_stack_add_titled_with_icon(
|
||||||
|
ADW_VIEW_STACK(app.side_stack), scroller, name.c_str(), "",
|
||||||
|
p.icon.empty() ? "applications-engineering-symbolic" : p.icon.c_str());
|
||||||
|
app.source_panels.push_back({name, p.title, p.widget});
|
||||||
|
}
|
||||||
|
// Disconnected until the first Connected event says otherwise.
|
||||||
|
apply_source_panels(app, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_source_panels(App& app, bool connected) {
|
||||||
|
for (const SourcePanelPage& p : app.source_panels) {
|
||||||
|
if (gtk_widget_get_sensitive(p.content) != (connected ? TRUE : FALSE)) {
|
||||||
|
gtk_widget_set_sensitive(p.content, connected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sync_source_panels(App& app) {
|
||||||
|
if (app.source_panels.empty() || app.model == nullptr) return;
|
||||||
|
apply_source_panels(app, app.model->stats().connected.load(std::memory_order_relaxed));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui_impl
|
||||||
|
} // namespace udpscope
|
||||||
Reference in new issue
Block a user