From 4d96822c0cd1711449c45cce15ea19f8a23e0612 Mon Sep 17 00:00:00 2001 From: Praktimarc <61456874+praktimarc@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:45:02 +0100 Subject: [PATCH] fix: filter public messages correctly (#163) Fixes #162 --- .../view/Kst4ContestApplication.java | 28 +++++--- .../view/ComposeMessageTableRefreshTest.kt | 60 ++++++++++++++++ ...026-11-03-public-message-tab-filter-fix.md | 68 +++++++++++++++++++ 3 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 app-desktop/src/test/kotlin/kst4contest/view/ComposeMessageTableRefreshTest.kt create mode 100644 docs/superpowers/plans/2026-11-03-public-message-tab-filter-fix.md diff --git a/app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java b/app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java index 2d7f21b9..15cceb19 100644 --- a/app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java +++ b/app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java @@ -3032,6 +3032,20 @@ public class Kst4ContestApplication implements StatusUpdateListener, SettingsHos private java.util.function.Consumer> composeChatListener; private java.util.function.Consumer> composeClusterListener; + /** + * Fills the three global Compose message tables from their filtered + * controller views. + */ + /* package */ static void refreshComposeMessageTables( + final ChatController controller, + final DataTableState directed, + final DataTableState publicMessages, + final DataTableState qsoOfTheOther) { + directed.replaceRows(controller.toMeMessages()); + publicMessages.replaceRows(controller.toAllMessages()); + qsoOfTheOther.replaceRows(controller.toOtherMessages()); + } + /** * Opens the Compose main window beside the JavaFX one. * @@ -3342,11 +3356,11 @@ public class Kst4ContestApplication implements StatusUpdateListener, SettingsHos return kotlin.Unit.INSTANCE; }); - composeChatListener = rows -> uiDispatcher.runOnUi(() -> { - directed.replaceRows(chatcontroller.toMeMessages()); - publicMessages.replaceRows(rows); - qsoOfTheOther.replaceRows(chatcontroller.toOtherMessages()); - }); + composeChatListener = rows -> uiDispatcher.runOnUi(() -> refreshComposeMessageTables( + chatcontroller, + directed, + publicMessages, + qsoOfTheOther)); chatcontroller.getLst_globalChatMessageList().addListener(composeChatListener); composeClusterListener = rows -> uiDispatcher.runOnUi(() -> cluster.replaceRows(rows)); @@ -3361,10 +3375,8 @@ public class Kst4ContestApplication implements StatusUpdateListener, SettingsHos stations.replaceRows(initialMembers); newJoined.updateActiveMembers(initialMembers); newJoined.updateEvents(chatcontroller.getRecentChatJoins().snapshot()); - directed.replaceRows(chatcontroller.toMeMessages()); - publicMessages.replaceRows(chatcontroller.getLst_globalChatMessageList().snapshot()); + refreshComposeMessageTables(chatcontroller, directed, publicMessages, qsoOfTheOther); cluster.replaceRows(chatcontroller.getLst_clusterMemberList().snapshot()); - qsoOfTheOther.replaceRows(chatcontroller.toOtherMessages()); /* * The only push that does not check the feed for null, and deliberately so: the * feed is assigned earlier in this same method, so a guard here could never fire diff --git a/app-desktop/src/test/kotlin/kst4contest/view/ComposeMessageTableRefreshTest.kt b/app-desktop/src/test/kotlin/kst4contest/view/ComposeMessageTableRefreshTest.kt new file mode 100644 index 00000000..052abf45 --- /dev/null +++ b/app-desktop/src/test/kotlin/kst4contest/view/ComposeMessageTableRefreshTest.kt @@ -0,0 +1,60 @@ +package kst4contest.view + +import kst4contest.controller.ChatController +import kst4contest.model.ChatMessage +import kst4contest.view.compose.DataTableState +import kst4contest.view.compose.RowKeys +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` + +class ComposeMessageTableRefreshTest { + + @Test + fun `refresh keeps public table on the filtered controller view`() { + val controller = mock(ChatController::class.java) + val privateToMe = message("private to me") + val publicInitial = message("public initial") + val betweenOthers = message("between others") + val directed = table() + val publicMessages = table() + val qsoOfTheOther = table() + + `when`(controller.toMeMessages()).thenReturn(listOf(privateToMe)) + `when`(controller.toAllMessages()).thenReturn(listOf(publicInitial)) + `when`(controller.toOtherMessages()).thenReturn(listOf(betweenOthers)) + + Kst4ContestApplication.refreshComposeMessageTables( + controller, + directed, + publicMessages, + qsoOfTheOther, + ) + + assertEquals(listOf(privateToMe), directed.rows) + assertEquals(listOf(publicInitial), publicMessages.rows) + assertEquals(listOf(betweenOthers), qsoOfTheOther.rows) + + val publicLater = message("public later") + `when`(controller.toAllMessages()).thenReturn(listOf(publicLater, publicInitial)) + + Kst4ContestApplication.refreshComposeMessageTables( + controller, + directed, + publicMessages, + qsoOfTheOther, + ) + + assertEquals(listOf(publicLater, publicInitial), publicMessages.rows) + } + + private fun table() = DataTableState( + columns = emptyList(), + rowKey = RowKeys.byReference(), + ) + + private fun message(text: String) = ChatMessage().apply { + messageText = text + } +} diff --git a/docs/superpowers/plans/2026-11-03-public-message-tab-filter-fix.md b/docs/superpowers/plans/2026-11-03-public-message-tab-filter-fix.md new file mode 100644 index 00000000..6414566b --- /dev/null +++ b/docs/superpowers/plans/2026-11-03-public-message-tab-filter-fix.md @@ -0,0 +1,68 @@ +# Public Message Tab Filter Fix Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the main-window **Public messages** tab show only ON4KST messages whose protocol receiver is public (`0`, represented as `ALL`). + +**Architecture:** Keep the global message roster as the complete event store and reuse `ChatController.toAllMessages()` as the single existing public-message view. A small package-visible refresh method updates all three Compose message tables so initial population and live updates use the same filtered sources. + +**Tech Stack:** Java 21, Kotlin 2.2, Compose Multiplatform, JUnit 5, Mockito, Gradle + +**Spec:** `github_docs/en-User-Interface.md` and `github_docs/de-Benutzeroberflaeche.md` document the intended global-tab semantics. + +## Global Constraints + +- Preserve ON4KST parsing, the complete global message history, message order and chat-category identity. +- Preserve the selected-station message modes and the existing `pm to me`/`pm to other` semantics. +- Do not stage, commit, push or publish without separate authorization. +- Source comments remain English. + +## Review Focus + +- A genuine public frame remains in the public tab after initial population and a later refresh. +- A PM to the local callsign never enters the public tab. +- A directed message between other stations never enters the public tab. +- Directed and QSO-of-the-other tables continue to receive their existing filtered views. +- Repeated table refreshes replace rows without changing their newest-first order. + +--- + +### Task 1: Route the Compose public table through the existing filtered view + +**Files:** +- Modify: `app-desktop/src/main/java/kst4contest/view/Kst4ContestApplication.java` +- Create: `app-desktop/src/test/kotlin/kst4contest/view/ComposeMessageTableRefreshTest.kt` + +**Interfaces:** +- Consumes: `ChatController.toMeMessages()`, `ChatController.toAllMessages()`, and `ChatController.toOtherMessages()` +- Produces: package-visible `Kst4ContestApplication.refreshComposeMessageTables(...)` + +- [x] **Step 1: Write the failing regression test** + +Create a mixed-message test whose mocked controller exposes separate directed, public and other-station views. Assert that the public table receives only `toAllMessages()`, then change the mocked public result, refresh again and assert the replacement remains filtered. + +- [x] **Step 2: Run the focused test and verify it fails** + +Run: `./gradlew.bat :app-desktop:test --tests kst4contest.view.ComposeMessageTableRefreshTest --console=plain` + +Expected: compilation failure because `refreshComposeMessageTables(...)` does not yet exist. + +- [x] **Step 3: Implement the shared refresh method** + +Add package-visible static `refreshComposeMessageTables(ChatController, DataTableState, DataTableState, DataTableState)`. It must populate the tables from `toMeMessages()`, `toAllMessages()` and `toOtherMessages()` respectively. + +- [x] **Step 4: Use the method for both update paths** + +Call the shared method from the global-message listener on the UI dispatcher and from the initial Compose table population. Remove both raw writes to the public table. + +- [x] **Step 5: Run focused and related tests** + +Run the new test plus `MessageTabsScreenTest`, `MessageBusManagementThreadChatFrameTest`, `SelectedStationMessageFlowTest`, and `SelectedStationMessagePredicateTest`. + +- [x] **Step 6: Run full verification** + +Run `./gradlew.bat build --console=plain`, inspect test-result XML and changed-file static-analysis reports, then run `git diff --check` and `git status --short --branch`. + +- [x] **Step 7: Assess documentation impact** + +Confirm the German and English manuals already state that the global public tab contains public messages only. Update no documentation unless implementation reveals a conflicting invariant.