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:
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
+ }
+ }
+ }
+ }
+ }
+}