Fence deferred message actions, release voice views and flush state before shutdown

This commit is contained in:
evgeny committed 2026-10-28 00:59:00 +01:00
1 parent adefef675c
commit 1235f770eb
3 files changed
+45 -20

No files matched your search

+12 -1
View File
@@ -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<UtunNode**>(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<UtunNode**>(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() {
+15 -7
View File
@@ -99,12 +99,13 @@ struct VoicePlayState {
float displayElapsed = 0.0f;
};
static QMap<QString, VoicePlayState> s_voiceStates;
static QTimer* s_voiceTimer = nullptr;
static QAbstractItemView* s_voiceView = nullptr;
static QPointer<QTimer> s_voiceTimer;
static QPointer<QAbstractItemView> 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<QAbstractItemView*>(const_cast<QWidget*>(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();
}
+18 -12
View File
@@ -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<QWidget> prevFocus = QApplication::focusWidget();
const auto conversations = m_db && isDmChannel(channelId) ? m_db->getDmConversations() : QList<DmConversation>();
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<MessageList> 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;
}