From 1235f770ebe2f190a5813ce8b34b161bbaafa5c2 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 23:51:19 +0100 Subject: [PATCH] Fence deferred message actions, release voice views and flush state before shutdown --- tools/chatgui/src/mainwindow.cpp | 13 +++++++++++- tools/chatgui/src/messagedelegate.cpp | 22 +++++++++++++------- tools/chatgui/src/messagelist.cpp | 30 ++++++++++++++++----------- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index c5032637..877eae77 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -1257,7 +1257,18 @@ void MainWindow::requestShutdown() { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: requestShutdown — saving geometry and stopping node"); if (m_messageNotification) m_messageNotification->dismiss("shutdown"); saveWindowGeometry(); - if (m_node) m_node->requestStop(); + if (m_messageList) m_messageList->saveChannelState(); + // FIFO: stop исполняется после уже принятых записей UI. Node живёт до join его worker. + auto** request = static_cast(u_malloc(sizeof(UtunNode*))); + if (!request) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "desktop shutdown queue allocation failed; stopping directly"); m_node->requestStop(); return; + } + *request = m_node; + if (gui_bridge_post_uasync_owned_fn([](void* arg) { + auto* node = *static_cast(arg); u_free(arg); node->requestStop(); + }, request) < 0) { + u_free(request); DEBUG_WARN(DEBUG_CATEGORY_SYS, "desktop shutdown queue unavailable; stopping directly"); m_node->requestStop(); + } else DEBUG_INFO(DEBUG_CATEGORY_SYS, "desktop shutdown queued after UI state writes"); } void MainWindow::onShutdownCheck() { diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index ee484a63..e06617f1 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -99,12 +99,13 @@ struct VoicePlayState { float displayElapsed = 0.0f; }; static QMap s_voiceStates; -static QTimer* s_voiceTimer = nullptr; -static QAbstractItemView* s_voiceView = nullptr; +static QPointer s_voiceTimer; +static QPointer s_voiceView; static void ensureVoiceTimer() { if (!s_voiceTimer) { - s_voiceTimer = new QTimer(); + s_voiceTimer = new QTimer(QCoreApplication::instance()); + s_voiceTimer->setObjectName("messageVoiceTimer"); s_voiceTimer->setInterval(16); QObject::connect(s_voiceTimer, &QTimer::timeout, []() { bool anyActive = false; @@ -1080,7 +1081,15 @@ void MessageDelegate::paintBody(QPainter* painter, const QStyleOptionViewItem& o } if (L.isVoice) { - if (option.widget) s_voiceView = (QAbstractItemView*)option.widget; + auto* view = qobject_cast(const_cast(option.widget)); + if (view && s_voiceView != view) { + s_voiceView = view; + QObject::connect(view, &QObject::destroyed, QCoreApplication::instance(), [] { + if (s_voiceView) return; // Уничтожен прежний view, текущий продолжает воспроизведение. + MessageDelegate::stopAllAutoPlay(); + GUI_DEBUG("voice delegate: view destroyed, playback state released"); + }); + } drawBubble(painter, L, option); if (!L.narrow) drawAvatar(painter, L, index); @@ -1722,7 +1731,6 @@ void MessageDelegate::startAutoPlay(const QString& filePath, float actualDuratio void MessageDelegate::stopAllAutoPlay() { VoicePlayer::stop(); - for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it) { - it->playing = false; it->paused = false; it->loading = false; - } + s_voiceStates.clear(); + if (s_voiceTimer) s_voiceTimer->stop(); } diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index 93355258..72b3b2a0 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -723,7 +723,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent req->data = (uint8_t*)(req + 1); req->data_len = (uint32_t)textData.size(); memcpy(req->data, textData.constData(), textData.size()); - if (gui_bridge_post_bound_fn(dm_send_trampoline, req, &req->inst) < 0) { + if (gui_bridge_submit_dm_message(req) < 0) { u_free(req); m_inputBar->submissionResult(request, false); } return; @@ -876,7 +876,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent } void MessageList::loadChannel(const QString& channelId) { - QWidget* prevFocus = QApplication::focusWidget(); + QPointer prevFocus = QApplication::focusWidget(); const auto conversations = m_db && isDmChannel(channelId) ? m_db->getDmConversations() : QList(); quint64 generation = 0; @@ -1005,7 +1005,8 @@ void MessageList::loadChannel(const QString& channelId) { if (sameChannel) { bool wasBottom = m_wasAtBottom; qint64 savedTop = m_savedTopMsgId; - QTimer::singleShot(0, this, [this, wasBottom, savedTop]() { + QTimer::singleShot(0, this, [this, wasBottom, savedTop, channelId, anchor = m_historyAnchor]() { + if (channelId != m_currentChannelId || anchor != m_historyAnchor) return; if (wasBottom || savedTop == 0) { m_view->scrollToBottom(); } else { @@ -1170,16 +1171,14 @@ void MessageList::updateReadPosition() { for (int row = m_model->rowCount() - 1; row >= 0; --row) { QModelIndex idx = m_model->index(row, 0); QRect vr = m_view->visualRect(idx); - if (vr.isValid() && vr.bottom() <= vpHeight) { + if (vr.isValid() && vr.top() >= 0 && vr.bottom() < vpHeight) { lastFullyVisibleRow = row; - break; + newReadMsgId = qMax(newReadMsgId, m_model->item(row)->data(MsgIdRole).toLongLong()); } } if (lastFullyVisibleRow >= 0) { - qint64 msgId = m_model->item(lastFullyVisibleRow)->data(MsgIdRole).toLongLong(); - if (msgId > m_lastReadMsgId) - newReadMsgId = msgId; + GUI_DEBUG("read watermark: maximum visible row id=%lld", (long long)newReadMsgId); } QModelIndex topIdx = m_view->indexAt(QPoint(0, 0)); @@ -1260,7 +1259,8 @@ void MessageList::onAttachmentDownloaded(const QString& chId, int64_t msgId) { if (m.contentType == "audio/opus" && wasPending) { QString fp = item->data(MsgVoiceFileRole).toString(); if (!fp.isEmpty() && QFile::exists(fp)) { - QTimer::singleShot(500, this, [this, fp]() { + QTimer::singleShot(500, this, [this, fp, chId]() { + if (m_currentChannelId != chId) return; MessageDelegate::stopAllAutoPlay(); VoicePlayer::playOpusFile(fp, this, [fp](float actualDur, uint64_t playbackId) { MessageDelegate::startAutoPlay(fp, actualDur, playbackId); }); }); @@ -1383,7 +1383,7 @@ void MessageList::onMessageUpdated(const uint8_t* data, int len) { } void MessageList::onMessageContextMenu(const QPoint& pos) { - QModelIndex idx = m_view->indexAt(pos); + QPersistentModelIndex idx(m_view->indexAt(pos)); if (!idx.isValid()) return; QString ct = idx.data(MsgContentTypeRole).toString(); @@ -1396,7 +1396,8 @@ void MessageList::onMessageContextMenu(const QPoint& pos) { QString displayName = idx.data(MsgFileDisplayNameRole).toString(); bool downloaded = !filePath.isEmpty() && QFile::exists(filePath); - QMenu menu(m_view); + QMenu menu; + QPointer guard(this); QString messageId = idx.data(MsgMessageKeyRole).toString(); if (!isDm && messageId.size() == 64) { const QString channel = idx.data(MsgChannelIdRole).toString(); @@ -1430,6 +1431,8 @@ void MessageList::onMessageContextMenu(const QPoint& pos) { if ((isVideo || isImage) && !downloaded && !isDm) dlAct = menu.addAction(QCoreApplication::translate("ChatGui", "Download")); QAction* chosen = menu.exec(m_view->viewport()->mapToGlobal(pos)); + if (!guard) return; + if (!idx.isValid()) { GUI_WARN("message menu cancelled: row was replaced while menu was open"); return; } if (saveAct && chosen == saveAct) { QString base = displayName; @@ -1720,6 +1723,10 @@ void MessageList::onRadioToggled(bool on) { quint64 groupId = m_currentChannelId.toULongLong(); if (groupId == 0) return; + if (!on) { + RadioAudioEngine::instance()->stop(); m_talkingLabel->hide(); m_radioOn = false; updateRadioUi(); + } + struct radio_set_active_arg* a = (struct radio_set_active_arg*)u_malloc(sizeof(*a)); if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioPanel: active request allocation failed group=%016llx", (unsigned long long)groupId); @@ -1731,7 +1738,6 @@ void MessageList::onRadioToggled(bool on) { a->on = on ? 1 : 0; if (gui_bridge_post_bound_fn(radio_set_active_trampoline, a, &a->inst) < 0) { u_free(a); - if (!on) RadioAudioEngine::instance()->stop(); QSignalBlocker blocker(m_radioBtn); m_radioBtn->setChecked(m_radioOn); return; }