mirror of
https://github.com/eried/portapack-mayhem.git
synced 2024-10-01 01:26:06 -04:00
Appstart and applist from serial (#1736)
* Start app, and list fixed ones. * Add ext app support
This commit is contained in:
parent
4b93e78dd9
commit
9d7e06c255
@ -6,6 +6,44 @@ namespace ui {
|
|||||||
|
|
||||||
/* static */ std::vector<DynamicBitmap<16, 16>> ExternalItemsMenuLoader::bitmaps;
|
/* static */ std::vector<DynamicBitmap<16, 16>> ExternalItemsMenuLoader::bitmaps;
|
||||||
|
|
||||||
|
// iterates over all ppma-s, and if it is runnable on the current system, it'll call the callback, and pass info.
|
||||||
|
/* static */ void ExternalItemsMenuLoader::load_all_external_items_callback(std::function<void(AppInfoConsole&)> callback) {
|
||||||
|
if (!callback) return;
|
||||||
|
if (sd_card::status() != sd_card::Status::Mounted)
|
||||||
|
return;
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(u"APPS", u"*.ppma")) {
|
||||||
|
auto filePath = u"/APPS/" + entry.path();
|
||||||
|
File app;
|
||||||
|
|
||||||
|
auto openError = app.open(filePath);
|
||||||
|
if (openError)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
application_information_t application_information = {};
|
||||||
|
|
||||||
|
auto readResult = app.read(&application_information, sizeof(application_information_t));
|
||||||
|
if (!readResult)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (application_information.header_version != CURRENT_HEADER_VERSION)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool versionMatches = VERSION_MD5 == application_information.app_version;
|
||||||
|
if (!versionMatches) continue;
|
||||||
|
// here the app is startable and good.
|
||||||
|
std::string appshortname = filePath.filename().string();
|
||||||
|
if (appshortname.size() >= 5 && appshortname.substr(appshortname.size() - 5) == ".ppma") {
|
||||||
|
// Remove the ".ppma" suffix
|
||||||
|
appshortname = appshortname.substr(0, appshortname.size() - 5);
|
||||||
|
}
|
||||||
|
AppInfoConsole info{
|
||||||
|
.appCallName = appshortname.c_str(),
|
||||||
|
.appFriendlyName = reinterpret_cast<char*>(&application_information.app_name[0]),
|
||||||
|
.appLocation = application_information.menu_location};
|
||||||
|
callback(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* static */ std::vector<GridItem> ExternalItemsMenuLoader::load_external_items(app_location_t app_location, NavigationView& nav) {
|
/* static */ std::vector<GridItem> ExternalItemsMenuLoader::load_external_items(app_location_t app_location, NavigationView& nav) {
|
||||||
bitmaps.clear();
|
bitmaps.clear();
|
||||||
|
|
||||||
@ -47,7 +85,9 @@ namespace ui {
|
|||||||
bitmaps.push_back(std::move(dyn_bmp));
|
bitmaps.push_back(std::move(dyn_bmp));
|
||||||
|
|
||||||
gridItem.on_select = [&nav, app_location, filePath]() {
|
gridItem.on_select = [&nav, app_location, filePath]() {
|
||||||
run_external_app(nav, filePath);
|
if (!run_external_app(nav, filePath)) {
|
||||||
|
nav.display_modal("Error", "The .ppma file in your APPS\nfolder can't be read. Please\nupdate your SD Card content.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
gridItem.color = Color::light_grey();
|
gridItem.color = Color::light_grey();
|
||||||
@ -65,19 +105,19 @@ namespace ui {
|
|||||||
return external_apps;
|
return external_apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* static */ void ExternalItemsMenuLoader::run_external_app(ui::NavigationView& nav, std::filesystem::path filePath) {
|
/* static */ bool ExternalItemsMenuLoader::run_external_app(ui::NavigationView& nav, std::filesystem::path filePath) {
|
||||||
File app;
|
File app;
|
||||||
|
|
||||||
auto openError = app.open(filePath);
|
auto openError = app.open(filePath);
|
||||||
if (openError)
|
if (openError)
|
||||||
chDbgPanic("file gone");
|
return false;
|
||||||
|
|
||||||
application_information_t application_information = {};
|
application_information_t application_information = {};
|
||||||
|
|
||||||
auto readResult = app.read(&application_information, sizeof(application_information_t));
|
auto readResult = app.read(&application_information, sizeof(application_information_t));
|
||||||
|
|
||||||
if (!readResult)
|
if (!readResult)
|
||||||
chDbgPanic("no data");
|
return false;
|
||||||
|
|
||||||
app.seek(0);
|
app.seek(0);
|
||||||
|
|
||||||
@ -93,7 +133,7 @@ namespace ui {
|
|||||||
|
|
||||||
readResult = app.read(&application_information.memory_location[file_read_index], bytes_to_read);
|
readResult = app.read(&application_information.memory_location[file_read_index], bytes_to_read);
|
||||||
if (!readResult)
|
if (!readResult)
|
||||||
chDbgPanic("read error");
|
return false;
|
||||||
|
|
||||||
if (readResult.value() < std::filesystem::max_file_block_size)
|
if (readResult.value() < std::filesystem::max_file_block_size)
|
||||||
break;
|
break;
|
||||||
@ -114,7 +154,7 @@ namespace ui {
|
|||||||
|
|
||||||
readResult = app.read(target_memory, bytes_to_read);
|
readResult = app.read(target_memory, bytes_to_read);
|
||||||
if (!readResult)
|
if (!readResult)
|
||||||
chDbgPanic("read error #2");
|
return false;
|
||||||
|
|
||||||
if (readResult.value() != bytes_to_read)
|
if (readResult.value() != bytes_to_read)
|
||||||
break;
|
break;
|
||||||
@ -126,7 +166,7 @@ namespace ui {
|
|||||||
|
|
||||||
readResult = app.read(&application_information.memory_location[file_read_index], bytes_to_read);
|
readResult = app.read(&application_information.memory_location[file_read_index], bytes_to_read);
|
||||||
if (!readResult)
|
if (!readResult)
|
||||||
chDbgPanic("read error #3");
|
return false;
|
||||||
|
|
||||||
if (readResult.value() < std::filesystem::max_file_block_size)
|
if (readResult.value() < std::filesystem::max_file_block_size)
|
||||||
break;
|
break;
|
||||||
@ -134,6 +174,7 @@ namespace ui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
application_information.externalAppEntry(nav);
|
application_information.externalAppEntry(nav);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
@ -54,11 +54,11 @@ class ExternalItemsMenuLoader {
|
|||||||
public:
|
public:
|
||||||
static std::vector<GridItem> load_external_items(app_location_t, NavigationView&);
|
static std::vector<GridItem> load_external_items(app_location_t, NavigationView&);
|
||||||
ExternalItemsMenuLoader() = delete;
|
ExternalItemsMenuLoader() = delete;
|
||||||
|
static bool run_external_app(ui::NavigationView&, std::filesystem::path);
|
||||||
|
static void load_all_external_items_callback(std::function<void(AppInfoConsole&)> callback);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static std::vector<DynamicBitmap<16, 16>> bitmaps;
|
static std::vector<DynamicBitmap<16, 16>> bitmaps;
|
||||||
|
|
||||||
static void run_external_app(ui::NavigationView&, std::filesystem::path);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
@ -111,6 +111,169 @@ namespace pmem = portapack::persistent_memory;
|
|||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
|
// When adding or removing apps from the Menu, please update it here:
|
||||||
|
std::vector<AppInfoConsole> NavigationView::fixedAppListFC = {
|
||||||
|
{"adsbrx", "ADS-B", RX},
|
||||||
|
{"ais", "AIS Boats", RX},
|
||||||
|
{"aprsrx", "APRS", RX},
|
||||||
|
{"audio", "Audio", RX},
|
||||||
|
{"blerx", "BLE Rx", RX},
|
||||||
|
{"ert", "ERT Meter", RX},
|
||||||
|
{"level", "Level", RX},
|
||||||
|
{"pocsagrx", "POCSAG", RX},
|
||||||
|
{"radiosnde", "Radiosnde", RX},
|
||||||
|
{"recon", "Recon", RX},
|
||||||
|
{"search", "Search", RX},
|
||||||
|
{"tpms", "TPMS Cars", RX},
|
||||||
|
{"weather", "Weather", RX},
|
||||||
|
{"subghzd", "SubGhzD", RX},
|
||||||
|
{"adsbtx", "ADS-B", TX},
|
||||||
|
{"aprstx", "APRS TX", TX},
|
||||||
|
{"bht", "BHT Xy/EP", TX},
|
||||||
|
{"bletx", "BLE Tx", TX},
|
||||||
|
{"morsetx", "Morse", TX},
|
||||||
|
{"ooktx", "OOK", TX},
|
||||||
|
{"pocsatx", "POCSAG TX", TX},
|
||||||
|
{"rdstx", "RDS TX", TX},
|
||||||
|
{"soundbrd", "Soundbrd", TX},
|
||||||
|
{"sstvtx", "SSTV", TX},
|
||||||
|
{"touchtune", "TouchTune", TX},
|
||||||
|
{"capture", "Capture", RX},
|
||||||
|
{"replay", "Replay", TX},
|
||||||
|
{"remote", "Remote", TX},
|
||||||
|
{"scanner", "Scanner", RX},
|
||||||
|
{"microphone", "Microphone", TX},
|
||||||
|
{"lookingglass", "Looking Glass", RX}};
|
||||||
|
|
||||||
|
bool NavigationView::StartAppByName(const char* name) {
|
||||||
|
pop();
|
||||||
|
if (strcmp(name, "adsbrx") == 0) {
|
||||||
|
push<ADSBRxView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "ais") == 0) {
|
||||||
|
push<AISAppView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "aprsrx") == 0) {
|
||||||
|
push<APRSRXView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "audio") == 0) {
|
||||||
|
push<AnalogAudioView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "blerx") == 0) {
|
||||||
|
push<BLERxView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "ert") == 0) {
|
||||||
|
push<ERTAppView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "level") == 0) {
|
||||||
|
push<LevelView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "pocsagrx") == 0) {
|
||||||
|
push<POCSAGAppView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "radiosnode") == 0) {
|
||||||
|
push<SondeView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "recon") == 0) {
|
||||||
|
push<ReconView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "search") == 0) {
|
||||||
|
push<SearchView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "tpms") == 0) {
|
||||||
|
push<TPMSAppView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "weather") == 0) {
|
||||||
|
push<WeatherView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "subghzd") == 0) {
|
||||||
|
push<SubGhzDView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "adsbtx") == 0) {
|
||||||
|
push<ADSBTxView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "aprstx") == 0) {
|
||||||
|
push<APRSTXView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "bht") == 0) {
|
||||||
|
push<BHTView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "bletx") == 0) {
|
||||||
|
push<BLETxView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "morsetx") == 0) {
|
||||||
|
push<MorseView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "ooktx") == 0) {
|
||||||
|
push<EncodersView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "pocsatx") == 0) {
|
||||||
|
push<POCSAGTXView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "rdstx") == 0) {
|
||||||
|
push<RDSView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "soundbrd") == 0) {
|
||||||
|
push<SoundBoardView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "sstvtx") == 0) {
|
||||||
|
push<SSTVTXView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "touchtune") == 0) {
|
||||||
|
push<TouchTunesView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "capture") == 0) {
|
||||||
|
push<CaptureAppView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "replay") == 0) {
|
||||||
|
push<PlaylistView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "remote") == 0) {
|
||||||
|
push<RemoteView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "scanner") == 0) {
|
||||||
|
push<ScannerView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "microphone") == 0) {
|
||||||
|
push<MicTXView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "lookingglass") == 0) {
|
||||||
|
push<GlassView>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/* StatusTray ************************************************************/
|
/* StatusTray ************************************************************/
|
||||||
|
|
||||||
StatusTray::StatusTray(Point pos)
|
StatusTray::StatusTray(Point pos)
|
||||||
@ -751,6 +914,9 @@ SystemView::SystemView(
|
|||||||
Context& SystemView::context() const {
|
Context& SystemView::context() const {
|
||||||
return context_;
|
return context_;
|
||||||
}
|
}
|
||||||
|
NavigationView* SystemView::get_navigation_view() {
|
||||||
|
return &navigation_view;
|
||||||
|
}
|
||||||
|
|
||||||
void SystemView::toggle_overlay() {
|
void SystemView::toggle_overlay() {
|
||||||
switch (++overlay_active) {
|
switch (++overlay_active) {
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
#include "diskio.h"
|
#include "diskio.h"
|
||||||
#include "lfsr_random.hpp"
|
#include "lfsr_random.hpp"
|
||||||
#include "sd_card.hpp"
|
#include "sd_card.hpp"
|
||||||
|
#include "external_app.hpp"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@ -57,6 +57,12 @@ enum modal_t {
|
|||||||
ABORT
|
ABORT
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct AppInfoConsole {
|
||||||
|
const char* appCallName;
|
||||||
|
const char* appFriendlyName;
|
||||||
|
const app_location_t appLocation;
|
||||||
|
};
|
||||||
|
|
||||||
class NavigationView : public View {
|
class NavigationView : public View {
|
||||||
public:
|
public:
|
||||||
std::function<void(const View&)> on_view_changed{};
|
std::function<void(const View&)> on_view_changed{};
|
||||||
@ -99,6 +105,8 @@ class NavigationView : public View {
|
|||||||
* Returns true if the handler was bound successfully. */
|
* Returns true if the handler was bound successfully. */
|
||||||
bool set_on_pop(std::function<void()> on_pop);
|
bool set_on_pop(std::function<void()> on_pop);
|
||||||
|
|
||||||
|
static std::vector<AppInfoConsole> fixedAppListFC; // fixed app list for console. vector, so can be incomplete and still iterateable
|
||||||
|
bool StartAppByName(const char* name); // Starts a View (app) by name stored in fixedAppListFC. This is to start apps from console
|
||||||
private:
|
private:
|
||||||
struct ViewState {
|
struct ViewState {
|
||||||
std::unique_ptr<View> view;
|
std::unique_ptr<View> view;
|
||||||
@ -321,6 +329,8 @@ class SystemView : public View {
|
|||||||
void toggle_overlay();
|
void toggle_overlay();
|
||||||
void paint_overlay();
|
void paint_overlay();
|
||||||
|
|
||||||
|
NavigationView* get_navigation_view();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint8_t overlay_active{0};
|
uint8_t overlay_active{0};
|
||||||
|
|
||||||
|
@ -40,9 +40,12 @@
|
|||||||
#include "ff.h"
|
#include "ff.h"
|
||||||
#include "chprintf.h"
|
#include "chprintf.h"
|
||||||
#include "chqueues.h"
|
#include "chqueues.h"
|
||||||
|
#include "ui_external_items_menu_loader.hpp"
|
||||||
#include "untar.hpp"
|
#include "untar.hpp"
|
||||||
#include "ui_widget.hpp"
|
#include "ui_widget.hpp"
|
||||||
|
|
||||||
|
#include "ui_navigation.hpp"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <codecvt>
|
#include <codecvt>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -843,6 +846,78 @@ static void cmd_accessibility_readcurr(BaseSequentialStream* chp, int argc, char
|
|||||||
chprintf(chp, "\r\nok\r\n");
|
chprintf(chp, "\r\nok\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void cmd_appstart(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||||
|
(void)argc;
|
||||||
|
(void)argv;
|
||||||
|
if (argc != 1) {
|
||||||
|
chprintf(chp, "Usage: appstart APPCALLNAME");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto evtd = getEventDispatcherInstance();
|
||||||
|
if (!evtd) return;
|
||||||
|
auto top_widget = evtd->getTopWidget();
|
||||||
|
if (!top_widget) return;
|
||||||
|
auto nav = static_cast<ui::SystemView*>(top_widget)->get_navigation_view();
|
||||||
|
if (!nav) return;
|
||||||
|
if (nav->StartAppByName(argv[0])) {
|
||||||
|
chprintf(chp, "ok\r\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// since ext app loader changed, we can just pass the string to it, and it"ll return if started or not.
|
||||||
|
std::string appwithpath = "/APPS/";
|
||||||
|
appwithpath += argv[0];
|
||||||
|
appwithpath += ".ppma";
|
||||||
|
bool ret = ui::ExternalItemsMenuLoader::run_external_app(*nav, path_from_string8((char*)appwithpath.c_str()));
|
||||||
|
if (!ret) {
|
||||||
|
chprintf(chp, "error\r\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chprintf(chp, "ok\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void printAppInfo(BaseSequentialStream* chp, ui::AppInfoConsole& element) {
|
||||||
|
if (strlen(element.appCallName) == 0) return;
|
||||||
|
chprintf(chp, element.appCallName);
|
||||||
|
chprintf(chp, " ");
|
||||||
|
chprintf(chp, element.appFriendlyName);
|
||||||
|
chprintf(chp, " ");
|
||||||
|
switch (element.appLocation) {
|
||||||
|
case RX:
|
||||||
|
chprintf(chp, "[RX]\r\n");
|
||||||
|
break;
|
||||||
|
case TX:
|
||||||
|
chprintf(chp, "[TX]\r\n");
|
||||||
|
break;
|
||||||
|
case UTILITIES:
|
||||||
|
chprintf(chp, "[UTIL]\r\n");
|
||||||
|
break;
|
||||||
|
case DEBUG:
|
||||||
|
chprintf(chp, "[DEBUG]\r\n");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the installed apps, those can be called by appstart APPNAME
|
||||||
|
static void cmd_applist(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||||
|
(void)argc;
|
||||||
|
(void)argv;
|
||||||
|
auto evtd = getEventDispatcherInstance();
|
||||||
|
if (!evtd) return;
|
||||||
|
auto top_widget = evtd->getTopWidget();
|
||||||
|
if (!top_widget) return;
|
||||||
|
auto nav = static_cast<ui::SystemView*>(top_widget)->get_navigation_view();
|
||||||
|
if (!nav) return;
|
||||||
|
for (auto element : ui::NavigationView::fixedAppListFC) {
|
||||||
|
printAppInfo(chp, element);
|
||||||
|
}
|
||||||
|
ui::ExternalItemsMenuLoader::load_all_external_items_callback([chp](ui::AppInfoConsole& info) {
|
||||||
|
printAppInfo(chp, info);
|
||||||
|
});
|
||||||
|
chprintf(chp, "ok\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
static void cmd_cpld_read(BaseSequentialStream* chp, int argc, char* argv[]) {
|
static void cmd_cpld_read(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||||
const char* usage =
|
const char* usage =
|
||||||
"usage: cpld_read <device> <target>\r\n"
|
"usage: cpld_read <device> <target>\r\n"
|
||||||
@ -1032,6 +1107,8 @@ static const ShellCommand commands[] = {
|
|||||||
{"cpld_read", cmd_cpld_read},
|
{"cpld_read", cmd_cpld_read},
|
||||||
{"accessibility_readall", cmd_accessibility_readall},
|
{"accessibility_readall", cmd_accessibility_readall},
|
||||||
{"accessibility_readcurr", cmd_accessibility_readcurr},
|
{"accessibility_readcurr", cmd_accessibility_readcurr},
|
||||||
|
{"applist", cmd_applist},
|
||||||
|
{"appstart", cmd_appstart},
|
||||||
{NULL, NULL}};
|
{NULL, NULL}};
|
||||||
|
|
||||||
static const ShellConfig shell_cfg1 = {
|
static const ShellConfig shell_cfg1 = {
|
||||||
|
Loading…
Reference in New Issue
Block a user