qt-chat-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 07336a9284b182a23b3899d0b47350cc481e6430
parent b19a8d09d985c6d7eef29cd02b995e3f2c9e2e05
Author: Silas Brack <silasbrack@gmail.com>
Date:   Fri, 26 Dec 2025 22:09:37 +0100

Idk

Diffstat:
MCMakeLists.txt | 3++-
Mderivation_demo.nix | 2++
Mflake.nix | 1+
Mmain.qml | 20++++++++++++++++++++
Mnetworkmanager.cpp | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnetworkmanager.h | 21+++++++++++++++++++++
Mqml/views/ProfileView.qml | 52+++++++++++++++++++++++++++++++++++++++++++++++++++-
7 files changed, 209 insertions(+), 2 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -15,6 +15,7 @@ endif() # Add here any library you need find_package(Qt5 COMPONENTS Widgets Qml Quick Network REQUIRED) +find_package(Qt5Keychain REQUIRED) # Add the resource file as it *must* be compiled (also possible with # qmake or manually with rcc) @@ -28,7 +29,7 @@ add_executable(demo ) # Add here any library you need -target_link_libraries(demo Qt5::Widgets Qt5::Qml Qt5::Quick Qt5::Network) +target_link_libraries(demo Qt5::Widgets Qt5::Qml Qt5::Quick Qt5::Network qt5keychain) # Install the binary install(TARGETS demo DESTINATION bin) diff --git a/derivation_demo.nix b/derivation_demo.nix @@ -3,6 +3,7 @@ , cmake , qt5 , wrapQtAppsHook +, qtkeychain }: stdenv.mkDerivation rec { pname = "Beegram"; @@ -16,6 +17,7 @@ stdenv.mkDerivation rec { qt5.qtbase qt5.qtdeclarative qt5.qtquickcontrols2 + qtkeychain ]; # macOS-specific: Create .app bundle diff --git a/flake.nix b/flake.nix @@ -34,6 +34,7 @@ libsForQt5.qt5.qtbase libsForQt5.qt5.qtdeclarative libsForQt5.qt5.qtquickcontrols2 + qtkeychain libsForQt5.wrapQtAppsHook ]; }; diff --git a/main.qml b/main.qml @@ -14,6 +14,26 @@ Window { property var navigationStack: [] property var stackView: stackViewItem // Expose for child views + Component.onCompleted: { + // Check for saved credentials on startup + NetworkManager.checkSavedCredentials() + } + + // Handle auto-login result + Connections { + target: NetworkManager + + function onAutoLoginSuccess() { + console.log("Auto-login successful, navigating to chat list") + stackViewItem.replace("qrc:/qml/views/ChatListView.qml") + } + + function onAutoLoginFailed(reason) { + console.log("Auto-login failed:", reason) + // Stay on login view (already loaded by default) + } + } + Item { id: stackViewItem anchors.fill: parent diff --git a/networkmanager.cpp b/networkmanager.cpp @@ -5,6 +5,7 @@ #include <QJsonArray> #include <QUrlQuery> #include <QDebug> +#include <qt5keychain/keychain.h> NetworkManager::NetworkManager(QObject *parent) : QObject(parent) @@ -114,6 +115,10 @@ void NetworkManager::onLoginFinished() m_authToken = authToken; qDebug() << "Login successful, token stored"; + // Save token to keychain + saveTokenToKeychain(authToken); + + emit authenticationChanged(); emit loginSuccess(authToken); // Clean up @@ -844,3 +849,110 @@ void NetworkManager::resetReconnectBackoff() m_reconnectAttempts = 0; m_reconnectTimer->stop(); } + +// Keychain Methods + +void NetworkManager::saveTokenToKeychain(const QString& token) +{ + QKeychain::WritePasswordJob *job = new QKeychain::WritePasswordJob("Beegram", this); + job->setKey("auth_token"); + job->setTextData(token); + + connect(job, &QKeychain::WritePasswordJob::finished, this, [this, job]() { + onKeychainWriteFinished(job); + }); + + job->start(); +} + +void NetworkManager::loadTokenFromKeychain() +{ + QKeychain::ReadPasswordJob *job = new QKeychain::ReadPasswordJob("Beegram", this); + job->setKey("auth_token"); + + connect(job, &QKeychain::ReadPasswordJob::finished, this, [this, job]() { + onKeychainReadFinished(job); + }); + + job->start(); +} + +void NetworkManager::deleteTokenFromKeychain() +{ + QKeychain::DeletePasswordJob *job = new QKeychain::DeletePasswordJob("Beegram", this); + job->setKey("auth_token"); + + connect(job, &QKeychain::DeletePasswordJob::finished, this, [this, job]() { + onKeychainDeleteFinished(job); + }); + + job->start(); +} + +// Keychain Slots + +void NetworkManager::onKeychainReadFinished(QKeychain::ReadPasswordJob* job) +{ + if (job->error() == QKeychain::NoError) { + QString token = job->textData(); + if (!token.isEmpty()) { + m_authToken = token; + qDebug() << "Token loaded from keychain"; + emit authenticationChanged(); + emit autoLoginSuccess(); + } else { + qDebug() << "No token found in keychain"; + emit autoLoginFailed("No saved credentials"); + } + } else { + qDebug() << "Keychain read error:" << job->errorString(); + emit autoLoginFailed("Failed to read saved credentials"); + } + + job->deleteLater(); +} + +void NetworkManager::onKeychainWriteFinished(QKeychain::WritePasswordJob* job) +{ + if (job->error() != QKeychain::NoError) { + qWarning() << "Failed to save token to keychain:" << job->errorString(); + } else { + qDebug() << "Token saved to keychain successfully"; + } + + job->deleteLater(); +} + +void NetworkManager::onKeychainDeleteFinished(QKeychain::DeletePasswordJob* job) +{ + if (job->error() != QKeychain::NoError && job->error() != QKeychain::EntryNotFound) { + qWarning() << "Failed to delete token from keychain:" << job->errorString(); + } else { + qDebug() << "Token deleted from keychain"; + } + + job->deleteLater(); +} + +// Public Methods + +void NetworkManager::logout() +{ + // Clear in-memory token + m_authToken.clear(); + + // Unsubscribe from any active SSE connections + unsubscribeFromChat(); + + // Delete token from keychain + deleteTokenFromKeychain(); + + emit authenticationChanged(); + qDebug() << "User logged out"; +} + +void NetworkManager::checkSavedCredentials() +{ + qDebug() << "Checking for saved credentials..."; + loadTokenFromKeychain(); +} diff --git a/networkmanager.h b/networkmanager.h @@ -9,18 +9,28 @@ #include <QSet> #include <QSystemTrayIcon> +namespace QKeychain { + class ReadPasswordJob; + class WritePasswordJob; + class DeletePasswordJob; +} + class NetworkManager : public QObject { Q_OBJECT Q_PROPERTY(QString currentUserId READ currentUserId CONSTANT) + Q_PROPERTY(bool isAuthenticated READ isAuthenticated NOTIFY authenticationChanged) public: explicit NetworkManager(QObject *parent = nullptr); ~NetworkManager(); QString currentUserId() const { return m_currentUserId; } + bool isAuthenticated() const { return !m_authToken.isEmpty(); } Q_INVOKABLE void login(const QString& email, const QString& password); + Q_INVOKABLE void logout(); + Q_INVOKABLE void checkSavedCredentials(); Q_INVOKABLE void postChat(const QString& name); Q_INVOKABLE void fetchChats(const QString& cursor = ""); Q_INVOKABLE void postMessage(int chatId, const QString& content); @@ -33,6 +43,9 @@ public: signals: void loginSuccess(QString authToken); void loginFailed(QString error); + void authenticationChanged(); + void autoLoginSuccess(); + void autoLoginFailed(QString reason); void chatCreated(QVariantMap chatData); void chatsFetched(QVariantList chats, QString cursor); void messageCreated(QVariantMap messageData); @@ -47,6 +60,9 @@ signals: private slots: void onLoginFinished(); + void onKeychainReadFinished(QKeychain::ReadPasswordJob* job); + void onKeychainWriteFinished(QKeychain::WritePasswordJob* job); + void onKeychainDeleteFinished(QKeychain::DeletePasswordJob* job); void onPostChatFinished(); void onPostChatParticipationFinished(); void onFetchChatsFinished(); @@ -63,6 +79,11 @@ private: // Helper methods void createChatParticipation(int chatId, const QVariantMap& chatData); + // Keychain methods + void saveTokenToKeychain(const QString& token); + void loadTokenFromKeychain(); + void deleteTokenFromKeychain(); + // Helper methods for SSE void parseSSEEvent(const QString& eventData); bool isDuplicateMessage(int messageId); diff --git a/qml/views/ProfileView.qml b/qml/views/ProfileView.qml @@ -132,7 +132,57 @@ Item { Layout.fillWidth: true icon: "\ud83d\udd12" label: "Privacy and Security" - showDivider: false + } + + // Spacer + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: TelegramStyle.standardMargin + color: "#f5f5f5" + } + + // Logout button + Rectangle { + Layout.fillWidth: true + height: 50 + color: logoutMouseArea.containsMouse ? "#ffebee" : "white" + + Behavior on color { + ColorAnimation { duration: 100 } + } + + RowLayout { + anchors.fill: parent + anchors.margins: TelegramStyle.standardMargin + spacing: TelegramStyle.standardMargin + + Text { + text: "\ud83d\udebb" + font.pixelSize: 24 + } + + Text { + Layout.fillWidth: true + text: "Logout" + font.pixelSize: TelegramStyle.fontSizeNormal + color: "#f44336" + } + } + + MouseArea { + id: logoutMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + console.log("Logging out...") + NetworkManager.logout() + // Navigate back to login screen + if (typeof stackView !== 'undefined' && stackView) { + stackView.replace(Qt.resolvedUrl("LoginView.qml")) + } + } + } } } }