import "./delegates" import "./device-verification" import "./emoji" import QtGraphicalEffects 1.0 import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 import im.nheko 1.0 import im.nheko.EmojiModel 1.0 Page { id: timelineRoot property var colors: currentActivePalette property var systemInactive property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive property int avatarSize: 40 property real highlightHue: colors.highlight.hslHue property real highlightSat: colors.highlight.hslSaturation property real highlightLight: colors.highlight.hslLightness palette: colors FontMetrics { id: fontMetrics } EmojiPicker { id: emojiPopup width: 7 * 52 + 20 height: 6 * 52 colors: palette model: EmojiProxyModel { category: EmojiCategory.People sourceModel: EmojiModel { } } } Menu { id: messageContextMenu property string eventId property int eventType property bool isEncrypted function show(eventId_, eventType_, isEncrypted_, showAt_, position) { eventId = eventId_; eventType = eventType_; isEncrypted = isEncrypted_; if (position) popup(position, showAt_); else popup(showAt_); } modal: true MenuItem { text: qsTr("React") onClicked: emojiPopup.show(messageContextMenu.parent, messageContextMenu.eventId) } MenuItem { text: qsTr("Reply") onClicked: chat.model.replyAction(messageContextMenu.eventId) } MenuItem { text: qsTr("Read receipts") onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId) } MenuItem { text: qsTr("Mark as read") } MenuItem { text: qsTr("View raw message") onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId) } MenuItem { visible: messageContextMenu.isEncrypted height: visible ? implicitHeight : 0 text: qsTr("View decrypted raw message") onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId) } MenuItem { text: qsTr("Redact message") onTriggered: chat.model.redactEvent(messageContextMenu.eventId) } MenuItem { visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker height: visible ? implicitHeight : 0 text: qsTr("Save as") onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId) } } Rectangle { anchors.fill: parent color: colors.window Component { id: deviceVerificationDialog DeviceVerification { } } Connections { function onNewDeviceVerificationRequest(flow, transactionId, userId, deviceId, isRequest) { var dialog = deviceVerificationDialog.createObject(timelineRoot, { "flow": flow }); dialog.show(); } target: TimelineManager } Connections { function onOpenProfile(profile) { var userProfile = userProfileComponent.createObject(timelineRoot, { "profile": profile }); userProfile.show(); } target: TimelineManager.timeline } Label { visible: !TimelineManager.timeline && !TimelineManager.isInitialSync anchors.centerIn: parent text: qsTr("No room open") font.pointSize: 24 color: colors.text } BusyIndicator { visible: running anchors.centerIn: parent running: TimelineManager.isInitialSync height: 200 width: 200 z: 3 } ColumnLayout { anchors.fill: parent Rectangle { id: topBar Layout.fillWidth: true implicitHeight: topLayout.height + 16 z: 3 color: colors.base MouseArea { anchors.fill: parent onClicked: TimelineManager.openRoomSettings() } GridLayout { //Layout.margins: 8 id: topLayout anchors.left: parent.left anchors.right: parent.right anchors.margins: 8 anchors.verticalCenter: parent.verticalCenter ImageButton { id: backToRoomsButton Layout.column: 0 Layout.row: 0 Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter visible: TimelineManager.isNarrowView image: ":/icons/icons/ui/angle-pointing-to-left.png" ToolTip.visible: hovered ToolTip.text: qsTr("Back to room list") onClicked: TimelineManager.backToRooms() } Avatar { Layout.column: 1 Layout.row: 0 Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter width: avatarSize height: avatarSize url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" displayName: chat.model ? chat.model.roomName : qsTr("No room selected") MouseArea { anchors.fill: parent onClicked: TimelineManager.openRoomSettings() } } Label { Layout.fillWidth: true Layout.column: 2 Layout.row: 0 color: colors.text font.pointSize: fontMetrics.font.pointSize * 1.1 text: chat.model ? chat.model.roomName : qsTr("No room selected") MouseArea { anchors.fill: parent onClicked: TimelineManager.openRoomSettings() } } MatrixText { Layout.fillWidth: true Layout.column: 2 Layout.row: 1 Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines clip: true text: chat.model ? chat.model.roomTopic : "" } ImageButton { id: roomOptionsButton Layout.column: 3 Layout.row: 0 Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter image: ":/icons/icons/ui/vertical-ellipsis.png" ToolTip.visible: hovered ToolTip.text: qsTr("Room options") onClicked: roomOptionsMenu.popup(roomOptionsButton) Menu { id: roomOptionsMenu MenuItem { text: qsTr("Invite users") onTriggered: TimelineManager.openInviteUsersDialog() } MenuItem { text: qsTr("Members") onTriggered: TimelineManager.openMemberListDialog() } MenuItem { text: qsTr("Leave room") onTriggered: TimelineManager.openLeaveRoomDialog() } MenuItem { text: qsTr("Settings") onTriggered: TimelineManager.openRoomSettings() } } } } } ListView { id: chat property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2) visible: TimelineManager.timeline != null cacheBuffer: 400 Layout.fillWidth: true Layout.fillHeight: true model: TimelineManager.timeline boundsBehavior: Flickable.StopAtBounds pixelAligned: true spacing: 4 verticalLayoutDirection: ListView.BottomToTop onCountChanged: { if (atYEnd) model.currentIndex = 0; } // Mark last event as read, since we are at the bottom ScrollHelper { flickable: parent anchors.fill: parent } Shortcut { sequence: StandardKey.MoveToPreviousPage onActivated: { chat.contentY = chat.contentY - chat.height / 2; chat.returnToBounds(); } } Shortcut { sequence: StandardKey.MoveToNextPage onActivated: { chat.contentY = chat.contentY + chat.height / 2; chat.returnToBounds(); } } Shortcut { sequence: StandardKey.Cancel onActivated: chat.model.reply = undefined } Shortcut { sequence: "Alt+Up" onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0) } Shortcut { sequence: "Alt+Down" onActivated: { var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1; chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined; } } Component { id: userProfileComponent UserProfile { } } section { property: "section" } Component { id: sectionHeader Column { property var modelData property string section property string nextSection topPadding: 4 bottomPadding: 4 spacing: 8 visible: !!modelData width: parent.width height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 Label { id: dateBubble anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined visible: section.includes(" ") text: chat.model.formatDateSeparator(modelData.timestamp) color: colors.text height: fontMetrics.height * 1.4 width: contentWidth * 1.2 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter background: Rectangle { radius: parent.height / 2 color: colors.base } } Row { height: userName.height spacing: 8 Avatar { width: avatarSize height: avatarSize url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") displayName: modelData.userName userid: modelData.userId MouseArea { anchors.fill: parent onClicked: chat.model.openUserProfile(modelData.userId) cursorShape: Qt.PointingHandCursor propagateComposedEvents: true } } Label { id: userName text: TimelineManager.escapeEmoji(modelData.userName) color: TimelineManager.userColor(modelData.userId, colors.window) textFormat: Text.RichText MouseArea { anchors.fill: parent Layout.alignment: Qt.AlignHCenter onClicked: chat.model.openUserProfile(modelData.userId) cursorShape: Qt.PointingHandCursor propagateComposedEvents: true } } } } } ScrollBar.vertical: ScrollBar { id: scrollbar } delegate: Item { id: wrapper // This would normally be previousSection, but our model's order is inverted. property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1 property Item section anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined width: chat.delegateMaxWidth height: section ? section.height + timelinerow.height : timelinerow.height onSectionBoundaryChanged: { if (sectionBoundary) { var properties = { "modelData": model.dump, "section": ListView.section, "nextSection": ListView.nextSection }; section = sectionHeader.createObject(wrapper, properties); } else { section.destroy(); section = null; } } TimelineRow { id: timelinerow y: section ? section.y + section.height : 0 } Connections { function onMovementEnded() { if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) chat.model.currentIndex = index; } target: chat } } footer: BusyIndicator { anchors.horizontalCenter: parent.horizontalCenter running: chat.model && chat.model.paginationInProgress height: 50 width: 50 z: 3 } } Item { id: chatFooter implicitHeight: Math.max(fontMetrics.height * 1.2, footerContent.height) Layout.fillWidth: true z: 3 Column { id: footerContent anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom Rectangle { id: typingRect anchors.left: parent.left anchors.right: parent.right color: (chat.model && chat.model.typingUsers.length > 0) ? colors.window : "transparent" height: typingDisplay.height Label { id: typingDisplay anchors.left: parent.left anchors.leftMargin: 10 anchors.right: parent.right anchors.rightMargin: 10 color: colors.text text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : "" textFormat: Text.RichText } } Rectangle { id: replyPopup anchors.left: parent.left anchors.right: parent.right visible: chat.model && chat.model.reply // Height of child, plus margins, plus border height: replyPreview.height + 10 color: colors.base Reply { id: replyPreview anchors.left: parent.left anchors.leftMargin: 10 anchors.right: closeReplyButton.left anchors.rightMargin: 20 anchors.bottom: parent.bottom modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : { } userColor: TimelineManager.userColor(modelData.userId, colors.window) } ImageButton { id: closeReplyButton anchors.right: parent.right anchors.rightMargin: 15 anchors.top: replyPreview.top hoverEnabled: true width: 16 height: 16 image: ":/icons/icons/ui/remove-symbol.png" ToolTip.visible: closeReplyButton.hovered ToolTip.text: qsTr("Close") onClicked: chat.model.reply = undefined } } } } ActiveCallBar { Layout.fillWidth: true z: 3 } } } systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled } }