From cab2ad8a83a150f2e5d627a37ddf373a4f1a0beb Mon Sep 17 00:00:00 2001 From: Torlando <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:59:11 +0000 Subject: [PATCH] fix(ui): conversation-row selection on SHORT_CLICKED, not CLICKED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long-press to delete a conversation, then release: the row fired on_conversation_long_pressed (confirm dialog) AND, on release, on_conversation_clicked, navigating into the conversation and hiding the dialog. Root cause: LVGL 8.4 lv_indev.c indev_proc_release() (lines 973-980) sends LV_EVENT_CLICKED on every pointer release without scrolling, including long-press release — only LV_EVENT_SHORT_CLICKED is gated on long_pr_sent == 0. The row had both CLICKED and LONG_PRESSED bound. Selection is now bound to LV_EVENT_SHORT_CLICKED. Trackball selection is unaffected: the keypad path sends SHORT_CLICKED + CLICKED on a plain enter release and suppresses both when long_pr_sent is set (lv_indev.c:526-531, 678-692). No programmatic CLICKED sends exist in the app. This row is the only object with the CLICKED + LONG_PRESSED double-bind (bubble/textarea long-press sites are single-bound, verified). Contract pins: selection must bind SHORT_CLICKED, no CLICKED binding on the row, long-press delete binding unchanged. 184/184 contracts, tdeck + tdeck-release green. --- .../UI/LXMF/ConversationListScreen.cpp | 9 +++- ...est_conversation_row_longpress_contract.py | 52 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/build_scripts/test_conversation_row_longpress_contract.py diff --git a/lib/tdeck_ui/UI/LXMF/ConversationListScreen.cpp b/lib/tdeck_ui/UI/LXMF/ConversationListScreen.cpp index 771f4206..03031116 100644 --- a/lib/tdeck_ui/UI/LXMF/ConversationListScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/ConversationListScreen.cpp @@ -532,7 +532,14 @@ void ConversationListScreen::create_conversation_item(const ConversationItem& it // Store peer hash in user data using pool (avoids per-item heap allocations) _peer_hash_pool.push_back(item.peer_hash); lv_obj_set_user_data(container, &_peer_hash_pool.back()); - lv_obj_add_event_cb(container, on_conversation_clicked, LV_EVENT_CLICKED, this); + // SHORT_CLICKED (not CLICKED): in LVGL 8.4 indev_proc_release() + // (lv_indev.c:973-980) CLICKED is sent on EVERY pointer release + // without scrolling — including a long-press release — while + // SHORT_CLICKED is gated on long_pr_sent == 0. With both CLICKED and + // LONG_PRESSED bound to the row, a long-press to delete would fire + // on_conversation_long_pressed AND, on release, on_conversation_clicked, + // navigating into the conversation and hiding the confirm dialog. + lv_obj_add_event_cb(container, on_conversation_clicked, LV_EVENT_SHORT_CLICKED, this); lv_obj_add_event_cb(container, on_conversation_long_pressed, LV_EVENT_LONG_PRESSED, this); // Track container for focus group management diff --git a/tests/build_scripts/test_conversation_row_longpress_contract.py b/tests/build_scripts/test_conversation_row_longpress_contract.py new file mode 100644 index 00000000..74246a70 --- /dev/null +++ b/tests/build_scripts/test_conversation_row_longpress_contract.py @@ -0,0 +1,52 @@ +"""Source contract: conversation-row selection must not fire on long-press release. + +LVGL 8.4 lv_indev.c indev_proc_release() (lines ~973-980) sends +LV_EVENT_CLICKED on EVERY pointer release without scrolling, including a +long-press release — only LV_EVENT_SHORT_CLICKED is gated on +long_pr_sent == 0. With both CLICKED and LONG_PRESSED bound to the +conversation row, a long-press to open the delete dialog would also, on +release, fire the selection handler and navigate into the conversation, +hiding the confirm dialog. + +The row therefore binds selection to LV_EVENT_SHORT_CLICKED. Trackball +selection is unaffected: the keypad indev path sends SHORT_CLICKED and +CLICKED for an enter release without a long press, and suppresses both +when long_pr_sent is set. +""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +LIST_CPP = ROOT / "lib/tdeck_ui/UI/LXMF/ConversationListScreen.cpp" + + +def _source() -> str: + return LIST_CPP.read_text() + + +def test_row_selection_uses_short_clicked(): + source = _source() + assert ( + "lv_obj_add_event_cb(container, on_conversation_clicked, LV_EVENT_SHORT_CLICKED, this)" + in source + ), ( + "conversation row selection must bind LV_EVENT_SHORT_CLICKED — " + "LV_EVENT_CLICKED fires on long-press release in LVGL 8.4 and " + "would navigate into the chat while the delete dialog is open" + ) + + +def test_row_has_no_clicked_binding(): + source = _source() + assert "on_conversation_clicked, LV_EVENT_CLICKED" not in source, ( + "stale LV_EVENT_CLICKED binding on the conversation row would " + "re-introduce the long-press-release navigation" + ) + + +def test_row_long_press_delete_binding_unchanged(): + source = _source() + assert ( + "lv_obj_add_event_cb(container, on_conversation_long_pressed, LV_EVENT_LONG_PRESSED, this)" + in source + ), "long-press delete on the conversation row must remain bound"