mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-29 08:18:12 +00:00
Contacts with at least one message float to the top, ordered by total message count descending. Contacts with no messages keep their original relative order. Sorting is applied each time the message screen is opened (buildContactList), so the list always reflects the current session's activity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1230 lines
44 KiB
C++
1230 lines
44 KiB
C++
#pragma once
|
|
// Custom screen — not part of upstream UITask.cpp
|
|
// Included by UITask.cpp after SettingsScreen.h is defined.
|
|
|
|
class QuickMsgScreen : public UIScreen {
|
|
UITask* _task;
|
|
|
|
enum Phase { MODE_SELECT, CONTACT_PICK, DM_HIST, MSG_PICK, CHANNEL_PICK, CHANNEL_HIST, KEYBOARD };
|
|
Phase _phase;
|
|
|
|
// MODE_SELECT
|
|
int _mode_sel; // 0=Direct, 1=Channel, 2=Room Servers
|
|
|
|
// CONTACT_PICK
|
|
int _contact_sel, _contact_scroll;
|
|
int _num_contacts;
|
|
uint16_t _sorted[MAX_CONTACTS];
|
|
ContactInfo _sel_contact;
|
|
bool _room_mode; // true = picking a room server, false = picking a DM contact
|
|
|
|
// CHANNEL_PICK
|
|
int _channel_sel, _channel_scroll;
|
|
int _num_channels;
|
|
uint8_t _channel_indices[MAX_GROUP_CHANNELS];
|
|
uint8_t _ch_unread[MAX_GROUP_CHANNELS];
|
|
int _sel_channel_idx;
|
|
bool _sending_to_channel;
|
|
|
|
// MSG_PICK (shared)
|
|
int _msg_sel, _msg_scroll;
|
|
int _active_msgs[QUICK_MSGS_MAX];
|
|
int _active_msg_count;
|
|
|
|
// CHANNEL_HIST
|
|
int _hist_sel, _hist_scroll;
|
|
FullscreenMsgView _fs;
|
|
int _unread_at_entry; // _ch_unread value when entering CHANNEL_HIST
|
|
int _viewing_max_seen; // highest _hist_sel reached in current session
|
|
static const int CH_HIST_MAX = 32;
|
|
|
|
// KEYBOARD
|
|
KeyboardWidget _kb;
|
|
|
|
// Context menu (opened by KEY_CONTEXT_MENU in CHANNEL_PICK / CONTACT_PICK / histories)
|
|
PopupMenu _ctx_menu;
|
|
bool _ctx_dirty;
|
|
char _ctx_notif_item[22];
|
|
char _ctx_melody_item[20];
|
|
char _reply_prefix[36]; // "@[nick] " built when reply is triggered
|
|
bool _reply_mode; // true while composing a reply (prefix is prepended)
|
|
|
|
struct ChHistEntry { uint8_t ch_idx; char text[140]; uint32_t timestamp; };
|
|
ChHistEntry _hist[CH_HIST_MAX];
|
|
int _hist_head, _hist_count;
|
|
|
|
// DM_HIST
|
|
struct DmHistEntry { uint8_t prefix[4]; uint8_t outgoing; char text[80]; uint32_t timestamp; };
|
|
static const int DM_HIST_MAX = 64;
|
|
DmHistEntry _dm_hist[DM_HIST_MAX];
|
|
int _dm_hist_head, _dm_hist_count;
|
|
int _dm_hist_sel, _dm_hist_scroll;
|
|
FullscreenMsgView _dm_fs;
|
|
|
|
static const int VISIBLE = 4;
|
|
static const int ITEM_H = 12;
|
|
static const int START_Y = 12;
|
|
static const int HIST_VISIBLE = 2; // 2-line boxed history entries
|
|
static const int HIST_ITEM_H = 21; // box(19) + gap(2)
|
|
static const int HIST_BOX_H = 19;
|
|
static const int HIST_START_Y = 11;
|
|
|
|
void expandMsg(const char* tmpl, char* out, int out_len) const {
|
|
double lat = 0, lon = 0;
|
|
bool gps_valid = false;
|
|
#if ENV_INCLUDE_GPS == 1
|
|
LocationProvider* loc = sensors.getLocationProvider();
|
|
if (loc && loc->isValid()) {
|
|
lat = loc->getLatitude() / 1000000.0;
|
|
lon = loc->getLongitude() / 1000000.0;
|
|
gps_valid = true;
|
|
}
|
|
#endif
|
|
NodePrefs* np = _task->getNodePrefs();
|
|
float batt = (float)board.getBattMilliVolts() / 1000.0f;
|
|
::expandMsg(tmpl, out, out_len, lat, lon, gps_valid,
|
|
rtc_clock.getCurrentTime(),
|
|
np ? np->tz_offset_hours : 0,
|
|
&sensors, batt);
|
|
}
|
|
|
|
// Strip "@[nick] " reply prefix from a message body for compact list display.
|
|
static const char* skipReplyPrefix(const char* text) {
|
|
if (text[0] == '@' && text[1] == '[') {
|
|
const char* close = strchr(text + 2, ']');
|
|
if (close && close[1] == ' ' && close[2]) return close + 2;
|
|
}
|
|
return text;
|
|
}
|
|
|
|
// Build "@[nick] " prefix from a channel message text ("nick: body") into _reply_prefix.
|
|
// Returns false if sender is "Me" (own message — no reply prefix needed).
|
|
bool buildChannelReplyPrefix(const char* text) {
|
|
const char* sep = strstr(text, ": ");
|
|
if (!sep) return false;
|
|
int slen = (int)(sep - text);
|
|
if (slen == 2 && strncmp(text, "Me", 2) == 0) return false;
|
|
if (slen > 31) slen = 31;
|
|
snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.*s] ", slen, text);
|
|
return true;
|
|
}
|
|
|
|
void startReply(bool to_channel) {
|
|
_sending_to_channel = to_channel;
|
|
_reply_mode = true;
|
|
setupMsgPick();
|
|
_phase = MSG_PICK;
|
|
}
|
|
|
|
void setupMsgPick() {
|
|
_msg_sel = _msg_scroll = 0;
|
|
_active_msg_count = 0;
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
if (p) {
|
|
for (int i = 0; i < QUICK_MSGS_MAX; i++) {
|
|
if (p->custom_msgs[i][0] != '\0')
|
|
_active_msgs[_active_msg_count++] = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
void renderScrollHints(DisplayDriver& display, int scroll, int count) {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
if (scroll > 0) {
|
|
display.setCursor(display.width() - 6, START_Y);
|
|
display.print("^");
|
|
}
|
|
if (scroll + VISIBLE < count) {
|
|
display.setCursor(display.width() - 6, START_Y + (VISIBLE-1)*ITEM_H);
|
|
display.print("v");
|
|
}
|
|
}
|
|
|
|
static void fmtMsgAge(char* buf, int n, uint32_t timestamp) {
|
|
uint32_t now = rtc_clock.getCurrentTime();
|
|
if (timestamp == 0 || now < timestamp) { buf[0] = '\0'; return; }
|
|
uint32_t age = now - timestamp;
|
|
if (age < 60) snprintf(buf, n, "%us", age);
|
|
else if (age < 3600) snprintf(buf, n, "%um", age / 60);
|
|
else if (age < 86400) snprintf(buf, n, "%uh", age / 3600);
|
|
else snprintf(buf, n, ">1d");
|
|
}
|
|
|
|
// count history entries for a specific channel
|
|
int histCountForChannel(int ch_idx) const {
|
|
int n = 0;
|
|
for (int i = 0; i < _hist_count; i++) {
|
|
if (_hist[(_hist_head + i) % CH_HIST_MAX].ch_idx == (uint8_t)ch_idx) n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
// get ring-buffer position of j-th history entry for channel (newest first)
|
|
int histEntryForChannel(int ch_idx, int j) const {
|
|
int n = 0;
|
|
for (int i = _hist_count - 1; i >= 0; i--) {
|
|
int pos = (_hist_head + i) % CH_HIST_MAX;
|
|
if (_hist[pos].ch_idx == (uint8_t)ch_idx) {
|
|
if (n == j) return pos;
|
|
n++;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void storeDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) {
|
|
int pos;
|
|
if (_dm_hist_count < DM_HIST_MAX) {
|
|
pos = (_dm_hist_head + _dm_hist_count) % DM_HIST_MAX;
|
|
_dm_hist_count++;
|
|
} else {
|
|
pos = _dm_hist_head;
|
|
_dm_hist_head = (_dm_hist_head + 1) % DM_HIST_MAX;
|
|
}
|
|
memcpy(_dm_hist[pos].prefix, pub_key, 4);
|
|
_dm_hist[pos].outgoing = outgoing ? 1 : 0;
|
|
_dm_hist[pos].timestamp = rtc_clock.getCurrentTime();
|
|
strncpy(_dm_hist[pos].text, text, sizeof(DmHistEntry::text) - 1);
|
|
_dm_hist[pos].text[sizeof(DmHistEntry::text) - 1] = '\0';
|
|
}
|
|
|
|
int dmHistCountForContact(const uint8_t* prefix) const {
|
|
int n = 0;
|
|
for (int i = 0; i < _dm_hist_count; i++)
|
|
if (memcmp(_dm_hist[(_dm_hist_head + i) % DM_HIST_MAX].prefix, prefix, 4) == 0) n++;
|
|
return n;
|
|
}
|
|
|
|
int dmHistEntryForContact(const uint8_t* prefix, int j) const { // j=0 = newest
|
|
int n = 0;
|
|
for (int i = _dm_hist_count - 1; i >= 0; i--) {
|
|
int pos = (_dm_hist_head + i) % DM_HIST_MAX;
|
|
if (memcmp(_dm_hist[pos].prefix, prefix, 4) == 0) {
|
|
if (n == j) return pos;
|
|
n++;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void afterSend(bool ok, const char* msg) {
|
|
_reply_mode = false;
|
|
if (ok && _sending_to_channel) {
|
|
_hist_sel = 0;
|
|
_hist_scroll = 0;
|
|
_phase = CHANNEL_HIST; // set before addChannelMsg so viewing=true, no unread bump
|
|
char entry[sizeof(ChHistEntry::text)];
|
|
snprintf(entry, sizeof(entry), "Me: %s", msg);
|
|
addChannelMsg(_sel_channel_idx, entry);
|
|
// After inserting sent msg at index 0, the unread index range is stale.
|
|
// User is active in this channel — treat as fully read.
|
|
_ch_unread[_sel_channel_idx] = 0;
|
|
_unread_at_entry = 0;
|
|
_viewing_max_seen = 0;
|
|
_task->showAlert("Sent!", 600);
|
|
} else if (ok) {
|
|
storeDMMsg(_sel_contact.id.pub_key, true, msg);
|
|
_dm_hist_sel = 0;
|
|
_dm_hist_scroll = 0;
|
|
_phase = DM_HIST;
|
|
_task->showAlert("Sent!", 600);
|
|
} else {
|
|
_task->showAlert("Send failed", 1500);
|
|
_task->gotoHomeScreen();
|
|
}
|
|
}
|
|
|
|
bool sendText(const char* msg) {
|
|
if (_sending_to_channel) {
|
|
ChannelDetails ch;
|
|
if (!the_mesh.getChannel(_sel_channel_idx, ch)) return false;
|
|
return the_mesh.sendGroupMessage(rtc_clock.getCurrentTime(), ch.channel,
|
|
the_mesh.getNodeName(), msg, strlen(msg));
|
|
} else {
|
|
uint32_t expected_ack = 0, est_timeout = 0;
|
|
return the_mesh.sendMessage(_sel_contact, rtc_clock.getCurrentTime(), 0,
|
|
msg, expected_ack, est_timeout) > 0;
|
|
}
|
|
}
|
|
|
|
void buildContactList() {
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
ContactInfo c;
|
|
int total = the_mesh.getNumContacts();
|
|
_num_contacts = 0;
|
|
if (_room_mode) {
|
|
bool fav_only = (p && p->room_fav_only);
|
|
for (int i = 0; i < total; i++) {
|
|
if (!the_mesh.getContactByIdx(i, c) || c.type != ADV_TYPE_ROOM) continue;
|
|
if (fav_only && !(c.flags & 0x01)) continue;
|
|
_sorted[_num_contacts++] = i;
|
|
}
|
|
} else {
|
|
bool show_all = (p && p->dm_show_all);
|
|
for (int i = 0; i < total; i++) {
|
|
if (!the_mesh.getContactByIdx(i, c) || c.type != ADV_TYPE_CHAT) continue;
|
|
if (!show_all && !(c.flags & 0x01)) continue;
|
|
_sorted[_num_contacts++] = i;
|
|
}
|
|
// Sort by message count descending; contacts with no messages keep original order.
|
|
int counts[MAX_CONTACTS];
|
|
for (int i = 0; i < _num_contacts; i++) {
|
|
the_mesh.getContactByIdx(_sorted[i], c);
|
|
counts[i] = dmHistCountForContact(c.id.pub_key);
|
|
}
|
|
for (int i = 1; i < _num_contacts; i++) {
|
|
if (counts[i] == 0) continue;
|
|
uint16_t key = _sorted[i]; int kc = counts[i];
|
|
int j = i;
|
|
while (j > 0 && counts[j-1] < kc) {
|
|
_sorted[j] = _sorted[j-1]; counts[j] = counts[j-1]; j--;
|
|
}
|
|
_sorted[j] = key; counts[j] = kc;
|
|
}
|
|
}
|
|
}
|
|
|
|
void buildChannelList() {
|
|
_num_channels = 0;
|
|
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
|
ChannelDetails ch;
|
|
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
|
|
_channel_indices[_num_channels++] = (uint8_t)i;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Returns per-channel notification state: 0=follow global, 1=muted, 2=force-on
|
|
uint8_t chNotifState(uint8_t ch_idx) const {
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
if (!p) return 0;
|
|
uint64_t mask = 1ULL << ch_idx;
|
|
if (!(p->ch_notif_override & mask)) return 0;
|
|
return (p->ch_notif_muted & mask) ? 1 : 2;
|
|
}
|
|
|
|
void setChNotifState(uint8_t ch_idx, uint8_t state) {
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
if (!p) return;
|
|
uint64_t mask = 1ULL << ch_idx;
|
|
if (state == 0) {
|
|
p->ch_notif_override &= ~mask;
|
|
p->ch_notif_muted &= ~mask;
|
|
} else if (state == 1) {
|
|
p->ch_notif_override |= mask;
|
|
p->ch_notif_muted |= mask;
|
|
} else {
|
|
p->ch_notif_override |= mask;
|
|
p->ch_notif_muted &= ~mask;
|
|
}
|
|
}
|
|
|
|
uint8_t dmNotifState(const uint8_t* pub_key) const {
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
if (!p) return 0;
|
|
for (int i = 0; i < NodePrefs::DM_NOTIF_TABLE_MAX; i++)
|
|
if (p->dm_notif[i].state && memcmp(p->dm_notif[i].prefix, pub_key, 4) == 0)
|
|
return p->dm_notif[i].state;
|
|
return 0;
|
|
}
|
|
|
|
void setDmNotifState(const uint8_t* pub_key, uint8_t state) {
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
if (!p) return;
|
|
for (int i = 0; i < NodePrefs::DM_NOTIF_TABLE_MAX; i++) {
|
|
if (p->dm_notif[i].state && memcmp(p->dm_notif[i].prefix, pub_key, 4) == 0) {
|
|
if (state == 0) { memset(&p->dm_notif[i], 0, sizeof(p->dm_notif[i])); }
|
|
else p->dm_notif[i].state = state;
|
|
return;
|
|
}
|
|
}
|
|
if (state == 0) return;
|
|
for (int i = 0; i < NodePrefs::DM_NOTIF_TABLE_MAX; i++) {
|
|
if (p->dm_notif[i].state == 0) {
|
|
memcpy(p->dm_notif[i].prefix, pub_key, 4);
|
|
p->dm_notif[i].state = state;
|
|
return;
|
|
}
|
|
}
|
|
// table full — overwrite slot 0
|
|
memcpy(p->dm_notif[0].prefix, pub_key, 4);
|
|
p->dm_notif[0].state = state;
|
|
}
|
|
|
|
uint8_t chNotifMelody(uint8_t ch_idx) const {
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
if (!p) return 0;
|
|
uint64_t mask = 1ULL << ch_idx;
|
|
if (!(p->ch_notif_melody_set & mask)) return 0;
|
|
return (p->ch_notif_melody_2 & mask) ? 2 : 1;
|
|
}
|
|
|
|
void setChNotifMelody(uint8_t ch_idx, uint8_t slot) {
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
if (!p) return;
|
|
uint64_t mask = 1ULL << ch_idx;
|
|
if (slot == 0) {
|
|
p->ch_notif_melody_set &= ~mask;
|
|
p->ch_notif_melody_2 &= ~mask;
|
|
} else if (slot == 1) {
|
|
p->ch_notif_melody_set |= mask;
|
|
p->ch_notif_melody_2 &= ~mask;
|
|
} else {
|
|
p->ch_notif_melody_set |= mask;
|
|
p->ch_notif_melody_2 |= mask;
|
|
}
|
|
}
|
|
|
|
uint8_t dmMelodySlot(const uint8_t* pub_key) const {
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
if (!p) return 0;
|
|
for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++)
|
|
if (p->dm_melody[i].slot && memcmp(p->dm_melody[i].prefix, pub_key, 4) == 0)
|
|
return p->dm_melody[i].slot;
|
|
return 0;
|
|
}
|
|
|
|
void setDmMelody(const uint8_t* pub_key, uint8_t slot) {
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
if (!p) return;
|
|
for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++) {
|
|
if (p->dm_melody[i].slot && memcmp(p->dm_melody[i].prefix, pub_key, 4) == 0) {
|
|
if (slot == 0) memset(&p->dm_melody[i], 0, sizeof(p->dm_melody[i]));
|
|
else p->dm_melody[i].slot = slot;
|
|
return;
|
|
}
|
|
}
|
|
if (slot == 0) return;
|
|
for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++) {
|
|
if (p->dm_melody[i].slot == 0) {
|
|
memcpy(p->dm_melody[i].prefix, pub_key, 4);
|
|
p->dm_melody[i].slot = slot;
|
|
return;
|
|
}
|
|
}
|
|
memcpy(p->dm_melody[0].prefix, pub_key, 4);
|
|
p->dm_melody[0].slot = slot;
|
|
}
|
|
|
|
public:
|
|
QuickMsgScreen(UITask* task)
|
|
: _task(task), _phase(MODE_SELECT), _mode_sel(0),
|
|
_contact_sel(0), _contact_scroll(0), _num_contacts(0), _room_mode(false),
|
|
_channel_sel(0), _channel_scroll(0), _num_channels(0),
|
|
_sel_channel_idx(0), _sending_to_channel(false),
|
|
_msg_sel(0), _msg_scroll(0), _active_msg_count(0),
|
|
_hist_sel(0), _hist_scroll(0),
|
|
_unread_at_entry(0), _viewing_max_seen(0),
|
|
_hist_head(0), _hist_count(0),
|
|
_dm_hist_head(0), _dm_hist_count(0),
|
|
_dm_hist_sel(-1), _dm_hist_scroll(0),
|
|
_ctx_dirty(false), _reply_mode(false) {
|
|
memset(_ch_unread, 0, sizeof(_ch_unread));
|
|
}
|
|
|
|
void addChannelMsg(uint8_t ch_idx, const char* text) {
|
|
int pos;
|
|
if (_hist_count < CH_HIST_MAX) {
|
|
pos = (_hist_head + _hist_count) % CH_HIST_MAX;
|
|
_hist_count++;
|
|
} else {
|
|
pos = _hist_head;
|
|
_hist_head = (_hist_head + 1) % CH_HIST_MAX;
|
|
}
|
|
_hist[pos].ch_idx = ch_idx;
|
|
_hist[pos].timestamp = rtc_clock.getCurrentTime();
|
|
strncpy(_hist[pos].text, text, sizeof(_hist[pos].text) - 1);
|
|
_hist[pos].text[sizeof(_hist[pos].text) - 1] = '\0';
|
|
|
|
if (ch_idx < MAX_GROUP_CHANNELS) {
|
|
bool viewing = (_phase == CHANNEL_HIST && _sel_channel_idx == (int)ch_idx);
|
|
if (!viewing && _ch_unread[ch_idx] < 99) _ch_unread[ch_idx]++;
|
|
}
|
|
}
|
|
|
|
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text) {
|
|
storeDMMsg(pub_key, outgoing, text);
|
|
}
|
|
|
|
int getDMUnreadTotal() const {
|
|
return _task->getDMUnreadTotal();
|
|
}
|
|
|
|
int getTotalChannelUnread() const {
|
|
int total = 0;
|
|
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) total += _ch_unread[i];
|
|
return total;
|
|
}
|
|
|
|
void clearAllChannelUnread() {
|
|
memset(_ch_unread, 0, sizeof(_ch_unread));
|
|
}
|
|
|
|
void updateChannelUnread() {
|
|
if (_hist_sel < 0 || _sel_channel_idx < 0 || _sel_channel_idx >= MAX_GROUP_CHANNELS) return;
|
|
if (_hist_sel > _viewing_max_seen) _viewing_max_seen = _hist_sel;
|
|
// histEntryForChannel is newest-first: index 0 = newest (unread), higher = older.
|
|
// Each step down from 0 sees one more message; seen count = max_seen + 1.
|
|
int remaining = _unread_at_entry - (_viewing_max_seen + 1);
|
|
_ch_unread[_sel_channel_idx] = (uint8_t)(remaining > 0 ? remaining : 0);
|
|
}
|
|
|
|
void reset() {
|
|
_phase = MODE_SELECT;
|
|
_mode_sel = 0;
|
|
_contact_sel = _contact_scroll = 0;
|
|
_msg_sel = _msg_scroll = 0;
|
|
_channel_sel = _channel_scroll = 0;
|
|
_sending_to_channel = false;
|
|
|
|
_room_mode = false;
|
|
buildContactList();
|
|
buildChannelList();
|
|
|
|
_ctx_menu.active = false;
|
|
_ctx_dirty = false;
|
|
_unread_at_entry = 0;
|
|
_viewing_max_seen = 0;
|
|
}
|
|
|
|
int render(DisplayDriver& display) override {
|
|
display.setTextSize(1);
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
if (_phase == MODE_SELECT) {
|
|
display.drawTextCentered(display.width()/2, 0, "MESSAGE");
|
|
display.fillRect(0, 10, display.width(), 1);
|
|
const char* opts[] = { "Direct message", "Channels", "Room Servers" };
|
|
int badges[3] = {
|
|
getDMUnreadTotal(),
|
|
_task->getChannelUnreadCount(),
|
|
_task->getRoomUnreadCount()
|
|
};
|
|
for (int i = 0; i < 3; i++) {
|
|
int y = START_Y + i * ITEM_H;
|
|
bool sel = (i == _mode_sel);
|
|
if (sel) {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.fillRect(0, y - 1, display.width(), ITEM_H - 1);
|
|
display.setColor(DisplayDriver::DARK);
|
|
} else {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
}
|
|
display.setCursor(0, y);
|
|
display.print(sel ? ">" : " ");
|
|
display.setCursor(8, y);
|
|
display.print(opts[i]);
|
|
if (badges[i] > 0) {
|
|
char badge[5];
|
|
snprintf(badge, sizeof(badge), "%d", badges[i]);
|
|
int bw = display.getTextWidth(badge) + 2;
|
|
display.setCursor(display.width() - bw, y);
|
|
display.print(badge);
|
|
}
|
|
}
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
} else if (_phase == CONTACT_PICK) {
|
|
display.drawTextCentered(display.width()/2, 0, _room_mode ? "SELECT ROOM" : "SELECT CONTACT");
|
|
display.fillRect(0, 10, display.width(), 1);
|
|
|
|
if (_num_contacts == 0) {
|
|
display.drawTextCentered(display.width()/2, 32, _room_mode ? "No room servers" : "No favourites");
|
|
return 5000;
|
|
}
|
|
|
|
for (int i = 0; i < VISIBLE && (_contact_scroll+i) < _num_contacts; i++) {
|
|
int list_idx = _contact_scroll + i;
|
|
int mesh_idx = _sorted[list_idx];
|
|
bool sel = (list_idx == _contact_sel);
|
|
int y = START_Y + i * ITEM_H;
|
|
|
|
if (sel) {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.fillRect(0, y - 1, display.width(), ITEM_H - 1);
|
|
display.setColor(DisplayDriver::DARK);
|
|
} else {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
}
|
|
|
|
ContactInfo c;
|
|
if (the_mesh.getContactByIdx(mesh_idx, c)) {
|
|
display.setCursor(0, y);
|
|
display.print(sel ? ">" : " ");
|
|
char filtered[sizeof(c.name)];
|
|
display.translateUTF8ToBlocks(filtered, c.name, sizeof(filtered));
|
|
uint8_t dm_unread = _task->getDMUnread(c.id.pub_key);
|
|
char badge[5] = "";
|
|
int bw = 0;
|
|
if (dm_unread > 0) {
|
|
snprintf(badge, sizeof(badge), "%d", (int)dm_unread);
|
|
bw = display.getTextWidth(badge) + 2;
|
|
}
|
|
display.drawTextEllipsized(8, y, display.width() - 8 - bw - 1, filtered);
|
|
if (dm_unread > 0) {
|
|
display.setCursor(display.width() - bw + 1, y);
|
|
display.print(badge);
|
|
}
|
|
}
|
|
}
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
renderScrollHints(display, _contact_scroll, _num_contacts);
|
|
|
|
// Context menu overlay
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
|
|
|
} else if (_phase == CHANNEL_PICK) {
|
|
display.drawTextCentered(display.width()/2, 0, "SELECT CHANNEL");
|
|
display.fillRect(0, 10, display.width(), 1);
|
|
|
|
if (_num_channels == 0) {
|
|
display.drawTextCentered(display.width()/2, 32, "No channels");
|
|
return 5000;
|
|
}
|
|
|
|
for (int i = 0; i < VISIBLE && (_channel_scroll+i) < _num_channels; i++) {
|
|
int list_idx = _channel_scroll + i;
|
|
bool sel = (list_idx == _channel_sel);
|
|
int y = START_Y + i * ITEM_H;
|
|
|
|
if (sel) {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.fillRect(0, y - 1, display.width(), ITEM_H - 1);
|
|
display.setColor(DisplayDriver::DARK);
|
|
} else {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
}
|
|
display.setCursor(0, y);
|
|
display.print(sel ? ">" : " ");
|
|
ChannelDetails ch;
|
|
if (the_mesh.getChannel(_channel_indices[list_idx], ch)) {
|
|
uint8_t unread = _ch_unread[_channel_indices[list_idx]];
|
|
char badge[5] = "";
|
|
int bw = 0;
|
|
if (unread > 0) {
|
|
snprintf(badge, sizeof(badge), "%d", (int)unread);
|
|
bw = display.getTextWidth(badge) + 2;
|
|
}
|
|
display.drawTextEllipsized(8, y, display.width() - 10 - bw, ch.name);
|
|
if (unread > 0) {
|
|
display.setCursor(display.width() - bw, y);
|
|
display.print(badge);
|
|
}
|
|
}
|
|
}
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
renderScrollHints(display, _channel_scroll, _num_channels);
|
|
|
|
// Context menu overlay
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
|
|
|
} else if (_phase == DM_HIST) {
|
|
display.setTextSize(1);
|
|
char filtered_name[sizeof(_sel_contact.name)];
|
|
display.translateUTF8ToBlocks(filtered_name, _sel_contact.name, sizeof(filtered_name));
|
|
|
|
if (_dm_fs.active && _dm_hist_sel >= 0) {
|
|
int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel);
|
|
if (ring_pos >= 0) {
|
|
const DmHistEntry& e = _dm_hist[ring_pos];
|
|
const char* sender = e.outgoing ? "Me" : filtered_name;
|
|
int dm_count = dmHistCountForContact(_sel_contact.id.pub_key);
|
|
int ret = _dm_fs.render(display, sender, e.text,
|
|
_dm_hist_sel < dm_count - 1,
|
|
_dm_hist_sel > 0);
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
|
return ret;
|
|
}
|
|
return 500;
|
|
}
|
|
|
|
char title[24];
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
snprintf(title, sizeof(title), "%.23s", filtered_name);
|
|
display.drawTextCentered(display.width()/2, 0, title);
|
|
display.fillRect(0, 9, display.width(), 1);
|
|
|
|
int dm_count = dmHistCountForContact(_sel_contact.id.pub_key);
|
|
|
|
for (int i = 0; i < HIST_VISIBLE && (_dm_hist_scroll + i) < dm_count; i++) {
|
|
int item = _dm_hist_scroll + i;
|
|
bool sel = (item == _dm_hist_sel);
|
|
int y = HIST_START_Y + i * HIST_ITEM_H;
|
|
|
|
int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, item);
|
|
if (ring_pos < 0) continue;
|
|
|
|
const DmHistEntry& e = _dm_hist[ring_pos];
|
|
const char* sender = e.outgoing ? "Me" : filtered_name;
|
|
|
|
char age[6]; fmtMsgAge(age, sizeof(age), e.timestamp);
|
|
int age_w = age[0] ? display.getTextWidth(age) + 3 : 0;
|
|
|
|
if (sel) {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.fillRect(0, y, display.width(), HIST_BOX_H);
|
|
display.setColor(DisplayDriver::DARK);
|
|
display.drawTextEllipsized(3, y + 1, display.width() - 6 - age_w, sender);
|
|
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
|
|
display.drawTextEllipsized(3, y + 10, display.width() - 6, skipReplyPrefix(e.text));
|
|
} else {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.drawRect(0, y, display.width(), HIST_BOX_H);
|
|
display.fillRect(1, y + 1, display.width() - 2, 8);
|
|
display.setColor(DisplayDriver::DARK);
|
|
display.drawTextEllipsized(3, y + 1, display.width() - 6 - age_w, sender);
|
|
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.drawTextEllipsized(3, y + 10, display.width() - 6, skipReplyPrefix(e.text));
|
|
}
|
|
}
|
|
|
|
if (dm_count == 0) {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.drawTextCentered(display.width()/2, 32, "No messages yet");
|
|
}
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
if (_dm_hist_scroll > 0) {
|
|
display.setCursor(display.width() - 6, HIST_START_Y + 1);
|
|
display.print("^");
|
|
}
|
|
if (_dm_hist_scroll + HIST_VISIBLE < dm_count) {
|
|
display.setCursor(display.width() - 6, HIST_START_Y + HIST_VISIBLE * HIST_ITEM_H - 10);
|
|
display.print("v");
|
|
}
|
|
|
|
bool compose_sel = (_dm_hist_sel == -1);
|
|
const char* ctxt = "[+ send]";
|
|
int ctw = display.getTextWidth(ctxt);
|
|
int cbx = 1, cby = 55;
|
|
if (compose_sel) {
|
|
display.fillRect(cbx, cby - 1, ctw + 4, 9);
|
|
display.setColor(DisplayDriver::DARK);
|
|
}
|
|
display.setCursor(cbx + 2, cby);
|
|
display.print(ctxt);
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
|
return dm_count > 0 ? 500 : 2000;
|
|
|
|
} else if (_phase == CHANNEL_HIST) {
|
|
if (_fs.active && _hist_sel >= 0) {
|
|
int fs_hist_count = histCountForChannel(_sel_channel_idx);
|
|
int ring_pos = histEntryForChannel(_sel_channel_idx, _hist_sel);
|
|
if (ring_pos >= 0) {
|
|
const char* ftext = _hist[ring_pos].text;
|
|
const char* fsep = strstr(ftext, ": ");
|
|
char fsender[22], fmsg[140];
|
|
if (fsep) {
|
|
int nl = fsep - ftext; if (nl > 21) nl = 21;
|
|
strncpy(fsender, ftext, nl); fsender[nl] = '\0';
|
|
strncpy(fmsg, fsep + 2, sizeof(fmsg) - 1); fmsg[sizeof(fmsg)-1] = '\0';
|
|
} else {
|
|
strncpy(fsender, "?", sizeof(fsender));
|
|
strncpy(fmsg, ftext, sizeof(fmsg) - 1); fmsg[sizeof(fmsg)-1] = '\0';
|
|
}
|
|
int ret = _fs.render(display, fsender, fmsg,
|
|
_hist_sel < fs_hist_count - 1,
|
|
_hist_sel > 0);
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
|
return ret;
|
|
}
|
|
return 2000;
|
|
}
|
|
|
|
ChannelDetails ch;
|
|
the_mesh.getChannel(_sel_channel_idx, ch);
|
|
char title[24];
|
|
snprintf(title, sizeof(title), "%.23s", ch.name);
|
|
display.drawTextCentered(display.width()/2, 0, title);
|
|
display.fillRect(0, 9, display.width(), 1);
|
|
|
|
int ch_hist_count = histCountForChannel(_sel_channel_idx);
|
|
|
|
for (int i = 0; i < HIST_VISIBLE && (_hist_scroll + i) < ch_hist_count; i++) {
|
|
int item = _hist_scroll + i;
|
|
bool sel = (item == _hist_sel);
|
|
int y = HIST_START_Y + i * HIST_ITEM_H;
|
|
|
|
int ring_pos = histEntryForChannel(_sel_channel_idx, item);
|
|
if (ring_pos < 0) continue;
|
|
|
|
const char* text = _hist[ring_pos].text;
|
|
const char* sep = strstr(text, ": ");
|
|
char sender[22], msg_part[79];
|
|
if (sep) {
|
|
int nl = sep - text; if (nl > 21) nl = 21;
|
|
strncpy(sender, text, nl); sender[nl] = '\0';
|
|
strncpy(msg_part, sep + 2, sizeof(msg_part) - 1);
|
|
msg_part[sizeof(msg_part) - 1] = '\0';
|
|
} else {
|
|
strncpy(sender, "?", sizeof(sender));
|
|
strncpy(msg_part, text, sizeof(msg_part) - 1);
|
|
msg_part[sizeof(msg_part) - 1] = '\0';
|
|
}
|
|
|
|
char age[6]; fmtMsgAge(age, sizeof(age), _hist[ring_pos].timestamp);
|
|
int age_w = age[0] ? display.getTextWidth(age) + 3 : 0;
|
|
|
|
if (sel) {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.fillRect(0, y, display.width(), HIST_BOX_H);
|
|
display.setColor(DisplayDriver::DARK);
|
|
display.drawTextEllipsized(3, y + 1, display.width() - 6 - age_w, sender);
|
|
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
|
|
display.drawTextEllipsized(3, y + 10, display.width() - 6, skipReplyPrefix(msg_part));
|
|
} else {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.drawRect(0, y, display.width(), HIST_BOX_H);
|
|
display.fillRect(1, y + 1, display.width() - 2, 8);
|
|
display.setColor(DisplayDriver::DARK);
|
|
display.drawTextEllipsized(3, y + 1, display.width() - 6 - age_w, sender);
|
|
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.drawTextEllipsized(3, y + 10, display.width() - 6, skipReplyPrefix(msg_part));
|
|
}
|
|
}
|
|
|
|
if (ch_hist_count == 0) {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.drawTextCentered(display.width()/2, 32, "No messages yet");
|
|
}
|
|
|
|
// scroll hints
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
if (_hist_scroll > 0) {
|
|
display.setCursor(display.width() - 6, HIST_START_Y + 1);
|
|
display.print("^");
|
|
}
|
|
if (_hist_scroll + HIST_VISIBLE < ch_hist_count) {
|
|
display.setCursor(display.width() - 6, HIST_START_Y + HIST_VISIBLE * HIST_ITEM_H - 10);
|
|
display.print("v");
|
|
}
|
|
|
|
// small compose button (bottom-left, always bordered, inverted when selected)
|
|
bool compose_sel = (_hist_sel == -1);
|
|
const char* ctxt = "[+ send]";
|
|
int ctw = display.getTextWidth(ctxt);
|
|
int cbx = 1, cby = 55;
|
|
if (compose_sel) {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.fillRect(cbx - 1, cby - 1, ctw + 4, 10);
|
|
display.setColor(DisplayDriver::DARK);
|
|
} else {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.drawRect(cbx - 1, cby - 1, ctw + 4, 10);
|
|
}
|
|
display.setCursor(cbx + 1, cby);
|
|
display.print(ctxt);
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
|
|
|
} else if (_phase == KEYBOARD) {
|
|
return _kb.render(display);
|
|
|
|
} else { // MSG_PICK
|
|
char title[24];
|
|
if (_reply_mode) {
|
|
int rlen = (int)strlen(_reply_prefix) - 4; // exclude "@[" and "] "
|
|
if (rlen < 0) rlen = 0;
|
|
if (rlen > 20) rlen = 20;
|
|
char nick_raw[32], nick_trans[32];
|
|
snprintf(nick_raw, sizeof(nick_raw), "%.*s", rlen, _reply_prefix + 2);
|
|
display.translateUTF8ToBlocks(nick_trans, nick_raw, sizeof(nick_trans));
|
|
snprintf(title, sizeof(title), "RE:%s", nick_trans);
|
|
} else if (_sending_to_channel) {
|
|
ChannelDetails ch;
|
|
the_mesh.getChannel(_sel_channel_idx, ch);
|
|
snprintf(title, sizeof(title), "%.23s", ch.name);
|
|
} else {
|
|
snprintf(title, sizeof(title), "TO:%.14s", _sel_contact.name);
|
|
}
|
|
display.drawTextCentered(display.width()/2, 0, title);
|
|
display.fillRect(0, 10, display.width(), 1);
|
|
|
|
int total_msg_items = 1 + _active_msg_count;
|
|
for (int i = 0; i < VISIBLE && (_msg_scroll+i) < total_msg_items; i++) {
|
|
int idx = _msg_scroll + i;
|
|
bool sel = (idx == _msg_sel);
|
|
int y = START_Y + i * ITEM_H;
|
|
|
|
if (sel) {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
display.fillRect(0, y - 1, display.width(), ITEM_H - 1);
|
|
display.setColor(DisplayDriver::DARK);
|
|
} else {
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
}
|
|
display.setCursor(0, y);
|
|
display.print(sel ? ">" : " ");
|
|
|
|
if (idx == 0) {
|
|
display.setCursor(8, y);
|
|
display.print("Custom message...");
|
|
} else {
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
int slot = _active_msgs[idx - 1];
|
|
const char* tmpl = p ? p->custom_msgs[slot] : "";
|
|
display.drawTextEllipsized(8, y, display.width() - 10, tmpl);
|
|
}
|
|
}
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
renderScrollHints(display, _msg_scroll, total_msg_items);
|
|
}
|
|
return 2000;
|
|
}
|
|
|
|
bool handleInput(char c) override {
|
|
if (_phase == MODE_SELECT) {
|
|
if (c == KEY_CANCEL) { _task->gotoHomeScreen(); return true; }
|
|
if (c == KEY_UP && _mode_sel > 0) { _mode_sel--; return true; }
|
|
if (c == KEY_DOWN && _mode_sel < 2) { _mode_sel++; return true; }
|
|
if (c == KEY_ENTER) {
|
|
if (_mode_sel == 1) {
|
|
_phase = CHANNEL_PICK;
|
|
} else {
|
|
_room_mode = (_mode_sel == 2);
|
|
if (_room_mode) _task->clearRoomUnread();
|
|
buildContactList();
|
|
_contact_sel = _contact_scroll = 0;
|
|
_phase = CONTACT_PICK;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} else if (_phase == CONTACT_PICK) {
|
|
// Context menu consumes all input while open
|
|
if (_ctx_menu.active) {
|
|
auto res = _ctx_menu.handleInput(c);
|
|
if (res == PopupMenu::SELECTED && _num_contacts > 0) {
|
|
ContactInfo ci;
|
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], ci)) {
|
|
if (_ctx_menu.selectedIndex() == 0) {
|
|
_task->clearDMUnread(ci.id.pub_key);
|
|
} else if (_ctx_menu.selectedIndex() == 1) {
|
|
uint8_t nstate = dmNotifState(ci.id.pub_key);
|
|
setDmNotifState(ci.id.pub_key, (nstate + 1) % 3);
|
|
_ctx_dirty = true;
|
|
} else {
|
|
uint8_t slot = dmMelodySlot(ci.id.pub_key);
|
|
setDmMelody(ci.id.pub_key, (slot + 1) % 3);
|
|
_ctx_dirty = true;
|
|
}
|
|
}
|
|
}
|
|
if (res != PopupMenu::NONE && _ctx_dirty) {
|
|
the_mesh.savePrefs(); _ctx_dirty = false;
|
|
}
|
|
return true;
|
|
}
|
|
if (c == KEY_CANCEL) { _room_mode = false; _phase = MODE_SELECT; return true; }
|
|
if (c == KEY_UP && _contact_sel > 0) {
|
|
_contact_sel--;
|
|
if (_contact_sel < _contact_scroll) _contact_scroll = _contact_sel;
|
|
return true;
|
|
}
|
|
if (c == KEY_DOWN && _contact_sel < _num_contacts - 1) {
|
|
_contact_sel++;
|
|
if (_contact_sel >= _contact_scroll + VISIBLE) _contact_scroll = _contact_sel - VISIBLE + 1;
|
|
return true;
|
|
}
|
|
if (c == KEY_ENTER && _num_contacts > 0) {
|
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) {
|
|
_task->clearDMUnread(_sel_contact.id.pub_key);
|
|
_dm_hist_sel = -1;
|
|
_dm_hist_scroll = 0;
|
|
_dm_fs.active = false;
|
|
_phase = DM_HIST;
|
|
}
|
|
return true;
|
|
}
|
|
if (c == KEY_CONTEXT_MENU && _num_contacts > 0 && !_room_mode) {
|
|
static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" };
|
|
ContactInfo ci;
|
|
the_mesh.getContactByIdx(_sorted[_contact_sel], ci);
|
|
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s",
|
|
NOTIF_LABELS[dmNotifState(ci.id.pub_key)]);
|
|
{ static const char* ML[] = { "global", "M1", "M2" };
|
|
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s",
|
|
ML[dmMelodySlot(ci.id.pub_key)]); }
|
|
_ctx_menu.begin("Contact options", 3);
|
|
_ctx_menu.addItem("Mark as read");
|
|
_ctx_menu.addItem(_ctx_notif_item);
|
|
_ctx_menu.addItem(_ctx_melody_item);
|
|
_ctx_dirty = false;
|
|
return true;
|
|
}
|
|
|
|
} else if (_phase == CHANNEL_PICK) {
|
|
// Context menu consumes all input while open
|
|
if (_ctx_menu.active) {
|
|
auto res = _ctx_menu.handleInput(c);
|
|
if (res == PopupMenu::SELECTED && _num_channels > 0) {
|
|
uint8_t ch_idx = _channel_indices[_channel_sel];
|
|
if (_ctx_menu.selectedIndex() == 0) {
|
|
_ch_unread[ch_idx] = 0;
|
|
} else if (_ctx_menu.selectedIndex() == 1) {
|
|
uint8_t nstate = chNotifState(ch_idx);
|
|
setChNotifState(ch_idx, (nstate + 1) % 3);
|
|
_ctx_dirty = true;
|
|
} else {
|
|
uint8_t slot = chNotifMelody(ch_idx);
|
|
setChNotifMelody(ch_idx, (slot + 1) % 3);
|
|
_ctx_dirty = true;
|
|
}
|
|
}
|
|
if (res != PopupMenu::NONE && _ctx_dirty) {
|
|
the_mesh.savePrefs(); _ctx_dirty = false;
|
|
}
|
|
return true;
|
|
}
|
|
if (c == KEY_CANCEL) { _phase = MODE_SELECT; return true; }
|
|
if (c == KEY_UP && _channel_sel > 0) {
|
|
_channel_sel--;
|
|
if (_channel_sel < _channel_scroll) _channel_scroll = _channel_sel;
|
|
return true;
|
|
}
|
|
if (c == KEY_DOWN && _channel_sel < _num_channels - 1) {
|
|
_channel_sel++;
|
|
if (_channel_sel >= _channel_scroll + VISIBLE) _channel_scroll = _channel_sel - VISIBLE + 1;
|
|
return true;
|
|
}
|
|
if (c == KEY_ENTER && _num_channels > 0) {
|
|
_sel_channel_idx = _channel_indices[_channel_sel];
|
|
int hc = histCountForChannel(_sel_channel_idx);
|
|
_unread_at_entry = (int)_ch_unread[_sel_channel_idx];
|
|
_hist_scroll = 0;
|
|
_hist_sel = hc > 0 ? 0 : -1;
|
|
_viewing_max_seen = _hist_sel >= 0 ? _hist_sel : 0;
|
|
_phase = CHANNEL_HIST;
|
|
updateChannelUnread();
|
|
return true;
|
|
}
|
|
if (c == KEY_CONTEXT_MENU && _num_channels > 0) {
|
|
uint8_t ch_idx = _channel_indices[_channel_sel];
|
|
static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" };
|
|
snprintf(_ctx_notif_item, sizeof(_ctx_notif_item), "Notif: %s",
|
|
NOTIF_LABELS[chNotifState(ch_idx)]);
|
|
{ static const char* ML[] = { "global", "M1", "M2" };
|
|
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s",
|
|
ML[chNotifMelody(ch_idx)]); }
|
|
_ctx_menu.begin("Channel options", 3);
|
|
_ctx_menu.addItem("Mark all read");
|
|
_ctx_menu.addItem(_ctx_notif_item);
|
|
_ctx_menu.addItem(_ctx_melody_item);
|
|
_ctx_dirty = false;
|
|
return true;
|
|
}
|
|
|
|
} else if (_phase == DM_HIST) {
|
|
int dm_count = dmHistCountForContact(_sel_contact.id.pub_key);
|
|
if (_dm_fs.active) {
|
|
if (_ctx_menu.active) {
|
|
auto res = _ctx_menu.handleInput(c);
|
|
if (res == PopupMenu::SELECTED) {
|
|
_dm_fs.active = false;
|
|
startReply(false);
|
|
} else if (res != PopupMenu::NONE) {
|
|
_ctx_menu.active = false;
|
|
}
|
|
return true;
|
|
}
|
|
auto res = _dm_fs.handleInput(c);
|
|
if (res == FullscreenMsgView::PREV) {
|
|
if (_dm_hist_sel < dm_count - 1) { _dm_hist_sel++; _dm_fs.scroll = 0; }
|
|
} else if (res == FullscreenMsgView::NEXT) {
|
|
if (_dm_hist_sel > 0) { _dm_hist_sel--; _dm_fs.scroll = 0; }
|
|
} else if (res == FullscreenMsgView::CLOSE) {
|
|
_dm_fs.active = false;
|
|
} else if (res == FullscreenMsgView::REPLY) {
|
|
int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel);
|
|
if (ring_pos >= 0 && !_dm_hist[ring_pos].outgoing) {
|
|
snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.31s] ", _sel_contact.name);
|
|
_ctx_menu.begin("Options", 1);
|
|
_ctx_menu.addItem("Reply");
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
if (_ctx_menu.active) {
|
|
auto res = _ctx_menu.handleInput(c);
|
|
if (res == PopupMenu::SELECTED) {
|
|
startReply(false);
|
|
} else if (res != PopupMenu::NONE) {
|
|
_ctx_menu.active = false;
|
|
}
|
|
return true;
|
|
}
|
|
if (c == KEY_CANCEL) { _phase = CONTACT_PICK; return true; }
|
|
if (c == KEY_UP) {
|
|
if (_dm_hist_sel > 0) {
|
|
_dm_hist_sel--;
|
|
if (_dm_hist_sel < _dm_hist_scroll) _dm_hist_scroll = _dm_hist_sel;
|
|
} else if (_dm_hist_sel == 0) {
|
|
_dm_hist_sel = -1;
|
|
}
|
|
return true;
|
|
}
|
|
if (c == KEY_DOWN) {
|
|
if (_dm_hist_sel == -1 && dm_count > 0) { _dm_hist_sel = 0; _dm_hist_scroll = 0; }
|
|
else if (_dm_hist_sel >= 0 && _dm_hist_sel < dm_count - 1) {
|
|
_dm_hist_sel++;
|
|
if (_dm_hist_sel >= _dm_hist_scroll + HIST_VISIBLE)
|
|
_dm_hist_scroll = _dm_hist_sel - HIST_VISIBLE + 1;
|
|
}
|
|
return true;
|
|
}
|
|
if (c == KEY_ENTER) {
|
|
if (_dm_hist_sel >= 0) {
|
|
_dm_fs.begin();
|
|
} else {
|
|
_sending_to_channel = false;
|
|
setupMsgPick();
|
|
_phase = MSG_PICK;
|
|
}
|
|
return true;
|
|
}
|
|
if (c == KEY_CONTEXT_MENU && _dm_hist_sel >= 0) {
|
|
int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_sel);
|
|
if (ring_pos >= 0 && !_dm_hist[ring_pos].outgoing) {
|
|
snprintf(_reply_prefix, sizeof(_reply_prefix), "@[%.31s] ", _sel_contact.name);
|
|
_ctx_menu.begin("Options", 1);
|
|
_ctx_menu.addItem("Reply");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} else if (_phase == CHANNEL_HIST) {
|
|
int ch_hist_count = histCountForChannel(_sel_channel_idx);
|
|
if (_fs.active) {
|
|
if (_ctx_menu.active) {
|
|
auto res = _ctx_menu.handleInput(c);
|
|
if (res == PopupMenu::SELECTED) {
|
|
_fs.active = false;
|
|
startReply(true);
|
|
} else if (res != PopupMenu::NONE) {
|
|
_ctx_menu.active = false;
|
|
}
|
|
return true;
|
|
}
|
|
auto res = _fs.handleInput(c);
|
|
if (res == FullscreenMsgView::PREV) {
|
|
if (_hist_sel < ch_hist_count - 1) { _hist_sel++; _fs.scroll = 0; updateChannelUnread(); }
|
|
} else if (res == FullscreenMsgView::NEXT) {
|
|
if (_hist_sel > 0) { _hist_sel--; _fs.scroll = 0; updateChannelUnread(); }
|
|
} else if (res == FullscreenMsgView::CLOSE) {
|
|
_fs.active = false;
|
|
} else if (res == FullscreenMsgView::REPLY) {
|
|
int ring_pos = histEntryForChannel(_sel_channel_idx, _hist_sel);
|
|
if (ring_pos >= 0 && buildChannelReplyPrefix(_hist[ring_pos].text)) {
|
|
_ctx_menu.begin("Options", 1);
|
|
_ctx_menu.addItem("Reply");
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
if (_ctx_menu.active) {
|
|
auto res = _ctx_menu.handleInput(c);
|
|
if (res == PopupMenu::SELECTED) {
|
|
startReply(true);
|
|
} else if (res != PopupMenu::NONE) {
|
|
_ctx_menu.active = false;
|
|
}
|
|
return true;
|
|
}
|
|
if (c == KEY_CANCEL) { _phase = CHANNEL_PICK; return true; }
|
|
if (c == KEY_UP) {
|
|
if (_hist_sel > 0) { _hist_sel--; if (_hist_sel < _hist_scroll) _hist_scroll = _hist_sel; }
|
|
else if (_hist_sel == 0) _hist_sel = -1;
|
|
updateChannelUnread();
|
|
return true;
|
|
}
|
|
if (c == KEY_DOWN) {
|
|
if (_hist_sel == -1 && ch_hist_count > 0) { _hist_sel = 0; _hist_scroll = 0; }
|
|
else if (_hist_sel >= 0 && _hist_sel < ch_hist_count - 1) {
|
|
_hist_sel++;
|
|
if (_hist_sel >= _hist_scroll + HIST_VISIBLE) _hist_scroll = _hist_sel - HIST_VISIBLE + 1;
|
|
}
|
|
updateChannelUnread();
|
|
return true;
|
|
}
|
|
if (c == KEY_ENTER) {
|
|
if (_hist_sel >= 0) {
|
|
_fs.begin();
|
|
updateChannelUnread();
|
|
} else {
|
|
_sending_to_channel = true;
|
|
setupMsgPick();
|
|
_phase = MSG_PICK;
|
|
}
|
|
return true;
|
|
}
|
|
if (c == KEY_CONTEXT_MENU && _hist_sel >= 0) {
|
|
int ring_pos = histEntryForChannel(_sel_channel_idx, _hist_sel);
|
|
if (ring_pos >= 0 && buildChannelReplyPrefix(_hist[ring_pos].text)) {
|
|
_ctx_menu.begin("Options", 1);
|
|
_ctx_menu.addItem("Reply");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} else if (_phase == KEYBOARD) {
|
|
auto res = _kb.handleInput(c);
|
|
if (res == KeyboardWidget::CANCELLED) {
|
|
_phase = MSG_PICK;
|
|
} else if (res == KeyboardWidget::DONE) {
|
|
int min_len = _reply_mode ? (int)strlen(_reply_prefix) : 0;
|
|
if (_kb.len > min_len) {
|
|
char expanded[KB_MAX_LEN + 1];
|
|
expandMsg(_kb.buf, expanded, sizeof(expanded));
|
|
bool ok = sendText(expanded);
|
|
afterSend(ok, expanded);
|
|
}
|
|
}
|
|
return true;
|
|
|
|
} else { // MSG_PICK
|
|
int total_msg_items = 1 + _active_msg_count;
|
|
if (c == KEY_CANCEL) {
|
|
_reply_mode = false;
|
|
_phase = _sending_to_channel ? CHANNEL_HIST : DM_HIST;
|
|
return true;
|
|
}
|
|
if (c == KEY_UP && _msg_sel > 0) {
|
|
_msg_sel--;
|
|
if (_msg_sel < _msg_scroll) _msg_scroll = _msg_sel;
|
|
return true;
|
|
}
|
|
if (c == KEY_DOWN && _msg_sel < total_msg_items - 1) {
|
|
_msg_sel++;
|
|
if (_msg_sel >= _msg_scroll + VISIBLE) _msg_scroll = _msg_sel - VISIBLE + 1;
|
|
return true;
|
|
}
|
|
if (c == KEY_ENTER) {
|
|
if (_msg_sel == 0) {
|
|
_kb.begin(_reply_mode ? _reply_prefix : "");
|
|
kbAddSensorPlaceholders(_kb, &sensors);
|
|
_phase = KEYBOARD;
|
|
return true;
|
|
}
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
int slot = _active_msgs[_msg_sel - 1];
|
|
const char* tmpl = p ? p->custom_msgs[slot] : "OK";
|
|
char msg[140];
|
|
if (_reply_mode) {
|
|
char body[140];
|
|
expandMsg(tmpl, body, sizeof(body));
|
|
snprintf(msg, sizeof(msg), "%s%s", _reply_prefix, body);
|
|
} else {
|
|
expandMsg(tmpl, msg, sizeof(msg));
|
|
}
|
|
bool ok = sendText(msg);
|
|
afterSend(ok, msg);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|