commit bc21606bf9288440732b083590fc3a91ac4f1eb6
parent 53c7670531b915dbf620a984895c9a1bf8a842ab
Author: Silas Brack <silasbrack@gmail.com>
Date: Fri, 26 Dec 2025 14:44:16 +0100
Working
Diffstat:
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")