qt-chat-app

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

commit bc21606bf9288440732b083590fc3a91ac4f1eb6
parent 53c7670531b915dbf620a984895c9a1bf8a842ab
Author: Silas Brack <silasbrack@gmail.com>
Date:   Fri, 26 Dec 2025 14:44:16 +0100

Working

Diffstat:
M.gitignore | 1+
MCMakeLists.txt | 6+++---
Mmain.cpp | 9++++++++-
Anetworkmanager.cpp | 392+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anetworkmanager.h | 38++++++++++++++++++++++++++++++++++++++
Mqml.qrc | 2++
Mqml/components/Avatar.qml | 5++++-
Mqml/components/ChatListItem.qml | 7++++---
Mqml/components/TopBar.qml | 22++++++++++++++++++++++
Aqml/models/ChatListModel.qml | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/models/MessageListModel.qml | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mqml/models/qmldir | 2++
Mqml/views/ChatListView.qml | 54+++++++++++++++++++++++++++++++++++++++++++++++++++---
Mqml/views/MessageThreadView.qml | 67++++++++++++++++++++++++++++++++++++++++++++++---------------------
14 files changed, 740 insertions(+), 32 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,2 +1,3 @@ result +tags diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -14,13 +14,13 @@ if(CMAKE_VERSION VERSION_LESS "3.7.0") endif() # Add here any library you need -find_package(Qt5 COMPONENTS Widgets Qml Quick REQUIRED) +find_package(Qt5 COMPONENTS Widgets Qml Quick Network REQUIRED) # Add the resource file as it *must* be compiled (also possible with # qmake or manually with rcc) # https://doc.qt.io/qt-5/resources.html # https://doc.qt.io/qt-5/qtcore-cmake-qt5-add-resources.html -set(SOURCES main.cpp) # A variable is needed as I guess it will add more sources inside +set(SOURCES main.cpp networkmanager.cpp) # A variable is needed as I guess it will add more sources inside qt5_add_resources(SOURCES qml.qrc) add_executable(demo @@ -28,7 +28,7 @@ add_executable(demo ) # Add here any library you need -target_link_libraries(demo Qt5::Widgets Qt5::Qml Qt5::Quick) +target_link_libraries(demo Qt5::Widgets Qt5::Qml Qt5::Quick Qt5::Network) # Install the binary install(TARGETS demo DESTINATION bin) diff --git a/main.cpp b/main.cpp @@ -1,13 +1,20 @@ //https://riptutorial.com/qml/example/14475/creating-a-qtquick-window-from-cplusplus #include <QGuiApplication> #include <QQmlApplicationEngine> +#include <QQmlContext> +#include "networkmanager.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; + + // Create and register NetworkManager + NetworkManager networkManager; + engine.rootContext()->setContextProperty("NetworkManager", &networkManager); + engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); - + return app.exec(); } diff --git a/networkmanager.cpp b/networkmanager.cpp @@ -0,0 +1,392 @@ +#include "networkmanager.h" +#include <QNetworkRequest> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QUrlQuery> +#include <QDebug> + +NetworkManager::NetworkManager(QObject *parent) + : QObject(parent) + , m_manager(new QNetworkAccessManager(this)) +{ +} + +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"); + QJsonObject jsonObj; + jsonObj["title"] = name; + 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::onPostChatFinished); +} + +void NetworkManager::onPostChatFinished() +{ + 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:" << 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: %1").arg(statusCode); + 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()) { + QString errorMsg = "Invalid JSON response"; + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Convert to QVariantMap for QML + QJsonObject jsonObj = jsonDoc.object(); + QVariantMap chatData = jsonObj.toVariantMap(); + + emit chatCreated(chatData); + + // Clean up + reply->deleteLater(); +} + +void NetworkManager::fetchChats(const QString& cursor) +{ + // Create the GET request with properly encoded URL + QUrl url("http://localhost:4000/api/records/v1/chat"); + QUrlQuery query; + query.addQueryItem("limit", "15"); + query.addQueryItem("order", "-created_at,-id"); + + // Add cursor parameter if provided + if (!cursor.isEmpty()) { + query.addQueryItem("cursor", cursor); + } + + url.setQuery(query); + + QNetworkRequest request(url); + + // Make the GET request + QNetworkReply* reply = m_manager->get(request); + + // Connect to the finished signal + connect(reply, &QNetworkReply::finished, this, &NetworkManager::onFetchChatsFinished); +} + +void NetworkManager::onFetchChatsFinished() +{ + 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 fetching chats:" << 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 fetching chats: %1").arg(statusCode); + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Read response data + QByteArray responseData = reply->readAll(); + + // Parse JSON response + QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData); + if (jsonDoc.isNull()) { + QString errorMsg = "Invalid JSON response"; + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + QVariantList chatsList; + QString nextCursor = ""; + + // Handle paginated response format: {"cursor": "...", "records": [...]} + if (jsonDoc.isObject()) { + QJsonObject rootObj = jsonDoc.object(); + + // Extract cursor for next page + if (rootObj.contains("cursor")) { + nextCursor = rootObj["cursor"].toString(); + } + + if (rootObj.contains("records") && rootObj["records"].isArray()) { + QJsonArray jsonArray = rootObj["records"].toArray(); + for (const QJsonValue& value : jsonArray) { + if (value.isObject()) { + chatsList.append(value.toObject().toVariantMap()); + } + } + } else { + QString errorMsg = "Invalid JSON response - missing 'records' array"; + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + } + // Handle direct array format: [...] + else if (jsonDoc.isArray()) { + QJsonArray jsonArray = jsonDoc.array(); + for (const QJsonValue& value : jsonArray) { + if (value.isObject()) { + chatsList.append(value.toObject().toVariantMap()); + } + } + } else { + QString errorMsg = "Invalid JSON response format"; + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + emit chatsFetched(chatsList, nextCursor); + + // Clean up + reply->deleteLater(); +} + +void NetworkManager::postMessage(int chatId, const QString& content) +{ + // Create the request + QNetworkRequest request; + request.setUrl(QUrl("http://localhost:4000/api/records/v1/message")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + // Create JSON body + QJsonObject jsonObj; + jsonObj["chat_id"] = chatId; + jsonObj["content"] = content; + 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::onPostMessageFinished); + + qDebug() << "Sending POST request to create message for chat:" << chatId; +} + +void NetworkManager::onPostMessageFinished() +{ + 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 message:" << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Get HTTP status code + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qDebug() << "Create message HTTP status code:" << statusCode; + + // Check if status code is success (200-299) + if (statusCode < 200 || statusCode >= 300) { + QString errorMsg = QString("HTTP error creating message: %1").arg(statusCode); + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Read response data + QByteArray responseData = reply->readAll(); + qDebug() << "Message created response:" << responseData; + + // Parse JSON response + QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData); + if (jsonDoc.isNull() || !jsonDoc.isObject()) { + QString errorMsg = "Invalid JSON response"; + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Convert to QVariantMap for QML + QJsonObject jsonObj = jsonDoc.object(); + QVariantMap messageData = jsonObj.toVariantMap(); + + qDebug() << "Message created successfully:" << messageData; + emit messageCreated(messageData); + + // Clean up + reply->deleteLater(); +} + +void NetworkManager::fetchMessages(int chatId, const QString& cursor) +{ + // Create the GET request with properly encoded URL + QUrl url("http://localhost:4000/api/records/v1/message"); + QUrlQuery query; + query.addQueryItem("limit", "20"); + query.addQueryItem("order", "-sent_at,-id"); + query.addQueryItem("chat_id", QString::number(chatId)); + + // Add cursor parameter if provided + if (!cursor.isEmpty()) { + query.addQueryItem("cursor", cursor); + } + + url.setQuery(query); + + QNetworkRequest request(url); + + // Make the GET request + QNetworkReply* reply = m_manager->get(request); + + // Connect to the finished signal + connect(reply, &QNetworkReply::finished, this, &NetworkManager::onFetchMessagesFinished); + + qDebug() << "Fetching messages from server with URL:" << url.toString(); +} + +void NetworkManager::onFetchMessagesFinished() +{ + 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 fetching messages:" << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Get HTTP status code + int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qDebug() << "Fetch messages HTTP status code:" << statusCode; + + // Check if status code is success (200-299) + if (statusCode < 200 || statusCode >= 300) { + QString errorMsg = QString("HTTP error fetching messages: %1").arg(statusCode); + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + // Read response data + QByteArray responseData = reply->readAll(); + qDebug() << "Messages response data:" << responseData; + + // Parse JSON response + QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData); + if (jsonDoc.isNull()) { + QString errorMsg = "Invalid JSON response"; + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + QVariantList messagesList; + QString nextCursor = ""; + + // Handle paginated response format: {"cursor": "...", "records": [...]} + if (jsonDoc.isObject()) { + QJsonObject rootObj = jsonDoc.object(); + + // Extract cursor for next page + if (rootObj.contains("cursor")) { + nextCursor = rootObj["cursor"].toString(); + } + + if (rootObj.contains("records") && rootObj["records"].isArray()) { + QJsonArray jsonArray = rootObj["records"].toArray(); + for (const QJsonValue& value : jsonArray) { + if (value.isObject()) { + messagesList.append(value.toObject().toVariantMap()); + } + } + } else { + QString errorMsg = "Invalid JSON response - missing 'records' array"; + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + } + // Handle direct array format: [...] + else if (jsonDoc.isArray()) { + QJsonArray jsonArray = jsonDoc.array(); + for (const QJsonValue& value : jsonArray) { + if (value.isObject()) { + messagesList.append(value.toObject().toVariantMap()); + } + } + } else { + QString errorMsg = "Invalid JSON response format"; + qDebug() << errorMsg; + emit requestFailed(errorMsg); + reply->deleteLater(); + return; + } + + qDebug() << "Fetched" << messagesList.size() << "messages successfully, next cursor:" << nextCursor; + emit messagesFetched(messagesList, nextCursor); + + // Clean up + reply->deleteLater(); +} diff --git a/networkmanager.h b/networkmanager.h @@ -0,0 +1,38 @@ +#ifndef NETWORKMANAGER_H +#define NETWORKMANAGER_H + +#include <QObject> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QVariantMap> + +class NetworkManager : public QObject +{ + Q_OBJECT + +public: + explicit NetworkManager(QObject *parent = nullptr); + + Q_INVOKABLE void postChat(const QString& name); + Q_INVOKABLE void fetchChats(const QString& cursor = ""); + Q_INVOKABLE void postMessage(int chatId, const QString& content); + Q_INVOKABLE void fetchMessages(int chatId, const QString& cursor = ""); + +signals: + void chatCreated(QVariantMap chatData); + void chatsFetched(QVariantList chats, QString cursor); + void messageCreated(QVariantMap messageData); + void messagesFetched(QVariantList messages, QString cursor); + void requestFailed(QString error); + +private slots: + void onPostChatFinished(); + void onFetchChatsFinished(); + void onPostMessageFinished(); + void onFetchMessagesFinished(); + +private: + QNetworkAccessManager* m_manager; +}; + +#endif // NETWORKMANAGER_H diff --git a/qml.qrc b/qml.qrc @@ -18,6 +18,8 @@ <!-- Models --> <file>qml/models/MockData.qml</file> + <file>qml/models/ChatListModel.qml</file> + <file>qml/models/MessageListModel.qml</file> <file>qml/models/qmldir</file> <!-- Styles --> diff --git a/qml/components/Avatar.qml b/qml/components/Avatar.qml @@ -9,6 +9,7 @@ Item { property int size: TelegramStyle.avatarSize property string imageSource: "" property string name: "" + property int chatId: 0 property bool showOnlineIndicator: false // Background circle with color based on name @@ -69,7 +70,9 @@ Item { } function getInitials(name) { - if (!name || name === "") return "?" + if (!name || name === "") { + return chatId > 0 ? chatId.toString() : "?" + } var parts = name.split(" ") if (parts.length >= 2) { return (parts[0][0] + parts[1][0]).toUpperCase() diff --git a/qml/components/ChatListItem.qml b/qml/components/ChatListItem.qml @@ -22,7 +22,8 @@ Rectangle { // Avatar Avatar { size: TelegramStyle.avatarSize - name: chatData ? chatData.name : "" + name: chatData ? chatData.title : "" + chatId: chatData ? chatData.id : 0 imageSource: chatData ? chatData.avatar : "" showOnlineIndicator: chatData ? chatData.isOnline : false } @@ -38,7 +39,7 @@ Rectangle { Text { Layout.fillWidth: true - text: chatData ? chatData.name : "" + text: chatData ? chatData.title : "" font.bold: chatData ? chatData.unreadCount > 0 : false font.pixelSize: TelegramStyle.fontSizeNormal color: TelegramStyle.textPrimary @@ -46,7 +47,7 @@ Rectangle { } Text { - text: chatData ? chatData.timestamp : "" + text: chatData ? chatData.created_at : "" font.pixelSize: TelegramStyle.fontSizeSmall color: chatData && chatData.unreadCount > 0 ? TelegramStyle.unreadBadge : TelegramStyle.textSecondary } diff --git a/qml/components/TopBar.qml b/qml/components/TopBar.qml @@ -10,9 +10,11 @@ Rectangle { property string title: "" property bool showBackButton: false property bool showMenuButton: false + property bool showAddButton: false signal backClicked() signal menuClicked() + signal addClicked() RowLayout { anchors.fill: parent @@ -51,6 +53,26 @@ Rectangle { verticalAlignment: Text.AlignVCenter } + // Add button + Item { + visible: showAddButton + Layout.preferredWidth: 40 + Layout.fillHeight: true + + Text { + anchors.centerIn: parent + text: "\u002B" // Plus sign + color: "white" + font.pixelSize: 24 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.addClicked() + } + } + // Menu button Item { visible: showMenuButton diff --git a/qml/models/ChatListModel.qml b/qml/models/ChatListModel.qml @@ -0,0 +1,73 @@ +import QtQuick 2.5 + +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 + model.append({ + id: serverData.id || nextId++, + title: serverData.title || serverData.name || "Unknown", + lastMessage: serverData.lastMessage || "", + created_at: serverData.created_at || serverData.timestamp || "now", + unreadCount: serverData.unreadCount || 0, + avatar: serverData.avatar || "", + isOnline: serverData.isOnline || false + }) + } + + function loadChats(chats, cursor, isInitialLoad) { + // Clear existing chats on initial load + if (isInitialLoad) { + model.clear() + } + + // Load chats from server + for (var i = 0; i < chats.length; i++) { + var chat = chats[i] + model.append({ + id: chat.id, + title: chat.title, + lastMessage: "", + created_at: chat.created_at, + unreadCount: 0, + avatar: "", + isOnline: false + }) + } + + // Update cursor for next page + currentCursor = cursor || "" + hasMore = (cursor !== undefined && cursor !== null && cursor !== "") + isLoadingMore = false + } + + function loadMore() { + if (!hasMore || isLoadingMore) { + return + } + isLoadingMore = true + NetworkManager.fetchChats(currentCursor) + } + + // Listen for fetched chats from server + Connections { + target: NetworkManager + function onChatsFetched(chats, cursor) { + var isInitial = (model.count === 0) + root.loadChats(chats, cursor, isInitial) + } + } + + Component.onCompleted: { + // Fetch chats from server + isLoadingMore = true + NetworkManager.fetchChats("") + } +} diff --git a/qml/models/MessageListModel.qml b/qml/models/MessageListModel.qml @@ -0,0 +1,94 @@ +import QtQuick 2.5 + +Item { + id: root + + property ListModel model: ListModel {} + property int chatId: -1 + property string currentCursor: "" + 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.insert(0, { + 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) { + model.clear() + } + + // Load messages from server (insert at beginning to maintain bottom-to-top order) + for (var i = messages.length - 1; i >= 0; i--) { + var message = messages[i] + 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 + isRead: true + }) + } + + // Update cursor for next page + currentCursor = cursor || "" + hasMore = (cursor !== undefined && cursor !== null && cursor !== "") + isLoadingMore = false + } + + function loadMore() { + if (!hasMore || isLoadingMore || chatId < 0) { + return + } + + isLoadingMore = true + NetworkManager.fetchMessages(chatId, currentCursor) + } + + // Listen for fetched messages from server + Connections { + target: NetworkManager + function onMessagesFetched(messages, cursor) { + // Only process messages if they belong to this chat + if (messages.length > 0) { + var firstMessageChatId = messages[0].chat_id + if (firstMessageChatId !== root.chatId) { + console.log("Ignoring messages for chat", firstMessageChatId, "(this is chat", root.chatId + ")") + return + } + } + + console.log("Received", messages.length, "messages for chat", root.chatId, "cursor:", cursor) + var isInitial = (model.count === 0) + root.loadMessages(messages, cursor, isInitial) + } + } + + onChatIdChanged: { + if (chatId >= 0) { + // Reset and fetch messages when chat changes + console.log("Fetching messages for chat:", chatId) + model.clear() + currentCursor = "" + hasMore = true + isLoadingMore = true + NetworkManager.fetchMessages(chatId, "") + } + } +} diff --git a/qml/models/qmldir b/qml/models/qmldir @@ -1 +1,3 @@ singleton MockData 1.0 MockData.qml +ChatListModel 1.0 ChatListModel.qml +MessageListModel 1.0 MessageListModel.qml diff --git a/qml/views/ChatListView.qml b/qml/views/ChatListView.qml @@ -7,6 +7,26 @@ import "../styles" Item { id: root + property bool isLoading: false + + // Dynamic chat list model + ChatListModel { + id: chatListModel + } + + // Handle network responses + Connections { + target: NetworkManager + function onChatCreated(chatData) { + chatListModel.addChat(chatData) + root.isLoading = false + } + function onRequestFailed(error) { + console.error("Failed to create chat:", error) + root.isLoading = false + } + } + ColumnLayout { anchors.fill: parent spacing: 0 @@ -17,12 +37,18 @@ Item { title: "Telegram" showBackButton: false showMenuButton: true + showAddButton: true + enabled: !root.isLoading onMenuClicked: { // Navigate to settings if (typeof stackView !== 'undefined' && stackView) { stackView.push(Qt.resolvedUrl("SettingsView.qml")) } } + onAddClicked: { + root.isLoading = true + NetworkManager.postChat("New Chat") + } } // Chat list @@ -34,19 +60,41 @@ Item { ListView { id: chatListView anchors.fill: parent - model: MockData.chats + model: chatListModel.model clip: true + // Detect when user scrolls to bottom + onAtYEndChanged: { + // Only trigger if we have items in the list (avoid triggering on empty initial load) + if (atYEnd && model.count > 0 && chatListModel.hasMore && !chatListModel.isLoadingMore) { + chatListModel.loadMore() + } + } + delegate: ChatListItem { width: chatListView.width - chatData: modelData + chatData: model onClicked: { // Navigate to message thread if (typeof stackView !== 'undefined' && stackView) { - stackView.push(Qt.resolvedUrl("MessageThreadView.qml"), { chatId: modelData.id }) + stackView.push(Qt.resolvedUrl("MessageThreadView.qml"), { chatId: model.id }) } } } + + // Loading indicator at bottom + footer: Item { + width: chatListView.width + height: chatListModel.isLoadingMore ? 60 : 0 + visible: chatListModel.isLoadingMore + + Text { + anchors.centerIn: parent + text: "Loading more chats..." + color: TelegramStyle.textSecondary + font.pixelSize: TelegramStyle.fontSizeSmall + } + } } } } diff --git a/qml/views/MessageThreadView.qml b/qml/views/MessageThreadView.qml @@ -8,19 +8,26 @@ Item { id: root property int chatId: -1 - property var chat: getChatById(chatId) - property var messages: { - var chatIdStr = chatId.toString() - return MockData.messages[chatIdStr] || [] + property bool isSending: false + + // Dynamic message list model + MessageListModel { + id: messageListModel + chatId: root.chatId } - function getChatById(id) { - for (var i = 0; i < MockData.chats.length; i++) { - if (MockData.chats[i].id === id) { - return MockData.chats[i] - } + // Handle network responses + Connections { + target: NetworkManager + function onMessageCreated(messageData) { + console.log("Message created successfully:", JSON.stringify(messageData)) + messageListModel.addMessage(messageData) + root.isSending = false + } + function onRequestFailed(error) { + console.error("Failed:", error) + root.isSending = false } - return { "name": "Unknown", "isOnline": false } } ColumnLayout { @@ -30,7 +37,7 @@ Item { // Top bar TopBar { Layout.fillWidth: true - title: chat ? chat.name : "Chat" + title: chatId >= 0 ? "Chat " + chatId : "Chat" showBackButton: true showMenuButton: true onBackClicked: { @@ -39,7 +46,6 @@ Item { } } onMenuClicked: { - // Could open profile or options console.log("Menu clicked for chat:", chatId) } } @@ -58,33 +64,52 @@ Item { verticalLayoutDirection: ListView.BottomToTop clip: true - // Reverse the messages array for bottom-to-top display - model: { - var reversed = [] - for (var i = messages.length - 1; i >= 0; i--) { - reversed.push(messages[i]) + model: messageListModel.model + + // Detect when user scrolls to top (to load older messages) + onAtYBeginningChanged: { + if (atYBeginning && model.count > 0 && messageListModel.hasMore && !messageListModel.isLoadingMore) { + console.log("Reached top, loading more messages...") + messageListModel.loadMore() } - return reversed } delegate: MessageBubble { width: messageList.width - messageData: modelData + messageData: model } // Auto-scroll to bottom on load Component.onCompleted: { positionViewAtBeginning() } + + // Loading indicator at top (for older messages) + header: Item { + width: messageList.width + height: messageListModel.isLoadingMore ? 60 : 0 + visible: messageListModel.isLoadingMore + + Text { + anchors.centerIn: parent + text: "Loading more messages..." + color: TelegramStyle.textSecondary + font.pixelSize: TelegramStyle.fontSizeSmall + } + } } } // Input area MessageInput { Layout.fillWidth: true + enabled: !root.isSending && root.chatId >= 0 onSendMessage: { - // Mock: just log the message - console.log("Send message:", text, "to chat:", chatId) + if (text.trim().length > 0 && root.chatId >= 0) { + console.log("Sending message:", text, "to chat:", chatId) + root.isSending = true + NetworkManager.postMessage(root.chatId, text) + } } onAttachClicked: { console.log("Attach clicked")