qt-chat-app

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

commit b19a8d09d985c6d7eef29cd02b995e3f2c9e2e05
parent 2ae0c6e07e0ea9ffd660c332d772cf8c9e7f3789
Author: Silas Brack <silasbrack@gmail.com>
Date:   Fri, 26 Dec 2025 21:51:24 +0100

Working version

Diffstat:
DINSTALL.md | 104-------------------------------------------------------------------------------
Mderivation_demo.nix | 4++--
Mflake.nix | 2+-
Alogo.png | 0
Mmain.cpp | 4++++
Mmain.qml | 10++++++++--
Mnetworkmanager.cpp | 228++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mnetworkmanager.h | 13+++++++++++++
Mqml.qrc | 3++-
Mqml/components/MessageInput.qml | 2+-
Mqml/models/ChatListModel.qml | 4++--
Mqml/models/MessageListModel.qml | 41+++++++++++++----------------------------
Dqml/models/MockData.qml | 282-------------------------------------------------------------------------------
Mqml/models/qmldir | 1-
Aqml/views/LoginView.qml | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mqml/views/MessageThreadView.qml | 2+-
Mqml/views/ProfileView.qml | 15+++++++--------
17 files changed, 453 insertions(+), 453 deletions(-)

diff --git a/INSTALL.md b/INSTALL.md @@ -1,104 +0,0 @@ -# Installation Guide - -## Prerequisites - -Make sure you have Nix installed with flake support enabled. If not: - -```bash -# Install Nix (if not already installed) -curl -L https://nixos.org/nix/install | sh - -# Enable flakes (add to ~/.config/nix/nix.conf or /etc/nix/nix.conf) -experimental-features = nix-command flakes -``` - -## Building the Application - -### Option 1: Build and Run Directly - -```bash -# Build the application -nix build - -# The app will be in ./result/Applications/telegram-clone.app/ -# Run it directly -./result/Applications/telegram-clone.app/Contents/MacOS/demo -``` - -### Option 2: Install to User Profile - -```bash -# Install to your Nix profile -nix profile install . - -# The app will be available in ~/.nix-profile/Applications/ -``` - -### Option 3: Install to /Applications - -```bash -# Build the app -nix build - -# Copy to your Applications folder -cp -r ./result/Applications/telegram-clone.app /Applications/ - -# Or create a symlink -ln -s $(pwd)/result/Applications/telegram-clone.app /Applications/telegram-clone.app -``` - -### Option 4: Create a DMG for Distribution - -```bash -# Build the app first -nix build - -# Create a DMG (requires macOS tools) -hdiutil create -volname "Telegram Clone" \ - -srcfolder ./result/Applications/telegram-clone.app \ - -ov -format UDZO \ - telegram-clone.dmg -``` - -## Development - -Enter the development environment with all dependencies: - -```bash -nix develop -``` - -This gives you access to cmake, Qt5, and all build tools needed for development. - -## Running from Source - -```bash -# Enter dev environment -nix develop - -# Build manually -mkdir build && cd build -cmake .. -make - -# Run -./demo -``` - -## Uninstall - -```bash -# If installed via nix profile -nix profile remove telegram-clone - -# If copied to /Applications -rm -rf /Applications/telegram-clone.app -``` - -## Cross-Platform Notes - -This flake supports both: -- Intel Macs (x86_64-darwin) -- Apple Silicon Macs (aarch64-darwin) - -Nix will automatically build for your current architecture. diff --git a/derivation_demo.nix b/derivation_demo.nix @@ -5,7 +5,7 @@ , wrapQtAppsHook }: stdenv.mkDerivation rec { - pname = "telegram-clone"; + pname = "Beegram"; version = "1.0.0"; src = ./.; @@ -48,7 +48,7 @@ stdenv.mkDerivation rec { ''; meta = with lib; { - description = "Telegram clone Qt application"; + description = "Beegram, a messaging application built using Qt."; license = licenses.mit; platforms = platforms.darwin; maintainers = [ ]; diff --git a/flake.nix b/flake.nix @@ -1,5 +1,5 @@ { - description = "Telegram clone Qt application"; + description = "Beegram, a messaging application built using Qt."; inputs = { # Pin to a version compatible with macOS 13 diff --git a/logo.png b/logo.png Binary files differ. diff --git a/main.cpp b/main.cpp @@ -2,12 +2,16 @@ #include <QApplication> #include <QQmlApplicationEngine> #include <QQmlContext> +#include <QIcon> #include "networkmanager.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); + // Set application icon + app.setWindowIcon(QIcon(":/logo.png")); + QQmlApplicationEngine engine; // Create and register NetworkManager diff --git a/main.qml b/main.qml @@ -8,7 +8,7 @@ Window { height: 700 minimumWidth: 800 minimumHeight: 600 - title: "Telegram Clone" + title: "Beegram" // Simple stack-based navigation using Loader property var navigationStack: [] @@ -21,7 +21,7 @@ Window { Loader { id: contentLoader anchors.fill: parent - source: "qrc:/qml/views/ChatListView.qml" + source: "qrc:/qml/views/LoginView.qml" onLoaded: { if (item) { @@ -50,5 +50,11 @@ Window { contentLoader.setSource(prev.source, prev.properties) } } + + function replace(source, properties) { + // Clear navigation stack and load new page + navigationStack = [] + contentLoader.setSource(source, properties || {}) + } } } diff --git a/networkmanager.cpp b/networkmanager.cpp @@ -9,6 +9,7 @@ NetworkManager::NetworkManager(QObject *parent) : QObject(parent) , m_manager(new QNetworkAccessManager(this)) + , m_currentUserId("gpJmuOPMT7yUqP3lAAI_hA==") , m_sseReply(nullptr) , m_currentSseChat(-1) , m_reconnectTimer(new QTimer(this)) @@ -19,7 +20,7 @@ NetworkManager::NetworkManager(QObject *parent) connect(m_reconnectTimer, &QTimer::timeout, this, &NetworkManager::attemptReconnect); // Initialize system tray icon - m_trayIcon->setIcon(QIcon::fromTheme("mail-message-new")); + m_trayIcon->setIcon(QIcon(":/logo.png")); m_trayIcon->show(); } @@ -28,14 +29,112 @@ NetworkManager::~NetworkManager() unsubscribeFromChat(); } +void NetworkManager::login(const QString& email, const QString& password) +{ + // Create the request + QNetworkRequest request; + request.setUrl(QUrl("http://localhost:4000/api/auth/v1/login")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + // Create JSON body + QJsonObject jsonObj; + jsonObj["email"] = email; + jsonObj["password"] = password; + QJsonDocument jsonDoc(jsonObj); + QByteArray jsonData = jsonDoc.toJson(); + + // Make the POST request + QNetworkReply* reply = m_manager->post(request, jsonData); + + // Connect to the finished signal + connect(reply, &QNetworkReply::finished, this, &NetworkManager::onLoginFinished); +} + +void NetworkManager::onLoginFinished() +{ + QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender()); + if (!reply) { + return; + } + + // Check for network errors + if (reply->error() != QNetworkReply::NoError) { + QString errorMsg = reply->errorString(); + qDebug() << "Network error during login:" << errorMsg; + emit loginFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Get HTTP status code + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check if status code is success (200-299) + if (statusCode < 200 || statusCode >= 300) { + QString errorMsg = QString("Login failed with status code: %1").arg(statusCode); + qDebug() << errorMsg; + emit loginFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Read response data + QByteArray responseData = reply->readAll(); + + // Parse JSON response + QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData); + if (jsonDoc.isNull() || !jsonDoc.isObject()) { + QString errorMsg = "Invalid JSON response from login"; + qDebug() << errorMsg; + emit loginFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Extract auth_token from response + QJsonObject jsonObj = jsonDoc.object(); + if (!jsonObj.contains("auth_token")) { + QString errorMsg = "Login response missing auth_token"; + qDebug() << errorMsg; + emit loginFailed(errorMsg); + reply->deleteLater(); + return; + } + + QString authToken = jsonObj["auth_token"].toString(); + if (authToken.isEmpty()) { + QString errorMsg = "Login response has empty auth_token"; + qDebug() << errorMsg; + emit loginFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Store the auth token + m_authToken = authToken; + qDebug() << "Login successful, token stored"; + + emit loginSuccess(authToken); + + // Clean up + reply->deleteLater(); +} + void NetworkManager::postChat(const QString& name) { // Create the request QNetworkRequest request; request.setUrl(QUrl("http://localhost:4000/api/records/v1/chat")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + // Add Authorization header if token exists + if (!m_authToken.isEmpty()) { + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_authToken).toUtf8()); + } + QJsonObject jsonObj; jsonObj["title"] = name; + jsonObj["created_by"] = m_currentUserId; QJsonDocument jsonDoc(jsonObj); QByteArray jsonData = jsonDoc.toJson(); @@ -53,29 +152,32 @@ void NetworkManager::onPostChatFinished() return; } + // Get HTTP status code + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Read response data for debugging + QByteArray responseData = reply->readAll(); + // Check for network errors if (reply->error() != QNetworkReply::NoError) { QString errorMsg = reply->errorString(); qDebug() << "Network error:" << errorMsg; + qDebug() << "Response body:" << responseData; emit requestFailed(errorMsg); reply->deleteLater(); return; } - // Get HTTP status code - int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - // Check if status code is success (200-299) if (statusCode < 200 || statusCode >= 300) { QString errorMsg = QString("HTTP error: %1").arg(statusCode); + qDebug() << errorMsg; + qDebug() << "Response body:" << responseData; emit requestFailed(errorMsg); reply->deleteLater(); return; } - // Read response data - QByteArray responseData = reply->readAll(); - // Parse JSON response QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData); if (jsonDoc.isNull() || !jsonDoc.isObject()) { @@ -90,7 +192,82 @@ void NetworkManager::onPostChatFinished() QJsonObject jsonObj = jsonDoc.object(); QVariantMap chatData = jsonObj.toVariantMap(); - emit chatCreated(chatData); + // Extract chat ID and create participation entry + int chatId = chatData.value("id", -1).toInt(); + if (chatId > 0) { + // Create participation entry and pass chatData for later emission + createChatParticipation(chatId, chatData); + } else { + emit chatCreated(chatData); + } + + // Clean up + reply->deleteLater(); +} + +void NetworkManager::createChatParticipation(int chatId, const QVariantMap& chatData) +{ + // Create the request + QNetworkRequest request; + request.setUrl(QUrl("http://localhost:4000/api/records/v1/chat_participation")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + // Add Authorization header if token exists + if (!m_authToken.isEmpty()) { + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_authToken).toUtf8()); + } + + // Create JSON body + QJsonObject jsonObj; + jsonObj["chat_id"] = chatId; + jsonObj["user_id"] = m_currentUserId; + QJsonDocument jsonDoc(jsonObj); + QByteArray jsonData = jsonDoc.toJson(); + + // Make the POST request + QNetworkReply* reply = m_manager->post(request, jsonData); + + // Store chat data for later emission + reply->setProperty("chatData", chatData); + + // Connect to the finished signal + connect(reply, &QNetworkReply::finished, this, &NetworkManager::onPostChatParticipationFinished); +} + +void NetworkManager::onPostChatParticipationFinished() +{ + QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender()); + if (!reply) { + return; + } + + // Check for network errors + if (reply->error() != QNetworkReply::NoError) { + QString errorMsg = reply->errorString(); + qDebug() << "Network error creating chat participation:" << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Get HTTP status code + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + // Check if status code is success (200-299) + if (statusCode < 200 || statusCode >= 300) { + QString errorMsg = QString("HTTP error creating chat participation: %1").arg(statusCode); + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Retrieve the stored chat data and emit chatCreated signal + QVariant chatDataVariant = reply->property("chatData"); + if (chatDataVariant.isValid()) { + QVariantMap chatData = chatDataVariant.toMap(); + emit chatCreated(chatData); + } // Clean up reply->deleteLater(); @@ -99,8 +276,9 @@ void NetworkManager::onPostChatFinished() void NetworkManager::fetchChats(const QString& cursor) { // Create the GET request with properly encoded URL - QUrl url("http://localhost:4000/api/records/v1/chat"); + QUrl url("http://localhost:4000/api/records/v1/user_chats"); QUrlQuery query; + // query.addQueryItem("filter[user_id][$eq]", "EbBlvV9iR_-_UumVnu2JIw=="); query.addQueryItem("limit", "15"); query.addQueryItem("order", "-created_at,-id"); @@ -113,6 +291,11 @@ void NetworkManager::fetchChats(const QString& cursor) QNetworkRequest request(url); + // Add Authorization header if token exists + if (!m_authToken.isEmpty()) { + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_authToken).toUtf8()); + } + // Make the GET request QNetworkReply* reply = m_manager->get(request); @@ -217,10 +400,16 @@ void NetworkManager::postMessage(int chatId, const QString& content) request.setUrl(QUrl("http://localhost:4000/api/records/v1/message")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + // Add Authorization header if token exists + if (!m_authToken.isEmpty()) { + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_authToken).toUtf8()); + } + // Create JSON body QJsonObject jsonObj; jsonObj["chat_id"] = chatId; jsonObj["content"] = content; + jsonObj["sender_id"] = m_currentUserId; QJsonDocument jsonDoc(jsonObj); QByteArray jsonData = jsonDoc.toJson(); @@ -300,6 +489,11 @@ void NetworkManager::fetchMessages(int chatId, const QString& cursor) QNetworkRequest request(url); + // Add Authorization header if token exists + if (!m_authToken.isEmpty()) { + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_authToken).toUtf8()); + } + // Make the GET request QNetworkReply* reply = m_manager->get(request); @@ -421,6 +615,11 @@ void NetworkManager::subscribeToChat(int chatId) request.setRawHeader("Accept", "text/event-stream"); request.setRawHeader("Cache-Control", "no-cache"); + // Add Authorization header if token exists + if (!m_authToken.isEmpty()) { + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_authToken).toUtf8()); + } + // Create persistent GET request m_sseReply = m_manager->get(request); m_currentSseChat = chatId; @@ -434,7 +633,6 @@ void NetworkManager::subscribeToChat(int chatId) QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::errorOccurred), this, &NetworkManager::onSseError); - // qDebug() << "SSE: Subscribed to chat" << chatId << "URL:" << urlStr; emit sseConnected(chatId); resetReconnectBackoff(); } @@ -459,7 +657,6 @@ void NetworkManager::unsubscribeFromChat() m_recentMessageIds.clear(); m_reconnectTimer->stop(); - // qDebug() << "SSE: Unsubscribed from chat" << previousChat; emit sseDisconnected(previousChat, "User initiated"); } @@ -518,8 +715,6 @@ void NetworkManager::parseSSEEvent(const QString& eventData) QString data = dataLines.join("\n"); if (data.isEmpty()) return; - // qDebug() << "SSE: Received event, type:" << eventType << "data:" << data; - // Parse JSON data QJsonDocument jsonDoc = QJsonDocument::fromJson(data.toUtf8()); if (!jsonDoc.isObject()) { @@ -547,7 +742,6 @@ void NetworkManager::parseSSEEvent(const QString& eventData) // Check for duplicates if (isDuplicateMessage(messageId)) { - // qDebug() << "SSE: Ignoring duplicate message" << messageId; return; } @@ -573,7 +767,6 @@ void NetworkManager::parseSSEEvent(const QString& eventData) 5000); // Emit to QML layer - // qDebug() << "SSE: New message received:" << messageId; emit messageReceived(messageData); } @@ -603,8 +796,6 @@ void NetworkManager::onSseFinished() int chatId = m_currentSseChat; QString reason = "Connection closed by server"; - // qDebug() << "SSE: Connection finished for chat" << chatId; - // Cleanup m_sseReply->deleteLater(); m_sseReply = nullptr; @@ -635,8 +826,6 @@ void NetworkManager::attemptReconnect() if (m_currentSseChat < 0) return; int chatId = m_currentSseChat; - // qDebug() << "SSE: Attempting reconnect to chat" << chatId - // << "attempt" << m_reconnectAttempts; subscribeToChat(chatId); } @@ -647,7 +836,6 @@ void NetworkManager::scheduleReconnect() int delay = qMin(1000 * (1 << m_reconnectAttempts), 30000); m_reconnectAttempts++; - // qDebug() << "SSE: Scheduling reconnect in" << delay << "ms"; m_reconnectTimer->start(delay); } diff --git a/networkmanager.h b/networkmanager.h @@ -12,11 +12,15 @@ class NetworkManager : public QObject { Q_OBJECT + Q_PROPERTY(QString currentUserId READ currentUserId CONSTANT) public: explicit NetworkManager(QObject *parent = nullptr); ~NetworkManager(); + QString currentUserId() const { return m_currentUserId; } + + Q_INVOKABLE void login(const QString& email, const QString& password); Q_INVOKABLE void postChat(const QString& name); Q_INVOKABLE void fetchChats(const QString& cursor = ""); Q_INVOKABLE void postMessage(int chatId, const QString& content); @@ -27,6 +31,8 @@ public: Q_INVOKABLE void unsubscribeFromChat(); signals: + void loginSuccess(QString authToken); + void loginFailed(QString error); void chatCreated(QVariantMap chatData); void chatsFetched(QVariantList chats, QString cursor); void messageCreated(QVariantMap messageData); @@ -40,7 +46,9 @@ signals: void sseError(QString error); private slots: + void onLoginFinished(); void onPostChatFinished(); + void onPostChatParticipationFinished(); void onFetchChatsFinished(); void onPostMessageFinished(); void onFetchMessagesFinished(); @@ -52,6 +60,9 @@ private slots: void attemptReconnect(); private: + // Helper methods + void createChatParticipation(int chatId, const QVariantMap& chatData); + // Helper methods for SSE void parseSSEEvent(const QString& eventData); bool isDuplicateMessage(int messageId); @@ -60,6 +71,8 @@ private: void resetReconnectBackoff(); QNetworkAccessManager* m_manager; + QString m_authToken; + QString m_currentUserId; // SSE members QNetworkReply* m_sseReply; diff --git a/qml.qrc b/qml.qrc @@ -1,8 +1,10 @@ <RCC> <qresource prefix="/"> <file>main.qml</file> + <file>logo.png</file> <!-- Views --> + <file>qml/views/LoginView.qml</file> <file>qml/views/ChatListView.qml</file> <file>qml/views/MessageThreadView.qml</file> <file>qml/views/ProfileView.qml</file> @@ -17,7 +19,6 @@ <file>qml/components/MessageInput.qml</file> <!-- Models --> - <file>qml/models/MockData.qml</file> <file>qml/models/ChatListModel.qml</file> <file>qml/models/MessageListModel.qml</file> <file>qml/models/qmldir</file> diff --git a/qml/components/MessageInput.qml b/qml/components/MessageInput.qml @@ -63,7 +63,7 @@ Rectangle { } Keys.onReturnPressed: { - if (event.modifiers & Qt.ControlModifier) { + if (messageInput.text.trim().length > 0) { sendButton.clicked() } } diff --git a/qml/models/ChatListModel.qml b/qml/models/ChatListModel.qml @@ -4,15 +4,15 @@ Item { id: root property ListModel model: ListModel {} - property int nextId: 100 // Start after mock data IDs property string currentCursor: "" property bool hasMore: true property bool isLoadingMore: false function addChat(serverData) { // Transform server response to chat object format + console.log("asdf", serverData.id); model.append({ - id: serverData.id || nextId++, + id: serverData.id || 123, title: serverData.title || serverData.name || "Unknown", lastMessage: serverData.lastMessage || "", created_at: serverData.created_at || serverData.timestamp || "now", diff --git a/qml/models/MessageListModel.qml b/qml/models/MessageListModel.qml @@ -9,24 +9,6 @@ Item { property bool hasMore: true property bool isLoadingMore: false - function addMessage(serverData) { - // Handle different response formats - var messageId = serverData.id - if (!messageId && serverData.ids && serverData.ids.length > 0) { - messageId = parseInt(serverData.ids[0]) - } - - // Transform server response to message object format - model.append({ - id: messageId || 0, - senderId: serverData.sender_id || 0, - text: serverData.content || "", - timestamp: serverData.sent_at || new Date().toISOString(), - isOutgoing: true, // Newly created messages are outgoing - isRead: true - }) - } - function loadMessages(messages, cursor, isInitialLoad) { // Clear existing messages on initial load if (isInitialLoad) { @@ -35,12 +17,13 @@ Item { // Initial load: DB returns newest first, so reverse to get oldest first for (var i = messages.length - 1; i >= 0; i--) { var message = messages[i] + var senderId = message.sender_id || "" model.append({ id: message.id, - senderId: message.sender_id || 0, - text: message.content || "", - timestamp: message.sent_at || "", - isOutgoing: false, // TODO: determine based on current user + senderId: senderId, + text: message.content, + timestamp: message.sent_at, + isOutgoing: (senderId === NetworkManager.currentUserId), isRead: true }) } @@ -49,12 +32,13 @@ Item { // We want to insert them at the top, oldest of this batch first for (var i = 0; i < messages.length; i++) { var message = messages[i] + var senderId = message.sender_id || "" model.insert(0, { id: message.id, - senderId: message.sender_id || 0, + senderId: senderId, text: message.content || "", timestamp: message.sent_at || "", - isOutgoing: false, // TODO: determine based on current user + isOutgoing: (senderId === NetworkManager.currentUserId), isRead: true }) } @@ -100,12 +84,13 @@ Item { } // Transform and add to model (append for newest at bottom) + var senderId = messageData.sender_id || "" model.append({ id: messageData.id || 0, - senderId: messageData.sender_id || 0, - text: messageData.content || "", - timestamp: messageData.sent_at || new Date().toISOString(), - isOutgoing: false, + senderId: senderId, + text: messageData.content, + timestamp: messageData.sent_at, + isOutgoing: (senderId === NetworkManager.currentUserId), isRead: false }) } diff --git a/qml/models/MockData.qml b/qml/models/MockData.qml @@ -1,282 +0,0 @@ -pragma Singleton -import QtQuick 2.5 - -QtObject { - // Current user - readonly property var currentUser: ({ - "id": 0, - "name": "You", - "phone": "+1 234 567 8900", - "bio": "Available", - "avatar": "" - }) - - // Chat list - readonly property var chats: [ - { - "id": 1, - "name": "Alice Johnson", - "lastMessage": "See you tomorrow!", - "timestamp": "14:32", - "unreadCount": 2, - "avatar": "", - "isOnline": true - }, - { - "id": 2, - "name": "Bob Smith", - "lastMessage": "Thanks for the update", - "timestamp": "Yesterday", - "unreadCount": 0, - "avatar": "", - "isOnline": false - }, - { - "id": 3, - "name": "Carol Williams", - "lastMessage": "That sounds great!", - "timestamp": "13:45", - "unreadCount": 1, - "avatar": "", - "isOnline": true - }, - { - "id": 4, - "name": "Dev Team", - "lastMessage": "Code review completed", - "timestamp": "Monday", - "unreadCount": 0, - "avatar": "", - "isOnline": false - }, - { - "id": 5, - "name": "Emma Davis", - "lastMessage": "Check out this photo!", - "timestamp": "10:20", - "unreadCount": 3, - "avatar": "", - "isOnline": true - }, - { - "id": 6, - "name": "Frank Miller", - "lastMessage": "Meeting at 3 PM", - "timestamp": "09:15", - "unreadCount": 0, - "avatar": "", - "isOnline": false - }, - { - "id": 7, - "name": "Grace Lee", - "lastMessage": "Perfect, thank you!", - "timestamp": "Sunday", - "unreadCount": 0, - "avatar": "", - "isOnline": true - }, - { - "id": 8, - "name": "Henry Cooper", - "lastMessage": "Let me know when you're free", - "timestamp": "Saturday", - "unreadCount": 0, - "avatar": "", - "isOnline": false - } - ] - - // Messages by chat ID - readonly property var messages: ({ - "1": [ - { - "id": 1, - "senderId": 1, - "text": "Hey, how are you?", - "timestamp": "14:28", - "isOutgoing": false, - "isRead": true - }, - { - "id": 2, - "senderId": 0, - "text": "I'm good! How about you?", - "timestamp": "14:30", - "isOutgoing": true, - "isRead": true - }, - { - "id": 3, - "senderId": 1, - "text": "See you tomorrow!", - "timestamp": "14:32", - "isOutgoing": false, - "isRead": false - } - ], - "2": [ - { - "id": 1, - "senderId": 2, - "text": "Did you get my email about the project?", - "timestamp": "Yesterday", - "isOutgoing": false, - "isRead": true - }, - { - "id": 2, - "senderId": 0, - "text": "Yes, I did! Everything looks good.", - "timestamp": "Yesterday", - "isOutgoing": true, - "isRead": true - }, - { - "id": 3, - "senderId": 2, - "text": "Thanks for the update", - "timestamp": "Yesterday", - "isOutgoing": false, - "isRead": true - } - ], - "3": [ - { - "id": 1, - "senderId": 0, - "text": "Want to grab lunch today?", - "timestamp": "13:40", - "isOutgoing": true, - "isRead": true - }, - { - "id": 2, - "senderId": 3, - "text": "That sounds great!", - "timestamp": "13:45", - "isOutgoing": false, - "isRead": false - } - ], - "4": [ - { - "id": 1, - "senderId": 4, - "text": "The new feature is ready for review", - "timestamp": "Monday", - "isOutgoing": false, - "isRead": true - }, - { - "id": 2, - "senderId": 0, - "text": "Great! I'll check it out", - "timestamp": "Monday", - "isOutgoing": true, - "isRead": true - }, - { - "id": 3, - "senderId": 4, - "text": "Code review completed", - "timestamp": "Monday", - "isOutgoing": false, - "isRead": true - } - ], - "5": [ - { - "id": 1, - "senderId": 5, - "text": "Check out this photo!", - "timestamp": "10:20", - "isOutgoing": false, - "isRead": false, - "attachments": [ - { - "type": "image", - "filename": "vacation.jpg" - } - ] - }, - { - "id": 2, - "senderId": 5, - "text": "From my weekend trip", - "timestamp": "10:21", - "isOutgoing": false, - "isRead": false - }, - { - "id": 3, - "senderId": 5, - "text": "It was amazing!", - "timestamp": "10:22", - "isOutgoing": false, - "isRead": false - } - ], - "6": [ - { - "id": 1, - "senderId": 0, - "text": "What time works for you?", - "timestamp": "09:10", - "isOutgoing": true, - "isRead": true - }, - { - "id": 2, - "senderId": 6, - "text": "Meeting at 3 PM", - "timestamp": "09:15", - "isOutgoing": false, - "isRead": true - } - ], - "7": [ - { - "id": 1, - "senderId": 7, - "text": "Could you send me the document?", - "timestamp": "Sunday", - "isOutgoing": false, - "isRead": true - }, - { - "id": 2, - "senderId": 0, - "text": "Sure, here it is", - "timestamp": "Sunday", - "isOutgoing": true, - "isRead": true, - "attachments": [ - { - "type": "file", - "filename": "report.pdf", - "size": "2.4 MB" - } - ] - }, - { - "id": 3, - "senderId": 7, - "text": "Perfect, thank you!", - "timestamp": "Sunday", - "isOutgoing": false, - "isRead": true - } - ], - "8": [ - { - "id": 1, - "senderId": 8, - "text": "Let me know when you're free", - "timestamp": "Saturday", - "isOutgoing": false, - "isRead": true - } - ] - }) -} diff --git a/qml/models/qmldir b/qml/models/qmldir @@ -1,3 +1,2 @@ -singleton MockData 1.0 MockData.qml ChatListModel 1.0 ChatListModel.qml MessageListModel 1.0 MessageListModel.qml diff --git a/qml/views/LoginView.qml b/qml/views/LoginView.qml @@ -0,0 +1,191 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.3 +import "../styles" + +Item { + id: root + + function performLogin() { + errorMessage.text = "" + + if (emailInput.text === "") { + errorMessage.text = "Please enter your email" + return + } + + if (passwordInput.text === "") { + errorMessage.text = "Please enter your password" + return + } + + NetworkManager.login(emailInput.text, passwordInput.text) + } + + Rectangle { + anchors.fill: parent + color: TelegramStyle.backgroundColor + + // Center the login form + Item { + anchors.centerIn: parent + width: 400 + height: columnLayout.height + + ColumnLayout { + id: columnLayout + width: parent.width + spacing: TelegramStyle.standardMargin * 2 + + // Title + Text { + Layout.alignment: Qt.AlignHCenter + text: "Login" + font.pixelSize: 32 + font.bold: true + color: TelegramStyle.textPrimary + } + + // Email input + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + Text { + text: "Email" + font.pixelSize: TelegramStyle.fontSizeSmall + color: TelegramStyle.textSecondary + } + + Rectangle { + Layout.fillWidth: true + height: 48 + border.color: emailInput.activeFocus ? TelegramStyle.primaryColor : TelegramStyle.divider + border.width: emailInput.activeFocus ? 2 : 1 + radius: 6 + color: "white" + + TextInput { + id: emailInput + anchors.fill: parent + anchors.margins: TelegramStyle.standardMargin + verticalAlignment: TextInput.AlignVCenter + font.pixelSize: TelegramStyle.fontSizeNormal + color: TelegramStyle.textPrimary + selectByMouse: true + + Text { + anchors.fill: parent + verticalAlignment: TextInput.AlignVCenter + text: "you@example.com" + color: TelegramStyle.textSecondary + visible: !emailInput.text && !emailInput.activeFocus + } + } + } + } + + // Password input + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + Text { + text: "Password" + font.pixelSize: TelegramStyle.fontSizeSmall + color: TelegramStyle.textSecondary + } + + Rectangle { + Layout.fillWidth: true + height: 48 + border.color: passwordInput.activeFocus ? TelegramStyle.primaryColor : TelegramStyle.divider + border.width: passwordInput.activeFocus ? 2 : 1 + radius: 6 + color: "white" + + TextInput { + id: passwordInput + anchors.fill: parent + anchors.margins: TelegramStyle.standardMargin + verticalAlignment: TextInput.AlignVCenter + font.pixelSize: TelegramStyle.fontSizeNormal + color: TelegramStyle.textPrimary + echoMode: TextInput.Password + selectByMouse: true + + Text { + anchors.fill: parent + verticalAlignment: TextInput.AlignVCenter + text: "Enter your password" + color: TelegramStyle.textSecondary + visible: !passwordInput.text && !passwordInput.activeFocus + } + + // Submit on Enter key + Keys.onReturnPressed: root.performLogin() + } + } + } + + // Error message + Text { + id: errorMessage + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + text: "" + font.pixelSize: TelegramStyle.fontSizeSmall + color: "#f44336" + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + visible: text !== "" + } + + // Login button + Rectangle { + id: loginButton + Layout.fillWidth: true + height: 48 + radius: 6 + color: loginMouseArea.containsMouse ? TelegramStyle.accentColor : TelegramStyle.primaryColor + + Behavior on color { + ColorAnimation { duration: 100 } + } + + Text { + anchors.centerIn: parent + text: "Login" + font.pixelSize: TelegramStyle.fontSizeNormal + font.bold: true + color: "white" + } + + MouseArea { + id: loginMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.performLogin() + } + } + } + } + } + + // Handle login success/failure + Connections { + target: NetworkManager + + function onLoginSuccess(authToken) { + console.log("Login successful!") + // Navigate to chat list + if (typeof stackView !== 'undefined' && stackView) { + stackView.replace(Qt.resolvedUrl("ChatListView.qml")) + } + } + + function onLoginFailed(error) { + errorMessage.text = "Login failed: " + error + } + } +} diff --git a/qml/views/MessageThreadView.qml b/qml/views/MessageThreadView.qml @@ -21,7 +21,7 @@ Item { Connections { target: NetworkManager function onMessageCreated(messageData) { - messageListModel.addMessage(messageData) + // Don't add message locally - wait for SSE to receive it root.isSending = false } function onRequestFailed(error) { diff --git a/qml/views/ProfileView.qml b/qml/views/ProfileView.qml @@ -1,7 +1,6 @@ import QtQuick 2.5 import QtQuick.Layouts 1.3 import "../components" -import "../models" import "../styles" Item { @@ -53,14 +52,14 @@ Item { Avatar { Layout.alignment: Qt.AlignHCenter size: TelegramStyle.largeAvatarSize - name: MockData.currentUser.name - imageSource: MockData.currentUser.avatar + name: "User" + imageSource: "" showOnlineIndicator: false } Text { Layout.alignment: Qt.AlignHCenter - text: MockData.currentUser.name + text: "User" font.pixelSize: 20 font.bold: true color: TelegramStyle.textPrimary @@ -68,7 +67,7 @@ Item { Text { Layout.alignment: Qt.AlignHCenter - text: MockData.currentUser.bio + text: "Available" font.pixelSize: TelegramStyle.fontSizeSmall color: TelegramStyle.textSecondary } @@ -92,21 +91,21 @@ Item { Layout.fillWidth: true icon: "\ud83d\udcde" label: "Phone" - value: MockData.currentUser.phone + value: "+1 000 000 0000" } ProfileInfoItem { Layout.fillWidth: true icon: "\ud83d\udd16" label: "Username" - value: "@" + MockData.currentUser.name.toLowerCase().replace(" ", "") + value: "@user" } ProfileInfoItem { Layout.fillWidth: true icon: "\u2139" label: "Bio" - value: MockData.currentUser.bio + value: "Available" showDivider: false }