import QtQuick 2.15 import QtQuick.Controls 2.3 import QtGraphicalEffects 1.0 import QtQuick.Dialogs 1.2 import QtQuick.Layouts 1.1 import trinity.matrix 1.0 Rectangle { id: client color: myPalette.window property bool shouldScroll: false property var openProfile: function(member) { var popup = Qt.createComponent("qrc:/Profile.qml") var popupContainer = popup.createObject(window, {"parent": window, "member": member}) popupContainer.open() } property var openRoom: function(room) { var popup = Qt.createComponent("qrc:/RoomSettings.qml") var popupContainer = popup.createObject(window, {"parent": window, "room": room}) popupContainer.open() } Rectangle { anchors.fill: parent visible: !matrix.initialSyncComplete z: 1000 MouseArea { anchors.fill: parent hoverEnabled: true } BusyIndicator { id: syncIndicator anchors.centerIn: parent } Text { anchors.top: syncIndicator.bottom anchors.horizontalCenter: parent.horizontalCenter text: "Synchronizing events..." } Button { anchors.bottom: parent.bottom anchors.bottomMargin: 15 anchors.horizontalCenter: parent.horizontalCenter text: "Log out" } } Rectangle { id: channels width: 180 anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom Rectangle { id: profileRect anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right height: 45 color: myPalette.mid Text { text: "Placeholder name" } Button { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 10 text: "Switch" onClicked: accountMenu.popup() Menu { id: accountMenu Repeater { model: app.accounts MenuItem { text: modelData.profileName onClicked: app.switchAccount(modelData.profileName) } } MenuSeparator {} MenuItem { text: "Add new account" onClicked: app.addAccount("test") } } } } ListView { id: channelListView anchors.left: parent.left anchors.right: parent.right anchors.top: profileRect.bottom anchors.bottom: parent.bottom anchors.margins: 10 spacing: 5 model: matrix.roomListModel section.property: "section" section.criteria: ViewSection.FullString section.delegate: Rectangle { width: parent.width height: 25 color: "transparent" Text { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 5 text: section color: myPalette.text textFormat: Text.PlainText } } delegate: Rectangle { anchors.left: parent.left anchors.right: parent.right height: 25 property bool selected: channelListView.currentIndex === matrix.roomListModel.getOriginalIndex(index) color: selected ? myPalette.highlight : "transparent" radius: 5 RoundedImage { id: roomAvatar anchors.top: parent.top anchors.topMargin: 5 anchors.left: parent.left anchors.leftMargin: 5 width: 18 height: 18 source: avatarURL ? avatarURL : "placeholder.png" } Text { text: alias anchors.verticalCenter: parent.verticalCenter anchors.left: roomAvatar.right anchors.leftMargin: 5 anchors.right: parent.right anchors.rightMargin: 5 color: selected ? "white" : (highlightCount > 0 ? "red" : (notificationCount > 0 ? "blue" : myPalette.text)) textFormat: Text.PlainText elide: Text.ElideRight } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.LeftButton | Qt.RightButton onReleased: { if(mouse.button == Qt.LeftButton) { if(!selected) { var originalIndex = matrix.roomListModel.getOriginalIndex(index) matrix.changeCurrentRoom(originalIndex) channelListView.currentIndex = originalIndex } } else contextMenu.popup() } } Menu { id: contextMenu MenuItem { text: "Mark As Read" onReleased: matrix.readUpTo(matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)), 0) } MenuSeparator {} GroupBox { title: "Notification Settings" Column { spacing: 10 RadioButton { text: "All messages" ToolTip.text: "Recieve a notification for all messages in this room." ToolTip.visible: hovered onReleased: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel = 2 checked: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel === 2 } RadioButton { text: "Only Mentions" ToolTip.text: "Recieve a notification for mentions in this room." ToolTip.visible: hovered onReleased: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel = 1 checked: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel === 1 } RadioButton { text: "Mute" ToolTip.text: "Don't get notifications or unread indicators for this room." ToolTip.visible: hovered onReleased: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel = 0 checked: matrix.getRoom(matrix.roomListModel.getOriginalIndex(index)).notificationLevel === 3 } } } MenuSeparator {} MenuItem { text: "Settings" onReleased: openRoom(matrix.getRoom(matrix.roomListModel.getOriginalIndex(index))) } MenuSeparator {} MenuItem { text: "Leave Room" onReleased: { showDialog("Leave Confirmation", "Are you sure you want to leave " + alias + "?", [ { text: "Yes", onClicked: function(dialog) { matrix.leaveRoom(id) dialog.close() } }, { text: "No", onClicked: function(dialog) { dialog.close() } } ]) } } } } } Button { width: parent.width anchors.bottom: directoryButton.top text: "Switch account" onClicked: app.addAccount("test") } Button { id: communitiesButton width: parent.width anchors.bottom: parent.bottom text: "Communities" onClicked: stack.push("qrc:/Communities.qml") } Button { id: directoryButton width: parent.width anchors.bottom: communitiesButton.top text: "Directory" onClicked: stack.push("qrc:/Directory.qml") } } Rectangle { id: rightArea height: parent.height anchors.left: channels.right anchors.right: parent.right Rectangle { id: overlay z: 999 anchors.top: roomHeader.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom visible: matrix.currentRoom.guestDenied Text { id: invitedByLabel anchors.centerIn: parent color: myPalette.text text: "You have been invited to this room by " + matrix.currentRoom.invitedBy textFormat: Text.PlainText } RowLayout { anchors.top: invitedByLabel.bottom anchors.topMargin: 15 anchors.horizontalCenter: parent.horizontalCenter Button { text: "Accept" onReleased: { matrix.joinRoom(matrix.currentRoom.id) } } Button { text: "Deny" onReleased: { matrix.joinRoom(matrix.currentRoom.id) } } } } Rectangle { id: roomHeader height: 45 anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right color: myPalette.mid RoundedImage { id: channelAvatar anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 15 width: 33 height: 33 source: matrix.currentRoom.avatar ? matrix.currentRoom.avatar : "placeholder.png" } Text { id: channelTitle font.pointSize: 12 anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 15 anchors.left: channelAvatar.right text: matrix.currentRoom.name color: myPalette.text textFormat: Text.PlainText MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onReleased: openRoom(matrix.currentRoom) } } Text { id: channelTopic font.pointSize: 12 anchors.verticalCenter: parent.verticalCenter maximumLineCount: 1 anchors.left: channelTitle.right anchors.leftMargin: 10 anchors.right: audioCallButton.left text: { if(matrix.currentRoom.direct) return ""; if(matrix.currentRoom.topic.length == 0) return "This room has no topic set." else return matrix.currentRoom.topic } color: myPalette.text elide: Text.ElideRight MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onReleased: showDialog(matrix.currentRoom.name, matrix.currentRoom.topic) } textFormat: Text.PlainText } ToolBarButton { id: videoCallButton anchors.verticalCenter: parent.verticalCenter anchors.right: audioCallButton.left anchors.rightMargin: 10 name: "Video Call" toolIcon: "icons/memberlist.png" } ToolBarButton { id: audioCallButton anchors.verticalCenter: parent.verticalCenter anchors.right: notificationsButton.left anchors.rightMargin: 10 name: "Voice Call" toolIcon: "icons/memberlist.png" } ToolBarButton { id: notificationsButton anchors.verticalCenter: parent.verticalCenter anchors.right: roomInfoButton.left anchors.rightMargin: 10 name: "Notifications" toolIcon: "icons/memberlist.png" } ToolBarButton { id: roomInfoButton anchors.verticalCenter: parent.verticalCenter anchors.right: settingsButton.left anchors.rightMargin: 10 onPressed: { if(memberList.width == 0) memberList.width = 200 else memberList.width = 0 } name: "Room Info" toolIcon: "icons/memberlist.png" isActivated: memberList.width == 200 } ToolBarButton { id: settingsButton anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 15 onPressed: stack.push("qrc:/Settings.qml") name: "Settings" toolIcon: "icons/settings.png" } } Rectangle { id: messagesArea anchors.top: roomHeader.bottom anchors.left: parent.left anchors.bottom: parent.bottom anchors.right: memberList.left Rectangle { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.bottom: messageInputParent.top clip: true color: myPalette.light ListView { id: messages model: matrix.eventModel anchors.fill: parent cacheBuffer: 200 delegate: Rectangle { anchors.left: messages.contentItem.left anchors.right: messages.contentItem.right anchors.margins: 10 height: (condense ? 5 : 25) + messageArea.height color: "transparent" property string attachment: display.attachment property var sender: matrix.resolveMemberId(display.sender) property var eventId: display.eventId property var msg: display.msg RoundedImage { id: avatar width: 33 height: 33 anchors.top: parent.top anchors.topMargin: 5 anchors.left: parent.left anchors.leftMargin: 5 source: sender == null ? "placeholder.png" : (sender.avatarURL ? sender.avatarURL : "placeholder.png") visible: !condense } Text { id: senderText text: condense || sender == null ? "" : sender.displayName color: myPalette.text font.bold: true anchors.left: avatar.right anchors.leftMargin: 10 textFormat: Text.PlainText } MouseArea { id: senderClickArea anchors.left: avatar.left anchors.right: senderText.right anchors.top: avatar.top anchors.bottom: avatar.bottom cursorShape: Qt.PointingHandCursor onClicked: openProfile(sender) } Text { text: condense ? "" : timestamp color: myPalette.dark anchors.left: senderText.right anchors.leftMargin: 10 textFormat: Text.PlainText } Rectangle { id: messageArea y: condense ? 0 : 20 height: { if(display.msgType === "text") return message.contentHeight else if(display.msgType === "image") return messageThumbnail.height else return preview.height } anchors.left: condense ? parent.left : avatar.right anchors.leftMargin: condense ? 48 : 10 anchors.right: parent.right color: "transparent" TextEdit { id: message width: parent.width text: display.msg wrapMode: Text.WordWrap textFormat: Text.RichText readOnly: true selectByMouse: true color: display.sent ? myPalette.text : myPalette.mid visible: display.msgType === "text" } Image { id: messageThumbnail visible: display.msgType === "image" source: display.thumbnail fillMode: Image.PreserveAspectFit width: Math.min(sourceSize.width, 400) } MouseArea { enabled: display.msgType === "image" cursorShape: Qt.PointingHandCursor anchors.fill: messageThumbnail onReleased: showImage(display.attachment) } Rectangle { id: preview width: 350 height: 45 visible: display.msgType === "file" radius: 5 color: Qt.rgba(0.05, 0.05, 0.05, 1.0) Text { id: previewFilename x: 15 y: 7 text: display.msg color: "#048dc2" } Text { id: previewFilesize x: 15 y: 22 font.pointSize: 9 text: display.attachmentSize / 1000.0 + " KB" color: "gray" } ToolButton { id: previewFileDownload width: 25 height: 25 anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 10 Image { id: downloadButtonImage anchors.fill: parent sourceSize.width: parent.width sourceSize.height: parent.height source: "icons/download.png" } ColorOverlay { anchors.fill: parent source: downloadButtonImage color: parent.hovered ? "white" : Qt.rgba(0.8, 0.8, 0.8, 1.0) } onClicked: { console.log(attachment) Qt.openUrlExternally(attachment) } } } } MouseArea { anchors.fill: messageArea acceptedButtons: Qt.RightButton propagateComposedEvents: true onClicked: contextMenu.popup() } Menu { id: contextMenu MenuItem { text: "Remove" onReleased: matrix.removeMessage(eventId) } MenuItem { text: "Permalink" onReleased: Qt.openUrlExternally("https://matrix.to/#/" + matrix.currentRoom.id + "/" + eventId) } MenuItem { text: "Quote" onReleased: messageInput.append("> " + msg + "\n\n") } } } ScrollBar.vertical: ScrollBar {} boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.VerticalFlick verticalLayoutDirection: ListView.BottomToTop onMovingVerticallyChanged: { if(verticalVelocity < 0) matrix.readMessageHistory(matrix.currentRoom) } // we scrolled onContentYChanged: { var index = indexAt(0, contentY + height - 5) matrix.readUpTo(matrix.currentRoom, index) } // a new message was added onContentHeightChanged: { var index = indexAt(0, contentY + height - 5) matrix.readUpTo(matrix.currentRoom, index) //print(contentHeight + "," + height); if(contentHeight < height) { matrix.readMessageHistory(matrix.currentRoom) } } } } Rectangle { id: messageInputParent anchors.bottom: parent.bottom width: parent.width height: 55 color: "transparent" ToolButton { id: attachButton icon.name: "mail-attachment" width: 30 height: 30 anchors.top: parent.top anchors.topMargin: 5 anchors.left: parent.left anchors.leftMargin: 5 ToolTip.text: "Attach File" ToolTip.visible: hovered onReleased: openAttachmentFileDialog.open() } TextArea { id: messageInput width: parent.width - attachButton.width - 10 height: 30 anchors.bottom: parent.bottom anchors.bottomMargin: 20 anchors.left: attachButton.right anchors.leftMargin: 5 anchors.right: parent.right anchors.rightMargin: 5 placeholderText: "Send a " + (matrix.currentRoom.encrypted ? "encrypted" : "unencrypted") + " message to " + matrix.currentRoom.name Keys.onReturnPressed: { if(event.modifiers & Qt.ShiftModifier) { event.accepted = false } else { event.accepted = true matrix.sendMessage(matrix.currentRoom, text) clear() } } onTextChanged: { height = Math.max(30, contentHeight + 13) parent.height = Math.max(55, contentHeight + 20) } } ToolButton { id: markdownButton icon.name: "text-x-generic" width: 20 height: 20 anchors.top: messageInput.top anchors.topMargin: 5 anchors.right: emojiButton.left anchors.rightMargin: 5 ToolTip.text: "Markdown is " + (matrix.markdownEnabled ? "enabled" : "disabled") ToolTip.visible: hovered onReleased: matrix.markdownEnabled = !matrix.markdownEnabled } ToolButton { id: emojiButton icon.name: "face-smile" width: 20 height: 20 anchors.top: messageInput.top anchors.topMargin: 5 anchors.right: messageInput.right anchors.rightMargin: 5 ToolTip.text: "Add emoji" ToolTip.visible: hovered } Text { id: typingLabel anchors.bottom: messageInputParent.bottom color: myPalette.text text: matrix.typingText textFormat: Text.PlainText } } } Rectangle { id: memberList anchors.top: roomHeader.bottom anchors.right: parent.right color: myPalette.midlight width: matrix.currentRoom.direct ? 0 : 200 height: parent.height - roomHeader.height ListView { id: memberListView model: matrix.memberModel clip: true anchors.top: parent.top anchors.bottom: inviteButton.top anchors.left: parent.left anchors.right: parent.right section.property: "section" section.criteria: ViewSection.FullString section.delegate: Rectangle { width: parent.width height: 25 color: "transparent" Text { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 5 text: section color: myPalette.text textFormat: Text.PlainText } } delegate: Rectangle { width: memberListView.contentItem.width height: 50 color: "transparent" property string memberId: id RoundedImage { id: memberAvatar width: 33 height: 33 anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 10 source: avatarURL ? avatarURL : "placeholder.png" } Text { anchors.left: memberAvatar.right anchors.leftMargin: 10 anchors.verticalCenter: parent.verticalCenter color: myPalette.text text: displayName textFormat: Text.PlainText } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton cursorShape: Qt.PointingHandCursor onClicked: function(mouse) { if(mouse.button == Qt.LeftButton) { openProfile(matrix.resolveMemberId(id)) } else { memberContextMenu.popup() } } } Menu { id: memberContextMenu MenuItem { text: "Profile" onReleased: openProfile(matrix.resolveMemberId(id)) } MenuItem { text: "Mention" onReleased: messageInput.append(displayName + ": ") } MenuItem { text: "Start Direct Chat" onReleased: matrix.startDirectChat(id) } MenuSeparator {} Menu { title: "Invite to room" Repeater { model: matrix.roomListModel MenuItem { text: alias onReleased: { matrix.inviteToRoom(matrix.resolveRoomId(id), memberId) } } } } } } ScrollBar.vertical: ScrollBar {} boundsBehavior: Flickable.StopAtBounds flickableDirection: Flickable.VerticalFlick } Button { id: inviteButton anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right anchors.margins: 8 text: "Invite to room" onClicked: { var popup = Qt.createComponent("qrc:/InviteDialog.qml") var popupContainer = popup.createObject(window, {"parent": window}) popupContainer.open() } } } } Timer { id: syncTimer interval: 1500 running: true onTriggered: { shouldScroll = messages.contentY == messages.contentHeight - messages.height matrix.sync() } } Timer { id: memberTimer interval: 60000 running: true onTriggered: matrix.updateMembers(matrix.currentRoom) } Timer { id: typingTimer interval: 15000 //15 seconds running: true onTriggered: { if(messageInput.text.length !== 0) matrix.setTyping(matrix.currentRoom) } } Connections { target: matrix function onSyncFinished() { syncTimer.start() if(shouldScroll) messages.positionViewAtEnd() } function onInitialSyncFinished() { matrix.changeCurrentRoom(0) } function onCurrentRoomChanged() { if(messages.contentY < messages.originY + 5) matrix.readMessageHistory(matrix.currentRoom) } function onMessage(room, sender, content) { var notificationLevel = room.notificationLevel var shouldDisplay = false if(notificationLevel === 2) { shouldDisplay = true } else if(notificationLevel === 1) { if(content.includes(matrix.displayName)) shouldDisplay = true } if(shouldDisplay) desktop.showMessage(matrix.resolveMemberId(sender).displayName + " (" + room.name + ")", content) } } FileDialog { id: openAttachmentFileDialog folder: shortcuts.home selectExisting: true selectFolder: false selectMultiple: false onAccepted: { matrix.uploadAttachment(matrix.currentRoom, fileUrl) close() } onRejected: close() } }