diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 7df2170f..5465c83d 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -30,6 +30,7 @@ extern "C" { #include "call_audio_engine.h" #include "radio_audio_engine.h" #include "ptt_hotkey_manager.h" +#include "message_notification.h" extern "C" { #include "../../lib/audio_compressor.h" @@ -92,9 +93,9 @@ static void saveLastChannelId(const QString& channelId) { postUiStateKv("last_channel_id", channelId); } -static void onMsgReceivedCallback(const char* ch_id, int ch_id_len, uint64_t author) { +static void onMsgReceivedCallback(const char* ch_id, int ch_id_len, uint64_t author, int initial_sync) { if (s_mainWindow) - s_mainWindow->onMessageReceived(ch_id, ch_id_len, author); + s_mainWindow->onMessageReceived(ch_id, ch_id_len, author, initial_sync != 0); } static void onChannelUpdatedCallback(const char* ch_id, int ch_id_len) { @@ -181,6 +182,7 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, MainWindow::~MainWindow() { s_mainWindow = nullptr; + if (m_messageNotification) m_messageNotification->dismiss("shutdown"); delete AvatarManager::instance(); delete GroupAvatarManager::instance(); gui_bridge_set_group_avatar_result_cb(nullptr); @@ -799,6 +801,7 @@ void MainWindow::onMessageLinkActivated(const QString& link) { void MainWindow::onDmMsgReceived(const QString& convId, quint64 author) { QString key = QStringLiteral("dm:") + convId; + showIncomingMessageNotification(key, author); if (m_currentChannelId == key) { m_messageList->refresh(); return; @@ -813,6 +816,18 @@ void MainWindow::setupTray() { m_trayIcon->setIcon(QIcon(":/icons/app_icon.png")); m_trayIcon->setToolTip("vibechat"); + m_messageNotification = new MessageNotification(this); + connect(m_messageNotification, &MessageNotification::activated, this, [this](const QString& target) { + if (isMinimized()) setWindowState(windowState() & ~Qt::WindowMinimized); + show(); + raise(); + activateWindow(); + reloadChannels(); + m_channelList->selectChannel(target); + if (m_currentChannelId != target) GUI_WARN("notification: target no longer available target=%s", qPrintable(target)); + else GUI_INFO("notification: opened chat target=%s", qPrintable(target)); + }); + m_trayMenu = new QMenu(this); QAction *settingsAction = m_trayMenu->addAction(QCoreApplication::translate("ChatGui", "Settings")); connect(settingsAction, &QAction::triggered, this, &MainWindow::showSettings); @@ -837,6 +852,30 @@ void MainWindow::setupTray() { m_trayIcon->show(); } +void MainWindow::showIncomingMessageNotification(const QString& target, quint64 author) { + if (!m_db || !m_db->isOpen() || !m_messageNotification) { + GUI_WARN("notification: GUI not ready target=%s author=%llu", qPrintable(target), (unsigned long long)author); + return; + } + if (author == m_db->myNodeId() || isActiveWindow()) { + GUI_DEBUG("notification: skipped target=%s own=%d window_active=%d", + qPrintable(target), int(author == m_db->myNodeId()), int(isActiveWindow())); + return; + } + QString channel; + if (target.startsWith(QStringLiteral("dm:"))) channel = QCoreApplication::translate("ChatGui", "Direct messages"); + else { + for (const auto& row : m_db->getChannels()) { + if (row.channelId == target) { channel = row.name.isEmpty() ? target : row.name; break; } + } + if (channel.isEmpty()) { + GUI_WARN("notification: channel no longer available target=%s", qPrintable(target)); + return; + } + } + m_messageNotification->showMessage(target, m_db->getDisplayName(author), channel, m_trayIcon->geometry()); +} + void MainWindow::closeEvent(QCloseEvent *event) { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: closeEvent — hiding to tray"); saveWindowGeometry(); @@ -848,6 +887,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { 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(); } @@ -951,8 +991,9 @@ void MainWindow::openImage(const QString& filePath) { m_imageViewer->showFullScreen(); } -void MainWindow::onMessageReceived(const char* ch_id, int ch_id_len, uint64_t author) { +void MainWindow::onMessageReceived(const char* ch_id, int ch_id_len, uint64_t author, bool initialSync) { QString cid = QString::fromUtf8(ch_id, ch_id_len); + if (!initialSync) showIncomingMessageNotification(cid, author); if (m_db && author != m_db->myNodeId() && m_db->getUiStateInt(QStringLiteral("ch_%1_sound_on_message").arg(cid), 1)) SoundManager::instance()->play("msg_incoming"); diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index baa62592..691750b0 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -24,6 +24,7 @@ class ImageViewerWindow; class CallWindow; class CallAudioEngine; class MemberFileBrowser; +class MessageNotification; struct AutoPlayItem { QString channelId; @@ -59,7 +60,7 @@ private slots: void autoPlayTimerTick(); public: - void onMessageReceived(const char* ch_id, int ch_id_len, uint64_t author); + void onMessageReceived(const char* ch_id, int ch_id_len, uint64_t author, bool initialSync); void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen); void onMemberUpdated(const QString& chId, const uint8_t* data); void onMemberRemoved(const QString& chId, uint64_t nodeId); @@ -92,6 +93,7 @@ private: void saveWindowGeometry(); void restoreWindowGeometry(); void setupTray(); + void showIncomingMessageNotification(const QString& target, quint64 author); void setupNode(); void setupMessaging(); void setupSoundFromConfig(); @@ -108,6 +110,7 @@ private: QSplitter *m_splitter = nullptr; QSystemTrayIcon *m_trayIcon; QMenu *m_trayMenu; + MessageNotification* m_messageNotification = nullptr; DbManager *m_db; UtunNode *m_node = nullptr; QString m_cfgPath; diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index be3ed5c9..1c59d09b 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -123,13 +123,14 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { case GUI_EVT_MSG_RECEIVED: if (dlen >= 2) { uint8_t chLen = d[0]; - if (dlen >= 1 + chLen + 8) { + if (chLen && dlen == 1 + chLen + 9 && d[1 + chLen + 8] <= 1) { uint64_t author = 0; memcpy(&author, d + 1 + chLen, 8); + const int initialSync = d[1 + chLen + 8]; QString chId = QString::fromUtf8((const char*)d + 1, chLen); - DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: MSG_RECEIVED ch=%s author=%llu", - qPrintable(chId), (unsigned long long)author); - if (g_msg_received_cb) g_msg_received_cb((const char*)d + 1, chLen, author); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: MSG_RECEIVED ch=%s author=%llu initial_sync=%d", + qPrintable(chId), (unsigned long long)author, initialSync); + if (g_msg_received_cb) g_msg_received_cb((const char*)d + 1, chLen, author, initialSync); } else { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "gui_bridge: MSG_RECEIVED invalid data length %d", dlen); }