qt-chat-app

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

commit 53c7670531b915dbf620a984895c9a1bf8a842ab
parent caf2d34280254455ab5dde758e5047f899a47a98
Author: Silas Brack <silasbrack@gmail.com>
Date:   Fri, 26 Dec 2025 12:20:59 +0100

First working telegram clone version

Diffstat:
Mmain.qml | 53++++++++++++++++++++++++++++++++++++++++++++++++-----
Mqml.qrc | 22++++++++++++++++++++++
Aqml/components/AttachmentPreview.qml | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/components/Avatar.qml | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/components/ChatListItem.qml | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/components/MessageBubble.qml | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/components/MessageInput.qml | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/components/TopBar.qml | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/models/MockData.qml | 282+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/models/qmldir | 1+
Aqml/styles/TelegramStyle.qml | 34++++++++++++++++++++++++++++++++++
Aqml/styles/qmldir | 1+
Aqml/views/ChatListView.qml | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/views/MessageThreadView.qml | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/views/ProfileView.qml | 292+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aqml/views/SettingsView.qml | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
16 files changed, 1584 insertions(+), 5 deletions(-)

diff --git a/main.qml b/main.qml @@ -1,11 +1,54 @@ import QtQuick 2.5 import QtQuick.Window 2.2 -Window { // Must be this type to be loaded by QQmlApplicationEngine. +Window { + id: mainWindow visible: true - title: qsTr("Hello World") - Text { - text: qsTr("Hello World") - anchors.centerIn: parent + width: 900 + height: 700 + minimumWidth: 800 + minimumHeight: 600 + title: "Telegram Clone" + + // Simple stack-based navigation using Loader + property var navigationStack: [] + property var stackView: stackViewItem // Expose for child views + + Item { + id: stackViewItem + anchors.fill: parent + + Loader { + id: contentLoader + anchors.fill: parent + source: "qrc:/qml/views/ChatListView.qml" + + onLoaded: { + if (item) { + item.anchors.fill = contentLoader + } + } + } + + // Navigation functions + function push(source, properties) { + // Save current state + if (contentLoader.source != "") { + navigationStack.push({ + source: contentLoader.source, + properties: {} + }) + } + + // Load new page + contentLoader.setSource(source, properties || {}) + } + + function pop() { + if (navigationStack.length > 0) { + var prev = navigationStack.pop() + contentLoader.setSource(prev.source, prev.properties) + } + } } } diff --git a/qml.qrc b/qml.qrc @@ -1,5 +1,27 @@ <RCC> <qresource prefix="/"> <file>main.qml</file> + + <!-- Views --> + <file>qml/views/ChatListView.qml</file> + <file>qml/views/MessageThreadView.qml</file> + <file>qml/views/ProfileView.qml</file> + <file>qml/views/SettingsView.qml</file> + + <!-- Components --> + <file>qml/components/Avatar.qml</file> + <file>qml/components/TopBar.qml</file> + <file>qml/components/ChatListItem.qml</file> + <file>qml/components/MessageBubble.qml</file> + <file>qml/components/AttachmentPreview.qml</file> + <file>qml/components/MessageInput.qml</file> + + <!-- Models --> + <file>qml/models/MockData.qml</file> + <file>qml/models/qmldir</file> + + <!-- Styles --> + <file>qml/styles/TelegramStyle.qml</file> + <file>qml/styles/qmldir</file> </qresource> </RCC> diff --git a/qml/components/AttachmentPreview.qml b/qml/components/AttachmentPreview.qml @@ -0,0 +1,135 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.3 +import "../styles" + +ColumnLayout { + id: root + spacing: 4 + + property var attachments: [] + + Repeater { + model: attachments + + Loader { + Layout.maximumWidth: 300 + sourceComponent: { + if (modelData.type === "image") return imagePreview + if (modelData.type === "video") return videoPreview + if (modelData.type === "file") return filePreview + return null + } + + property var attachmentData: modelData + } + } + + // Image preview component + Component { + id: imagePreview + + Rectangle { + width: 200 + height: 150 + radius: TelegramStyle.bubbleRadius + color: "#e0e0e0" + + Text { + anchors.centerIn: parent + text: "\ud83d\uddbc" // Frame with picture emoji + font.pixelSize: 48 + } + + Text { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: 8 + text: attachmentData.filename || "Image" + font.pixelSize: TelegramStyle.fontSizeSmall + color: TelegramStyle.textSecondary + } + } + } + + // Video preview component + Component { + id: videoPreview + + Rectangle { + width: 200 + height: 150 + radius: TelegramStyle.bubbleRadius + color: "#333333" + + Text { + anchors.centerIn: parent + text: "\u25b6" // Play button + font.pixelSize: 48 + color: "white" + } + + Text { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.margins: 8 + text: attachmentData.filename || "Video" + font.pixelSize: TelegramStyle.fontSizeSmall + color: "white" + } + } + } + + // File preview component + Component { + id: filePreview + + Rectangle { + width: 250 + height: 60 + radius: TelegramStyle.bubbleRadius + color: "#f5f5f5" + border.color: TelegramStyle.divider + border.width: 1 + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 12 + + // File icon + Rectangle { + Layout.preferredWidth: 44 + Layout.preferredHeight: 44 + radius: 4 + color: TelegramStyle.primaryColor + + Text { + anchors.centerIn: parent + text: "\ud83d\udcc4" // Page facing up emoji + font.pixelSize: 24 + } + } + + // File info + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + Layout.fillWidth: true + text: attachmentData.filename || "Document" + font.pixelSize: TelegramStyle.fontSizeNormal + color: TelegramStyle.textPrimary + elide: Text.ElideRight + } + + Text { + text: attachmentData.size || "Unknown size" + font.pixelSize: TelegramStyle.fontSizeSmall + color: TelegramStyle.textSecondary + } + } + } + } + } +} diff --git a/qml/components/Avatar.qml b/qml/components/Avatar.qml @@ -0,0 +1,91 @@ +import QtQuick 2.5 +import "../styles" + +Item { + id: root + width: size + height: size + + property int size: TelegramStyle.avatarSize + property string imageSource: "" + property string name: "" + property bool showOnlineIndicator: false + + // Background circle with color based on name + Rectangle { + id: circle + anchors.fill: parent + radius: size / 2 + color: getColorForName(name) + + // Initials + Text { + visible: imageSource === "" + anchors.centerIn: parent + text: getInitials(name) + color: "white" + font.pixelSize: size / 2.5 + font.bold: true + } + + // Image overlay (if available) + Image { + visible: imageSource !== "" + anchors.fill: parent + source: imageSource + fillMode: Image.PreserveAspectCrop + smooth: true + + layer.enabled: true + layer.effect: ShaderEffect { + property variant source: parent + fragmentShader: " + varying highp vec2 qt_TexCoord0; + uniform sampler2D source; + uniform lowp float qt_Opacity; + void main() { + lowp vec4 tex = texture2D(source, qt_TexCoord0); + highp vec2 center = vec2(0.5, 0.5); + highp float dist = distance(qt_TexCoord0, center); + lowp float alpha = smoothstep(0.5, 0.49, dist); + gl_FragColor = vec4(tex.rgb, tex.a * alpha) * qt_Opacity; + } + " + } + } + } + + // Online indicator + Rectangle { + visible: showOnlineIndicator + width: size / 4 + height: size / 4 + radius: width / 2 + color: TelegramStyle.onlineIndicator + border.color: "white" + border.width: 2 + anchors.right: parent.right + anchors.bottom: parent.bottom + } + + function getInitials(name) { + if (!name || name === "") return "?" + var parts = name.split(" ") + if (parts.length >= 2) { + return (parts[0][0] + parts[1][0]).toUpperCase() + } + return name.substring(0, Math.min(2, name.length)).toUpperCase() + } + + function getColorForName(name) { + var colors = ["#e57373", "#ba68c8", "#64b5f6", + "#4db6ac", "#81c784", "#ffb74d"] + if (!name || name === "") return colors[0] + + var hash = 0 + for (var i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash) + } + return colors[Math.abs(hash) % colors.length] + } +} diff --git a/qml/components/ChatListItem.qml b/qml/components/ChatListItem.qml @@ -0,0 +1,104 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.3 +import "../styles" + +Rectangle { + id: root + height: 72 + color: mouseArea.containsMouse ? "#f5f5f5" : "white" + + property var chatData: null + signal clicked() + + Behavior on color { + ColorAnimation { duration: 100 } + } + + RowLayout { + anchors.fill: parent + anchors.margins: TelegramStyle.standardMargin + spacing: TelegramStyle.standardMargin + + // Avatar + Avatar { + size: TelegramStyle.avatarSize + name: chatData ? chatData.name : "" + imageSource: chatData ? chatData.avatar : "" + showOnlineIndicator: chatData ? chatData.isOnline : false + } + + // Content + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + // Name and timestamp row + RowLayout { + Layout.fillWidth: true + + Text { + Layout.fillWidth: true + text: chatData ? chatData.name : "" + font.bold: chatData ? chatData.unreadCount > 0 : false + font.pixelSize: TelegramStyle.fontSizeNormal + color: TelegramStyle.textPrimary + elide: Text.ElideRight + } + + Text { + text: chatData ? chatData.timestamp : "" + font.pixelSize: TelegramStyle.fontSizeSmall + color: chatData && chatData.unreadCount > 0 ? TelegramStyle.unreadBadge : TelegramStyle.textSecondary + } + } + + // Last message and unread badge row + RowLayout { + Layout.fillWidth: true + + Text { + Layout.fillWidth: true + text: chatData ? chatData.lastMessage : "" + font.pixelSize: TelegramStyle.fontSizeSmall + color: TelegramStyle.textSecondary + elide: Text.ElideRight + } + + // Unread badge + Rectangle { + visible: chatData ? chatData.unreadCount > 0 : false + width: 20 + height: 20 + radius: 10 + color: TelegramStyle.unreadBadge + + Text { + anchors.centerIn: parent + text: chatData ? chatData.unreadCount : "" + color: "white" + font.pixelSize: 11 + font.bold: true + } + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.clicked() + } + + // Divider + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: TelegramStyle.standardMargin + TelegramStyle.avatarSize + TelegramStyle.standardMargin + height: 1 + color: TelegramStyle.divider + } +} diff --git a/qml/components/MessageBubble.qml b/qml/components/MessageBubble.qml @@ -0,0 +1,81 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.3 +import "../styles" + +Item { + id: root + height: bubbleColumn.height + 10 + width: parent.width + + property var messageData: null + property bool isOutgoing: messageData ? messageData.isOutgoing : false + + RowLayout { + anchors.fill: parent + spacing: 8 + + // Left spacer for outgoing messages + Item { + Layout.fillWidth: !isOutgoing + visible: !isOutgoing + } + + // Message content + ColumnLayout { + id: bubbleColumn + spacing: 4 + Layout.maximumWidth: root.width * 0.7 + + // Attachment preview (if available) + Loader { + active: messageData && messageData.attachments !== undefined + sourceComponent: attachmentComponent + Layout.alignment: isOutgoing ? Qt.AlignRight : Qt.AlignLeft + } + + // Message bubble + Rectangle { + id: bubble + Layout.alignment: isOutgoing ? Qt.AlignRight : Qt.AlignLeft + width: messageText.width + 24 + height: messageText.height + 16 + radius: TelegramStyle.bubbleRadius + color: isOutgoing ? TelegramStyle.outgoingBubble : TelegramStyle.incomingBubble + border.color: isOutgoing ? "transparent" : TelegramStyle.divider + border.width: 1 + + Text { + id: messageText + anchors.centerIn: parent + text: messageData ? messageData.text : "" + wrapMode: Text.WordWrap + font.pixelSize: TelegramStyle.fontSizeNormal + color: TelegramStyle.textPrimary + width: Math.min(implicitWidth, root.width * 0.6) + } + } + + // Timestamp + Text { + Layout.alignment: isOutgoing ? Qt.AlignRight : Qt.AlignLeft + text: messageData ? messageData.timestamp : "" + font.pixelSize: TelegramStyle.fontSizeSmall + color: TelegramStyle.textSecondary + } + } + + // Right spacer for incoming messages + Item { + Layout.fillWidth: isOutgoing + visible: isOutgoing + } + } + + Component { + id: attachmentComponent + + AttachmentPreview { + attachments: messageData ? messageData.attachments : [] + } + } +} diff --git a/qml/components/MessageInput.qml b/qml/components/MessageInput.qml @@ -0,0 +1,112 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.3 +import "../styles" + +Rectangle { + id: root + height: TelegramStyle.inputBarHeight + color: "white" + border.color: TelegramStyle.divider + border.width: 1 + + signal sendMessage(string text) + signal attachClicked() + + RowLayout { + anchors.fill: parent + anchors.margins: TelegramStyle.smallMargin + spacing: TelegramStyle.smallMargin + + // Attach button + Item { + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + + Text { + anchors.centerIn: parent + text: "\ud83d\udcce" // Paperclip emoji + font.pixelSize: 24 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.attachClicked() + } + } + + // Text input + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: 20 + color: "#f5f5f5" + border.color: TelegramStyle.divider + border.width: 1 + + TextInput { + id: messageInput + anchors.fill: parent + anchors.margins: 12 + verticalAlignment: TextInput.AlignVCenter + font.pixelSize: TelegramStyle.fontSizeNormal + color: TelegramStyle.textPrimary + clip: true + + // Placeholder text + Text { + visible: messageInput.text.length === 0 + anchors.verticalCenter: parent.verticalCenter + text: "Type a message..." + color: TelegramStyle.textSecondary + font.pixelSize: TelegramStyle.fontSizeNormal + } + + Keys.onReturnPressed: { + if (event.modifiers & Qt.ControlModifier) { + sendButton.clicked() + } + } + } + } + + // Send button + Rectangle { + id: sendButton + Layout.preferredWidth: 40 + Layout.preferredHeight: 40 + radius: 20 + color: messageInput.text.trim().length > 0 ? TelegramStyle.primaryColor : TelegramStyle.divider + + Behavior on color { + ColorAnimation { duration: 150 } + } + + Text { + anchors.centerIn: parent + text: "\u27a4" // Right arrow + font.pixelSize: 20 + color: "white" + } + + MouseArea { + anchors.fill: parent + enabled: messageInput.text.trim().length > 0 + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + if (messageInput.text.trim().length > 0) { + root.sendMessage(messageInput.text) + messageInput.text = "" + } + } + } + + function clicked() { + if (messageInput.text.trim().length > 0) { + root.sendMessage(messageInput.text) + messageInput.text = "" + } + } + } + } +} diff --git a/qml/components/TopBar.qml b/qml/components/TopBar.qml @@ -0,0 +1,74 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.3 +import "../styles" + +Rectangle { + id: root + height: TelegramStyle.topBarHeight + color: TelegramStyle.primaryColor + + property string title: "" + property bool showBackButton: false + property bool showMenuButton: false + + signal backClicked() + signal menuClicked() + + RowLayout { + anchors.fill: parent + anchors.leftMargin: TelegramStyle.smallMargin + anchors.rightMargin: TelegramStyle.smallMargin + spacing: TelegramStyle.smallMargin + + // Back button + Item { + visible: showBackButton + Layout.preferredWidth: 40 + Layout.fillHeight: true + + Text { + anchors.centerIn: parent + text: "\u2190" // Left arrow + color: "white" + font.pixelSize: 24 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.backClicked() + } + } + + // Title + Text { + Layout.fillWidth: true + text: root.title + color: "white" + font.pixelSize: TelegramStyle.fontSizeLarge + font.bold: true + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + // Menu button + Item { + visible: showMenuButton + Layout.preferredWidth: 40 + Layout.fillHeight: true + + Text { + anchors.centerIn: parent + text: "\u22ee" // Vertical ellipsis + color: "white" + font.pixelSize: 24 + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.menuClicked() + } + } + } +} diff --git a/qml/models/MockData.qml b/qml/models/MockData.qml @@ -0,0 +1,282 @@ +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 @@ -0,0 +1 @@ +singleton MockData 1.0 MockData.qml diff --git a/qml/styles/TelegramStyle.qml b/qml/styles/TelegramStyle.qml @@ -0,0 +1,34 @@ +pragma Singleton +import QtQuick 2.5 + +QtObject { + // Colors (Telegram-inspired) + readonly property color primaryColor: "#0088cc" // Telegram blue + readonly property color accentColor: "#54a9eb" + readonly property color backgroundColor: "#ffffff" + readonly property color chatListBg: "#ffffff" + readonly property color messageThreadBg: "#e6ebee" + readonly property color outgoingBubble: "#dcf8c6" // Light green + readonly property color incomingBubble: "#ffffff" + readonly property color textPrimary: "#000000" + readonly property color textSecondary: "#888888" + readonly property color divider: "#e0e0e0" + readonly property color unreadBadge: "#0088cc" + readonly property color onlineIndicator: "#4caf50" // Green + + // Dimensions + readonly property int avatarSize: 48 + readonly property int smallAvatarSize: 32 + readonly property int largeAvatarSize: 80 + readonly property int bubbleRadius: 8 + readonly property int standardMargin: 12 + readonly property int smallMargin: 6 + readonly property int topBarHeight: 56 + readonly property int inputBarHeight: 60 + + // Typography + readonly property int fontSizeLarge: 16 + readonly property int fontSizeNormal: 14 + readonly property int fontSizeSmall: 12 + readonly property string fontFamily: "Sans Serif" +} diff --git a/qml/styles/qmldir b/qml/styles/qmldir @@ -0,0 +1 @@ +singleton TelegramStyle 1.0 TelegramStyle.qml diff --git a/qml/views/ChatListView.qml b/qml/views/ChatListView.qml @@ -0,0 +1,53 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.3 +import "../components" +import "../models" +import "../styles" + +Item { + id: root + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // Top bar + TopBar { + Layout.fillWidth: true + title: "Telegram" + showBackButton: false + showMenuButton: true + onMenuClicked: { + // Navigate to settings + if (typeof stackView !== 'undefined' && stackView) { + stackView.push(Qt.resolvedUrl("SettingsView.qml")) + } + } + } + + // Chat list + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: TelegramStyle.chatListBg + + ListView { + id: chatListView + anchors.fill: parent + model: MockData.chats + clip: true + + delegate: ChatListItem { + width: chatListView.width + chatData: modelData + onClicked: { + // Navigate to message thread + if (typeof stackView !== 'undefined' && stackView) { + stackView.push(Qt.resolvedUrl("MessageThreadView.qml"), { chatId: modelData.id }) + } + } + } + } + } + } +} diff --git a/qml/views/MessageThreadView.qml b/qml/views/MessageThreadView.qml @@ -0,0 +1,94 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.3 +import "../components" +import "../models" +import "../styles" + +Item { + id: root + + property int chatId: -1 + property var chat: getChatById(chatId) + property var messages: { + var chatIdStr = chatId.toString() + return MockData.messages[chatIdStr] || [] + } + + function getChatById(id) { + for (var i = 0; i < MockData.chats.length; i++) { + if (MockData.chats[i].id === id) { + return MockData.chats[i] + } + } + return { "name": "Unknown", "isOnline": false } + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // Top bar + TopBar { + Layout.fillWidth: true + title: chat ? chat.name : "Chat" + showBackButton: true + showMenuButton: true + onBackClicked: { + if (typeof stackView !== 'undefined' && stackView) { + stackView.pop() + } + } + onMenuClicked: { + // Could open profile or options + console.log("Menu clicked for chat:", chatId) + } + } + + // Messages area + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: TelegramStyle.messageThreadBg + + ListView { + id: messageList + anchors.fill: parent + anchors.margins: TelegramStyle.smallMargin + spacing: 8 + 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]) + } + return reversed + } + + delegate: MessageBubble { + width: messageList.width + messageData: modelData + } + + // Auto-scroll to bottom on load + Component.onCompleted: { + positionViewAtBeginning() + } + } + } + + // Input area + MessageInput { + Layout.fillWidth: true + onSendMessage: { + // Mock: just log the message + console.log("Send message:", text, "to chat:", chatId) + } + onAttachClicked: { + console.log("Attach clicked") + } + } + } +} diff --git a/qml/views/ProfileView.qml b/qml/views/ProfileView.qml @@ -0,0 +1,292 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.3 +import "../components" +import "../models" +import "../styles" + +Item { + id: root + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // Top bar + TopBar { + Layout.fillWidth: true + title: "Profile" + showBackButton: true + showMenuButton: false + onBackClicked: { + if (typeof stackView !== 'undefined' && stackView) { + stackView.pop() + } + } + } + + // Profile content + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: TelegramStyle.backgroundColor + + Flickable { + anchors.fill: parent + contentHeight: profileColumn.height + clip: true + + ColumnLayout { + id: profileColumn + width: parent.width + spacing: 0 + + // Profile header with avatar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 200 + color: "white" + + ColumnLayout { + anchors.centerIn: parent + spacing: TelegramStyle.standardMargin + + Avatar { + Layout.alignment: Qt.AlignHCenter + size: TelegramStyle.largeAvatarSize + name: MockData.currentUser.name + imageSource: MockData.currentUser.avatar + showOnlineIndicator: false + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: MockData.currentUser.name + font.pixelSize: 20 + font.bold: true + color: TelegramStyle.textPrimary + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: MockData.currentUser.bio + font.pixelSize: TelegramStyle.fontSizeSmall + color: TelegramStyle.textSecondary + } + } + } + + // Spacer + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: TelegramStyle.standardMargin + color: "#f5f5f5" + } + + // Account section + ProfileSection { + Layout.fillWidth: true + title: "Account" + } + + ProfileInfoItem { + Layout.fillWidth: true + icon: "\ud83d\udcde" + label: "Phone" + value: MockData.currentUser.phone + } + + ProfileInfoItem { + Layout.fillWidth: true + icon: "\ud83d\udd16" + label: "Username" + value: "@" + MockData.currentUser.name.toLowerCase().replace(" ", "") + } + + ProfileInfoItem { + Layout.fillWidth: true + icon: "\u2139" + label: "Bio" + value: MockData.currentUser.bio + showDivider: false + } + + // Spacer + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: TelegramStyle.standardMargin + color: "#f5f5f5" + } + + // Settings section + ProfileSection { + Layout.fillWidth: true + title: "Settings" + } + + ProfileActionItem { + Layout.fillWidth: true + icon: "\ud83d\udd14" + label: "Notifications" + } + + ProfileActionItem { + Layout.fillWidth: true + icon: "\ud83d\udd12" + label: "Privacy and Security" + showDivider: false + } + } + } + } + } + + // Profile section header component + Component { + id: profileSectionComponent + Rectangle { + height: 40 + color: "#f5f5f5" + + property string title: "" + + Text { + anchors.left: parent.left + anchors.leftMargin: TelegramStyle.standardMargin + anchors.verticalCenter: parent.verticalCenter + text: parent.title + font.pixelSize: TelegramStyle.fontSizeSmall + font.bold: true + color: TelegramStyle.primaryColor + } + } + } +} + +// Profile section header +Rectangle { + id: profileSection + height: 40 + color: "#f5f5f5" + property string title: "" + + Text { + anchors.left: parent.left + anchors.leftMargin: TelegramStyle.standardMargin + anchors.verticalCenter: parent.verticalCenter + text: parent.title + font.pixelSize: TelegramStyle.fontSizeSmall + font.bold: true + color: TelegramStyle.primaryColor + } +} + +// Profile info item (non-clickable) +Rectangle { + id: profileInfoItem + height: 60 + color: "white" + + property string icon: "" + property string label: "" + property string value: "" + property bool showDivider: true + + RowLayout { + anchors.fill: parent + anchors.margins: TelegramStyle.standardMargin + spacing: TelegramStyle.standardMargin + + Text { + text: parent.parent.icon + font.pixelSize: 24 + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + text: parent.parent.parent.label + font.pixelSize: TelegramStyle.fontSizeSmall + color: TelegramStyle.textSecondary + } + + Text { + Layout.fillWidth: true + text: parent.parent.parent.value + font.pixelSize: TelegramStyle.fontSizeNormal + color: TelegramStyle.textPrimary + elide: Text.ElideRight + } + } + } + + Rectangle { + visible: showDivider + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: TelegramStyle.standardMargin * 2 + 24 + height: 1 + color: TelegramStyle.divider + } +} + +// Profile action item (clickable) +Rectangle { + id: profileActionItem + height: 50 + color: mouseArea.containsMouse ? "#f5f5f5" : "white" + + property string icon: "" + property string label: "" + property bool showDivider: true + + Behavior on color { + ColorAnimation { duration: 100 } + } + + RowLayout { + anchors.fill: parent + anchors.margins: TelegramStyle.standardMargin + spacing: TelegramStyle.standardMargin + + Text { + text: parent.parent.icon + font.pixelSize: 24 + } + + Text { + Layout.fillWidth: true + text: parent.parent.label + font.pixelSize: TelegramStyle.fontSizeNormal + color: TelegramStyle.textPrimary + } + + Text { + text: "\u203a" + font.pixelSize: 24 + color: TelegramStyle.textSecondary + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + console.log("Profile action clicked:", label) + } + } + + Rectangle { + visible: showDivider + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: TelegramStyle.standardMargin * 2 + 24 + height: 1 + color: TelegramStyle.divider + } +} diff --git a/qml/views/SettingsView.qml b/qml/views/SettingsView.qml @@ -0,0 +1,160 @@ +import QtQuick 2.5 +import QtQuick.Layouts 1.3 +import "../components" +import "../models" +import "../styles" + +Item { + id: root + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // Top bar + TopBar { + Layout.fillWidth: true + title: "Settings" + showBackButton: true + showMenuButton: false + onBackClicked: { + if (typeof stackView !== 'undefined' && stackView) { + stackView.pop() + } + } + } + + // Settings content + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: TelegramStyle.backgroundColor + + ListView { + id: settingsList + anchors.fill: parent + clip: true + + model: ListModel { + ListElement { + icon: "\ud83d\udc64" + title: "Profile" + subtitle: "Set your name and photo" + } + ListElement { + icon: "\ud83d\udd14" + title: "Notifications" + subtitle: "Message and group notifications" + } + ListElement { + icon: "\ud83d\udd12" + title: "Privacy and Security" + subtitle: "Phone number, last seen, profile photo" + } + ListElement { + icon: "\ud83d\udcca" + title: "Data and Storage" + subtitle: "Network usage, auto-download" + } + ListElement { + icon: "\ud83c\udfa8" + title: "Appearance" + subtitle: "Theme, chat background, night mode" + } + ListElement { + icon: "\ud83c\udf10" + title: "Language" + subtitle: "English" + } + ListElement { + icon: "\u2753" + title: "Help" + subtitle: "FAQ and support" + } + } + + delegate: Rectangle { + width: settingsList.width + height: 72 + color: settingsMouseArea.containsMouse ? "#f5f5f5" : "white" + + Behavior on color { + ColorAnimation { duration: 100 } + } + + RowLayout { + anchors.fill: parent + anchors.margins: TelegramStyle.standardMargin + spacing: TelegramStyle.standardMargin + + // Icon + Text { + Layout.preferredWidth: TelegramStyle.avatarSize + Layout.preferredHeight: TelegramStyle.avatarSize + text: model.icon + font.pixelSize: 32 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + // Content + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + Text { + Layout.fillWidth: true + text: model.title + font.pixelSize: TelegramStyle.fontSizeNormal + font.bold: false + color: TelegramStyle.textPrimary + } + + Text { + Layout.fillWidth: true + text: model.subtitle + font.pixelSize: TelegramStyle.fontSizeSmall + color: TelegramStyle.textSecondary + elide: Text.ElideRight + } + } + + // Arrow + Text { + text: "\u203a" // Right angle bracket + font.pixelSize: 24 + color: TelegramStyle.textSecondary + } + } + + MouseArea { + id: settingsMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (model.title === "Profile") { + // Navigate to profile + if (typeof stackView !== 'undefined' && stackView) { + stackView.push(Qt.resolvedUrl("ProfileView.qml")) + } + } else { + console.log("Settings item clicked:", model.title) + } + } + } + + // Divider + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: TelegramStyle.standardMargin + TelegramStyle.avatarSize + TelegramStyle.standardMargin + height: 1 + color: TelegramStyle.divider + } + } + } + } + } +}