From dd034a9d426cdbd12796cfbe76a9f6ec634922d7 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 12 Oct 2026 22:21:10 +0200 Subject: [PATCH] Cover channel closure and keep stream selection usable for long lists --- .../java/com/utun/chat/ui/screens/ChatScreen.kt | 3 ++- tools/chatgui/tests/test_stream_view.cpp | 13 ++++++++++++- tools/chatgui/transport/stream_bridge.cpp | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt index 70fe6b3f..bbbd53ab 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState @@ -107,7 +108,7 @@ fun ChatScreen( onDismissRequest = { chooseStream = false }, title = { Text(stringResource(R.string.stream_choose_source)) }, text = { - Column { + Column(Modifier.heightIn(max = 360.dp).verticalScroll(rememberScrollState())) { streams.forEach { source -> TextButton(onClick = { chooseStream = false; onWatchStream(source) }) { Text("${source.displayName} ยท ${source.id.toULong()}") diff --git a/tools/chatgui/tests/test_stream_view.cpp b/tools/chatgui/tests/test_stream_view.cpp index 182c82c1..69bb780c 100644 --- a/tools/chatgui/tests/test_stream_view.cpp +++ b/tools/chatgui/tests/test_stream_view.cpp @@ -45,7 +45,7 @@ class StreamViewTest : public QObject { } void publish(const QByteArray& video, const QByteArray& audio = {}) { - stream_media_block media{}; media.format = {160, 90, 90, STREAM_MEDIA_H264, static_cast(audio.isEmpty() ? 0 : STREAM_MEDIA_OPUS)}; + stream_media_block media{}; media.format = {160, 90, 90, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS}; media.first_frame_offset = 0; media.video_count = 1; media.video[0] = {++frameId, frameId * 100000, static_cast(video.size()), 0, static_cast(video.size()), STREAM_MEDIA_KEY, reinterpret_cast(video.constData())}; @@ -153,6 +153,17 @@ private slots: QCOMPARE(stream_get_info(publisher, &info), STREAM_OK); QCOMPARE(info.owners, 1u); } + void channelClosureNotifiesViewer() { + QVERIFY(gui_stream_attach(inst)); QVERIFY(gui_stream_watch(group)); + stream_info info{}; QCOMPARE(stream_get_info(publisher, &info), STREAM_OK); + QVERIFY(gui_stream_open(105, group, inst->node_id, info.descriptor.source_epoch)); + StreamRecord event; QVERIFY(receive(105, event)); QCOMPARE(event.kind, StreamRecordKind::Opened); + QByteArray channel = QByteArray::number(group); chat_core_delete_channel(inst, channel.constData()); + QVERIFY(receive(105, event)); QCOMPARE(event.kind, StreamRecordKind::Closed); QCOMPARE(event.reason, STREAM_CLOSE_GROUP); + QTRY_COMPARE(snapshots.last().mid(9), QByteArray("[]")); + QVERIFY(!stream_handle_active(publisher)); QCOMPARE(stream_handle_reason(publisher), STREAM_CLOSE_GROUP); + } + void cleanupTestCase() { poll.stop(); gui_stream_detach(inst); stream_close(publisher); uasync_mark_stopped(ua); utun_instance_destroy(inst); uasync_destroy(ua, 0); diff --git a/tools/chatgui/transport/stream_bridge.cpp b/tools/chatgui/transport/stream_bridge.cpp index c08f98a8..f79205fe 100644 --- a/tools/chatgui/transport/stream_bridge.cpp +++ b/tools/chatgui/transport/stream_bridge.cpp @@ -157,6 +157,7 @@ void gui_stream_detach(UTUN_INSTANCE* inst) { std::lock_guard guard(bridge.lock); if (!inst || bridge.inst != inst) return; if (bridge.pending) uasync_cancel_post(inst->ua, bridge.pending); + stream_watch_sources(inst, 0); bridge.pending = nullptr; stream_set_event_callback(inst, nullptr, nullptr); if (bridge.viewer.id) closeViewer(StreamRecordKind::Closed, STREAM_CLOSE_SERVICE); if (bridge.opening) output({bridge.requested.id, 0, StreamRecordKind::Closed, {}, 0, STREAM_CLOSE_SERVICE});