#include "mainwindow.h" #include "channellist.h" #include "messagelist.h" #include "messagedelegate.h" #include "accountlist.h" #include "invitedialog.h" #include "inviteby.h" #include "joindialog.h" #include "settingsdialog.h" #include "channelsettingsdialog.h" #include "connmonitorwindow.h" #include "creategroupdialog.h" #include "invite_link.h" #include "sound_manager.h" #include "voiceplayback.h" #include "audiorecorder.h" #include "voicemessageencoder.h" extern "C" { #include "../../lib/audio_compressor.h" } #include "animtimer.h" #include "../db/db_manager.h" #include "../transport/utun_node.h" #include "../transport/gui_bridge.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include "chat/chat_core.h" #include "../../lib/mem.h" #include "../../lib/debug_config.h" void chat_core_attachment_download_trampoline(void* arg); void chat_core_save_ui_state_trampoline(void* arg); } static MainWindow* s_mainWindow = nullptr; static void onMsgReceivedCallback(const char* ch_id, int ch_id_len, uint64_t author) { if (s_mainWindow) s_mainWindow->onMessageReceived(ch_id, ch_id_len, author); } static void onChannelUpdatedCallback(const char* ch_id, int ch_id_len) { Q_UNUSED(ch_id_len); Q_UNUSED(ch_id); if (s_mainWindow) s_mainWindow->reloadChannels(); } static void onMemberListCallback(const uint8_t* data, int len) { if (len < 3) return; uint8_t cl = data[0]; if (len < 1 + (int)cl + 2) return; uint16_t count; memcpy(&count, data + 1 + cl, 2); if (s_mainWindow) s_mainWindow->onMemberList(QString::fromUtf8((const char*)data + 1, cl), count, data + 1 + cl + 2, len - 1 - cl - 2); } static void onMemberUpdatedCallback(const uint8_t* data, int len) { if (len < 1 + 64 + 1 + 79) return; uint8_t cl = data[0]; if (len < 1 + (int)cl + 1 + 79) return; if (s_mainWindow) s_mainWindow->onMemberUpdated(QString::fromUtf8((const char*)data + 1, cl), data + 1 + cl + 1); } static void onMemberRemovedCallback(const uint8_t* data, int len) { if (len < 1 + 64 + 8) return; uint8_t cl = data[0]; if (len < 1 + (int)cl + 8) return; uint64_t nid; memcpy(&nid, data + 1 + cl, 8); if (s_mainWindow) s_mainWindow->onMemberRemoved(QString::fromUtf8((const char*)data + 1, cl), nid); } static void onAutoConnectStatusCallback(uint8_t status, uint16_t total_tried, uint16_t node_count, uint16_t connected) { if (s_mainWindow) s_mainWindow->onAutoConnectStatus(status, total_tried, node_count, connected); } static void onChannelPeersOnlineCallback(const char* ch_id, int ch_id_len, uint16_t online) { if (s_mainWindow) s_mainWindow->onChannelPeersOnline(ch_id, ch_id_len, online); } MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, const QString& dbPath, const QString& debugFile, const QString& debugLevel, const QString& debugCategories) : QMainWindow(parent) , m_channelList(nullptr), m_messageList(nullptr), m_accountList(nullptr) , m_trayIcon(nullptr), m_trayMenu(nullptr) , m_db(db), m_cfgPath(cfgPath), m_cfgDbPath(dbPath) , m_cfgDebugFile(debugFile), m_cfgDebugLevel(debugLevel) , m_cfgDebugCategories(debugCategories) , m_recorder(nullptr) , m_autoPlayTimer(new QTimer(this)) { setWindowTitle("Chat"); resize(900, 600); if (m_db) m_db->setDbPath(m_cfgDbPath); setupUi(); setupTray(); setupSoundFromConfig(); if (m_db) { setupBridgeCallbacks(); setupMessaging(); setupNode(); setupConnects(); } connect(m_autoPlayTimer, &QTimer::timeout, this, &MainWindow::autoPlayTimerTick); } MainWindow::~MainWindow() { s_mainWindow = nullptr; m_autoPlayTimer->stop(); gui_bridge_set_msg_received_cb(nullptr); gui_bridge_set_channel_updated_cb(nullptr); gui_bridge_set_member_list_cb(nullptr); gui_bridge_set_member_updated_cb(nullptr); gui_bridge_set_member_removed_cb(nullptr); gui_bridge_set_my_node_id_cb(nullptr); gui_bridge_set_auto_connect_status_cb(nullptr); gui_bridge_set_channel_peers_online_cb(nullptr); gui_bridge_set_db_ready_cb(nullptr); gui_bridge_set_attachment_downloaded_cb(nullptr); QCoreApplication::processEvents(QEventLoop::AllEvents); if (m_recorder) { m_recorder->shutdown(); m_recorder = nullptr; } SoundManager::instance()->shutdown(); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow — saving channel state"); if (m_messageList) m_messageList->saveChannelState(); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow — stopping utun node"); if (m_node) { m_node->stop(); delete m_node; m_node = nullptr; } DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow — node stop+delete done"); AnimTimer::shutdown(); MessageList::cleanupAnimations(); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow — animations cleaned"); DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow done"); } void MainWindow::setupUi() { auto *splitter = new QSplitter(Qt::Horizontal, this); m_channelList = new ChannelList(m_db, splitter); m_recorder = new AudioRecorder(this); m_messageList = new MessageList(m_db, m_recorder, splitter); m_accountList = new AccountList(m_db, splitter); splitter->addWidget(m_channelList); splitter->addWidget(m_messageList); splitter->addWidget(m_accountList); splitter->setStretchFactor(0, 2); splitter->setStretchFactor(1, 5); splitter->setStretchFactor(2, 1); splitter->setSizes({220, 480, 180}); setCentralWidget(splitter); } void MainWindow::setupNode() { m_node = new UtunNode(this); connect(m_node, &UtunNode::error, this, [](const QString& msg) { qWarning("UtunNode error: %s", qPrintable(msg)); }); connect(m_node, &UtunNode::started, this, [this]() { qDebug("UtunNode started"); }); connect(m_node, &UtunNode::stopped, this, [this]() { qDebug("UtunNode stopped"); qApp->quit(); }); if (!m_cfgDebugFile.isEmpty()) m_node->setDebugFile(m_cfgDebugFile); if (!m_cfgDebugLevel.isEmpty()) m_node->setDebugLevel(m_cfgDebugLevel); if (!m_cfgDebugCategories.isEmpty()) m_node->setDebugCategories(m_cfgDebugCategories); if (!m_cfgDbPath.isEmpty()) m_node->setDbPath(m_cfgDbPath); if (!m_node->start(m_cfgPath)) qWarning("Failed to start UtunNode"); else AnimTimer::instance()->logRegisteredIcons(); } void MainWindow::setupMessaging() { gui_bridge_init(); } void MainWindow::setupSoundFromConfig() { SoundManager* sm = SoundManager::instance(); if (!sm->init()) return; QString soundDir, msgInFile, msgOutFile, connectFile, disconnectFile; QFile cfgFile(m_cfgPath); if (cfgFile.open(QIODevice::ReadOnly | QIODevice::Text)) { bool inGui = false; QTextStream in(&cfgFile); while (!in.atEnd()) { QString line = in.readLine().trimmed(); if (line == "[gui]") { inGui = true; continue; } if (line.startsWith("[") && line.endsWith("]")) inGui = false; if (!inGui) continue; if (line.startsWith("sound_dir=")) soundDir = line.mid(10).trimmed(); else if (line.startsWith("sound_msg_incoming=")) msgInFile = line.mid(19).trimmed(); else if (line.startsWith("sound_msg_outgoing=")) msgOutFile = line.mid(19).trimmed(); else if (line.startsWith("sound_connect=")) connectFile = line.mid(14).trimmed(); else if (line.startsWith("sound_disconnect=")) disconnectFile = line.mid(17).trimmed(); } cfgFile.close(); } if (soundDir.isEmpty()) soundDir = "sounds"; QDir binDir(QApplication::applicationDirPath()); QString baseDir = QDir::isAbsolutePath(soundDir) ? soundDir : binDir.absoluteFilePath(soundDir); auto resolvePath = [&baseDir](const QString& f) { return f.isEmpty() ? QString() : QDir::cleanPath(baseDir + "/" + f); }; struct { const char* name; QString file; } events[] = { {"msg_incoming", msgInFile}, {"msg_outgoing", msgOutFile}, {"connect", connectFile}, {"disconnect", disconnectFile}, }; for (auto& e : events) { if (!e.file.isEmpty()) sm->loadSound(e.name, resolvePath(e.file)); } if (m_db && m_db->isOpen()) { sm->setEnabled(m_db->getUiStateInt("sound_enabled", 1)); sm->setVolume(m_db->getUiStateInt("sound_volume", 80) / 100.0f); QString se[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect"}; for (int i = 0; i < 4; i++) { const QString& ek = se[i]; sm->setEvent(ek, m_db->getUiStateInt(QStringLiteral("sound_%1_on").arg(ek), i == 0 ? 1 : 0), m_db->getUiStateInt(QStringLiteral("sound_%1_vol").arg(ek), 80) / 100.0f); } } if (m_recorder) { m_recorder->init(); if (m_db && m_db->isOpen()) { m_recorder->setCaptureDeviceIndex(m_db->getUiStateInt("audio_input_device", -1)); VoiceEncoder::setPreset(m_db->getUiStateInt("opus_codec_preset", 1)); int compEnabled = m_db->getUiStateInt("compressor_enabled", 0); m_recorder->setCompressorEnabled(compEnabled != 0); if (m_recorder->compressor()) { audio_compressor_config_t compCfg = {0}; compCfg.sample_rate = 48000; compCfg.channels = 1; compCfg.max_gain_db = (float)m_db->getUiStateInt("compressor_max_gain_db", 25); compCfg.lookback_ms = 200; compCfg.lookahead_ms = 100; compCfg.rise_rate_per_sec = (float)m_db->getUiStateInt("compressor_rise_rate", 10); compCfg.target_level = 1.0f; audio_compressor_configure(m_recorder->compressor(), &compCfg); } } DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "AudioRecorder initialized (captureDevice=%d, opusPreset=%d, compressor=%d)", m_recorder->captureDeviceIndex(), VoiceEncoder::preset(), m_recorder->isCompressorEnabled() ? 1 : 0); } } void MainWindow::setupBridgeCallbacks() { s_mainWindow = this; gui_bridge_set_msg_received_cb(onMsgReceivedCallback); gui_bridge_set_channel_updated_cb(onChannelUpdatedCallback); gui_bridge_set_member_list_cb(onMemberListCallback); gui_bridge_set_member_updated_cb(onMemberUpdatedCallback); gui_bridge_set_member_removed_cb(onMemberRemovedCallback); gui_bridge_set_my_node_id_cb([](uint64_t nid) { qDebug("MainWindow: MY_NODE_ID setMyNodeId(%016llx)", (unsigned long long)nid); if (s_mainWindow && s_mainWindow->m_db) s_mainWindow->m_db->setMyNodeId((quint64)nid); }); gui_bridge_set_auto_connect_status_cb(onAutoConnectStatusCallback); gui_bridge_set_channel_peers_online_cb(onChannelPeersOnlineCallback); gui_bridge_set_db_ready_cb([]() { if (!s_mainWindow || !s_mainWindow->m_db) return; s_mainWindow->m_db->setDb(chat_core_get_db()); s_mainWindow->m_channelList->loadChannels(); QString lastCid = QSettings().value("last_channel_id").toString(); if (!lastCid.isEmpty()) { s_mainWindow->m_channelList->selectChannel(lastCid); s_mainWindow->m_accountList->setChannel(lastCid); s_mainWindow->m_currentChannelId = lastCid; s_mainWindow->m_messageList->loadChannel(lastCid); } }); gui_bridge_set_attachment_downloaded_cb([](const char* ch_id, int ch_id_len, int64_t msg_id) { if (!s_mainWindow) return; QString chId = QString::fromUtf8(ch_id, ch_id_len); s_mainWindow->m_channelList->loadChannels(); s_mainWindow->m_messageList->onAttachmentDownloaded(chId, msg_id); s_mainWindow->onAttachmentDownloadedForAutoPlay(chId, msg_id); }); gui_bridge_set_download_progress_cb([](const char* ch_id, int ch_id_len, int64_t msg_id, int blocks_done, int num_blocks) { if (!s_mainWindow) return; QString chId = QString::fromUtf8(ch_id, ch_id_len); s_mainWindow->m_messageList->updateMessageProgress(chId, msg_id, blocks_done, num_blocks); }); } void MainWindow::setupConnects() { connect(m_channelList, &ChannelList::channelSelected, this, [this](const QString& channelId) { QSettings().setValue("last_channel_id", channelId); m_currentChannelId = channelId; m_messageList->loadChannel(channelId); m_accountList->setChannel(channelId); QByteArray cidBA = channelId.toUtf8(); char* cid = u_strdup(cidBA.constData()); if (cid) gui_bridge_post_uasync_fn(chat_core_connect_channel_trampoline, cid); }); connect(m_channelList, &ChannelList::inviteRequested, this, &MainWindow::showShareInviteDialog); connect(m_channelList, &ChannelList::inviteByLinkRequested, this, &MainWindow::showInviteByLinkDialog); connect(m_channelList, &ChannelList::joinRequested, this, &MainWindow::showJoinDialog); connect(m_channelList, &ChannelList::createGroupRequested, this, &MainWindow::onCreateGroupRequested); connect(m_channelList, &ChannelList::settingsRequested, this, &MainWindow::showSettings); connect(m_channelList, &ChannelList::connMonitorRequested, this, &MainWindow::showConnMonitor); connect(m_channelList, &ChannelList::channelSettingsRequested, this, &MainWindow::showChannelSettings); connect(m_accountList, &AccountList::memberRenamed, m_messageList, &MessageList::reloadLocalNicks); connect(m_messageList, &MessageList::readPositionChanged, this, [this](const QString& channelId, qint64 lastReadMsgId) { Q_UNUSED(lastReadMsgId); if (m_channelList && m_db) m_channelList->setChannelUnreadCount(channelId, m_db->getUnreadCount(channelId, lastReadMsgId)); }); } void MainWindow::setupTray() { m_trayIcon = new QSystemTrayIcon(this); m_trayIcon->setIcon(style()->standardIcon(QStyle::SP_ComputerIcon)); m_trayIcon->setToolTip("Chat"); m_trayMenu = new QMenu(this); QAction *settingsAction = m_trayMenu->addAction("Settings"); connect(settingsAction, &QAction::triggered, this, &MainWindow::showSettings); m_trayMenu->addSeparator(); QAction *showAction = m_trayMenu->addAction("Show"); connect(showAction, &QAction::triggered, this, [this]() { show(); raise(); activateWindow(); }); QAction *quitAction = m_trayMenu->addAction("Quit"); connect(quitAction, &QAction::triggered, this, [this]() { if (m_node) m_node->requestStop(); }); m_trayIcon->setContextMenu(m_trayMenu); connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::onTrayActivated); m_trayIcon->show(); } void MainWindow::closeEvent(QCloseEvent *event) { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: closeEvent — hiding to tray"); hide(); m_trayIcon->showMessage("Chat", "Application minimized to tray.", QSystemTrayIcon::Information, 2000); event->ignore(); } void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick) { show(); raise(); activateWindow(); } } void MainWindow::showShareInviteDialog(const QString& channelId) { if (!m_db || !m_db->isOpen()) return; QByteArray pubkey; if (m_node && m_node->isRunning()) pubkey = QByteArray::fromHex(m_node->pubKeyHex().toLatin1()); if (pubkey.size() != 32) { QMessageBox::warning(this, "Error", "Failed to get node public key."); return; } QList addrs = m_node->getInviteAddresses(m_db); QString link = encodeInviteLink(channelId.toULongLong(), pubkey, addrs); if (link.isEmpty()) { QMessageBox::warning(this, "Error", "Failed to create invite link."); return; } ShareInviteDialog dlg(link, this); dlg.exec(); } void MainWindow::showInviteByLinkDialog(const QString& channelId) { if (!m_node || !m_node->isRunning()) { QMessageBox::warning(this, "Error", "Node is not running."); return; } InviteByLinkDialog dlg(channelId, m_node, this); dlg.exec(); } void MainWindow::showJoinDialog() { if (!m_node || !m_node->isRunning()) { QMessageBox::warning(this, "Error", "Node is not running."); return; } QString autoLink; QClipboard* cb = QApplication::clipboard(); QString clipText = cb->text(QClipboard::Clipboard).trimmed(); if (clipText.startsWith("utun://") && decodeInviteLink(clipText).error.isEmpty()) autoLink = clipText; else if (cb->supportsSelection()) { QString selText = cb->text(QClipboard::Selection).trimmed(); if (selText.startsWith("utun://") && decodeInviteLink(selText).error.isEmpty()) autoLink = selText; } JoinDialog dlg(m_node, m_db, this, autoLink); dlg.exec(); } void MainWindow::showSettings() { SettingsDialog dlg(m_cfgPath, m_db, m_node, m_recorder, this); dlg.exec(); reloadChannels(); } void MainWindow::showConnMonitor() { if (m_connMonitor) { m_connMonitor->raise(); m_connMonitor->activateWindow(); return; } m_connMonitor = new ConnMonitorWindow(nullptr); connect(m_connMonitor, &QDialog::destroyed, this, [this] { m_connMonitor = nullptr; }); m_connMonitor->show(); } void MainWindow::onMessageReceived(const char* ch_id, int ch_id_len, uint64_t author) { QString cid = QString::fromUtf8(ch_id, ch_id_len); if (m_db && author != m_db->myNodeId()) SoundManager::instance()->play("msg_incoming"); if (m_messageList) { if (cid == m_currentChannelId) { m_messageList->refresh(); } else if (m_channelList) { int unread = m_channelList->getChannelUnreadCount(cid) + 1; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "MainWindow::onMessageReceived increment unread ch=%s %d->%d", qPrintable(cid), unread - 1, unread); m_channelList->setChannelUnreadCount(cid, unread); } } if (m_db && m_db->getUiStateInt("channel_sort", 0) == 1) reloadChannels(); if (m_db && m_db->isOpen() && m_db->getUiStateInt(QStringLiteral("ch_%1_auto_play_voice").arg(cid), 0)) { QTimer::singleShot(100, this, [this, cid]() { autoPlayCheckNewMessage(cid); }); } } void MainWindow::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) { m_accountList->onMemberList(chId, count, data, dataLen); } void MainWindow::onMemberUpdated(const QString& chId, const uint8_t* data) { m_accountList->onMemberUpdated(chId, data); } void MainWindow::onMemberRemoved(const QString& chId, uint64_t nodeId) { m_accountList->onMemberRemoved(chId, nodeId); } void MainWindow::onAutoConnectStatus(uint8_t status, uint16_t /*total_tried*/, uint16_t /*node_count*/, uint16_t /*connected*/) { if (m_channelList) { m_channelList->setAutoConnectActive(status == 0); } } void MainWindow::onChannelPeersOnline(const char* ch_id, int ch_id_len, uint16_t online) { if (m_channelList) { QString cid = QString::fromUtf8(ch_id, ch_id_len); m_channelList->setChannelPeersOnline(cid, (int)online); } } void MainWindow::reloadChannels() { if (m_channelList) { m_channelList->loadChannels(); if (!m_pendingChannelSelect.isEmpty()) { m_channelList->selectChannel(m_pendingChannelSelect); m_accountList->setChannel(m_pendingChannelSelect); m_messageList->loadChannel(m_pendingChannelSelect); QSettings().setValue("last_channel_id", m_pendingChannelSelect); m_currentChannelId = m_pendingChannelSelect; m_pendingChannelSelect.clear(); } } } void MainWindow::onCreateGroupRequested() { if (!m_node || !m_node->isRunning()) { QMessageBox::warning(this, "Error", "Node is not running."); return; } if (!gui_bridge_is_uasync_ready()) { QMessageBox::warning(this, "Error", "uTun thread is not ready yet. Please wait and try again."); return; } CreateGroupDialog dlg(this); if (dlg.exec() != QDialog::Accepted) return; QString name = dlg.groupName(); if (name.isEmpty()) return; char* name_copy = u_strdup(name.toUtf8().constData()); if (name_copy) gui_bridge_post_uasync_fn(chat_core_create_channel_auto_trampoline, name_copy); DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MainWindow: create group request posted name=%s", qPrintable(name)); } void MainWindow::showChannelSettings(const QString& channelId) { ChannelSettingsDialog dlg(channelId, m_db, this); dlg.exec(); } QString MainWindow::autoPlayMediaDir() const { if (m_db && m_db->isOpen()) return m_db->dbPath() + QStringLiteral("/media"); return QString(); } void MainWindow::autoPlayCheckNewMessage(const QString& channelId) { if (!m_db || !m_db->isOpen()) return; auto msgs = m_db->getMessages(channelId, 1); if (msgs.isEmpty()) return; const MessageRow& msg = msgs.first(); if (msg.isOutgoing || msg.contentType != QStringLiteral("audio/opus")) return; AutoPlayItem item; item.channelId = channelId; item.msgId = msg.id; item.downloadStarted = false; item.durationSec = 0.0f; QByteArray data = msg.data; if (data.startsWith("wf=")) { QString raw = QString::fromUtf8(data); int durPos = raw.indexOf(";dur=", 3); if (durPos > 0) { int durEnd = raw.indexOf(';', durPos + 5); if (durEnd > 0) { int durMs = raw.mid(durPos + 5, durEnd - durPos - 5).toInt(); item.durationSec = durMs / 1000.0f; } } } QJsonObject la = QJsonDocument::fromJson(msg.localAttrs).object(); QString fp = la.value(QStringLiteral("fp")).toString(); if (!fp.isEmpty()) { QString fullPath = autoPlayMediaDir() + QStringLiteral("/") + channelId + QStringLiteral("/") + fp; if (QFile::exists(fullPath)) { m_autoPlayQueue.enqueue(item); DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "auto-play: enqueue ch=%s msg=%lld file=%s", qPrintable(channelId), (long long)msg.id, qPrintable(fullPath)); if (!m_autoPlayTimer->isActive()) m_autoPlayTimer->start(200); if (!SoundManager::instance()->isCurrentPcmPlaying()) autoPlayTimerTick(); return; } } struct attachment_dl_req* req = (struct attachment_dl_req*) u_malloc(sizeof(struct attachment_dl_req)); if (!req) return; memset(req, 0, sizeof(*req)); strncpy(req->channel_id, channelId.toUtf8().constData(), sizeof(req->channel_id) - 1); req->msg_id = msg.id; gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req); item.downloadStarted = true; m_autoPlayQueue.enqueue(item); m_autoPlayDownloading.insert(msg.id); DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "auto-play: download started ch=%s msg=%lld", qPrintable(channelId), (long long)msg.id); if (!m_autoPlayTimer->isActive()) m_autoPlayTimer->start(200); } void MainWindow::autoPlayTimerTick() { if (SoundManager::instance()->isCurrentPcmPlaying()) return; while (!m_autoPlayQueue.isEmpty()) { AutoPlayItem& item = m_autoPlayQueue.head(); MessageRow msg = m_db->getMessageById(item.channelId, item.msgId); if (msg.id == 0) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "auto-play: msg %lld not found in DB, removing from queue", (long long)item.msgId); m_autoPlayQueue.dequeue(); continue; } QJsonObject la = QJsonDocument::fromJson(msg.localAttrs).object(); QString fp = la.value(QStringLiteral("fp")).toString(); if (!fp.isEmpty()) { QString fullPath = autoPlayMediaDir() + QStringLiteral("/") + item.channelId + QStringLiteral("/") + fp; if (QFile::exists(fullPath)) { QString chId = item.channelId; qint64 msgId = item.msgId; float dur = item.durationSec; m_autoPlayQueue.dequeue(); m_autoPlayDownloading.remove(msgId); DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "auto-play: playing ch=%s msg=%lld file=%s dur=%.1fs", qPrintable(chId), (long long)msgId, qPrintable(fullPath), (double)dur); MessageDelegate::stopAllAutoPlay(); float actualDur = VoicePlayer::playOpusFile(fullPath); MessageDelegate::startAutoPlay(fullPath, actualDur); return; } } if (!item.downloadStarted && !m_autoPlayDownloading.contains(item.msgId)) { struct attachment_dl_req* req = (struct attachment_dl_req*) u_malloc(sizeof(struct attachment_dl_req)); if (!req) return; memset(req, 0, sizeof(*req)); strncpy(req->channel_id, item.channelId.toUtf8().constData(), sizeof(req->channel_id) - 1); req->msg_id = item.msgId; gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req); item.downloadStarted = true; m_autoPlayDownloading.insert(item.msgId); DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "auto-play: download started ch=%s msg=%lld (from queue)", qPrintable(item.channelId), (long long)item.msgId); } return; } m_autoPlayTimer->stop(); } void MainWindow::onAttachmentDownloadedForAutoPlay(const QString& chId, int64_t msgId) { (void)chId; if (m_autoPlayDownloading.remove((qint64)msgId) == 0 && m_autoPlayQueue.isEmpty()) return; if (!m_autoPlayTimer->isActive()) m_autoPlayTimer->start(200); if (!SoundManager::instance()->isCurrentPcmPlaying()) autoPlayTimerTick(); }