You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

424 lines
16 KiB

#include "mainwindow.h"
#include "channellist.h"
#include "messagelist.h"
#include "accountlist.h"
#include "invitedialog.h"
#include "joindialog.h"
#include "settingsdialog.h"
#include "creategroupdialog.h"
#include "invite_link.h"
#include "sound_manager.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>
extern "C" {
#include "../transport/chat_core.h"
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
}
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)
{
setWindowTitle("Chat");
resize(900, 600);
setupUi();
setupTray();
setupSoundFromConfig();
if (m_db) {
setupBridgeCallbacks();
setupMessaging();
setupNode();
setupConnects();
}
}
MainWindow::~MainWindow() {
SoundManager::instance()->shutdown();
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "MainWindow: ~MainWindow — saving channel state");
if (m_messageList) m_messageList->saveChannelState();
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "MainWindow: ~MainWindow — stopping utun node");
if (m_node) { m_node->stop(); delete m_node; m_node = nullptr; }
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "MainWindow: ~MainWindow — node stop+delete done");
AnimTimer::shutdown();
MessageList::cleanupAnimations();
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "MainWindow: ~MainWindow — animations cleaned");
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "MainWindow: ~MainWindow done");
}
void MainWindow::setupUi() {
auto *splitter = new QSplitter(Qt::Horizontal, this);
m_channelList = new ChannelList(m_db, splitter);
m_messageList = new MessageList(m_db, 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, []() {
qDebug("UtunNode started");
});
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);
}
}
}
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);
}
});
}
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::showInviteDialog);
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_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_DEBUG, "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::showInviteDialog(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;
}
InviteDialog dlg(link, 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, 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);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MainWindow::onMessageReceived ch=%s author=%llu cur=%s",
qPrintable(cid), (unsigned long long)author, qPrintable(m_currentChannelId));
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();
}
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);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MainWindow::onChannelPeersOnline cid=%s online=%u",
qPrintable(cid), online);
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));
}