commit b19a8d09d985c6d7eef29cd02b995e3f2c9e2e05
parent 2ae0c6e07e0ea9ffd660c332d772cf8c9e7f3789
Author: Silas Brack <silasbrack@gmail.com>
Date: Fri, 26 Dec 2025 21:51:24 +0100
Working version
Diffstat:
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
}