fix(chat-ui): repair pager chat and contact protocol labels (#46 #49 #51)

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:
Trail Mate Dev
2026-06-02 04:48:23 +08:00
parent 13852e693b
commit f040faed0b
16 changed files with 419 additions and 67 deletions
@@ -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;