mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-08-27 21:19:46 +00:00
Carry conversation timestamps into the legacy pager list, force names/previews/time/unread labels to single-line bounded widths, keep MeshCore unknown senders from rendering as Me, show protocol prefixes in Contacts, and rebind the shared two-pane navigation after refresh so Team/Chat/Contacts filters become reachable.
This commit is contained in:
@@ -44,6 +44,7 @@ enum class MessageFailureKind : uint8_t
|
||||
{
|
||||
None,
|
||||
PeerKeyMissing,
|
||||
ChannelKeyMissing,
|
||||
LocalIdentityMissing,
|
||||
RadioSendFailed,
|
||||
AckTimeout,
|
||||
|
||||
@@ -19,6 +19,7 @@ struct ConversationRow
|
||||
ui::FixedText<64> subtitle;
|
||||
|
||||
uint16_t unread_count = 0;
|
||||
uint32_t last_timestamp = 0;
|
||||
bool selected = false;
|
||||
bool muted = false;
|
||||
|
||||
@@ -128,6 +129,7 @@ inline void resetConversationRow(ConversationRow& row)
|
||||
row.title.clear();
|
||||
row.subtitle.clear();
|
||||
row.unread_count = 0;
|
||||
row.last_timestamp = 0;
|
||||
row.selected = false;
|
||||
row.muted = false;
|
||||
row.kind = ConversationKind::None;
|
||||
|
||||
@@ -14,6 +14,13 @@ enum class UiActionFailure : uint8_t
|
||||
Unsupported,
|
||||
Busy,
|
||||
StorageError,
|
||||
PeerKeyMissing,
|
||||
ChannelKeyMissing,
|
||||
TxDisabled,
|
||||
RadioOffline,
|
||||
DutyCycleLimited,
|
||||
LocalIdentityMissing,
|
||||
RadioTxFailed,
|
||||
};
|
||||
|
||||
struct UiActionResult
|
||||
|
||||
@@ -35,6 +35,7 @@ int main()
|
||||
ui::chat::ChatWorkspaceSnapshot reset_probe;
|
||||
reset_probe.header.valid = true;
|
||||
reset_probe.conversation_count = 1;
|
||||
reset_probe.conversations[0].last_timestamp = 1234;
|
||||
reset_probe.conversations[0].last_delivery =
|
||||
ui::chat::MessageDeliveryState::Delivered;
|
||||
reset_probe.message_count = 1;
|
||||
@@ -48,6 +49,7 @@ int main()
|
||||
ui::chat::resetChatWorkspaceSnapshot(reset_probe);
|
||||
assert(!reset_probe.header.valid);
|
||||
assert(reset_probe.conversation_count == 0);
|
||||
assert(reset_probe.conversations[0].last_timestamp == 0);
|
||||
assert(reset_probe.conversations[0].last_delivery ==
|
||||
ui::chat::MessageDeliveryState::Unknown);
|
||||
assert(reset_probe.message_count == 0);
|
||||
|
||||
@@ -775,21 +775,7 @@ void on_ui_refreshed(Binding* binding)
|
||||
{
|
||||
if (!binding_alive(binding) || !binding->group) return;
|
||||
lv_group_set_editing(binding->group, false);
|
||||
prepare_touch_routing(binding);
|
||||
|
||||
if (binding->column == FocusColumn::List)
|
||||
{
|
||||
rebind_by_column(binding);
|
||||
return;
|
||||
}
|
||||
|
||||
lv_obj_t* focused = lv_group_get_focused(binding->group);
|
||||
if (is_visible(focused))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bind_filter_column(binding);
|
||||
rebind_by_column(binding);
|
||||
}
|
||||
|
||||
void focus_filter(Binding* binding)
|
||||
|
||||
@@ -26,7 +26,7 @@ void copyNodeLabel(ui::FixedText<32>& out, ::chat::NodeId node_id)
|
||||
{
|
||||
if (node_id == 0)
|
||||
{
|
||||
ui::copyText(out, "Me");
|
||||
ui::copyText(out, "Unknown");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,8 @@ ui::chat::MessageFailureKind mapDeliveryFailure(
|
||||
return ui::chat::MessageFailureKind::None;
|
||||
case ::chat::delivery::DeliveryFailureKind::PeerKeyMissing:
|
||||
return ui::chat::MessageFailureKind::PeerKeyMissing;
|
||||
case ::chat::delivery::DeliveryFailureKind::ChannelKeyMissing:
|
||||
return ui::chat::MessageFailureKind::ChannelKeyMissing;
|
||||
case ::chat::delivery::DeliveryFailureKind::LocalIdentityMissing:
|
||||
return ui::chat::MessageFailureKind::LocalIdentityMissing;
|
||||
case ::chat::delivery::DeliveryFailureKind::RadioSendFailed:
|
||||
@@ -141,6 +143,7 @@ bool ChatPresentationSource::buildChatWorkspaceSnapshot(
|
||||
row.kind = row.id.kind;
|
||||
row.protocol = row.id.protocol;
|
||||
row.unread_count = meta.unread < 0 ? 0U : static_cast<uint16_t>(meta.unread);
|
||||
row.last_timestamp = meta.last_timestamp;
|
||||
row.selected = row.id == request.selected;
|
||||
copyString(row.title, meta.name);
|
||||
copyString(row.subtitle, meta.preview);
|
||||
|
||||
@@ -6,6 +6,45 @@
|
||||
|
||||
namespace ui::presentation_sources
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
ui::UiActionFailure mapMeshFailure(::chat::MeshOperationFailure failure)
|
||||
{
|
||||
switch (failure)
|
||||
{
|
||||
case ::chat::MeshOperationFailure::InvalidInput:
|
||||
return ui::UiActionFailure::InvalidInput;
|
||||
case ::chat::MeshOperationFailure::Unsupported:
|
||||
return ui::UiActionFailure::Unsupported;
|
||||
case ::chat::MeshOperationFailure::NotReady:
|
||||
return ui::UiActionFailure::NotReady;
|
||||
case ::chat::MeshOperationFailure::TxDisabled:
|
||||
return ui::UiActionFailure::TxDisabled;
|
||||
case ::chat::MeshOperationFailure::RadioOffline:
|
||||
return ui::UiActionFailure::RadioOffline;
|
||||
case ::chat::MeshOperationFailure::DutyCycleLimited:
|
||||
return ui::UiActionFailure::DutyCycleLimited;
|
||||
case ::chat::MeshOperationFailure::LocalIdentityMissing:
|
||||
return ui::UiActionFailure::LocalIdentityMissing;
|
||||
case ::chat::MeshOperationFailure::PeerKeyMissing:
|
||||
return ui::UiActionFailure::PeerKeyMissing;
|
||||
case ::chat::MeshOperationFailure::ChannelKeyMissing:
|
||||
return ui::UiActionFailure::ChannelKeyMissing;
|
||||
case ::chat::MeshOperationFailure::Busy:
|
||||
return ui::UiActionFailure::Busy;
|
||||
case ::chat::MeshOperationFailure::RadioTxFailed:
|
||||
return ui::UiActionFailure::RadioTxFailed;
|
||||
case ::chat::MeshOperationFailure::EncodeFailed:
|
||||
case ::chat::MeshOperationFailure::CryptoFailed:
|
||||
case ::chat::MeshOperationFailure::Unknown:
|
||||
case ::chat::MeshOperationFailure::None:
|
||||
default:
|
||||
return ui::UiActionFailure::Rejected;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LegacyChatActionSink::LegacyChatActionSink(::chat::ChatService& chat_service)
|
||||
: chat_service_(chat_service)
|
||||
@@ -41,14 +80,14 @@ ui::UiActionResult LegacyChatActionSink::sendMessage(
|
||||
}
|
||||
|
||||
const std::string text(message.text, message.text_len);
|
||||
const ::chat::MessageId msg_id =
|
||||
chat_service_.sendText(core_id.channel, text, core_id.peer);
|
||||
if (msg_id == 0)
|
||||
const ::chat::MeshSendResult result =
|
||||
chat_service_.sendTextDetailed(core_id.channel, text, core_id.peer);
|
||||
if (!result.ok || result.msg_id == 0)
|
||||
{
|
||||
return ui::UiActionResult::fail(ui::UiActionFailure::Rejected);
|
||||
return ui::UiActionResult::fail(mapMeshFailure(result.failure));
|
||||
}
|
||||
|
||||
const ::chat::ChatMessage* sent = chat_service_.getMessage(msg_id);
|
||||
const ::chat::ChatMessage* sent = chat_service_.getMessage(result.msg_id);
|
||||
if (sent != nullptr && sent->status == ::chat::MessageStatus::Failed)
|
||||
{
|
||||
return ui::UiActionResult::fail(ui::UiActionFailure::Rejected);
|
||||
|
||||
@@ -214,6 +214,7 @@ bool TeamChatPresentationSource::buildChatWorkspaceSnapshot(
|
||||
(void)projector.project(entries.back(), display);
|
||||
ui::copyText(conversation.subtitle,
|
||||
display.summary.c_str());
|
||||
conversation.last_timestamp = entries.back().ts;
|
||||
conversation.last_delivery = entries.back().incoming
|
||||
? ui::chat::MessageDeliveryState::Received
|
||||
: ui::chat::MessageDeliveryState::Sent;
|
||||
|
||||
@@ -577,7 +577,7 @@ void ChatConversationScreen::createMessageItem(const ::ui::chat::MessageRow& row
|
||||
}
|
||||
else if (!row.sender_label.empty())
|
||||
{
|
||||
sender = row.sender_label.c_str();
|
||||
sender = inferred_sender.empty() ? row.sender_label.c_str() : inferred_sender;
|
||||
}
|
||||
else if (row.sender_node_id == 0)
|
||||
{
|
||||
|
||||
@@ -38,10 +38,13 @@ struct Metrics
|
||||
int filter_button_height = 28;
|
||||
int list_item_height = 28;
|
||||
int name_x = 10;
|
||||
int preview_x = 120;
|
||||
int preview_width = 130;
|
||||
int name_width = 96;
|
||||
int preview_x = 112;
|
||||
int preview_width = 112;
|
||||
int unread_x = 42;
|
||||
int unread_width = 28;
|
||||
int time_x = 10;
|
||||
int time_width = 38;
|
||||
};
|
||||
|
||||
Metrics current_metrics()
|
||||
@@ -54,10 +57,13 @@ Metrics current_metrics()
|
||||
if (profile.large_touch_hitbox)
|
||||
{
|
||||
metrics.name_x = 16;
|
||||
metrics.name_width = 146;
|
||||
metrics.preview_x = 170;
|
||||
metrics.preview_width = 280;
|
||||
metrics.preview_width = 236;
|
||||
metrics.unread_x = 72;
|
||||
metrics.unread_width = 44;
|
||||
metrics.time_x = 16;
|
||||
metrics.time_width = 52;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -135,10 +141,19 @@ void format_time_hhmm(char out[16], uint32_t ts)
|
||||
|
||||
std::string truncate_preview(const std::string& text)
|
||||
{
|
||||
static constexpr size_t kMaxPreviewBytes = 18;
|
||||
if (text.size() <= kMaxPreviewBytes)
|
||||
std::string one_line = text;
|
||||
for (char& ch : one_line)
|
||||
{
|
||||
return text;
|
||||
if (ch == '\r' || ch == '\n' || ch == '\t')
|
||||
{
|
||||
ch = ' ';
|
||||
}
|
||||
}
|
||||
|
||||
const size_t kMaxPreviewBytes = ::ui::page_profile::current().large_touch_hitbox ? 42U : 24U;
|
||||
if (one_line.size() <= kMaxPreviewBytes)
|
||||
{
|
||||
return one_line;
|
||||
}
|
||||
|
||||
auto utf8_char_bytes = [](unsigned char lead) -> size_t
|
||||
@@ -163,9 +178,9 @@ std::string truncate_preview(const std::string& text)
|
||||
};
|
||||
|
||||
size_t safe_len = 0;
|
||||
while (safe_len < text.size())
|
||||
while (safe_len < one_line.size())
|
||||
{
|
||||
const size_t next = utf8_char_bytes(static_cast<unsigned char>(text[safe_len]));
|
||||
const size_t next = utf8_char_bytes(static_cast<unsigned char>(one_line[safe_len]));
|
||||
if (safe_len + next > kMaxPreviewBytes)
|
||||
{
|
||||
break;
|
||||
@@ -177,7 +192,7 @@ std::string truncate_preview(const std::string& text)
|
||||
safe_len = kMaxPreviewBytes;
|
||||
}
|
||||
|
||||
std::string out = text.substr(0, safe_len);
|
||||
std::string out = one_line.substr(0, safe_len);
|
||||
out.append("...");
|
||||
return out;
|
||||
}
|
||||
@@ -201,12 +216,20 @@ void replace_all(std::string& text, const char* from, const char* to)
|
||||
|
||||
std::string compact_list_name(const std::string& name)
|
||||
{
|
||||
if (!::ui::components::info_card::use_tdeck_layout())
|
||||
std::string compact = name;
|
||||
for (char& ch : compact)
|
||||
{
|
||||
return name;
|
||||
if (ch == '\r' || ch == '\n' || ch == '\t')
|
||||
{
|
||||
ch = ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if (!::ui::components::info_card::use_tdeck_layout())
|
||||
{
|
||||
return compact;
|
||||
}
|
||||
|
||||
std::string compact = name;
|
||||
replace_all(compact, ::ui::i18n::tr("Broadcast"), ::ui::i18n::tr("Bcast"));
|
||||
replace_all(compact, "Primary", "Pri");
|
||||
replace_all(compact, "Secondary", "Sec");
|
||||
@@ -233,6 +256,17 @@ void style_filter_label(lv_obj_t* label)
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_CLIP);
|
||||
}
|
||||
|
||||
void apply_single_line(lv_obj_t* label)
|
||||
{
|
||||
if (!label)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_DOT);
|
||||
::ui::components::info_card::apply_single_line_label(label);
|
||||
}
|
||||
|
||||
lv_obj_t* create_root(lv_obj_t* parent)
|
||||
{
|
||||
return ::ui::components::two_pane_layout::create_root(parent);
|
||||
@@ -342,20 +376,28 @@ MessageItemWidgets create_message_item(lv_obj_t* parent)
|
||||
w.name_label = lv_label_create(w.btn);
|
||||
lv_obj_add_flag(w.name_label, LV_OBJ_FLAG_EVENT_BUBBLE);
|
||||
lv_obj_align(w.name_label, LV_ALIGN_LEFT_MID, metrics.name_x, 0);
|
||||
lv_obj_set_width(w.name_label, metrics.name_width);
|
||||
apply_single_line(w.name_label);
|
||||
|
||||
w.preview_label = lv_label_create(w.btn);
|
||||
lv_obj_add_flag(w.preview_label, LV_OBJ_FLAG_EVENT_BUBBLE);
|
||||
lv_obj_align(w.preview_label, LV_ALIGN_LEFT_MID, metrics.preview_x, 0);
|
||||
lv_label_set_long_mode(w.preview_label, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_width(w.preview_label, metrics.preview_width);
|
||||
apply_single_line(w.preview_label);
|
||||
|
||||
w.time_label = lv_label_create(w.btn);
|
||||
lv_obj_add_flag(w.time_label, LV_OBJ_FLAG_EVENT_BUBBLE);
|
||||
lv_obj_align(w.time_label, LV_ALIGN_RIGHT_MID, -metrics.time_x, 0);
|
||||
lv_obj_set_width(w.time_label, metrics.time_width);
|
||||
lv_obj_set_style_text_align(w.time_label, LV_TEXT_ALIGN_RIGHT, 0);
|
||||
apply_single_line(w.time_label);
|
||||
|
||||
w.unread_label = lv_label_create(w.btn);
|
||||
lv_obj_add_flag(w.unread_label, LV_OBJ_FLAG_EVENT_BUBBLE);
|
||||
lv_obj_align(w.unread_label, LV_ALIGN_RIGHT_MID, -metrics.unread_x, 0);
|
||||
lv_obj_set_width(w.unread_label, metrics.unread_width);
|
||||
lv_obj_set_style_text_align(w.unread_label, LV_TEXT_ALIGN_RIGHT, 0);
|
||||
apply_single_line(w.unread_label);
|
||||
}
|
||||
|
||||
return w;
|
||||
@@ -373,10 +415,22 @@ void populate_message_item(const MessageItemWidgets& widgets,
|
||||
{
|
||||
::ui::components::info_card::apply_single_line_label(widgets.name_label);
|
||||
}
|
||||
else
|
||||
{
|
||||
apply_single_line(widgets.name_label);
|
||||
}
|
||||
|
||||
const std::string preview = truncate_preview(conv.preview);
|
||||
lv_label_set_text(widgets.preview_label, preview.c_str());
|
||||
::ui::fonts::apply_chat_content_font(widgets.preview_label, preview.c_str());
|
||||
if (use_info_card)
|
||||
{
|
||||
::ui::components::info_card::apply_single_line_label(widgets.preview_label);
|
||||
}
|
||||
else
|
||||
{
|
||||
apply_single_line(widgets.preview_label);
|
||||
}
|
||||
|
||||
char time_buf[16];
|
||||
format_time_hhmm(time_buf, conv.last_timestamp);
|
||||
@@ -386,6 +440,10 @@ void populate_message_item(const MessageItemWidgets& widgets,
|
||||
{
|
||||
::ui::components::info_card::apply_single_line_label(widgets.time_label);
|
||||
}
|
||||
else
|
||||
{
|
||||
apply_single_line(widgets.time_label);
|
||||
}
|
||||
|
||||
if (conv.unread > 0)
|
||||
{
|
||||
@@ -403,6 +461,10 @@ void populate_message_item(const MessageItemWidgets& widgets,
|
||||
{
|
||||
::ui::components::info_card::apply_single_line_label(widgets.unread_label);
|
||||
}
|
||||
else
|
||||
{
|
||||
apply_single_line(widgets.unread_label);
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t* create_placeholder(lv_obj_t* parent)
|
||||
|
||||
@@ -112,7 +112,7 @@ chat::ConversationMeta legacyConversationMetaFromRow(
|
||||
meta.name = row.title.c_str();
|
||||
meta.preview = row.subtitle.c_str();
|
||||
meta.unread = static_cast<int>(row.unread_count);
|
||||
meta.last_timestamp = 0;
|
||||
meta.last_timestamp = row.last_timestamp;
|
||||
return meta;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ bool legacyTeamConversationMetaFromSnapshot(
|
||||
out.name = row.title.c_str();
|
||||
out.preview = row.subtitle.c_str();
|
||||
out.unread = static_cast<int>(row.unread_count);
|
||||
out.last_timestamp = 0;
|
||||
out.last_timestamp = row.last_timestamp;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -752,7 +752,39 @@ void UiController::handleSendMessage(const std::string& text)
|
||||
if (!result.ok)
|
||||
{
|
||||
const char* message = "Send failed";
|
||||
if (result.failure == ::ui::UiActionFailure::Unsupported)
|
||||
if (result.failure == ::ui::UiActionFailure::ChannelKeyMissing)
|
||||
{
|
||||
message = "Channel key missing";
|
||||
}
|
||||
else if (result.failure == ::ui::UiActionFailure::PeerKeyMissing)
|
||||
{
|
||||
message = "Peer key missing";
|
||||
}
|
||||
else if (result.failure == ::ui::UiActionFailure::TxDisabled)
|
||||
{
|
||||
message = "TX disabled";
|
||||
}
|
||||
else if (result.failure == ::ui::UiActionFailure::RadioOffline)
|
||||
{
|
||||
message = "Radio offline";
|
||||
}
|
||||
else if (result.failure == ::ui::UiActionFailure::DutyCycleLimited)
|
||||
{
|
||||
message = "TX rate limited";
|
||||
}
|
||||
else if (result.failure == ::ui::UiActionFailure::RadioTxFailed)
|
||||
{
|
||||
message = "Radio TX failed";
|
||||
}
|
||||
else if (result.failure == ::ui::UiActionFailure::LocalIdentityMissing)
|
||||
{
|
||||
message = "Identity missing";
|
||||
}
|
||||
else if (result.failure == ::ui::UiActionFailure::Busy)
|
||||
{
|
||||
message = "Radio busy";
|
||||
}
|
||||
else if (result.failure == ::ui::UiActionFailure::Unsupported)
|
||||
{
|
||||
message = "Conversation unsupported";
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
#include "ui/ui_theme.h"
|
||||
#include "ui_presentation/chat/chat_workspace_snapshot.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr lv_coord_t kActionBarHeight = 36;
|
||||
constexpr lv_coord_t kActionButtonHeight = 26;
|
||||
constexpr size_t kMaxPrefixedSenderLen = 20;
|
||||
|
||||
::ui::chat::MessageDeliveryState delivery_from_message_status(
|
||||
chat::MessageStatus status)
|
||||
@@ -64,6 +66,63 @@ std::string format_team_rich_payload_text(
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool sender_token_is_valid(const std::string& sender)
|
||||
{
|
||||
if (sender.empty() || sender.size() > kMaxPrefixedSenderLen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (char c : sender)
|
||||
{
|
||||
const unsigned char uc = static_cast<unsigned char>(c);
|
||||
if (!(std::isalnum(uc) || c == '_' || c == '-' || c == '.'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool split_prefixed_sender_text(const std::string& text,
|
||||
std::string* out_sender,
|
||||
std::string* out_body)
|
||||
{
|
||||
if (!out_sender || !out_body || text.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t sep = text.find(':');
|
||||
if (sep == std::string::npos || sep == 0 || sep > kMaxPrefixedSenderLen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string sender = text.substr(0, sep);
|
||||
while (!sender.empty() && sender.back() == ' ')
|
||||
{
|
||||
sender.pop_back();
|
||||
}
|
||||
if (!sender_token_is_valid(sender))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t body_start = sep + 1;
|
||||
while (body_start < text.size() && text[body_start] == ' ')
|
||||
{
|
||||
++body_start;
|
||||
}
|
||||
if (body_start >= text.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*out_sender = sender;
|
||||
*out_body = text.substr(body_start);
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace chat::ui
|
||||
@@ -182,13 +241,27 @@ void ChatConversationScreen::createMessageItem(const ::ui::chat::MessageRow& row
|
||||
LV_FLEX_ALIGN_START);
|
||||
|
||||
std::string display_text;
|
||||
const std::string body_text =
|
||||
std::string body_text =
|
||||
row.has_team_rich_payload
|
||||
? format_team_rich_payload_text(row.team_rich_payload)
|
||||
: std::string(row.text.c_str());
|
||||
if (!row.outgoing && !row.sender_label.empty())
|
||||
std::string sender_label = row.sender_label.c_str();
|
||||
if (!row.outgoing &&
|
||||
conv_.protocol == chat::MeshProtocol::MeshCore &&
|
||||
conv_.peer == 0 &&
|
||||
row.sender_node_id == 0)
|
||||
{
|
||||
display_text = row.sender_label.c_str();
|
||||
std::string parsed_sender;
|
||||
std::string parsed_body;
|
||||
if (split_prefixed_sender_text(body_text, &parsed_sender, &parsed_body))
|
||||
{
|
||||
sender_label = parsed_sender;
|
||||
body_text = parsed_body;
|
||||
}
|
||||
}
|
||||
if (!row.outgoing && !sender_label.empty())
|
||||
{
|
||||
display_text = sender_label;
|
||||
display_text += ": ";
|
||||
display_text += body_text;
|
||||
}
|
||||
|
||||
@@ -1952,6 +1952,44 @@ static void on_node_info_back_clicked(lv_event_t* /*e*/)
|
||||
close_node_info_screen();
|
||||
}
|
||||
|
||||
static const char* discovery_failure_message(chat::MeshOperationFailure failure,
|
||||
const char* fallback)
|
||||
{
|
||||
switch (failure)
|
||||
{
|
||||
case chat::MeshOperationFailure::NotReady:
|
||||
return "Mesh not ready";
|
||||
case chat::MeshOperationFailure::TxDisabled:
|
||||
return "TX disabled";
|
||||
case chat::MeshOperationFailure::RadioOffline:
|
||||
return "Radio offline";
|
||||
case chat::MeshOperationFailure::DutyCycleLimited:
|
||||
return "TX rate limited";
|
||||
case chat::MeshOperationFailure::LocalIdentityMissing:
|
||||
return "Identity missing";
|
||||
case chat::MeshOperationFailure::Busy:
|
||||
return "Radio busy";
|
||||
case chat::MeshOperationFailure::RadioTxFailed:
|
||||
return "Radio TX failed";
|
||||
case chat::MeshOperationFailure::EncodeFailed:
|
||||
return "Packet build failed";
|
||||
case chat::MeshOperationFailure::CryptoFailed:
|
||||
return "Signature failed";
|
||||
case chat::MeshOperationFailure::Unsupported:
|
||||
return "Unsupported";
|
||||
case chat::MeshOperationFailure::InvalidInput:
|
||||
return "Invalid action";
|
||||
case chat::MeshOperationFailure::PeerKeyMissing:
|
||||
return "Peer key missing";
|
||||
case chat::MeshOperationFailure::ChannelKeyMissing:
|
||||
return "Channel key missing";
|
||||
case chat::MeshOperationFailure::None:
|
||||
case chat::MeshOperationFailure::Unknown:
|
||||
break;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static void execute_discovery_command(uint8_t command_index)
|
||||
{
|
||||
DiscoveryActionSpec spec{};
|
||||
@@ -1990,10 +2028,14 @@ static void execute_discovery_command(uint8_t command_index)
|
||||
lv_timer_del(g_contacts_state.discover_scan_timer);
|
||||
g_contacts_state.discover_scan_timer = nullptr;
|
||||
}
|
||||
const bool ok = g_contacts_state.chat_service->triggerDiscoveryAction(chat::MeshDiscoveryAction::ScanLocal);
|
||||
if (!ok)
|
||||
const chat::MeshActionResult result =
|
||||
g_contacts_state.chat_service->triggerDiscoveryActionDetailed(
|
||||
chat::MeshDiscoveryAction::ScanLocal);
|
||||
if (!result.ok)
|
||||
{
|
||||
::ui::SystemNotification::show("Scan failed", 2000);
|
||||
::ui::SystemNotification::show(
|
||||
discovery_failure_message(result.failure, "Scan failed"),
|
||||
2000);
|
||||
return;
|
||||
}
|
||||
::ui::SystemNotification::show("Scanning 5s...", 1800);
|
||||
@@ -2006,8 +2048,9 @@ static void execute_discovery_command(uint8_t command_index)
|
||||
(spec.command == DiscoveryActionCommand::SendIdLocal)
|
||||
? chat::MeshDiscoveryAction::SendIdLocal
|
||||
: chat::MeshDiscoveryAction::SendIdBroadcast;
|
||||
const bool ok = g_contacts_state.chat_service->triggerDiscoveryAction(action);
|
||||
if (ok)
|
||||
const chat::MeshActionResult result =
|
||||
g_contacts_state.chat_service->triggerDiscoveryActionDetailed(action);
|
||||
if (result.ok)
|
||||
{
|
||||
::ui::SystemNotification::show(
|
||||
(spec.command == DiscoveryActionCommand::SendIdLocal) ? "ID local sent" : "ID bcast sent",
|
||||
@@ -2016,7 +2059,9 @@ static void execute_discovery_command(uint8_t command_index)
|
||||
else
|
||||
{
|
||||
::ui::SystemNotification::show(
|
||||
(spec.command == DiscoveryActionCommand::SendIdLocal) ? "ID local fail" : "ID bcast fail",
|
||||
discovery_failure_message(
|
||||
result.failure,
|
||||
(spec.command == DiscoveryActionCommand::SendIdLocal) ? "ID local fail" : "ID bcast fail"),
|
||||
2000);
|
||||
}
|
||||
}
|
||||
@@ -2547,22 +2592,10 @@ void refresh_ui()
|
||||
if (g_contacts_state.current_mode == ContactsMode::Contacts)
|
||||
{
|
||||
status_text = format_time_status(node.last_seen);
|
||||
const char* proto = node_protocol_short_label(node.protocol);
|
||||
if (proto[0] != '\0')
|
||||
{
|
||||
status_text += " ";
|
||||
status_text += proto;
|
||||
}
|
||||
}
|
||||
else if (g_contacts_state.current_mode == ContactsMode::Nearby)
|
||||
{
|
||||
status_text = format_time_status(node.last_seen);
|
||||
const char* proto = node_protocol_short_label(node.protocol);
|
||||
if (proto[0] != '\0')
|
||||
{
|
||||
status_text += " ";
|
||||
status_text += proto;
|
||||
}
|
||||
}
|
||||
else if (g_contacts_state.current_mode == ContactsMode::Ignored)
|
||||
{
|
||||
@@ -2573,12 +2606,6 @@ void refresh_ui()
|
||||
status_text += " / ";
|
||||
status_text += seen;
|
||||
}
|
||||
const char* proto = node_protocol_short_label(node.protocol);
|
||||
if (proto[0] != '\0')
|
||||
{
|
||||
status_text += " ";
|
||||
status_text += proto;
|
||||
}
|
||||
}
|
||||
else if (g_contacts_state.current_mode == ContactsMode::Team)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,35 @@ namespace layout
|
||||
static constexpr int kButtonSpacing = 3;
|
||||
static constexpr int kPanelGap = 3; // Gap between filter and list columns
|
||||
|
||||
const char* node_protocol_short_label(chat::contacts::NodeProtocolType protocol)
|
||||
{
|
||||
switch (protocol)
|
||||
{
|
||||
case chat::contacts::NodeProtocolType::LXMF:
|
||||
return "LX";
|
||||
case chat::contacts::NodeProtocolType::RNode:
|
||||
return "RN";
|
||||
case chat::contacts::NodeProtocolType::MeshCore:
|
||||
return "MC";
|
||||
case chat::contacts::NodeProtocolType::Meshtastic:
|
||||
return "MT";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
bool should_prefix_node_protocol(ContactsMode mode,
|
||||
chat::contacts::NodeProtocolType protocol)
|
||||
{
|
||||
if (protocol == chat::contacts::NodeProtocolType::Unknown)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return mode == ContactsMode::Contacts ||
|
||||
mode == ContactsMode::Nearby ||
|
||||
mode == ContactsMode::Ignored;
|
||||
}
|
||||
|
||||
lv_obj_t* create_root(lv_obj_t* parent)
|
||||
{
|
||||
const auto& profile = ::ui::page_profile::current();
|
||||
@@ -198,7 +227,7 @@ void ensure_list_subcontainers()
|
||||
|
||||
lv_obj_t* create_list_item(lv_obj_t* parent,
|
||||
const chat::contacts::NodeInfo& node,
|
||||
ContactsMode /*mode*/,
|
||||
ContactsMode mode,
|
||||
const char* status_text)
|
||||
{
|
||||
const auto& profile = ::ui::page_profile::current();
|
||||
@@ -229,6 +258,14 @@ lv_obj_t* create_list_item(lv_obj_t* parent,
|
||||
{
|
||||
display_name = node.short_name;
|
||||
}
|
||||
if (should_prefix_node_protocol(mode, node.protocol))
|
||||
{
|
||||
const char* proto = node_protocol_short_label(node.protocol);
|
||||
if (proto[0] != '\0')
|
||||
{
|
||||
display_name = "[" + std::string(proto) + "] " + display_name;
|
||||
}
|
||||
}
|
||||
|
||||
if (::ui::components::info_card::use_tdeck_layout())
|
||||
{
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
#include "chat/usecase/chat_service.h"
|
||||
#include "ui/presentation_sources/chat_presentation_source.h"
|
||||
#include "ui/presentation_sources/legacy_chat_action_sink.h"
|
||||
#include "sys/clock.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
@@ -34,6 +36,20 @@ class FakeMeshAdapter final : public ::chat::IMeshAdapter
|
||||
return send_ok;
|
||||
}
|
||||
|
||||
::chat::MeshSendResult sendTextDetailed(::chat::ChannelId channel,
|
||||
const std::string& text,
|
||||
::chat::MessageId forced_msg_id = 0,
|
||||
::chat::NodeId peer = 0) override
|
||||
{
|
||||
::chat::MessageId msg_id = forced_msg_id;
|
||||
const bool ok = sendText(channel, text, &msg_id, peer);
|
||||
if (ok)
|
||||
{
|
||||
return ::chat::MeshSendResult::success(msg_id);
|
||||
}
|
||||
return ::chat::MeshSendResult::fail(send_failure, fail_returns_msg_id ? msg_id : 0);
|
||||
}
|
||||
|
||||
bool pollIncomingText(::chat::MeshIncomingText* out) override
|
||||
{
|
||||
if (!out || incoming.empty())
|
||||
@@ -62,6 +78,8 @@ class FakeMeshAdapter final : public ::chat::IMeshAdapter
|
||||
|
||||
int send_count = 0;
|
||||
bool send_ok = true;
|
||||
bool fail_returns_msg_id = true;
|
||||
::chat::MeshOperationFailure send_failure = ::chat::MeshOperationFailure::Unknown;
|
||||
::chat::MessageId next_id = 100;
|
||||
::chat::ChannelId last_channel = ::chat::ChannelId::PRIMARY;
|
||||
std::string last_text;
|
||||
@@ -97,6 +115,15 @@ ui::chat::ConversationId broadcastConversation()
|
||||
return id;
|
||||
}
|
||||
|
||||
ui::chat::ConversationId meshCoreBroadcastConversation()
|
||||
{
|
||||
ui::chat::ConversationId id;
|
||||
id.kind = ui::chat::ConversationKind::Channel;
|
||||
id.protocol = ui::chat::ChatProtocolKind::MeshCore;
|
||||
id.primary = static_cast<uint32_t>(::chat::ChannelId::PRIMARY);
|
||||
return id;
|
||||
}
|
||||
|
||||
ui::chat::ConversationId systemConversation()
|
||||
{
|
||||
ui::chat::ConversationId id;
|
||||
@@ -110,6 +137,11 @@ ui::chat::ConversationId systemConversation()
|
||||
|
||||
int main()
|
||||
{
|
||||
sys::set_epoch_seconds_provider([]() -> uint32_t
|
||||
{ return 1700000000U; });
|
||||
sys::set_millis_provider([]() -> uint32_t
|
||||
{ return 5000U; });
|
||||
|
||||
::chat::ChatModel model;
|
||||
FakeMeshAdapter mesh;
|
||||
::chat::RamStore store;
|
||||
@@ -146,10 +178,14 @@ int main()
|
||||
assert(snapshot.conversation_count == 1);
|
||||
assert(snapshot.conversations[0].id == ada);
|
||||
assert(snapshot.conversations[0].selected);
|
||||
assert(snapshot.conversations[0].last_timestamp != 0);
|
||||
assert(std::strcmp(snapshot.conversations[0].title.c_str(), "04D2") == 0);
|
||||
assert(snapshot.message_count == 1);
|
||||
assert(snapshot.messages[0].conversation == ada);
|
||||
assert(snapshot.messages[0].outgoing);
|
||||
assert(std::strcmp(snapshot.messages[0].time_label.c_str(), "") != 0);
|
||||
assert(std::strtoul(snapshot.messages[0].time_label.c_str(), nullptr, 10) ==
|
||||
snapshot.conversations[0].last_timestamp);
|
||||
assert(snapshot.messages[0].delivery ==
|
||||
ui::chat::MessageDeliveryState::Delivered);
|
||||
assert(snapshot.messages[0].failure == ui::chat::MessageFailureKind::None);
|
||||
@@ -158,10 +194,23 @@ int main()
|
||||
assert(snapshot.composer_enabled);
|
||||
|
||||
mesh.send_ok = false;
|
||||
mesh.send_failure = ::chat::MeshOperationFailure::PeerKeyMissing;
|
||||
const ui::chat::SendMessageView failed_send{ada, "fail", 4};
|
||||
const auto rejected_send = sink.sendMessage(failed_send);
|
||||
assert(!rejected_send.ok);
|
||||
assert(rejected_send.failure == ui::UiActionFailure::Rejected);
|
||||
assert(rejected_send.failure == ui::UiActionFailure::PeerKeyMissing);
|
||||
|
||||
mesh.fail_returns_msg_id = false;
|
||||
mesh.send_failure = ::chat::MeshOperationFailure::ChannelKeyMissing;
|
||||
const auto channel_key_send = sink.sendMessage(failed_send);
|
||||
assert(!channel_key_send.ok);
|
||||
assert(channel_key_send.failure == ui::UiActionFailure::ChannelKeyMissing);
|
||||
|
||||
mesh.send_failure = ::chat::MeshOperationFailure::RadioOffline;
|
||||
const auto radio_offline_send = sink.sendMessage(failed_send);
|
||||
assert(!radio_offline_send.ok);
|
||||
assert(radio_offline_send.failure == ui::UiActionFailure::RadioOffline);
|
||||
mesh.fail_returns_msg_id = true;
|
||||
assert(delivery_read_model.upsert(::chat::delivery::toFailedDeliveryRecord(
|
||||
::chat::delivery::ChatDeliveryRef{0, 101, 0},
|
||||
::chat::delivery::SendFailureKind::PeerKeyMissing)));
|
||||
@@ -171,6 +220,14 @@ int main()
|
||||
assert(snapshot.messages[1].failure ==
|
||||
ui::chat::MessageFailureKind::PeerKeyMissing);
|
||||
|
||||
assert(delivery_read_model.upsert(::chat::delivery::toFailedDeliveryRecord(
|
||||
::chat::delivery::ChatDeliveryRef{0, 101, 0},
|
||||
::chat::delivery::SendFailureKind::ChannelKeyMissing)));
|
||||
assert(source.buildChatWorkspaceSnapshot(request, snapshot));
|
||||
assert(snapshot.message_count == 2);
|
||||
assert(snapshot.messages[1].failure ==
|
||||
ui::chat::MessageFailureKind::ChannelKeyMissing);
|
||||
|
||||
assert(sink.markRead(ada).ok);
|
||||
|
||||
::chat::MeshIncomingText incoming{};
|
||||
@@ -192,6 +249,26 @@ int main()
|
||||
assert(snapshot.messages[0].sender_node_id == 0x648144D4);
|
||||
assert(std::strcmp(snapshot.messages[0].sender_label.c_str(), "44D4") == 0);
|
||||
|
||||
service.setActiveProtocol(::chat::MeshProtocol::MeshCore);
|
||||
::chat::MeshIncomingText unknown_meshcore_incoming{};
|
||||
unknown_meshcore_incoming.channel = ::chat::ChannelId::PRIMARY;
|
||||
unknown_meshcore_incoming.from = 0;
|
||||
unknown_meshcore_incoming.to = 0xFFFFFFFFUL;
|
||||
unknown_meshcore_incoming.msg_id = 901;
|
||||
unknown_meshcore_incoming.text = "mc sender unknown";
|
||||
mesh.incoming.push_back(unknown_meshcore_incoming);
|
||||
service.processIncoming();
|
||||
|
||||
const ui::chat::ConversationId meshcore_broadcast = meshCoreBroadcastConversation();
|
||||
request.selected = meshcore_broadcast;
|
||||
assert(source.buildChatWorkspaceSnapshot(request, snapshot));
|
||||
assert(snapshot.header.valid);
|
||||
assert(snapshot.message_count == 1);
|
||||
assert(snapshot.messages[0].conversation == meshcore_broadcast);
|
||||
assert(!snapshot.messages[0].outgoing);
|
||||
assert(snapshot.messages[0].sender_node_id == 0);
|
||||
assert(std::strcmp(snapshot.messages[0].sender_label.c_str(), "Unknown") == 0);
|
||||
|
||||
const ui::chat::ConversationId team = teamConversation();
|
||||
assert(!sink.selectConversation(team).ok);
|
||||
assert(!sink.markRead(team).ok);
|
||||
@@ -216,5 +293,7 @@ int main()
|
||||
assert(!system_send.ok);
|
||||
assert(system_send.failure == ui::UiActionFailure::Unsupported);
|
||||
|
||||
sys::set_epoch_seconds_provider(nullptr);
|
||||
sys::set_millis_provider(nullptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ int main()
|
||||
ui::chat::ChatProtocolKind::TrailMate);
|
||||
assert(std::strcmp(overview.conversations[0].title.c_str(), "Alpha") == 0);
|
||||
assert(overview.conversations[0].unread_count == 2);
|
||||
assert(overview.conversations[0].last_timestamp == 102);
|
||||
assert(overview.message_count == 0);
|
||||
|
||||
const ui::chat::ConversationId team_id = overview.conversations[0].id;
|
||||
|
||||
Reference in New Issue
Block a user