stcp: ed25519 pubkey exchange in handshake, fix instance destroy segfault, fix chat_tcp_test
- u_async.c: process immediate_queue BEFORE epoll_wait in uasync_poll - stcp_link.c: defensive NULL etcp_conn/etcp_link in stcp_link_close - utun_instance.c: move stcp_server_list_destroy_all to Phase J (before ETCP), add deferred drain in both Phase J and L - stcp.h/c: STCP_HS_ENC_CLIENT 6→38, STCP_HS_ENC_SERVER 7→39, exchange ed25519_pubkey in both handshake directions - etcp_connections.c: copy peer_ed25519 from STCP to ETCP_CONN in etcp_link_enter_ready_tcp; guard link->etcp in tcp_link_close_cb - chat_sync.c: fix lk->conn NULL dereference for TCP links - test_stcp.c: update stcp_server_create/stcp_client_connect callsites - tools/chat_tcp_test: change transport udp→tcp
This commit is contained in:
1 parent
a3783b5f55
commit
38c9f229bc
1272 files changed
+968388
-1
No files matched your search
@@ -0,0 +1,629 @@
|
||||
#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 "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 <QSplitter>
|
||||
#include <QCloseEvent>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QStyle>
|
||||
#include <QMessageBox>
|
||||
#include <QDebug>
|
||||
#include <QSettings>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QDir>
|
||||
#include <cmath>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
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 onMembersChangedCallback(const char* ch_id, int ch_id_len) {
|
||||
Q_UNUSED(ch_id_len);
|
||||
if (s_mainWindow)
|
||||
s_mainWindow->onMembersChanged(ch_id);
|
||||
}
|
||||
|
||||
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() {
|
||||
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");
|
||||
if (m_accountList && m_node) m_accountList->setUtunInstance(m_node->instance());
|
||||
});
|
||||
connect(m_node, &UtunNode::stopped, this, [this]() {
|
||||
qDebug("UtunNode stopped");
|
||||
if (m_node) m_node->finalize();
|
||||
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_INFO(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_members_changed_cb(onMembersChangedCallback);
|
||||
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::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<NodeAddr> 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::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::onMembersChanged(const char* ch_id) {
|
||||
QString cur = QSettings().value("last_channel_id").toString();
|
||||
if (cur == QString::fromUtf8(ch_id))
|
||||
m_accountList->refresh();
|
||||
}
|
||||
|
||||
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)) {
|
||||
m_autoPlayQueue.dequeue();
|
||||
m_autoPlayDownloading.remove(item.msgId);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "auto-play: playing ch=%s msg=%lld file=%s dur=%.1fs",
|
||||
qPrintable(item.channelId), (long long)item.msgId, qPrintable(fullPath),
|
||||
(double)item.durationSec);
|
||||
MessageDelegate::stopAllAutoPlay();
|
||||
VoicePlayer::playOpusFile(fullPath);
|
||||
MessageDelegate::startAutoPlay(fullPath, item.durationSec);
|
||||
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();
|
||||
}
|
||||
Reference in new issue
Block a user