mirror of
https://github.com/meshtastic/firmware.git
synced 2025-06-10 07:02:11 +00:00
Improved message screen with scrolling
This commit is contained in:
parent
7856e069a5
commit
2711c53b5f
@ -980,146 +980,214 @@ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int
|
|||||||
validCached = true;
|
validCached = true;
|
||||||
return validCached;
|
return validCached;
|
||||||
}
|
}
|
||||||
|
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r)
|
||||||
/// Draw the last text message we received
|
|
||||||
static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
|
||||||
{
|
{
|
||||||
// the max length of this buffer is much longer than we can possibly print
|
display->fillRect(x + r, y, w - 2 * r, h);
|
||||||
static char tempBuf[237];
|
display->fillRect(x, y + r, r, h - 2 * r);
|
||||||
|
display->fillRect(x + w - r, y + r, r, h - 2 * r);
|
||||||
|
display->fillCircle(x + r + 1, y + r, r);
|
||||||
|
display->fillCircle(x + w - r - 1, y + r, r);
|
||||||
|
display->fillCircle(x + r + 1, y + h - r - 1, r);
|
||||||
|
display->fillCircle(x + w - r - 1, y + h - r - 1, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ****************************
|
||||||
|
// * Tex Message Screen *
|
||||||
|
// ****************************
|
||||||
|
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
|
{
|
||||||
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
|
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
||||||
// LOG_DEBUG("Draw text message from 0x%x: %s", mp.from,
|
|
||||||
// mp.decoded.variant.data.decoded.bytes);
|
|
||||||
|
|
||||||
// Demo for drawStringMaxWidth:
|
// === Setup display formatting ===
|
||||||
// with the third parameter you can define the width after which words will
|
|
||||||
// be wrapped. Currently only spaces and "-" are allowed for wrapping
|
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
|
||||||
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
|
|
||||||
display->setColor(BLACK);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For time delta
|
const int screenWidth = display->getWidth();
|
||||||
uint32_t seconds = sinceReceived(&mp);
|
const int screenHeight = display->getHeight();
|
||||||
uint32_t minutes = seconds / 60;
|
const int navHeight = FONT_HEIGHT_SMALL; // space reserved at bottom
|
||||||
uint32_t hours = minutes / 60;
|
const int scrollBottom = screenHeight - navHeight;
|
||||||
uint32_t days = hours / 24;
|
const int usableHeight = scrollBottom;
|
||||||
|
const int textWidth = screenWidth;
|
||||||
|
const int cornerRadius = 2;
|
||||||
|
|
||||||
// For timestamp
|
bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||||
|
bool isBold = config.display.heading_bold;
|
||||||
|
|
||||||
|
// === Construct Header String ===
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
|
||||||
|
char headerStr[80];
|
||||||
|
const char *sender = (node && node->has_user) ? node->user.short_name : "???";
|
||||||
|
uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
|
||||||
uint8_t timestampHours, timestampMinutes;
|
uint8_t timestampHours, timestampMinutes;
|
||||||
int32_t daysAgo;
|
int32_t daysAgo;
|
||||||
bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo);
|
bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo);
|
||||||
char from_string[75];
|
|
||||||
|
|
||||||
if (useTimestamp) {
|
|
||||||
std::string prefix = (daysAgo == 1 && display->width() >= 200) ? "Yesterday" : "At";
|
|
||||||
|
|
||||||
if (config.display.use_12h_clock) {
|
|
||||||
std::string meridiem = "AM";
|
|
||||||
if (timestampHours >= 12) {
|
|
||||||
meridiem = "PM";
|
|
||||||
}
|
|
||||||
if (timestampHours > 12) {
|
|
||||||
timestampHours -= 12;
|
|
||||||
}
|
|
||||||
if (timestampHours == 00) {
|
|
||||||
timestampHours = 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
snprintf(from_string, sizeof(from_string), "%s %d:%02d%s from", prefix.c_str(), timestampHours, timestampMinutes,
|
|
||||||
meridiem.c_str());
|
|
||||||
} else {
|
|
||||||
snprintf(from_string, sizeof(from_string), "%s %d:%02d from", prefix.c_str(), timestampHours, timestampMinutes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If bold, draw twice, shifting right by one pixel
|
|
||||||
for (uint8_t xOff = 0; xOff <= (config.display.heading_bold ? 1 : 0); xOff++) {
|
|
||||||
// Show a timestamp if received today, but longer than 15 minutes ago
|
|
||||||
if (useTimestamp && minutes >= 15 && daysAgo == 0) {
|
if (useTimestamp && minutes >= 15 && daysAgo == 0) {
|
||||||
display->drawStringf(xOff + x, 0 + y, tempBuf, "%s %s", from_string,
|
std::string prefix = (daysAgo == 1 && screenWidth >= 200) ? "Yesterday" : "At";
|
||||||
(node && node->has_user) ? node->user.short_name : "???");
|
std::string meridiem = "AM";
|
||||||
|
if (config.display.use_12h_clock) {
|
||||||
|
if (timestampHours >= 12) meridiem = "PM";
|
||||||
|
if (timestampHours > 12) timestampHours -= 12;
|
||||||
|
if (timestampHours == 0) timestampHours = 12;
|
||||||
|
snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes, meridiem.c_str(), sender);
|
||||||
} else {
|
} else {
|
||||||
display->drawStringf(xOff + x, 0 + y, tempBuf, "%s ago from %s",
|
snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes, sender);
|
||||||
screen->drawTimeDelta(days, hours, minutes, seconds).c_str(),
|
}
|
||||||
(node && node->has_user) ? node->user.short_name : "???");
|
} else {
|
||||||
|
snprintf(headerStr, sizeof(headerStr), "%s ago from %s", screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef EXCLUDE_EMOJI
|
||||||
|
// === Check for Emote and Draw It ===
|
||||||
|
struct Emote {
|
||||||
|
const char *code;
|
||||||
|
const uint8_t *bitmap;
|
||||||
|
int width, height;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Emote emotes[] = {
|
||||||
|
{ "\U0001F44D", thumbup, thumbs_width, thumbs_height },
|
||||||
|
{ "\U0001F44E", thumbdown, thumbs_width, thumbs_height },
|
||||||
|
{ "\U0001F60A", smiley, smiley_width, smiley_height },
|
||||||
|
{ "\U0001F600", smiley, smiley_width, smiley_height },
|
||||||
|
{ "\U0001F642", smiley, smiley_width, smiley_height },
|
||||||
|
{ "\U0001F609", smiley, smiley_width, smiley_height },
|
||||||
|
{ "\U0001F601", smiley, smiley_width, smiley_height },
|
||||||
|
{ "❓", question, question_width, question_height },
|
||||||
|
{ "‼️", bang, bang_width, bang_height },
|
||||||
|
{ "\U0001F4A9", poo, poo_width, poo_height },
|
||||||
|
{ "\U0001F923", haha, haha_width, haha_height },
|
||||||
|
{ "\U0001F44B", wave_icon, wave_icon_width, wave_icon_height },
|
||||||
|
{ "\U0001F920", cowboy, cowboy_width, cowboy_height },
|
||||||
|
{ "\U0001F42D", deadmau5, deadmau5_width, deadmau5_height },
|
||||||
|
{ "☀️", sun, sun_width, sun_height },
|
||||||
|
{ "\xE2\x98\x80\xEF\xB8\x8F", sun, sun_width, sun_height },
|
||||||
|
{ "☔", rain, rain_width, rain_height },
|
||||||
|
{ "\u2614", rain, rain_width, rain_height },
|
||||||
|
{ "☁️", cloud, cloud_width, cloud_height },
|
||||||
|
{ "🌫️", fog, fog_width, fog_height },
|
||||||
|
{ "\U0001F608", devil, devil_width, devil_height },
|
||||||
|
{ "♥️", heart, heart_width, heart_height },
|
||||||
|
{ "\U0001F9E1", heart, heart_width, heart_height },
|
||||||
|
{ "\U00002763", heart, heart_width, heart_height },
|
||||||
|
{ "\U00002764", heart, heart_width, heart_height },
|
||||||
|
{ "\U0001F495", heart, heart_width, heart_height },
|
||||||
|
{ "\U0001F496", heart, heart_width, heart_height },
|
||||||
|
{ "\U0001F497", heart, heart_width, heart_height },
|
||||||
|
{ "\U0001F498", heart, heart_width, heart_height }
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const Emote &e : emotes) {
|
||||||
|
if (strcmp(msg, e.code) == 0) {
|
||||||
|
// Draw header before showing emoji
|
||||||
|
if (isInverted) {
|
||||||
|
drawRoundedHighlight(display, x, 0, screenWidth, FONT_HEIGHT_SMALL - 1, cornerRadius);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
display->drawString(x + 3, 0, headerStr);
|
||||||
|
if (isBold) display->drawString(x + 4, 0, headerStr);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
} else {
|
||||||
|
display->drawString(x, 0, headerStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then draw emoji below header
|
||||||
|
display->drawXbm((screenWidth - e.width) / 2, (screenHeight - e.height) / 2 + FONT_HEIGHT_SMALL, e.width, e.height, e.bitmap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// === Word-wrap and build line list ===
|
||||||
|
char messageBuf[237];
|
||||||
|
snprintf(messageBuf, sizeof(messageBuf), "%s", msg);
|
||||||
|
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
lines.push_back(std::string(headerStr)); // Header line is always first
|
||||||
|
|
||||||
|
std::string word, line;
|
||||||
|
int msgLen = strlen(messageBuf);
|
||||||
|
for (int i = 0; i <= msgLen; ++i) {
|
||||||
|
char ch = messageBuf[i];
|
||||||
|
if (ch == ' ' || ch == '\0') {
|
||||||
|
if (!word.empty()) {
|
||||||
|
if (display->getStringWidth((line + word).c_str()) > textWidth) {
|
||||||
|
lines.push_back(line);
|
||||||
|
line = word + ' ';
|
||||||
|
} else {
|
||||||
|
line += word + ' ';
|
||||||
|
}
|
||||||
|
word.clear();
|
||||||
|
}
|
||||||
|
if (ch == '\0' && !line.empty()) {
|
||||||
|
lines.push_back(line);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
word += ch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
display->setColor(WHITE);
|
// === Scrolling logic ===
|
||||||
#ifndef EXCLUDE_EMOJI
|
const float rowHeight = FONT_HEIGHT_SMALL - 1;
|
||||||
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
const int totalHeight = lines.size() * rowHeight;
|
||||||
if (strcmp(msg, "\U0001F44D") == 0) {
|
const int scrollStop = std::max(0, totalHeight - usableHeight);
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2,
|
|
||||||
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height,
|
static float scrollY = 0.0f;
|
||||||
thumbup);
|
static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0;
|
||||||
} else if (strcmp(msg, "\U0001F44E") == 0) {
|
static bool waitingToReset = false, scrollStarted = false;
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2,
|
|
||||||
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height,
|
uint32_t now = millis();
|
||||||
thumbdown);
|
|
||||||
} else if (strcmp(msg, "\U0001F60A") == 0 || strcmp(msg, "\U0001F600") == 0 || strcmp(msg, "\U0001F642") == 0 ||
|
// === Smooth scrolling adjustment ===
|
||||||
strcmp(msg, "\U0001F609") == 0 ||
|
// You can tweak this divisor to change how smooth it scrolls.
|
||||||
strcmp(msg, "\U0001F601") == 0) { // matches 5 different common smileys, so that the phone user doesn't have to
|
// Lower = smoother, but can feel slow.
|
||||||
// remember which one is compatible
|
float delta = (now - lastTime) / 400.0f;
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - smiley_width) / 2,
|
lastTime = now;
|
||||||
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - smiley_height) / 2 + 2 + 5, smiley_width, smiley_height,
|
|
||||||
smiley);
|
const float scrollSpeed = 2.0f; // pixels per second
|
||||||
} else if (strcmp(msg, "❓") == 0) {
|
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - question_width) / 2,
|
// Delay scrolling start by 2 seconds
|
||||||
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - question_height) / 2 + 2 + 5, question_width, question_height,
|
if (scrollStartDelay == 0) scrollStartDelay = now;
|
||||||
question);
|
if (!scrollStarted && now - scrollStartDelay > 2000) scrollStarted = true;
|
||||||
} else if (strcmp(msg, "‼️") == 0) {
|
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - bang_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - bang_height) / 2 + 2 + 5,
|
if (totalHeight > usableHeight) {
|
||||||
bang_width, bang_height, bang);
|
if (scrollStarted) {
|
||||||
} else if (strcmp(msg, "\U0001F4A9") == 0) {
|
if (!waitingToReset) {
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - poo_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - poo_height) / 2 + 2 + 5,
|
scrollY += delta * scrollSpeed;
|
||||||
poo_width, poo_height, poo);
|
if (scrollY >= scrollStop) {
|
||||||
} else if (strcmp(msg, "\U0001F923") == 0) {
|
scrollY = scrollStop;
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - haha_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - haha_height) / 2 + 2 + 5,
|
waitingToReset = true;
|
||||||
haha_width, haha_height, haha);
|
pauseStart = now;
|
||||||
} else if (strcmp(msg, "\U0001F44B") == 0) {
|
}
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - wave_icon_width) / 2,
|
} else if (now - pauseStart > 3000) {
|
||||||
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - wave_icon_height) / 2 + 2 + 5, wave_icon_width,
|
scrollY = 0;
|
||||||
wave_icon_height, wave_icon);
|
waitingToReset = false;
|
||||||
} else if (strcmp(msg, "\U0001F920") == 0) {
|
scrollStarted = false;
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - cowboy_width) / 2,
|
scrollStartDelay = now;
|
||||||
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cowboy_height) / 2 + 2 + 5, cowboy_width, cowboy_height,
|
}
|
||||||
cowboy);
|
}
|
||||||
} else if (strcmp(msg, "\U0001F42D") == 0) {
|
} else {
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - deadmau5_width) / 2,
|
scrollY = 0;
|
||||||
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - deadmau5_height) / 2 + 2 + 5, deadmau5_width, deadmau5_height,
|
}
|
||||||
deadmau5);
|
|
||||||
} else if (strcmp(msg, "\xE2\x98\x80\xEF\xB8\x8F") == 0) {
|
int scrollOffset = static_cast<int>(scrollY);
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - sun_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - sun_height) / 2 + 2 + 5,
|
int yOffset = -scrollOffset;
|
||||||
sun_width, sun_height, sun);
|
|
||||||
} else if (strcmp(msg, "\u2614") == 0) {
|
// === Render visible lines ===
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - rain_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - rain_height) / 2 + 2 + 10,
|
for (size_t i = 0; i < lines.size(); ++i) {
|
||||||
rain_width, rain_height, rain);
|
int lineY = static_cast<int>(i * rowHeight + yOffset);
|
||||||
} else if (strcmp(msg, "☁️") == 0) {
|
if (lineY > -rowHeight && lineY < scrollBottom) {
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - cloud_width) / 2,
|
if (i == 0 && isInverted) {
|
||||||
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cloud_height) / 2 + 2 + 5, cloud_width, cloud_height, cloud);
|
drawRoundedHighlight(display, x, lineY, screenWidth, FONT_HEIGHT_SMALL - 1, cornerRadius);
|
||||||
} else if (strcmp(msg, "🌫️") == 0) {
|
display->setColor(BLACK);
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - fog_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - fog_height) / 2 + 2 + 5,
|
display->drawString(x + 3, lineY, lines[i].c_str());
|
||||||
fog_width, fog_height, fog);
|
if (isBold) display->drawString(x + 4, lineY, lines[i].c_str());
|
||||||
} else if (strcmp(msg, "\U0001F608") == 0) {
|
display->setColor(WHITE);
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - devil_width) / 2,
|
} else {
|
||||||
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - devil_height) / 2 + 2 + 5, devil_width, devil_height, devil);
|
display->drawString(x, lineY, lines[i].c_str());
|
||||||
} else if (strcmp(msg, "♥️") == 0 || strcmp(msg, "\U0001F9E1") == 0 || strcmp(msg, "\U00002763") == 0 ||
|
}
|
||||||
strcmp(msg, "\U00002764") == 0 || strcmp(msg, "\U0001F495") == 0 || strcmp(msg, "\U0001F496") == 0 ||
|
}
|
||||||
strcmp(msg, "\U0001F497") == 0 || strcmp(msg, "\U0001F498") == 0) {
|
|
||||||
display->drawXbm(x + (SCREEN_WIDTH - heart_width) / 2,
|
|
||||||
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - heart_height) / 2 + 2 + 5, heart_width, heart_height, heart);
|
|
||||||
} else {
|
|
||||||
snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes);
|
|
||||||
display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf);
|
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes);
|
|
||||||
display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a series of fields in a column, wrapping to multiple columns if needed
|
/// Draw a series of fields in a column, wrapping to multiple columns if needed
|
||||||
@ -1605,19 +1673,6 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
|||||||
// *********************************
|
// *********************************
|
||||||
// *Rounding Header when inverted *
|
// *Rounding Header when inverted *
|
||||||
// *********************************
|
// *********************************
|
||||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r)
|
|
||||||
{
|
|
||||||
// Center rectangles
|
|
||||||
display->fillRect(x + r, y, w - 2 * r, h);
|
|
||||||
display->fillRect(x, y + r, r, h - 2 * r);
|
|
||||||
display->fillRect(x + w - r, y + r, r, h - 2 * r);
|
|
||||||
|
|
||||||
// Rounded corners — visually balanced and centered
|
|
||||||
display->fillCircle(x + r + 1, y + r, r); // Top-left
|
|
||||||
display->fillCircle(x + w - r - 1, y + r, r); // Top-right
|
|
||||||
display->fillCircle(x + r + 1, y + h - r - 1, r); // Bottom-left
|
|
||||||
display->fillCircle(x + w - r - 1, y + h - r - 1, r); // Bottom-right
|
|
||||||
}
|
|
||||||
// h! Each node entry holds a reference to its info and how long ago it was heard from
|
// h! Each node entry holds a reference to its info and how long ago it was heard from
|
||||||
struct NodeEntry {
|
struct NodeEntry {
|
||||||
meshtastic_NodeInfoLite *node;
|
meshtastic_NodeInfoLite *node;
|
||||||
|
Loading…
Reference in New Issue
Block a user