From 5aee8d609a3fcca63bb9a0f983a77b45eebfefe7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 2 Jun 2023 01:45:24 +0200 Subject: [PATCH] Format qml --- .qmlformat.ini | 7 + resources/qml/Avatar.qml | 61 +- resources/qml/ChatPage.qml | 70 +- resources/qml/CommunitiesList.qml | 320 +++--- resources/qml/Completer.qml | 228 ++--- resources/qml/ElidedLabel.qml | 9 +- resources/qml/EncryptionIndicator.qml | 55 +- resources/qml/ForwardCompleter.qml | 69 +- resources/qml/ImageButton.qml | 15 +- resources/qml/MatrixText.qml | 18 +- resources/qml/MatrixTextField.qml | 132 ++- resources/qml/MessageInput.qml | 303 +++--- resources/qml/MessageInputWarning.qml | 22 +- resources/qml/MessageView.qml | 937 +++++++++-------- resources/qml/PrivacyScreen.qml | 38 +- resources/qml/QuickSwitcher.qml | 55 +- resources/qml/Reactions.qml | 76 +- resources/qml/ReplyPopup.qml | 72 +- resources/qml/RoomList.qml | 1235 +++++++++++------------ resources/qml/Root.qml | 464 ++++----- resources/qml/SelfVerificationCheck.qml | 176 ++-- resources/qml/StatusIndicator.qml | 24 +- resources/qml/TimelineRow.qml | 370 +++---- resources/qml/TimelineView.qml | 279 +++-- resources/qml/ToggleButton.qml | 79 +- resources/qml/TopBar.qml | 386 ++++--- resources/qml/TypingIndicator.qml | 10 +- resources/qml/UploadBox.qml | 67 +- 28 files changed, 2623 insertions(+), 2954 deletions(-) create mode 100644 .qmlformat.ini diff --git a/.qmlformat.ini b/.qmlformat.ini new file mode 100644 index 00000000..c136c23b --- /dev/null +++ b/.qmlformat.ini @@ -0,0 +1,7 @@ +[General] +FunctionsSpacing= +IndentWidth=4 +NewlineType=native +NormalizeOrder=true +ObjectsSpacing= +UseTabs=false diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 8302f8fa..53124f28 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -11,45 +11,44 @@ import im.nheko 1.0 AbstractButton { id: avatar + property alias color: bg.color + property bool crop: true + property string displayName + property string roomid + property alias textColor: label.color property string url property string userid - property string roomid - property string displayName - property alias textColor: label.color - property bool crop: true - property alias color: bg.color - width: 48 height: 48 + width: 48 + background: Rectangle { id: bg - radius: Settings.avatarCircles ? height / 2 : height / 8 + color: palette.alternateBase + radius: Settings.avatarCircles ? height / 2 : height / 8 } Label { id: label - enabled: false - anchors.fill: parent + color: palette.text + enabled: false + font.pixelSize: avatar.height / 2 + horizontalAlignment: Text.AlignHCenter text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") textFormat: Text.RichText - font.pixelSize: avatar.height / 2 verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter visible: img.status != Image.Ready && !Settings.useIdenticon - color: palette.text } - Image { id: identicon anchors.fill: parent - visible: Settings.useIdenticon && img.status != Image.Ready source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : "" + visible: Settings.useIdenticon && img.status != Image.Ready } - Image { id: img @@ -58,8 +57,6 @@ AbstractButton { fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit mipmap: true smooth: true - sourceSize.width: avatar.width * Screen.devicePixelRatio - sourceSize.height: avatar.height * Screen.devicePixelRatio source: if (avatar.url.startsWith('image://')) { return avatar.url + "?radius=" + (Settings.avatarCircles ? 100 : 25) + ((avatar.crop) ? "" : "&scale"); } else if (avatar.url.startsWith(':/')) { @@ -67,20 +64,12 @@ AbstractButton { } else { return ""; } - + sourceSize.height: avatar.height * Screen.devicePixelRatio + sourceSize.width: avatar.width * Screen.devicePixelRatio } - Rectangle { id: onlineIndicator - anchors.bottom: avatar.bottom - anchors.right: avatar.right - visible: !!userid - height: avatar.height / 6 - width: height - radius: Settings.avatarCircles ? height / 2 : height / 8 - color: updatePresence() - function updatePresence() { switch (Presence.userPresence(userid)) { case "online": @@ -94,22 +83,28 @@ AbstractButton { } } - Connections { - target: Presence + anchors.bottom: avatar.bottom + anchors.right: avatar.right + color: updatePresence() + height: avatar.height / 6 + radius: Settings.avatarCircles ? height / 2 : height / 8 + visible: !!userid + width: height + Connections { function onPresenceChanged(id) { - if (id == userid) onlineIndicator.color = onlineIndicator.updatePresence(); + if (id == userid) + onlineIndicator.color = onlineIndicator.updatePresence(); } + + target: Presence } } - CursorShape { anchors.fill: parent cursorShape: Qt.PointingHandCursor } - Ripple { color: Qt.rgba(palette.alternateBase.r, palette.alternateBase.g, palette.alternateBase.b, 0.5) } - } diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml index 564c093d..2803e97d 100644 --- a/resources/qml/ChatPage.qml +++ b/resources/qml/ChatPage.qml @@ -17,16 +17,16 @@ Rectangle { color: palette.window ColumnLayout { - spacing: 0 anchors.fill: parent + spacing: 0 Rectangle { id: offlineIndicator + Layout.fillWidth: true + Layout.preferredHeight: offlineLabel.height + Nheko.paddingMedium color: Nheko.theme.error visible: !TimelineManager.isConnected - Layout.preferredHeight: offlineLabel.height + Nheko.paddingMedium - Layout.fillWidth: true z: 1 Label { @@ -36,18 +36,9 @@ Rectangle { text: qsTr("No network connection") } } - AdaptiveLayout { id: adaptiveView - Layout.fillWidth: true - Layout.fillHeight: true - singlePageMode: communityListC.preferredWidth + roomListC.preferredWidth + timlineViewC.minimumWidth > width - pageIndex: 1 - - Component.onCompleted: initializePageIndex() - onSinglePageModeChanged: initializePageIndex() - function initializePageIndex() { if (!singlePageMode) adaptiveView.pageIndex = 0; @@ -57,67 +48,67 @@ Rectangle { adaptiveView.pageIndex = 1; } + Layout.fillHeight: true + Layout.fillWidth: true + pageIndex: 1 + singlePageMode: communityListC.preferredWidth + roomListC.preferredWidth + timlineViewC.minimumWidth > width + + Component.onCompleted: initializePageIndex() + onSinglePageModeChanged: initializePageIndex() + Connections { - target: Rooms function onCurrentRoomChanged() { adaptiveView.initializePageIndex(); } - } + target: Rooms + } AdaptiveLayoutElement { id: communityListC - visible: Settings.groupView - minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2 collapsedWidth: communitiesList.avatarSize + 2 * Nheko.paddingMedium - preferredWidth: Settings.communityListWidth >= minimumWidth ? Settings.communityListWidth : collapsedWidth maximumWidth: communitiesList.avatarSize * 10 + 2 * Nheko.paddingMedium + minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2 + preferredWidth: Settings.communityListWidth >= minimumWidth ? Settings.communityListWidth : collapsedWidth + visible: Settings.groupView CommunitiesList { id: communitiesList collapsed: parent.collapsed } - Binding { - target: Settings + delayed: true property: 'communityListWidth' + restoreMode: Binding.RestoreBindingOrValue + target: Settings value: communityListC.preferredWidth when: !adaptiveView.singlePageMode - delayed: true - restoreMode: Binding.RestoreBindingOrValue } - } - AdaptiveLayoutElement { id: roomListC - minimumWidth: roomlist.avatarSize * 4 + Nheko.paddingSmall * 2 - preferredWidth: (Settings.roomListWidth == - 1) - ? (roomlist.avatarSize * 5 + Nheko.paddingSmall * 2) - : (Settings.roomListWidth >= minimumWidth ? Settings.roomListWidth : collapsedWidth) - maximumWidth: roomlist.avatarSize * 10 + Nheko.paddingSmall * 2 collapsedWidth: roomlist.avatarSize + 2 * Nheko.paddingMedium + maximumWidth: roomlist.avatarSize * 10 + Nheko.paddingSmall * 2 + minimumWidth: roomlist.avatarSize * 4 + Nheko.paddingSmall * 2 + preferredWidth: (Settings.roomListWidth == -1) ? (roomlist.avatarSize * 5 + Nheko.paddingSmall * 2) : (Settings.roomListWidth >= minimumWidth ? Settings.roomListWidth : collapsedWidth) RoomList { id: roomlist - height: adaptiveView.height collapsed: parent.collapsed + height: adaptiveView.height } - Binding { - target: Settings + delayed: true property: 'roomListWidth' + restoreMode: Binding.RestoreBindingOrValue + target: Settings value: roomListC.preferredWidth when: !adaptiveView.singlePageMode - delayed: true - restoreMode: Binding.RestoreBindingOrValue } - } - AdaptiveLayoutElement { id: timlineViewC @@ -127,25 +118,20 @@ Rectangle { id: timeline privacyScreen: privacyScreen - showBackButton: adaptiveView.singlePageMode room: Rooms.currentRoom roomPreview: Rooms.currentRoomPreview.roomid ? Rooms.currentRoomPreview : null + showBackButton: adaptiveView.singlePageMode } - } - } - } - PrivacyScreen { id: privacyScreen anchors.fill: parent - visible: Settings.privacyScreen screenTimeout: Settings.privacyScreenTimeout timelineRoot: adaptiveView + visible: Settings.privacyScreen windowTarget: MainWindow } - } diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml index a210a4bb..62a29a2d 100644 --- a/resources/qml/CommunitiesList.qml +++ b/resources/qml/CommunitiesList.qml @@ -13,19 +13,24 @@ import im.nheko 1.0 Page { id: communitySidebar + //leftPadding: Nheko.paddingSmall //rightPadding: Nheko.paddingSmall property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 1.6) property bool collapsed: false + background: Rectangle { + color: Nheko.theme.sidebarBackground + } + // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu Connections { function onHideMenu() { - communityContextMenu.close() + communityContextMenu.close(); } + target: MainWindow } - ListView { id: communitiesList @@ -36,15 +41,158 @@ Page { ScrollBar.vertical: ScrollBar { id: scrollbar + parent: !collapsed && Settings.scrollbarsInRoomlist ? communitiesList : null } + delegate: ItemDelegate { + id: communityItem + + property color backgroundColor: palette.window + property color bubbleBackground: palette.highlight + property color bubbleText: palette.highlightedText + property color importantText: palette.text + required property var model + property color unimportantText: palette.buttonText + + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: model.tooltip + ToolTip.visible: hovered && collapsed + height: avatarSize + 2 * Nheko.paddingMedium + state: "normal" + width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0) + + background: Rectangle { + color: communityItem.backgroundColor + } + states: [ + State { + name: "highlight" + when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId === model.id) + + PropertyChanges { + backgroundColor: palette.dark + bubbleBackground: palette.highlight + bubbleText: palette.highlightedText + importantText: palette.brightText + target: communityItem + unimportantText: palette.brightText + } + }, + State { + name: "selected" + when: Communities.currentTagId == model.id + + PropertyChanges { + backgroundColor: palette.highlight + bubbleBackground: palette.highlightedText + bubbleText: palette.highlight + importantText: palette.highlightedText + target: communityItem + unimportantText: palette.highlightedText + } + } + ] + + onClicked: Communities.setCurrentTagId(model.id) + onPressAndHold: communityContextMenu.show(model.id, model.hidden, model.muted) + + Item { + anchors.fill: parent + + TapHandler { + acceptedButtons: Qt.RightButton + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + gesturePolicy: TapHandler.ReleaseWithinBounds + + onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted) + } + } + RowLayout { + id: r + + anchors.fill: parent + anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * model.depth)) + anchors.margins: Nheko.paddingMedium + spacing: Nheko.paddingMedium + + ImageButton { + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: fontMetrics.lineSpacing + Layout.preferredWidth: fontMetrics.lineSpacing + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: model.collapsed ? qsTr("Expand") : qsTr("Collapse") + ToolTip.visible: hovered + height: fontMetrics.lineSpacing + hoverEnabled: true + image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg" + visible: !communitySidebar.collapsed && model.collapsible + width: fontMetrics.lineSpacing + + onClicked: model.collapsed = !model.collapsed + } + Item { + Layout.preferredWidth: fontMetrics.lineSpacing + visible: !communitySidebar.collapsed && !model.collapsible && Communities.containsSubspaces + } + Avatar { + id: avatar + + Layout.alignment: Qt.AlignVCenter + color: communityItem.backgroundColor + displayName: model.displayName + enabled: false + height: avatarSize + roomid: model.id + url: { + if (model.avatarUrl.startsWith("mxc://")) + return model.avatarUrl.replace("mxc://", "image://MxcImage/"); + else + return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText; + } + width: avatarSize + + NotificationBubble { + anchors.bottom: avatar.bottom + anchors.margins: -Nheko.paddingSmall + anchors.right: avatar.right + bubbleBackgroundColor: communityItem.bubbleBackground + bubbleTextColor: communityItem.bubbleText + font.pixelSize: fontMetrics.font.pixelSize * 0.6 + hasLoudNotification: model.hasLoudNotification + mayBeVisible: communitySidebar.collapsed && !model.muted && Settings.spaceNotifications + notificationCount: model.unreadMessages + } + } + ElidedLabel { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + color: communityItem.importantText + elideWidth: width + fullText: model.displayName + textFormat: Text.PlainText + visible: !communitySidebar.collapsed + } + Item { + Layout.fillWidth: true + } + NotificationBubble { + Layout.alignment: Qt.AlignRight + Layout.leftMargin: Nheko.paddingSmall + bubbleBackgroundColor: communityItem.bubbleBackground + bubbleTextColor: communityItem.bubbleText + hasLoudNotification: model.hasLoudNotification + mayBeVisible: !communitySidebar.collapsed && !model.muted && Settings.spaceNotifications + notificationCount: model.unreadMessages + } + } + } Platform.Menu { id: communityContextMenu - property string tagId property bool hidden property bool muted + property string tagId function show(id_, hidden_, muted_) { tagId = id_; @@ -54,177 +202,19 @@ Page { } Platform.MenuItem { - text: qsTr("Do not show notification counts for this community or tag.") checkable: true checked: communityContextMenu.muted + text: qsTr("Do not show notification counts for this community or tag.") + onTriggered: Communities.toggleTagMute(communityContextMenu.tagId) } - Platform.MenuItem { - text: qsTr("Hide rooms with this tag or from this community by default.") checkable: true checked: communityContextMenu.hidden + text: qsTr("Hide rooms with this tag or from this community by default.") + onTriggered: Communities.toggleTagId(communityContextMenu.tagId) } - } - - delegate: ItemDelegate { - id: communityItem - - property color backgroundColor: palette.window - property color importantText: palette.text - property color unimportantText: palette.buttonText - property color bubbleBackground: palette.highlight - property color bubbleText: palette.highlightedText - required property var model - - height: avatarSize + 2 * Nheko.paddingMedium - width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0) - state: "normal" - ToolTip.visible: hovered && collapsed - ToolTip.text: model.tooltip - ToolTip.delay: Nheko.tooltipDelay - onClicked: Communities.setCurrentTagId(model.id) - onPressAndHold: communityContextMenu.show(model.id, model.hidden, model.muted) - states: [ - State { - name: "highlight" - when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId === model.id) - - PropertyChanges { - target: communityItem - backgroundColor: palette.dark - importantText: palette.brightText - unimportantText: palette.brightText - bubbleBackground: palette.highlight - bubbleText: palette.highlightedText - } - - }, - State { - name: "selected" - when: Communities.currentTagId == model.id - - PropertyChanges { - target: communityItem - backgroundColor: palette.highlight - importantText: palette.highlightedText - unimportantText: palette.highlightedText - bubbleBackground: palette.highlightedText - bubbleText: palette.highlight - } - - } - ] - - Item { - anchors.fill: parent - - TapHandler { - acceptedButtons: Qt.RightButton - onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted) - gesturePolicy: TapHandler.ReleaseWithinBounds - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad - } - - } - - RowLayout { - id: r - spacing: Nheko.paddingMedium - anchors.fill: parent - anchors.margins: Nheko.paddingMedium - anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * model.depth)) - - ImageButton { - visible: !communitySidebar.collapsed && model.collapsible - Layout.preferredHeight: fontMetrics.lineSpacing - Layout.preferredWidth: fontMetrics.lineSpacing - Layout.alignment: Qt.AlignVCenter - height: fontMetrics.lineSpacing - width: fontMetrics.lineSpacing - image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg" - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: model.collapsed ? qsTr("Expand") : qsTr("Collapse") - hoverEnabled: true - - onClicked: model.collapsed = !model.collapsed - } - - Item { - Layout.preferredWidth: fontMetrics.lineSpacing - visible: !communitySidebar.collapsed && !model.collapsible && Communities.containsSubspaces - } - - Avatar { - id: avatar - - enabled: false - Layout.alignment: Qt.AlignVCenter - height: avatarSize - width: avatarSize - url: { - if (model.avatarUrl.startsWith("mxc://")) - return model.avatarUrl.replace("mxc://", "image://MxcImage/"); - else - return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText; - } - roomid: model.id - displayName: model.displayName - color: communityItem.backgroundColor - - NotificationBubble { - notificationCount: model.unreadMessages - hasLoudNotification: model.hasLoudNotification - bubbleBackgroundColor: communityItem.bubbleBackground - bubbleTextColor: communityItem.bubbleText - font.pixelSize: fontMetrics.font.pixelSize * 0.6 - mayBeVisible: communitySidebar.collapsed && !model.muted && Settings.spaceNotifications - anchors.right: avatar.right - anchors.bottom: avatar.bottom - anchors.margins: -Nheko.paddingSmall - } - - } - - ElidedLabel { - visible: !communitySidebar.collapsed - Layout.alignment: Qt.AlignVCenter - color: communityItem.importantText - Layout.fillWidth: true - elideWidth: width - fullText: model.displayName - textFormat: Text.PlainText - } - - Item { - Layout.fillWidth: true - } - - NotificationBubble { - notificationCount: model.unreadMessages - hasLoudNotification: model.hasLoudNotification - bubbleBackgroundColor: communityItem.bubbleBackground - bubbleTextColor: communityItem.bubbleText - mayBeVisible: !communitySidebar.collapsed && !model.muted && Settings.spaceNotifications - Layout.alignment: Qt.AlignRight - Layout.leftMargin: Nheko.paddingSmall - } - - } - - background: Rectangle { - color: communityItem.backgroundColor - } - - } - } - - background: Rectangle { - color: Nheko.theme.sidebarBackground - } - } diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index 02dccfc9..00141d4d 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -11,117 +11,102 @@ import im.nheko 1.0 Control { id: popup - property alias currentIndex: listView.currentIndex - property string roomId - property string completerName - property var completer - property bool bottomToTop: true - property bool fullWidth: false - property bool centerRowContent: true property int avatarHeight: 24 property int avatarWidth: 24 + property bool bottomToTop: true + property bool centerRowContent: true + property var completer + property string completerName + property alias count: listView.count + property alias currentIndex: listView.currentIndex + property bool fullWidth: false + property string roomId property int rowMargin: 0 property int rowSpacing: Nheko.paddingSmall - property alias count: listView.count signal completionClicked(string completion) signal completionSelected(string id) - function up() { - if (bottomToTop) - down_(); - else - up_(); + function changeCompleter() { + if (completerName) { + completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : (popup.roomId != "" ? popup.roomId : room.roomId)); + completer.setSearchString(""); + } else { + completer = undefined; + } + currentIndex = -1; } - - function down() { - if (bottomToTop) - up_(); - else - down_(); - } - - function up_() { - currentIndex = currentIndex - 1; - if (currentIndex == -2) - currentIndex = listView.count - 1; - - } - - function down_() { - currentIndex = currentIndex + 1; - if (currentIndex >= listView.count) - currentIndex = -1; - - } - function currentCompletion() { if (currentIndex > -1 && currentIndex < listView.count) return completer.completionAt(currentIndex); else return null; } - + function down() { + if (bottomToTop) + up_(); + else + down_(); + } + function down_() { + currentIndex = currentIndex + 1; + if (currentIndex >= listView.count) + currentIndex = -1; + } function finishCompletion() { if (popup.completerName == "room") popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.roomid); else if (popup.completerName == "user") popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.userid); - } - - function changeCompleter() { - if (completerName) { - completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : (popup.roomId != "" ? popup.roomId : room.roomId)); - completer.setSearchString(""); - } else { - completer = undefined; - } - currentIndex = -1 + function up() { + if (bottomToTop) + down_(); + else + up_(); + } + function up_() { + currentIndex = currentIndex - 1; + if (currentIndex == -2) + currentIndex = listView.count - 1; } - onCompleterNameChanged: changeCompleter() - onRoomIdChanged: changeCompleter() bottomPadding: 1 leftPadding: 1 - topPadding: 1 rightPadding: 1 + topPadding: 1 + background: Rectangle { + border.color: palette.mid + color: palette.base + } contentItem: ListView { id: listView - // If we have fewer than 7 items, just use the list view's content height. + clip: true + displayMarginBeginning: height / 2 + displayMarginEnd: height / 2 + highlightFollowsCurrentItem: true + + // If we have fewer than 7 items, just use the list view's content height. // Otherwise, we want to show 7 items. Each item consists of row spacing between rows, row margins // on each side of a row, 1px of padding above the first item and below the last item, and nominally // some kind of content height. avatarHeight is used for just about every delegate, so we're using // that until we find something better. Put is all together and you have the formula below! - implicitHeight: Math.min(contentHeight, 6*rowSpacing + 7*(popup.avatarHeight + 2*rowMargin)) - clip: true - - Timer { - id: deadTimer - interval: 50 - } - - onContentYChanged: deadTimer.restart() + implicitHeight: Math.min(contentHeight, 6 * rowSpacing + 7 * (popup.avatarHeight + 2 * rowMargin)) // Broken, see https://bugreports.qt.io/browse/QTBUG-102811 //reuseItems: true implicitWidth: listView.contentItem.childrenRect.width model: completer - verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom - spacing: rowSpacing pixelAligned: true - highlightFollowsCurrentItem: true - - displayMarginBeginning: height / 2 - displayMarginEnd: height / 2 + spacing: rowSpacing + verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom delegate: Rectangle { property variant modelData: model ListView.delayRemove: true - color: model.index == popup.currentIndex ? palette.highlight : palette.base height: (chooser.child?.implicitHeight ?? 0) + 2 * popup.rowMargin implicitWidth: fullWidth ? ListView.view.width : chooser.child.implicitWidth + 4 @@ -131,26 +116,27 @@ Control { anchors.fill: parent hoverEnabled: true - onPositionChanged: if (!listView.moving && !deadTimer.running) popup.currentIndex = model.index + onClicked: { - popup.completionClicked(completer.completionAt(model.index)); - if (popup.completerName == "room") - popup.completionSelected(model.roomid); - else if (popup.completerName == "user") - popup.completionSelected(model.userid); + popup.completionClicked(completer.completionAt(model.index)); + if (popup.completerName == "room") + popup.completionSelected(model.roomid); + else if (popup.completerName == "user") + popup.completionSelected(model.userid); } + onPositionChanged: if (!listView.moving && !deadTimer.running) + popup.currentIndex = model.index } Ripple { color: Qt.rgba(palette.base.r, palette.base.g, palette.base.b, 0.5) } - DelegateChooser { id: chooser - roleValue: popup.completerName anchors.fill: parent anchors.margins: popup.rowMargin enabled: false + roleValue: popup.completerName DelegateChoice { roleValue: "user" @@ -162,28 +148,23 @@ Control { spacing: rowSpacing Avatar { - height: popup.avatarHeight - width: popup.avatarWidth displayName: model.displayName - userid: model.userid - url: model.avatarUrl.replace("mxc://", "image://MxcImage/") enabled: false + height: popup.avatarHeight + url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + userid: model.userid + width: popup.avatarWidth } - Label { - text: model.displayName color: model.index == popup.currentIndex ? palette.highlightedText : palette.text + text: model.displayName } - Label { - text: "(" + model.userid + ")" color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText + text: "(" + model.userid + ")" } - } - } - DelegateChoice { roleValue: "emoji" @@ -194,39 +175,33 @@ Control { spacing: rowSpacing Label { - visible: !!model.unicode - text: model.unicode color: model.index == popup.currentIndex ? palette.highlightedText : palette.text font: Settings.emojiFont + text: model.unicode + visible: !!model.unicode } - Avatar { - visible: !model.unicode - height: popup.avatarHeight - width: popup.avatarWidth + crop: false displayName: model.shortcode + enabled: false + height: popup.avatarHeight //userid: model.shortcode url: (model.url ? model.url : "").replace("mxc://", "image://MxcImage/") - enabled: false - crop: false + visible: !model.unicode + width: popup.avatarWidth } - Label { Layout.leftMargin: Nheko.paddingSmall Layout.rightMargin: Nheko.paddingSmall - text: model.shortcode color: model.index == popup.currentIndex ? palette.highlightedText : palette.text + text: model.shortcode } - Label { - text: "(" + model.packname + ")" color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText + text: "(" + model.packname + ")" } - } - } - DelegateChoice { roleValue: "command" @@ -237,20 +212,16 @@ Control { spacing: rowSpacing Label { - text: model.name color: model.index == popup.currentIndex ? palette.highlightedText : palette.text font.bold: true + text: model.name } - Label { - text: model.description color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText + text: model.description } - } - } - DelegateChoice { roleValue: "room" @@ -261,26 +232,22 @@ Control { spacing: rowSpacing Avatar { - height: popup.avatarHeight - width: popup.avatarWidth displayName: model.roomName + enabled: false + height: popup.avatarHeight roomid: model.roomid url: model.avatarUrl.replace("mxc://", "image://MxcImage/") - enabled: false + width: popup.avatarWidth } - Label { - text: model.roomName - font.pixelSize: popup.avatarHeight * 0.5 color: model.index == popup.currentIndex ? palette.highlightedText : palette.text font.italic: model.isTombstoned + font.pixelSize: popup.avatarHeight * 0.5 + text: model.roomName textFormat: Text.RichText } - } - } - DelegateChoice { roleValue: "roomAliases" @@ -291,41 +258,38 @@ Control { spacing: rowSpacing Avatar { - height: popup.avatarHeight - width: popup.avatarWidth displayName: model.roomName + enabled: false + height: popup.avatarHeight roomid: model.roomid url: model.avatarUrl.replace("mxc://", "image://MxcImage/") - enabled: false + width: popup.avatarWidth } - Label { - text: model.roomName color: model.index == popup.currentIndex ? palette.highlightedText : palette.text font.italic: model.isTombstoned + text: model.roomName textFormat: Text.RichText } - Label { - text: "(" + model.roomAlias + ")" color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText + text: "(" + model.roomAlias + ")" textFormat: Text.RichText } - } - } - } - } + onContentYChanged: deadTimer.restart() + + Timer { + id: deadTimer + + interval: 50 + } } - - background: Rectangle { - color: palette.base - border.color: palette.mid - } - + onCompleterNameChanged: changeCompleter() + onRoomIdChanged: changeCompleter() } diff --git a/resources/qml/ElidedLabel.qml b/resources/qml/ElidedLabel.qml index 2d53faff..153d7c33 100644 --- a/resources/qml/ElidedLabel.qml +++ b/resources/qml/ElidedLabel.qml @@ -9,21 +9,20 @@ import im.nheko 1.0 Label { id: root - property alias fullText: metrics.text property alias elideWidth: metrics.elideWidth + property alias fullText: metrics.text property int fullTextWidth: Math.ceil(metrics.advanceWidth) color: palette.text - text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(metrics.elidedText) - maximumLineCount: 1 elide: Text.ElideRight + maximumLineCount: 1 + text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(metrics.elidedText) textFormat: Text.PlainText TextMetrics { id: metrics - font.pointSize: root.font.pointSize elide: Text.ElideRight + font.pointSize: root.font.pointSize } - } diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml index c675fb52..fb9dc7b5 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml @@ -11,32 +11,40 @@ Image { id: stateImg property bool encrypted: false - property int trust: Crypto.Unverified - property string unencryptedIcon: ":/icons/icons/ui/shield-filled-cross.svg" - property color unencryptedColor: Nheko.theme.error - property color unencryptedHoverColor: unencryptedColor property bool hovered: ma.hovered - property string sourceUrl: { if (!encrypted) - return "image://colorimage/" + unencryptedIcon + "?"; - + return "image://colorimage/" + unencryptedIcon + "?"; switch (trust) { - case Crypto.Verified: + case Crypto.Verified: return "image://colorimage/:/icons/icons/ui/shield-filled-checkmark.svg?"; - case Crypto.TOFU: + case Crypto.TOFU: return "image://colorimage/:/icons/icons/ui/shield-filled.svg?"; - case Crypto.Unverified: + case Crypto.Unverified: return "image://colorimage/:/icons/icons/ui/shield-filled-exclamation-mark.svg?"; - default: + default: return "image://colorimage/:/icons/icons/ui/shield-filled-cross.svg?"; } } + property int trust: Crypto.Unverified + property color unencryptedColor: Nheko.theme.error + property color unencryptedHoverColor: unencryptedColor + property string unencryptedIcon: ":/icons/icons/ui/shield-filled-cross.svg" - width: 16 + ToolTip.text: { + if (!encrypted) + return qsTr("This message is not encrypted!"); + switch (trust) { + case Crypto.Verified: + return qsTr("Encrypted by a verified device"); + case Crypto.TOFU: + return qsTr("Encrypted by an unverified device, but you have trusted that user so far."); + default: + return qsTr("Encrypted by an unverified device or the key is from an untrusted source like the key backup."); + } + } + ToolTip.visible: stateImg.hovered height: 16 - sourceSize.height: height - sourceSize.width: width source: { if (encrypted) { switch (trust) { @@ -51,23 +59,12 @@ Image { return sourceUrl + (stateImg.hovered ? unencryptedHoverColor : unencryptedColor); } } - ToolTip.visible: stateImg.hovered - ToolTip.text: { - if (!encrypted) - return qsTr("This message is not encrypted!"); - - switch (trust) { - case Crypto.Verified: - return qsTr("Encrypted by a verified device"); - case Crypto.TOFU: - return qsTr("Encrypted by an unverified device, but you have trusted that user so far."); - default: - return qsTr("Encrypted by an unverified device or the key is from an untrusted source like the key backup."); - } - } + sourceSize.height: height + sourceSize.width: width + width: 16 HoverHandler { id: ma - } + } } diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml index a5787189..cc48c46f 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml @@ -16,13 +16,21 @@ Popup { mid = mid_in; } - x: Math.round(parent.width / 2 - width / 2) - y: Math.round(parent.height / 4) + leftPadding: 10 modal: true parent: Overlay.overlay - width: timelineRoot.width * 0.8 - leftPadding: 10 rightPadding: 10 + width: timelineRoot.width * 0.8 + x: Math.round(parent.width / 2 - width / 2) + y: Math.round(parent.height / 4) + + Overlay.modal: Rectangle { + color: Qt.rgba(palette.window.r, palette.window.g, palette.window.b, 0.7) + } + background: Rectangle { + color: palette.window + } + onOpened: { roomTextInput.forceActiveFocus(); } @@ -35,46 +43,40 @@ Popup { Label { id: titleLabel - text: qsTr("Forward Message") - font.bold: true bottomPadding: 10 color: palette.text + font.bold: true + text: qsTr("Forward Message") } - Reply { id: replyPreview - property var modelData: room ? room.getDump(mid, "") : { - } + property var modelData: room ? room.getDump(mid, "") : {} - width: parent.width - - userColor: TimelineManager.userColor(modelData.userId, palette.window) blurhash: modelData.blurhash ?? "" body: modelData.body ?? "" - formattedBody: modelData.formattedBody ?? "" + encryptionError: modelData.encryptionError ?? "" eventId: modelData.eventId ?? "" filename: modelData.filename ?? "" filesize: modelData.filesize ?? "" + formattedBody: modelData.formattedBody ?? "" + isOnlyEmoji: modelData.isOnlyEmoji ?? false + originalWidth: modelData.originalWidth ?? 0 proportionalHeight: modelData.proportionalHeight ?? 1 type: modelData.type ?? MtxEvent.UnknownMessage typeString: modelData.typeString ?? "" url: modelData.url ?? "" - originalWidth: modelData.originalWidth ?? 0 - isOnlyEmoji: modelData.isOnlyEmoji ?? false + userColor: TimelineManager.userColor(modelData.userId, palette.window) userId: modelData.userId ?? "" userName: modelData.userName ?? "" - encryptionError: modelData.encryptionError ?? "" + width: parent.width } - MatrixTextField { id: roomTextInput - width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 color: palette.text - onTextEdited: { - completerPopup.completer.searchString = text; - } + width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 + Keys.onPressed: { if (event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) { event.accepted = true; @@ -90,43 +92,32 @@ Popup { event.accepted = true; } } + onTextEdited: { + completerPopup.completer.searchString = text; + } } - Completer { id: completerPopup - width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 - completerName: "room" - fullWidth: true - centerRowContent: false avatarHeight: 24 avatarWidth: 24 bottomToTop: false + centerRowContent: false + completerName: "room" + fullWidth: true + width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2 } - } - Connections { function onCompletionSelected(id) { room.forwardMessage(messageContextMenu.eventId, id); forwardMessagePopup.close(); } - function onCountChanged() { if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count)) completerPopup.currentIndex = 0; - } target: completerPopup } - - background: Rectangle { - color: palette.window - } - - Overlay.modal: Rectangle { - color: Qt.rgba(palette.window.r, palette.window.g, palette.window.b, 0.7) - } - } diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml index ecb402c7..4115cd0a 100644 --- a/resources/qml/ImageButton.qml +++ b/resources/qml/ImageButton.qml @@ -10,38 +10,35 @@ import im.nheko 1.0 // for cursor shape AbstractButton { id: button - property alias cursor: mouseArea.cursorShape - property string image: undefined - property color highlightColor: palette.highlight property color buttonTextColor: palette.buttonText property bool changeColorOnHover: true + property alias cursor: mouseArea.cursorShape + property color highlightColor: palette.highlight + property string image: undefined property bool ripple: true focusPolicy: Qt.NoFocus - width: 16 height: 16 + width: 16 Image { id: buttonImg // Workaround, can't get icon.source working for now... anchors.fill: parent + fillMode: Image.PreserveAspectFit source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : "" sourceSize.height: button.height sourceSize.width: button.width - fillMode: Image.PreserveAspectFit } - CursorShape { id: mouseArea anchors.fill: parent cursorShape: Qt.PointingHandCursor } - Ripple { - enabled: button.ripple color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5) + enabled: button.ripple } - } diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index 96303a2b..057a632f 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -11,22 +11,23 @@ TextEdit { property alias cursorShape: cs.cursorShape - textFormat: TextEdit.RichText - readOnly: true - focus: false - wrapMode: Text.Wrap - selectByMouse: !Settings.mobileMode + ToolTip.text: hoveredLink + ToolTip.visible: hoveredLink || false // this always has to be enabled, otherwise you can't click links anymore! //enabled: selectByMouse color: palette.text - onLinkActivated: Nheko.openLink(link) - ToolTip.visible: hoveredLink || false - ToolTip.text: hoveredLink + focus: false + readOnly: true + selectByMouse: !Settings.mobileMode + textFormat: TextEdit.RichText + wrapMode: Text.Wrap + // Setting a tooltip delay makes the hover text empty .-. //ToolTip.delay: Nheko.tooltipDelay Component.onCompleted: { TimelineManager.fixImageRendering(r.textDocument, r); } + onLinkActivated: Nheko.openLink(link) CursorShape { id: cs @@ -34,5 +35,4 @@ TextEdit { anchors.fill: parent cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } - } diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml index f1ff2836..7209a5aa 100644 --- a/resources/qml/MatrixTextField.qml +++ b/resources/qml/MatrixTextField.qml @@ -7,67 +7,63 @@ import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import im.nheko 1.0 - ColumnLayout { id: c + property color backgroundColor: palette.base property alias color: labelC.color - property alias textPadding: input.padding - property alias text: input.text + property alias echoMode: input.echoMode + property alias font: input.font + property var hasClear: false property alias label: labelC.text property alias placeholderText: input.placeholderText - property alias font: input.font - property alias echoMode: input.echoMode property alias selectByMouse: input.selectByMouse - property var hasClear: false + property alias text: input.text + property alias textPadding: input.padding - Timer { - id: timer - interval: 350 - onTriggered: editingFinished() - } - - onTextChanged: timer.restart() - - signal textEdited signal accepted signal editingFinished - - function forceActiveFocus() { - input.forceActiveFocus(); - } + signal textEdited function clear() { input.clear(); } + function forceActiveFocus() { + input.forceActiveFocus(); + } ToolTip.delay: Nheko.tooltipDelay ToolTip.visible: hover.hovered - spacing: 0 - Item { - Layout.fillWidth: true - Layout.preferredHeight: labelC.contentHeight - Layout.margins: input.padding - Layout.bottomMargin: Nheko.paddingSmall - visible: labelC.text + onTextChanged: timer.restart() + Timer { + id: timer + + interval: 350 + + onTriggered: editingFinished() + } + Item { + Layout.bottomMargin: Nheko.paddingSmall + Layout.fillWidth: true + Layout.margins: input.padding + Layout.preferredHeight: labelC.contentHeight + visible: labelC.text z: 1 Label { id: labelC - y: contentHeight + input.padding + Nheko.paddingSmall - enabled: false - color: palette.text + enabled: false + font.letterSpacing: input.font.pixelSize * 0.02 font.pixelSize: input.font.pixelSize font.weight: Font.DemiBold - font.letterSpacing: input.font.pixelSize * 0.02 - width: parent.width - state: labelC.text && (input.activeFocus == true || input.text) ? "focused" : "" + width: parent.width + y: contentHeight + input.padding + Nheko.paddingSmall states: State { name: "focused" @@ -76,50 +72,40 @@ ColumnLayout { target: labelC y: 0 } - PropertyChanges { - target: input opacity: 1 + target: input } - } - transitions: Transition { from: "" - to: "focused" reversible: true + to: "focused" NumberAnimation { - target: labelC + alwaysRunToEnd: true + duration: 210 + easing.type: Easing.InCubic properties: "y" - duration: 210 - easing.type: Easing.InCubic - alwaysRunToEnd: true + target: labelC } - NumberAnimation { - target: input - properties: "opacity" + alwaysRunToEnd: true duration: 210 easing.type: Easing.InCubic - alwaysRunToEnd: true + properties: "opacity" + target: input } - } } } - TextField { id: input + Layout.fillWidth: true - color: labelC.color - opacity: labelC.text ? 0 : 1 focus: true - - onTextEdited: c.textEdited() - onAccepted: c.accepted() - onEditingFinished: c.editingFinished() + opacity: labelC.text ? 0 : 1 background: Rectangle { id: backgroundRect @@ -127,44 +113,46 @@ ColumnLayout { color: labelC.text ? "transparent" : backgroundColor } + onAccepted: c.accepted() + onEditingFinished: c.editingFinished() + onTextEdited: c.textEdited() + ImageButton { id: clearText + focusPolicy: Qt.NoFocus + hoverEnabled: true + image: ":/icons/icons/ui/round-remove-button.svg" visible: c.hasClear && searchField.text !== '' - image: ":/icons/icons/ui/round-remove-button.svg" - focusPolicy: Qt.NoFocus onClicked: { - searchField.clear() + searchField.clear(); topBar.searchString = ""; } - hoverEnabled: true + anchors { - top: parent.top bottom: parent.bottom right: parent.right rightMargin: Nheko.paddingSmall + top: parent.top } } - } - Rectangle { id: blueBar Layout.fillWidth: true - color: palette.highlight height: 1 Rectangle { id: blackBar - anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter - height: parent.height*2 - width: 0 + anchors.top: parent.top color: palette.text + height: parent.height * 2 + width: 0 states: State { name: "focused" @@ -174,31 +162,25 @@ ColumnLayout { target: blackBar width: blueBar.width } - } - transitions: Transition { from: "" - to: "focused" reversible: true - + to: "focused" NumberAnimation { - target: blackBar - properties: "width" + alwaysRunToEnd: true duration: 310 easing.type: Easing.InCubic - alwaysRunToEnd: true + properties: "width" + target: blackBar } - } - } - } - HoverHandler { id: hover + enabled: c.ToolTip.text } } diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml index 6220249b..e196b06d 100644 --- a/resources/qml/MessageInput.qml +++ b/resources/qml/MessageInput.qml @@ -14,60 +14,54 @@ import im.nheko 1.0 Rectangle { id: inputBar + property bool showAllButtons: width > 450 || (messageInput.length == 0 && !messageInput.inputMethodComposing) readonly property string text: messageInput.text - color: palette.window Layout.fillWidth: true - Layout.preferredHeight: row.implicitHeight Layout.minimumHeight: 40 - property bool showAllButtons: width > 450 || (messageInput.length == 0 && !messageInput.inputMethodComposing) - + Layout.preferredHeight: row.implicitHeight + color: palette.window Component { id: placeCallDialog PlaceCall { } - } - Component { id: screenShareDialog ScreenShare { } - } - RowLayout { id: row - visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false anchors.fill: parent spacing: 0 + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false ImageButton { - visible: CallManager.callsSupported && showAllButtons - opacity: (CallManager.haveCallInvite || CallManager.isOnCallOnOtherDevice) ? 0.3 : 1 Layout.alignment: Qt.AlignBottom - hoverEnabled: true - width: 22 - height: 22 - image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.svg" : ":/icons/icons/ui/place-call.svg" - ToolTip.visible: hovered - ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call")) Layout.margins: 8 + ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call")) + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.svg" : ":/icons/icons/ui/place-call.svg" + opacity: (CallManager.haveCallInvite || CallManager.isOnCallOnOtherDevice) ? 0.3 : 1 + visible: CallManager.callsSupported && showAllButtons + width: 22 + onClicked: { if (room) { if (CallManager.haveCallInvite) { - return ; + return; } else if (CallManager.isOnCall) { CallManager.hangUp(); - } - else if(CallManager.isOnCallOnOtherDevice) { + } else if (CallManager.isOnCallOnOtherDevice) { return; - } - else { + } else { var dialog = placeCallDialog.createObject(timelineRoot); dialog.open(); timelineRoot.destroyOnClose(dialog); @@ -75,18 +69,18 @@ Rectangle { } } } - ImageButton { - visible: showAllButtons Layout.alignment: Qt.AlignBottom - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/attach.svg" Layout.margins: 8 - onClicked: room.input.openFileSelection() - ToolTip.visible: hovered ToolTip.text: qsTr("Send a file") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/attach.svg" + visible: showAllButtons + width: 22 + + onClicked: room.input.openFileSelection() Rectangle { anchors.fill: parent @@ -98,112 +92,67 @@ Rectangle { height: parent.height / 2 running: parent.visible } - } - } - ScrollView { id: textInput Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true Layout.maximumHeight: Window.height / 4 Layout.minimumHeight: fontMetrics.lineSpacing Layout.preferredHeight: contentHeight - Layout.fillWidth: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - contentWidth: availableWidth TextArea { id: messageInput property int completerTriggeredAt: 0 + property string lastChar function insertCompletion(completion) { messageInput.remove(completerTriggeredAt, cursorPosition); messageInput.insert(cursorPosition, completion); } - function openCompleter(pos, type) { - if (popup.opened) return; + if (popup.opened) + return; completerTriggeredAt = pos; completer.completerName = type; popup.open(); - completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText); + completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText); } - function positionCursorAtEnd() { cursorPosition = messageInput.length; } - function positionCursorAtStart() { cursorPosition = 0; } - selectByMouse: true + background: null + bottomPadding: 8 + color: palette.text + focus: true + leftPadding: inputBar.showAllButtons ? 0 : 8 + padding: 0 placeholderText: qsTr("Write a message...") placeholderTextColor: palette.buttonText - color: palette.text - width: textInput.width - verticalAlignment: TextEdit.AlignVCenter - wrapMode: TextEdit.Wrap - padding: 0 + selectByMouse: true topPadding: 8 - bottomPadding: 8 - leftPadding: inputBar.showAllButtons? 0 : 8 - focus: true - property string lastChar - onTextChanged: { - if (room) - room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); - forceActiveFocus(); - if (cursorPosition > 0) - lastChar = text.charAt(cursorPosition-1) - else - lastChar = '' - if (lastChar == '@') { - messageInput.openCompleter(selectionStart-1, "user"); - } else if (lastChar == ':') { - messageInput.openCompleter(selectionStart-1, "emoji"); - } else if (lastChar == '#') { - messageInput.openCompleter(selectionStart-1, "roomAliases"); - } else if (lastChar == "/" && cursorPosition == 1) { - messageInput.openCompleter(selectionStart-1, "command"); - } - } - onCursorPositionChanged: { - if (!room) - return ; + verticalAlignment: TextEdit.AlignVCenter + width: textInput.width + wrapMode: TextEdit.Wrap - room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); - if (popup.opened && cursorPosition <= completerTriggeredAt) - popup.close(); - - if (popup.opened) - completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText); - - } - onPreeditTextChanged: { - if (popup.opened) - completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText); - } - onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text) - onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text) - // Ensure that we get escape key press events first. - Keys.onShortcutOverride: (event) => event.accepted = (popup.opened && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter || event.key === Qt.Key_Space)) - Keys.onPressed: (event) => { + Keys.onPressed: event => { if (event.matches(StandardKey.Paste)) { event.accepted = room.input.tryPasteAttachment(false); } else if (event.key == Qt.Key_Space) { // close popup if user enters space after colon if (cursorPosition == completerTriggeredAt + 1) popup.close(); - if (popup.opened && completer.count <= 0) popup.close(); - } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) { messageInput.clear(); } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) { @@ -218,8 +167,8 @@ Rectangle { completer.completerName = ""; popup.close(); } else if (event.matches(StandardKey.InsertLineSeparator)) { - if (popup.opened) popup.close(); - + if (popup.opened) + popup.close(); if (Settings.invertEnterKey && (!Qt.inputMethod.visible || Qt.platform.os === "windows")) { room.input.send(); event.accepted = true; @@ -253,16 +202,16 @@ Rectangle { console.log('"' + t + '"'); if (t == '@') { messageInput.openCompleter(pos, "user"); - return ; + return; } else if (t == ' ' || t == '\t') { messageInput.openCompleter(pos + 1, "user"); - return ; + return; } else if (t == ':') { messageInput.openCompleter(pos, "emoji"); - return ; + return; } else if (t == '~') { messageInput.openCompleter(pos, "customEmoji"); - return ; + return; } pos = pos - 1; } @@ -312,21 +261,53 @@ Rectangle { } } } - background: null + // Ensure that we get escape key press events first. + Keys.onShortcutOverride: event => event.accepted = (popup.opened && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter || event.key === Qt.Key_Space)) + onCursorPositionChanged: { + if (!room) + return; + room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); + if (popup.opened && cursorPosition <= completerTriggeredAt) + popup.close(); + if (popup.opened) + completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText); + } + onPreeditTextChanged: { + if (popup.opened) + completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText); + } + onSelectionEndChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + onSelectionStartChanged: room.input.updateState(selectionStart, selectionEnd, cursorPosition, text) + onTextChanged: { + if (room) + room.input.updateState(selectionStart, selectionEnd, cursorPosition, text); + forceActiveFocus(); + if (cursorPosition > 0) + lastChar = text.charAt(cursorPosition - 1); + else + lastChar = ''; + if (lastChar == '@') { + messageInput.openCompleter(selectionStart - 1, "user"); + } else if (lastChar == ':') { + messageInput.openCompleter(selectionStart - 1, "emoji"); + } else if (lastChar == '#') { + messageInput.openCompleter(selectionStart - 1, "roomAliases"); + } else if (lastChar == "/" && cursorPosition == 1) { + messageInput.openCompleter(selectionStart - 1, "command"); + } + } Connections { function onRoomChanged() { messageInput.clear(); if (room) messageInput.append(room.input.text); - completer.completerName = ""; messageInput.forceActiveFocus(); } target: timelineView } - Connections { function onCompletionClicked(completion) { messageInput.insertCompletion(completion); @@ -334,43 +315,39 @@ Rectangle { target: completer } - Popup { id: popup + background: null + padding: 0 x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height - background: null - padding: 0 + enter: Transition { + NumberAnimation { + duration: 100 + from: 0 + property: "opacity" + to: 1 + } + } + exit: Transition { + NumberAnimation { + duration: 100 + from: 1 + property: "opacity" + to: 0 + } + } Completer { - anchors.fill: parent id: completer + + anchors.fill: parent rowMargin: 2 rowSpacing: 0 } - - enter: Transition { - NumberAnimation { - property: "opacity" - from: 0 - to: 1 - duration: 100 - } - - } - - exit: Transition { - NumberAnimation { - property: "opacity" - from: 1 - to: 0 - duration: 100 - } - } } - Connections { function onTextChanged(newText) { messageInput.text = newText; @@ -380,16 +357,13 @@ Rectangle { ignoreUnknownSignals: true target: room ? room.input : null } - Connections { - function onReplyChanged() { - messageInput.forceActiveFocus(); - } - function onEditChanged() { messageInput.forceActiveFocus(); } - + function onReplyChanged() { + messageInput.forceActiveFocus(); + } function onThreadChanged() { messageInput.forceActiveFocus(); } @@ -397,7 +371,6 @@ Rectangle { ignoreUnknownSignals: true target: room } - Connections { function onFocusInput() { messageInput.forceActiveFocus(); @@ -405,59 +378,56 @@ Rectangle { target: TimelineManager } - MouseArea { + acceptedButtons: Qt.MiddleButton // workaround for wrong cursor shape on some platforms anchors.fill: parent - acceptedButtons: Qt.MiddleButton cursorShape: Qt.IBeamCursor - onPressed: (mouse) => mouse.accepted = room.input.tryPasteAttachment(true) + + onPressed: mouse => mouse.accepted = room.input.tryPasteAttachment(true) } - } - } - ImageButton { id: stickerButton - visible: showAllButtons Layout.alignment: Qt.AlignRight | Qt.AlignBottom Layout.margins: 8 - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/sticky-note-solid.svg" - ToolTip.visible: hovered ToolTip.text: qsTr("Stickers") - onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function(row) { - room.input.sticker(row); - TimelineManager.focusMessageInput(); - }) + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/sticky-note-solid.svg" + visible: showAllButtons + width: 22 + + onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function (row) { + room.input.sticker(row); + TimelineManager.focusMessageInput(); + }) StickerPicker { id: stickerPopup emoji: false } - } - ImageButton { id: emojiButton Layout.alignment: Qt.AlignRight | Qt.AlignBottom Layout.margins: 8 - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/smile.svg" - ToolTip.visible: hovered ToolTip.text: qsTr("Emoji") - onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, room.roomId, function(plaintext, markdown) { - messageInput.insert(messageInput.cursorPosition, markdown); - TimelineManager.focusMessageInput(); - }) + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/smile.svg" + width: 22 + + onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, room.roomId, function (plaintext, markdown) { + messageInput.insert(messageInput.cursorPosition, markdown); + TimelineManager.focusMessageInput(); + }) StickerPicker { id: emojiPopup @@ -465,28 +435,25 @@ Rectangle { emoji: true } } - ImageButton { Layout.alignment: Qt.AlignRight | Qt.AlignBottom Layout.margins: 8 - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/send.svg" Layout.rightMargin: 8 - ToolTip.visible: hovered ToolTip.text: qsTr("Send") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/send.svg" + width: 22 + onClicked: { room.input.send(); } } - } - Text { anchors.centerIn: parent - visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false text: qsTr("You don't have permission to send messages in this room") + visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false } - } diff --git a/resources/qml/MessageInputWarning.qml b/resources/qml/MessageInputWarning.qml index be73df2a..4d5578b3 100644 --- a/resources/qml/MessageInputWarning.qml +++ b/resources/qml/MessageInputWarning.qml @@ -10,37 +10,35 @@ import im.nheko 1.0 Rectangle { id: warningRoot - required property string text property color bubbleColor: Nheko.theme.error + required property string text - implicitHeight: visible ? warningDisplay.implicitHeight + 4 * Nheko.paddingSmall : 0 - height: implicitHeight Layout.fillWidth: true color: palette.window // required to hide the timeline behind this warning + height: implicitHeight + implicitHeight: visible ? warningDisplay.implicitHeight + 4 * Nheko.paddingSmall : 0 Rectangle { id: warningRect - visible: warningRoot.visible - // TODO: Qt.alpha() would make more sense but it wasn't working... - color: Qt.rgba(bubbleColor.r, bubbleColor.g, bubbleColor.b, 0.3) - border.width: 1 - border.color: bubbleColor - radius: 3 anchors.fill: parent anchors.margins: visible ? Nheko.paddingSmall : 0 + border.color: bubbleColor + border.width: 1 + // TODO: Qt.alpha() would make more sense but it wasn't working... + color: Qt.rgba(bubbleColor.r, bubbleColor.g, bubbleColor.b, 0.3) + radius: 3 + visible: warningRoot.visible z: 3 Label { id: warningDisplay anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter anchors.margins: Nheko.paddingSmall + anchors.verticalCenter: parent.verticalCenter text: warningRoot.text textFormat: Text.PlainText } - } - } diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 158bc236..57bfe216 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -14,89 +14,269 @@ import QtQuick.Layouts 1.2 import QtQuick.Window 2.13 import im.nheko 1.0 - Item { id: chatRoot - property int padding: Nheko.paddingMedium property int availableWidth: width - + property int padding: Nheko.paddingMedium property string searchString: "" // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu Connections { function onHideMenu() { - messageContextMenu.close() - replyContextMenu.close() + messageContextMenu.close(); + replyContextMenu.close(); } + target: MainWindow } - ScrollBar { id: scrollbar - parent: chat.parent - anchors.top: parent.top - anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.top: parent.top + parent: chat.parent } ListView { id: chat - anchors.fill: parent - - property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - (scrollbar.interactive? scrollbar.width : 0) - + property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - (scrollbar.interactive ? scrollbar.width : 0) readonly property alias filteringInProgress: filteredTimeline.filteringInProgress - displayMarginBeginning: height / 2 - displayMarginEnd: height / 2 - - TimelineFilter { - id: filteredTimeline - source: room - filterByThread: room ? room.thread : "" - filterByContent: chatRoot.searchString - } - - model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room + ScrollBar.vertical: scrollbar + anchors.fill: parent + anchors.rightMargin: scrollbar.interactive ? scrollbar.width : 0 // reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107 //onModelChanged: if (room) room.sendReset() //reuseItems: true boundsBehavior: Flickable.StopAtBounds + displayMarginBeginning: height / 2 + displayMarginEnd: height / 2 + model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room //pixelAligned: true spacing: 2 verticalLayoutDirection: ListView.BottomToTop - onCountChanged: { - // Mark timeline as read - if (atYEnd && room) model.currentIndex = 0; + + delegate: Item { + id: wrapper + + required property string blurhash + required property string body + required property string callType + required property var day + required property string duration + required property int encryptionError + required property string eventId + required property string filename + required property string filesize + required property string formattedBody + required property int index + required property bool isEditable + required property bool isEdited + required property bool isEncrypted + required property bool isOnlyEmoji + required property bool isSender + required property bool isStateEvent + required property int notificationlevel + required property int originalWidth + property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) + property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) + property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) + required property double proportionalHeight + required property var reactions + required property int relatedEventCacheBuster + required property string replyTo + required property string roomName + required property string roomTopic + property bool scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) + required property int status + required property string threadId + required property string thumbnailUrl + required property var timestamp + required property int trustlevel + required property int type + required property string typeString + required property string url + required property string userId + required property string userName + + ListView.delayRemove: true + anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined + height: section.active ? section.height + timelinerow.height : timelinerow.height + width: chat.delegateMaxWidth + + Loader { + id: section + + property var day: wrapper.day + property bool isSender: wrapper.isSender + property bool isStateEvent: wrapper.isStateEvent + property int parentWidth: parent.width + property var previousMessageDay: wrapper.previousMessageDay + property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent + property string previousMessageUserId: wrapper.previousMessageUserId + property date timestamp: wrapper.timestamp + property string userId: wrapper.userId + property string userName: wrapper.userName + + active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent + //asynchronous: true + sourceComponent: sectionHeader + visible: status == Loader.Ready + z: 4 + } + TimelineRow { + id: timelinerow + + blurhash: wrapper.blurhash + body: wrapper.body + callType: wrapper.callType + duration: wrapper.duration + encryptionError: wrapper.encryptionError + eventId: chat.model, wrapper.eventId + filename: wrapper.filename + filesize: wrapper.filesize + formattedBody: wrapper.formattedBody + index: wrapper.index + isEditable: wrapper.isEditable + isEdited: wrapper.isEdited + isEncrypted: wrapper.isEncrypted + isOnlyEmoji: wrapper.isOnlyEmoji + isSender: wrapper.isSender + isStateEvent: wrapper.isStateEvent + notificationlevel: wrapper.notificationlevel + originalWidth: wrapper.originalWidth + proportionalHeight: wrapper.proportionalHeight + reactions: wrapper.reactions + relatedEventCacheBuster: wrapper.relatedEventCacheBuster + replyTo: wrapper.replyTo + roomName: wrapper.roomName + roomTopic: wrapper.roomTopic + status: wrapper.status + threadId: wrapper.threadId + thumbnailUrl: wrapper.thumbnailUrl + timestamp: wrapper.timestamp + trustlevel: wrapper.trustlevel + type: chat.model, wrapper.type + typeString: wrapper.typeString + url: wrapper.url + userId: wrapper.userId + userName: wrapper.userName + y: section.visible && section.active ? section.y + section.height : 0 + + background: Rectangle { + id: scrollHighlight + + color: palette.highlight + enabled: false + opacity: 0 + visible: true + z: 1 + + states: State { + name: "revealed" + when: wrapper.scrolledToThis + } + transitions: Transition { + from: "" + to: "revealed" + + SequentialAnimation { + PropertyAnimation { + duration: 500 + easing.type: Easing.InOutQuad + from: 0 + properties: "opacity" + target: scrollHighlight + to: 1 + } + PropertyAnimation { + duration: 500 + easing.type: Easing.InOutQuad + from: 1 + properties: "opacity" + target: scrollHighlight + to: 0 + } + ScriptAction { + script: room.eventShown() + } + } + } + } + + onHoveredChanged: { + if (!Settings.mobileMode && hovered) { + if (!messageActions.hovered) { + messageActions.attached = timelinerow; + messageActions.model = timelinerow; + } + } + } + } + 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: Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: Nheko.paddingLarge + // hacky, but works + height: loadingSpinner.height + 2 * Nheko.paddingLarge + visible: (room && room.paginationInProgress) || chat.filteringInProgress + + Spinner { + id: loadingSpinner + + anchors.centerIn: parent + anchors.margins: Nheko.paddingLarge + foreground: palette.mid + running: (room && room.paginationInProgress) || chat.filteringInProgress + z: 3 + } } - ScrollBar.vertical: scrollbar + Window.onActiveChanged: readTimer.running = Window.active + onCountChanged: { + // Mark timeline as read + if (atYEnd && room) + model.currentIndex = 0; + } - anchors.rightMargin: scrollbar.interactive? scrollbar.width : 0 + TimelineFilter { + id: filteredTimeline + filterByContent: chatRoot.searchString + filterByThread: room ? room.thread : "" + source: room + } Control { id: messageActions property Item attached: null - property alias model: row.model // use comma to update on scroll property var attachedPos: chat.contentY, attached ? chat.mapFromItem(attached, attached ? attached.width - width : 0, -height) : null - padding: Nheko.paddingSmall + property alias model: row.model hoverEnabled: true + padding: Nheko.paddingSmall visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered) x: attached ? attachedPos.x : 0 y: attached ? attachedPos.y + Nheko.paddingSmall : 0 z: 10 background: Rectangle { - color: palette.window border.color: palette.buttonText border.width: 1 + color: palette.window radius: padding } - contentItem: RowLayout { id: row @@ -111,174 +291,166 @@ Item { delegate: AbstractButton { id: button - required property string modelData - - property color highlightColor: palette.highlight property color buttonTextColor: palette.buttonText + property color highlightColor: palette.highlight + required property string modelData property bool showImage: modelData.startsWith("mxc://") //Layout.preferredHeight: fontMetrics.height Layout.alignment: Qt.AlignBottom - focusPolicy: Qt.NoFocus - width: showImage ? 16 : buttonText.implicitWidth height: showImage ? 16 : buttonText.implicitHeight - implicitWidth: showImage ? 16 : buttonText.implicitWidth implicitHeight: showImage ? 16 : buttonText.implicitHeight - - Label { - id: buttonText - - visible: !button.showImage - - anchors.centerIn: parent - padding: 0 - text: button.modelData - color: button.hovered ? button.highlightColor : button.buttonTextColor - font.family: Settings.emojiFont - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - Image { - id: buttonImg - - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: button.showImage ? (button.modelData.replace("mxc://", "image://MxcImage/") + "?scale") : "" - sourceSize.height: button.height - sourceSize.width: button.width - fillMode: Image.PreserveAspectFit - } - - CursorShape { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - } - - Ripple { - color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5) - } + implicitWidth: showImage ? 16 : buttonText.implicitWidth + width: showImage ? 16 : buttonText.implicitWidth onClicked: { room.input.reaction(row.model.eventId, modelData); TimelineManager.focusMessageInput(); } + + Label { + id: buttonText + + anchors.centerIn: parent + color: button.hovered ? button.highlightColor : button.buttonTextColor + font.family: Settings.emojiFont + horizontalAlignment: Text.AlignHCenter + padding: 0 + text: button.modelData + verticalAlignment: Text.AlignVCenter + visible: !button.showImage + } + Image { + id: buttonImg + + // Workaround, can't get icon.source working for now... + anchors.fill: parent + fillMode: Image.PreserveAspectFit + source: button.showImage ? (button.modelData.replace("mxc://", "image://MxcImage/") + "?scale") : "" + sourceSize.height: button.height + sourceSize.width: button.width + } + CursorShape { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + Ripple { + color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5) + } } - } - ImageButton { - visible: !!row.model && row.model.isEditable - buttonTextColor: palette.buttonText - width: 16 - hoverEnabled: true - image: ":/icons/icons/ui/edit.svg" - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Edit") + ToolTip.visible: hovered + buttonTextColor: palette.buttonText + hoverEnabled: true + image: ":/icons/icons/ui/edit.svg" + visible: !!row.model && row.model.isEditable + width: 16 + onClicked: { - if (row.model.isEditable) room.edit = row.model.eventId; + if (row.model.isEditable) + room.edit = row.model.eventId; } } - ImageButton { id: reactButton - visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false - width: 16 - hoverEnabled: true - image: ":/icons/icons/ui/smile-add.svg" - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("React") - onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, room.roomId, function(plaintext, markdown) { - var event_id = row.model ? row.model.eventId : ""; - room.input.reaction(event_id, plaintext); - TimelineManager.focusMessageInput(); - }) - } - - ImageButton { - visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false - width: 16 - hoverEnabled: true - image: (row.model && row.model.threadId) ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg" ToolTip.visible: hovered + hoverEnabled: true + image: ":/icons/icons/ui/smile-add.svg" + visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false + width: 16 + + onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, room.roomId, function (plaintext, markdown) { + var event_id = row.model ? row.model.eventId : ""; + room.input.reaction(event_id, plaintext); + TimelineManager.focusMessageInput(); + }) + } + ImageButton { ToolTip.delay: Nheko.tooltipDelay ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread") - onClicked: room.thread = (row.model.threadId || row.model.eventId) - } - - ImageButton { + ToolTip.visible: hovered + hoverEnabled: true + image: (row.model && row.model.threadId) ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg" visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false width: 16 - hoverEnabled: true - image: ":/icons/icons/ui/reply.svg" - ToolTip.visible: hovered + + onClicked: room.thread = (row.model.threadId || row.model.eventId) + } + ImageButton { ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Reply") + ToolTip.visible: hovered + hoverEnabled: true + image: ":/icons/icons/ui/reply.svg" + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false + width: 16 + onClicked: room.reply = row.model.eventId } - ImageButton { - visible: !!row.model && filteredTimeline.filterByContent - buttonTextColor: palette.buttonText - width: 16 - hoverEnabled: true - image: ":/icons/icons/ui/go-to.svg" - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Go to message") + ToolTip.visible: hovered + buttonTextColor: palette.buttonText + hoverEnabled: true + image: ":/icons/icons/ui/go-to.svg" + visible: !!row.model && filteredTimeline.filterByContent + width: 16 + onClicked: { topBar.searchString = ""; room.showEvent(row.model.eventId); } } - ImageButton { id: optionsButton - width: 16 - hoverEnabled: true - image: ":/icons/icons/ui/options.svg" - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Options") + ToolTip.visible: hovered + hoverEnabled: true + image: ":/icons/icons/ui/options.svg" + width: 16 + onClicked: messageContextMenu.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton) } - } - } - Shortcut { sequence: StandardKey.MoveToPreviousPage + onActivated: { chat.contentY = chat.contentY - chat.height * 0.9; chat.returnToBounds(); } } - Shortcut { sequence: StandardKey.MoveToNextPage + onActivated: { chat.contentY = chat.contentY + chat.height * 0.9; chat.returnToBounds(); } } - Shortcut { sequence: StandardKey.Cancel + onActivated: { - if(room.input.uploads.length > 0) + if (room.input.uploads.length > 0) room.input.declineUploads(); - else if(room.reply) + else if (room.reply) room.reply = undefined; else if (room.edit) room.edit = undefined; else - room.thread = undefined + room.thread = undefined; TimelineManager.focusMessageInput(); } } @@ -287,19 +459,20 @@ Item { // Better solution welcome. Shortcut { sequence: "Alt+Up" + onActivated: room.reply = room.indexToId(room.reply ? room.idToIndex(room.reply) + 1 : 0) } - Shortcut { sequence: "Alt+Down" + onActivated: { var idx = room.reply ? room.idToIndex(room.reply) - 1 : -1; room.reply = idx >= 0 ? room.indexToId(idx) : null; } } - Shortcut { sequence: "Alt+F" + onActivated: { if (room.reply) { var forwardMess = forwardCompleterComponent.createObject(timelineRoot); @@ -310,355 +483,157 @@ Item { } } } - Shortcut { sequence: "Ctrl+E" + onActivated: { room.edit = room.reply; } } - - Window.onActiveChanged: readTimer.running = Window.active - Timer { id: readTimer + interval: 1000 + // force current read index to update onTriggered: { if (room) - room.setCurrentIndex(room.currentIndex); - + room.setCurrentIndex(room.currentIndex); } - interval: 1000 } - Component { id: sectionHeader Column { - topPadding: userName_.visible? 4: 0 - bottomPadding: Settings.bubbles? (isSender && previousMessageDay == day? 0 : 2) : 3 + bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3 + height: ((previousMessageDay !== day) ? dateBubble.height : 0) + (isStateEvent ? 0 : userName.height + 8) spacing: 8 + topPadding: userName_.visible ? 4 : 0 visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent) width: parentWidth - height: ((previousMessageDay !== day) ? dateBubble.height : 0) + (isStateEvent? 0 : userName.height +8 ) Label { id: dateBubble anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - visible: room && previousMessageDay !== day - text: room ? room.formatDateSeparator(timestamp) : "" color: palette.text height: Math.round(fontMetrics.height * 1.4) - width: contentWidth * 1.2 horizontalAlignment: Text.AlignHCenter + text: room ? room.formatDateSeparator(timestamp) : "" verticalAlignment: Text.AlignVCenter + visible: room && previousMessageDay !== day + width: contentWidth * 1.2 background: Rectangle { - radius: parent.height / 2 color: palette.window + radius: parent.height / 2 } - } - Row { + id: userInfo + + property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width + height: userName_.height spacing: 8 visible: !isStateEvent && (!isSender || !Settings.bubbles) - id: userInfo Avatar { id: messageUserAvatar - width: Nheko.avatarSize * (Settings.smallAvatars? 0.5 : 1) - height: Nheko.avatarSize * (Settings.smallAvatars? 0.5 : 1) - url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/") - displayName: userName - userid: userId - onClicked: room.openUserProfile(userId) - ToolTip.visible: messageUserAvatar.hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: userid - } + ToolTip.visible: messageUserAvatar.hovered + displayName: userName + height: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1) + url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/") + userid: userId + width: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1) + onClicked: room.openUserProfile(userId) + } Connections { function onRoomAvatarUrlChanged() { messageUserAvatar.url = room.avatarUrl(userId).replace("mxc://", "image://MxcImage/"); } - function onScrollToIndex(index) { chat.positionViewAtIndex(index, ListView.Center); } target: room } - property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width AbstractButton { id: userNameButton - contentItem: ElidedLabel { - id: userName_ - fullText: userName - color: TimelineManager.userColor(userId, palette.base) - textFormat: Text.RichText - elideWidth: Math.min(userInfo.remainingWidth-Math.min(statusMsg.implicitWidth,userInfo.remainingWidth/3), userName_.fullTextWidth) - } - ToolTip.visible: hovered + ToolTip.delay: Nheko.tooltipDelay ToolTip.text: userId - onClicked: room.openUserProfile(userId) + ToolTip.visible: hovered leftInset: 0 - rightInset: 0 leftPadding: 0 + rightInset: 0 rightPadding: 0 + contentItem: ElidedLabel { + id: userName_ + + color: TimelineManager.userColor(userId, palette.base) + elideWidth: Math.min(userInfo.remainingWidth - Math.min(statusMsg.implicitWidth, userInfo.remainingWidth / 3), userName_.fullTextWidth) + fullText: userName + textFormat: Text.RichText + } + + onClicked: room.openUserProfile(userId) + CursorShape { anchors.fill: parent cursorShape: Qt.PointingHandCursor } - } - Label { id: statusMsg - anchors.baseline: userNameButton.baseline - color: palette.buttonText - text: userStatus.replace(/\n/g, " ") - textFormat: Text.PlainText - elide: Text.ElideRight - width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing) - font.italic: true - font.pointSize: Math.floor(fontMetrics.font.pointSize * 0.8) + + property string userStatus: Presence.userStatus(userId) + + ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("%1's status message").arg(userName) ToolTip.visible: statusMsgHoverHandler.hovered - ToolTip.delay: Nheko.tooltipDelay + anchors.baseline: userNameButton.baseline + color: palette.buttonText + elide: Text.ElideRight + font.italic: true + font.pointSize: Math.floor(fontMetrics.font.pointSize * 0.8) + text: userStatus.replace(/\n/g, " ") + textFormat: Text.PlainText + width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing) HoverHandler { id: statusMsgHoverHandler - } - property string userStatus: Presence.userStatus(userId) + } Connections { - target: Presence function onPresenceChanged(id) { - if (id == userId) statusMsg.userStatus = Presence.userStatus(userId); + if (id == userId) + statusMsg.userStatus = Presence.userStatus(userId); } - } - } - } - - } - - } - - delegate: Item { - id: wrapper - - required property double proportionalHeight - required property int type - required property string typeString - required property int originalWidth - required property string blurhash - required property string body - required property string formattedBody - required property string eventId - required property string filename - required property string filesize - required property string url - required property string thumbnailUrl - required property string duration - required property bool isOnlyEmoji - required property bool isSender - required property bool isEncrypted - required property bool isEditable - required property bool isEdited - required property bool isStateEvent - property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index+1, Room.IsStateEvent) - required property string replyTo - required property string threadId - required property string userId - required property string roomTopic - required property string roomName - required property string callType - required property var reactions - required property int trustlevel - required property int notificationlevel - required property int encryptionError - required property var timestamp - required property int status - required property int index - required property int relatedEventCacheBuster - required property var day - property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index+1, Room.UserId) - property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index+1, Room.Day) - required property string userName - property bool scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) - - anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - width: chat.delegateMaxWidth - height: section.active ? section.height + timelinerow.height : timelinerow.height - ListView.delayRemove: true - - Loader { - id: section - - property int parentWidth: parent.width - property string userId: wrapper.userId - property string previousMessageUserId: wrapper.previousMessageUserId - property var day: wrapper.day - property var previousMessageDay: wrapper.previousMessageDay - property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent - property bool isStateEvent: wrapper.isStateEvent - property bool isSender: wrapper.isSender - property string userName: wrapper.userName - property date timestamp: wrapper.timestamp - - z: 4 - active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent - //asynchronous: true - sourceComponent: sectionHeader - visible: status == Loader.Ready - } - - TimelineRow { - id: timelinerow - - proportionalHeight: wrapper.proportionalHeight - type: chat.model, wrapper.type - typeString: wrapper.typeString - originalWidth: wrapper.originalWidth - blurhash: wrapper.blurhash - body: wrapper.body - formattedBody: wrapper.formattedBody - eventId: chat.model, wrapper.eventId - filename: wrapper.filename - filesize: wrapper.filesize - url: wrapper.url - thumbnailUrl: wrapper.thumbnailUrl - duration: wrapper.duration - isOnlyEmoji: wrapper.isOnlyEmoji - isSender: wrapper.isSender - isEncrypted: wrapper.isEncrypted - isEditable: wrapper.isEditable - isEdited: wrapper.isEdited - isStateEvent: wrapper.isStateEvent - replyTo: wrapper.replyTo - threadId: wrapper.threadId - userId: wrapper.userId - userName: wrapper.userName - roomTopic: wrapper.roomTopic - roomName: wrapper.roomName - callType: wrapper.callType - reactions: wrapper.reactions - trustlevel: wrapper.trustlevel - notificationlevel: wrapper.notificationlevel - encryptionError: wrapper.encryptionError - timestamp: wrapper.timestamp - status: wrapper.status - index: wrapper.index - relatedEventCacheBuster: wrapper.relatedEventCacheBuster - y: section.visible && section.active ? section.y + section.height : 0 - - onHoveredChanged: { - if (!Settings.mobileMode && hovered) { - if (!messageActions.hovered) { - messageActions.attached = timelinerow; - messageActions.model = timelinerow; + target: Presence } } } - background: Rectangle { - id: scrollHighlight - - opacity: 0 - visible: true - z: 1 - enabled: false - color: palette.highlight - - states: State { - name: "revealed" - when: wrapper.scrolledToThis - } - - transitions: Transition { - from: "" - to: "revealed" - - SequentialAnimation { - PropertyAnimation { - target: scrollHighlight - properties: "opacity" - easing.type: Easing.InOutQuad - from: 0 - to: 1 - duration: 500 - } - - PropertyAnimation { - target: scrollHighlight - properties: "opacity" - easing.type: Easing.InOutQuad - from: 1 - to: 0 - duration: 500 - } - - ScriptAction { - script: room.eventShown() - } - - } - - } - - } } - - 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: Item { - anchors.horizontalCenter: parent.horizontalCenter - anchors.margins: Nheko.paddingLarge - visible: (room && room.paginationInProgress) || chat.filteringInProgress - // hacky, but works - height: loadingSpinner.height + 2 * Nheko.paddingLarge - - Spinner { - id: loadingSpinner - - anchors.centerIn: parent - anchors.margins: Nheko.paddingLarge - running: (room && room.paginationInProgress) || chat.filteringInProgress - foreground: palette.mid - z: 3 - } - } } - Platform.Menu { id: messageContextMenu property string eventId - property string threadId + property int eventType + property bool isEditable + property bool isEncrypted + property bool isSender property string link property string text - property int eventType - property bool isEncrypted - property bool isEditable - property bool isSender + property string threadId function show(eventId_, threadId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) { eventId = eventId_; @@ -668,104 +643,106 @@ Item { isEditable = isEditable_; isSender = isSender_; if (text_) - text = text_; + text = text_; else - text = ""; + text = ""; if (link_) - link = link_; + link = link_; else - link = ""; + link = ""; if (showAt_) - open(showAt_); + open(showAt_); else - open(); + open(); } Component { id: removeReason + InputDialog { id: removeReasonDialog property string eventId - title: qsTr("Reason for removal") prompt: qsTr("Enter reason for removal or hit enter for no reason:") - onAccepted: function(text) { + title: qsTr("Reason for removal") + + onAccepted: function (text) { room.redactEvent(eventId, text); } } } - Platform.MenuItem { - visible: filteredTimeline.filterByContent - enabled: visible - text: qsTr("Go to &message") - onTriggered: function() { + enabled: visible + text: qsTr("Go to &message") + visible: filteredTimeline.filterByContent + + onTriggered: function () { topBar.searchString = ""; room.showEvent(messageContextMenu.eventId); } - } - + } Platform.MenuItem { - visible: messageContextMenu.text enabled: visible text: qsTr("&Copy") + visible: messageContextMenu.text + onTriggered: Clipboard.text = messageContextMenu.text } - Platform.MenuItem { - visible: messageContextMenu.link enabled: visible text: qsTr("Copy &link location") + visible: messageContextMenu.link + onTriggered: Clipboard.text = messageContextMenu.link } - Platform.MenuItem { id: reactionOption - visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false text: qsTr("Re&act") - onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function(plaintext, markdown) { - room.input.reaction(messageContextMenu.eventId, plaintext); - TimelineManager.focusMessageInput(); - }) - } + visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false + onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) { + room.input.reaction(messageContextMenu.eventId, plaintext); + TimelineManager.focusMessageInput(); + }) + } Platform.MenuItem { - visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false text: qsTr("Repl&y") + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false + onTriggered: room.reply = (messageContextMenu.eventId) } - Platform.MenuItem { - visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) enabled: visible text: qsTr("&Edit") + visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) + onTriggered: room.edit = (messageContextMenu.eventId) } - Platform.MenuItem { - visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) enabled: visible text: qsTr("&Thread") + visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) + onTriggered: room.thread = (messageContextMenu.threadId || messageContextMenu.eventId) } - Platform.MenuItem { - visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false) enabled: visible text: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? qsTr("Un&pin") : qsTr("&Pin") + visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false) + onTriggered: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? room.unpin(messageContextMenu.eventId) : room.pin(messageContextMenu.eventId) } - Platform.MenuItem { text: qsTr("&Read receipts") + onTriggered: room.showReadReceipts(messageContextMenu.eventId) } - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage text: qsTr("&Forward") + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage + onTriggered: { var forwardMess = forwardCompleterComponent.createObject(timelineRoot); forwardMess.setMessageEventId(messageContextMenu.eventId); @@ -773,28 +750,27 @@ Item { timelineRoot.destroyOnClose(forwardMess); } } - Platform.MenuItem { text: qsTr("&Mark as read") } - Platform.MenuItem { text: qsTr("View raw message") + onTriggered: room.viewRawMessage(messageContextMenu.eventId) } - Platform.MenuItem { - // TODO(Nico): Fix this still being iterated over, when using keyboard to select options - visible: messageContextMenu.isEncrypted enabled: visible text: qsTr("View decrypted raw message") + // TODO(Nico): Fix this still being iterated over, when using keyboard to select options + visible: messageContextMenu.isEncrypted + onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId) } - Platform.MenuItem { - visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender text: qsTr("Remo&ve message") - onTriggered: function() { + visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender + + onTriggered: function () { var dialog = removeReason.createObject(timelineRoot); dialog.eventId = messageContextMenu.eventId; dialog.show(); @@ -802,44 +778,40 @@ Item { timelineRoot.destroyOnClose(dialog); } } - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker enabled: visible text: qsTr("&Save as") + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + onTriggered: room.saveMedia(messageContextMenu.eventId) } - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker enabled: visible text: qsTr("&Open in external program") + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + onTriggered: room.openMedia(messageContextMenu.eventId) } - Platform.MenuItem { - visible: messageContextMenu.eventId enabled: visible text: qsTr("Copy link to eve&nt") + visible: messageContextMenu.eventId + onTriggered: room.copyLinkToEvent(messageContextMenu.eventId) } - } - Component { id: forwardCompleterComponent ForwardCompleter { } - } - Platform.Menu { id: replyContextMenu - property string text - property string link property string eventId + property string link + property string text function show(text_, link_, eventId_) { text = text_; @@ -849,85 +821,100 @@ Item { } Platform.MenuItem { - visible: replyContextMenu.text enabled: visible text: qsTr("&Copy") + visible: replyContextMenu.text + onTriggered: Clipboard.text = replyContextMenu.text } - Platform.MenuItem { - visible: replyContextMenu.link enabled: visible text: qsTr("Copy &link location") + visible: replyContextMenu.link + onTriggered: Clipboard.text = replyContextMenu.link } - Platform.MenuItem { - visible: true enabled: visible text: qsTr("&Go to quoted message") + visible: true + onTriggered: room.showEvent(replyContextMenu.eventId) } - } RoundButton { id: toEndButton - anchors { - bottom: parent.bottom - right: scrollbar.left - bottomMargin: Nheko.paddingMedium+(fullWidth-width)/2 - rightMargin: Nheko.paddingMedium+(fullWidth-width)/2 - } + property int fullWidth: 40 - width: 0 - height: width - radius: width/2 - onClicked: function() { chat.positionViewAtBeginning(); TimelineManager.focusMessageInput(); } + flat: true + height: width hoverEnabled: true + radius: width / 2 + width: 0 background: Rectangle { - color: toEndButton.down ? palette.highlight : palette.button - opacity: enabled ? 1 : 0.3 border.color: toEndButton.hovered ? palette.highlight : palette.buttonText border.width: 1 + color: toEndButton.down ? palette.highlight : palette.button + opacity: enabled ? 1 : 0.3 radius: toEndButton.radius } - states: [ State { name: "" - PropertyChanges { target: toEndButton; width: 0 } + + PropertyChanges { + target: toEndButton + width: 0 + } }, State { name: "shown" when: !chat.atYEnd - PropertyChanges { target: toEndButton; width: toEndButton.fullWidth } + + PropertyChanges { + target: toEndButton + width: toEndButton.fullWidth + } } ] - - Image { - id: buttonImg - anchors.fill: parent - anchors.margins: Nheko.paddingMedium - source: "image://colorimage/:/icons/icons/ui/download.svg?" + (toEndButton.down ? palette.highlightedText : palette.buttonText) - fillMode: Image.PreserveAspectFit - } - transitions: Transition { from: "" - to: "shown" reversible: true + to: "shown" SequentialAnimation { - PauseAnimation { duration: 500 } + PauseAnimation { + duration: 500 + } PropertyAnimation { - target: toEndButton - properties: "width" - easing.type: Easing.InOutQuad duration: 200 + easing.type: Easing.InOutQuad + properties: "width" + target: toEndButton } } } + + onClicked: function () { + chat.positionViewAtBeginning(); + TimelineManager.focusMessageInput(); + } + + anchors { + bottom: parent.bottom + bottomMargin: Nheko.paddingMedium + (fullWidth - width) / 2 + right: scrollbar.left + rightMargin: Nheko.paddingMedium + (fullWidth - width) / 2 + } + Image { + id: buttonImg + + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + fillMode: Image.PreserveAspectFit + source: "image://colorimage/:/icons/icons/ui/download.svg?" + (toEndButton.down ? palette.highlightedText : palette.buttonText) + } } } diff --git a/resources/qml/PrivacyScreen.qml b/resources/qml/PrivacyScreen.qml index da196667..a3539df7 100644 --- a/resources/qml/PrivacyScreen.qml +++ b/resources/qml/PrivacyScreen.qml @@ -11,9 +11,8 @@ Item { id: privacyScreen readonly property bool active: Settings.privacyScreen && screenSaver.state === "Visible" - property var timelineRoot property int screenTimeout - + property var timelineRoot required property var windowTarget Connections { @@ -24,29 +23,28 @@ Item { } else { if (timelineRoot.visible) screenSaverTimer.start(); - } } target: windowTarget } - Timer { id: screenSaverTimer interval: screenTimeout * 1000 running: !windowTarget.active + onTriggered: { screenSaver.state = "Visible"; } } - Item { id: screenSaver - state: "Invisible" anchors.fill: parent + state: "Invisible" visible: false + states: [ State { name: "Visible" @@ -55,20 +53,18 @@ Item { target: screenSaver visible: true } - PropertyChanges { - target: screenSaver opacity: 1 + target: screenSaver } }, State { name: "Invisible" PropertyChanges { - target: screenSaver opacity: 0 + target: screenSaver } - PropertyChanges { target: screenSaver visible: false @@ -78,39 +74,33 @@ Item { transitions: [ Transition { from: "Invisible" - to: "Visible" reversible: true + to: "Visible" SequentialAnimation { NumberAnimation { - target: screenSaver - property: "visible" duration: 0 - } - - NumberAnimation { + property: "visible" target: screenSaver - property: "opacity" + } + NumberAnimation { duration: 300 easing.type: Easing.Linear + property: "opacity" + target: screenSaver } - } - } ] MultiEffect { id: blur - blurEnabled: true - anchors.fill: parent - source: timelineRoot blur: 1.0 + blurEnabled: true blurMax: 32 + source: timelineRoot } - } - } diff --git a/resources/qml/QuickSwitcher.qml b/resources/qml/QuickSwitcher.qml index 5878b391..9ccefdec 100644 --- a/resources/qml/QuickSwitcher.qml +++ b/resources/qml/QuickSwitcher.qml @@ -11,33 +11,36 @@ Popup { id: quickSwitcher property int textHeight: Math.round(Qt.application.font.pixelSize * 2.4) + property int textMargin: Nheko.paddingSmall background: null - width: Math.min(Math.max(Math.round(parent.width / 2),450),parent.width) // limiting width to parent.width/2 can be a bit narrow + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + modal: true + parent: Overlay.overlay + width: Math.min(Math.max(Math.round(parent.width / 2), 450), parent.width) // limiting width to parent.width/2 can be a bit narrow x: Math.round(parent.width / 2 - contentWidth / 2) y: Math.round(parent.height / 4) - modal: true - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - parent: Overlay.overlay + + Overlay.modal: Rectangle { + color: "#aa1E1E1E" + } + + onClosed: TimelineManager.focusMessageInput() onOpened: { roomTextInput.forceActiveFocus(); } - onClosed: TimelineManager.focusMessageInput() - property int textMargin: Nheko.paddingSmall - Column{ + Column { anchors.fill: parent spacing: 1 MatrixTextField { id: roomTextInput - width: parent.width - font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6) color: palette.text - onTextEdited: { - completerPopup.completer.searchString = text; - } + font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6) + width: parent.width + Keys.onPressed: { if (event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) { event.accepted = true; @@ -45,49 +48,43 @@ Popup { } else if (event.key == Qt.Key_Down || event.key == Qt.Key_Tab) { event.accepted = true; if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier)) - completerPopup.up(); + completerPopup.up(); else - completerPopup.down(); + completerPopup.down(); } else if (event.matches(StandardKey.InsertParagraphSeparator)) { completerPopup.finishCompletion(); event.accepted = true; } } + onTextEdited: { + completerPopup.completer.searchString = text; + } } - Completer { id: completerPopup - visible: roomTextInput.text.length > 0 - width: parent.width - completerName: "room" - bottomToTop: false - fullWidth: true avatarHeight: quickSwitcher.textHeight avatarWidth: quickSwitcher.textHeight + bottomToTop: false centerRowContent: false + completerName: "room" + fullWidth: true rowMargin: Math.round(quickSwitcher.textMargin / 2) rowSpacing: quickSwitcher.textMargin + visible: roomTextInput.text.length > 0 + width: parent.width } } - Connections { function onCompletionSelected(id) { Rooms.setCurrentRoom(id); quickSwitcher.close(); } - function onCountChanged() { if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count)) - completerPopup.currentIndex = 0; - + completerPopup.currentIndex = 0; } target: completerPopup } - - Overlay.modal: Rectangle { - color: "#aa1E1E1E" - } - } diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml index caee708e..5ab58beb 100644 --- a/resources/qml/Reactions.qml +++ b/resources/qml/Reactions.qml @@ -11,10 +11,11 @@ import im.nheko 1.0 Flow { id: reactionFlow + property string eventId + // lower-contrast colors to avoid distracting from text & to enhance hover effect property color gentleHighlight: Qt.hsla(palette.highlight.hslHue, palette.highlight.hslSaturation, palette.highlight.hslLightness, 0.8) property color gentleText: Qt.hsla(palette.text.hslHue, palette.text.hslSaturation, palette.text.hslLightness, 0.6) - property string eventId property alias reactions: repeater.model spacing: 4 @@ -25,40 +26,39 @@ Flow { delegate: AbstractButton { id: reaction - hoverEnabled: true - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay - onClicked: { - console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); - room.input.reaction(reactionFlow.eventId, modelData.key); - } - Component.onCompleted: { - ToolTip.text = Qt.binding(function() { - if (textMetrics.elidedText === textMetrics.text) { - return modelData.users; - } - return modelData.displayKey + "\n" + modelData.users; - }) - } - leftPadding: textMetrics.height / 2 - rightPadding: textMetrics.height / 2 + ToolTip.visible: hovered + hoverEnabled: true + leftPadding: textMetrics.height / 2 + rightPadding: textMetrics.height / 2 + background: Rectangle { + anchors.centerIn: parent + border.color: reaction.hovered ? palette.text : gentleText + border.width: 1 + color: reaction.hovered ? palette.highlight : (modelData.selfReactedEvent !== '' ? gentleHighlight : palette.window) + implicitHeight: reaction.implicitHeight + implicitWidth: reaction.implicitWidth + radius: reaction.height / 2 + } contentItem: Row { spacing: textMetrics.height / 4 TextMetrics { id: textMetrics - font.family: Settings.emojiFont elide: Text.ElideRight elideWidth: 150 + font.family: Settings.emojiFont text: modelData.displayKey } - Text { id: reactionText anchors.baseline: reactionCounter.baseline + color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText : palette.text + font.family: Settings.emojiFont + maximumLineCount: 1 text: { // When an emoji font is selected that doesn't have …, it is dropped from elidedText. So we add it back. if (textMetrics.elidedText !== modelData.displayKey) { @@ -68,51 +68,45 @@ Flow { } return textMetrics.elidedText; } - font.family: Settings.emojiFont - color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText: palette.text - maximumLineCount: 1 visible: !modelData.key.startsWith("mxc://") } Image { anchors.verticalCenter: divider.verticalCenter + fillMode: Image.PreserveAspectFit height: textMetrics.height - width: textMetrics.height source: modelData.key.startsWith("mxc://") ? (modelData.key.replace("mxc://", "image://MxcImage/") + "?scale") : "" visible: modelData.key.startsWith("mxc://") - fillMode: Image.PreserveAspectFit + width: textMetrics.height } - Rectangle { id: divider + color: reaction.hovered ? palette.text : gentleText height: Math.floor(reactionCounter.implicitHeight * 1.4) width: 1 - color: reaction.hovered ? palette.text: gentleText } - Text { id: reactionCounter anchors.verticalCenter: divider.verticalCenter - text: modelData.count + color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText : palette.windowText font: reaction.font - color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText: palette.windowText + text: modelData.count } - } - background: Rectangle { - anchors.centerIn: parent - implicitWidth: reaction.implicitWidth - implicitHeight: reaction.implicitHeight - border.color: reaction.hovered ? palette.text: gentleText - color: reaction.hovered ? palette.highlight : (modelData.selfReactedEvent !== '' ? gentleHighlight : palette.window) - border.width: 1 - radius: reaction.height / 2 + Component.onCompleted: { + ToolTip.text = Qt.binding(function () { + if (textMetrics.elidedText === textMetrics.text) { + return modelData.users; + } + return modelData.displayKey + "\n" + modelData.users; + }); + } + onClicked: { + console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); + room.input.reaction(reactionFlow.eventId, modelData.key); } - } - } - } diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml index 6fceb4e5..ce24297c 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -12,91 +12,89 @@ Rectangle { id: replyPopup Layout.fillWidth: true - visible: room && (room.reply || room.edit || room.thread) + color: palette.window // Height of child, plus margins, plus border implicitHeight: (room && room.reply ? replyPreview.height : Math.max(closeEditButton.height, closeThreadButton.height)) + Nheko.paddingSmall - color: palette.window + visible: room && (room.reply || room.edit || room.thread) z: 3 Reply { id: replyPreview - property var modelData: room ? room.getDump(room.reply, room.id) : { - } + property var modelData: room ? room.getDump(room.reply, room.id) : {} - visible: room && room.reply anchors.left: parent.left - anchors.leftMargin: replyPopup.width < 450? Nheko.paddingSmall : (CallManager.callsSupported? 2*(22+16) : 1*(22+16)) + anchors.leftMargin: replyPopup.width < 450 ? Nheko.paddingSmall : (CallManager.callsSupported ? 2 * (22 + 16) : 1 * (22 + 16)) anchors.right: parent.right - anchors.rightMargin: replyPopup.width < 450? 2*(22+16) : 3*(22+16) + anchors.rightMargin: replyPopup.width < 450 ? 2 * (22 + 16) : 3 * (22 + 16) anchors.top: parent.top anchors.topMargin: Nheko.paddingSmall - userColor: TimelineManager.userColor(modelData.userId, palette.window) blurhash: modelData.blurhash ?? "" body: modelData.body ?? "" - formattedBody: modelData.formattedBody ?? "" + encryptionError: modelData.encryptionError ?? 0 eventId: modelData.eventId ?? "" filename: modelData.filename ?? "" filesize: modelData.filesize ?? "" + formattedBody: modelData.formattedBody ?? "" + isOnlyEmoji: modelData.isOnlyEmoji ?? false + originalWidth: modelData.originalWidth ?? 0 proportionalHeight: modelData.proportionalHeight ?? 1 type: modelData.type ?? MtxEvent.UnknownMessage typeString: modelData.typeString ?? "" url: modelData.url ?? "" - originalWidth: modelData.originalWidth ?? 0 - isOnlyEmoji: modelData.isOnlyEmoji ?? false + userColor: TimelineManager.userColor(modelData.userId, palette.window) userId: modelData.userId ?? "" userName: modelData.userName ?? "" - encryptionError: modelData.encryptionError ?? 0 + visible: room && room.reply width: parent.width } - ImageButton { id: closeReplyButton - visible: room && room.reply + ToolTip.text: qsTr("Close") + ToolTip.visible: closeReplyButton.hovered + anchors.margins: Nheko.paddingSmall anchors.right: replyPreview.right anchors.top: replyPreview.top - anchors.margins: Nheko.paddingSmall - hoverEnabled: true - width: 16 height: 16 + hoverEnabled: true image: ":/icons/icons/ui/dismiss.svg" - ToolTip.visible: closeReplyButton.hovered - ToolTip.text: qsTr("Close") + visible: room && room.reply + width: 16 + onClicked: room.reply = undefined } - ImageButton { id: closeEditButton - visible: room && room.edit - anchors.right: closeThreadButton.left + ToolTip.text: qsTr("Cancel Edit") + ToolTip.visible: closeEditButton.hovered anchors.margins: 8 + anchors.right: closeThreadButton.left anchors.top: parent.top + height: 22 hoverEnabled: true image: ":/icons/icons/ui/dismiss_edit.svg" + visible: room && room.edit width: 22 - height: 22 - ToolTip.visible: closeEditButton.hovered - ToolTip.text: qsTr("Cancel Edit") + onClicked: room.edit = undefined } - ImageButton { id: closeThreadButton - visible: room && room.thread - anchors.right: parent.right - anchors.margins: 8 - anchors.top: parent.top - hoverEnabled: true - buttonTextColor: room ? TimelineManager.userColor(room.thread, palette.base) : palette.buttonText - image: ":/icons/icons/ui/dismiss_thread.svg" - width: 22 - height: 22 - ToolTip.visible: closeThreadButton.hovered ToolTip.text: qsTr("Cancel Thread") + ToolTip.visible: closeThreadButton.hovered + anchors.margins: 8 + anchors.right: parent.right + anchors.top: parent.top + buttonTextColor: room ? TimelineManager.userColor(room.thread, palette.base) : palette.buttonText + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/dismiss_thread.svg" + visible: room && room.thread + width: 22 + onClicked: room.thread = undefined } - } diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 851608b6..b41696e0 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -18,448 +18,142 @@ Page { property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3) property bool collapsed: false - // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu - Connections { - function onHideMenu() { - userInfoMenu.close() - roomContextMenu.close() - } - target: MainWindow - } - - Component { - id: roomDirectoryComponent - - RoomDirectory { - } - - } - - Component { - id: createRoomComponent - - CreateRoom { - } - } - - Component { - id: createDirectComponent - - CreateDirect { - } - } - - ListView { - id: roomlist - - anchors.left: parent.left - anchors.right: parent.right - height: parent.height - model: Rooms - //reuseItems: true - - ScrollBar.vertical: ScrollBar { - id: scrollbar - parent: !collapsed && Settings.scrollbarsInRoomlist ? roomlist : null - } - - Connections { - function onCurrentRoomChanged() { - if (Rooms.currentRoom) - roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain); - - } - - target: Rooms - } - - Component { - id: roomWindowComponent - - ApplicationWindow { - id: roomWindowW - - property var room: null - property var roomPreview: null - - Component.onCompleted: { - MainWindow.addPerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW); - Nheko.setTransientParent(roomWindowW, null); - } - Component.onDestruction: MainWindow.removePerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW) - - height: 650 - width: 420 - minimumWidth: 150 - minimumHeight: 150 - color: palette.window - title: room.plainRoomName - //flags: Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowTitleHint - - Shortcut { - sequence: StandardKey.Cancel - onActivated: roomWindowW.close() - } - - TimelineView { - id: timeline - - privacyScreen: privacyScreen - anchors.fill: parent - room: roomWindowW.room - roomPreview: roomWindowW.roomPreview.roomid ? roomWindowW.roomPreview : null - } - - PrivacyScreen { - id: privacyScreen - - anchors.fill: parent - visible: Settings.privacyScreen - screenTimeout: Settings.privacyScreenTimeout - timelineRoot: timeline - windowTarget: roomWindowW - } - - onActiveChanged: { room.lastReadIdOnWindowFocus(); } - } - - } - - - Component { - id: nestedSpaceMenuLevel - - SpaceMenuLevel { - roomid: roomContextMenu.roomid - childMenu: rootSpaceMenu.childMenu - } - } - - - Platform.Menu { - id: roomContextMenu - - property string roomid - property var tags - - function show(roomid_, tags_) { - roomid = roomid_; - tags = tags_; - open(); - } - - InputDialog { - id: newTag - - title: qsTr("New tag") - prompt: qsTr("Enter the tag you want to use:") - onAccepted: function(text) { - Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true); - } - } - - Platform.MenuItem { - text: qsTr("Open separately") - onTriggered: { - var roomWindow = roomWindowComponent.createObject(null, { - "room": Rooms.getRoomById(roomContextMenu.roomid), - "roomPreview": Rooms.getRoomPreviewById(roomContextMenu.roomid) - }); - roomWindow.showNormal(); - destroyOnClose(roomWindow); - } - } - - Platform.MenuItem { - text: qsTr("Room settings") - onTriggered: TimelineManager.openRoomSettings(roomContextMenu.roomid) - } - - Platform.MenuItem { - text: qsTr("Leave room") - onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid) - } - - Platform.MenuItem { - text: qsTr("Copy room link") - onTriggered: Rooms.copyLink(roomContextMenu.roomid) - } - - Platform.Menu { - id: tagsMenu - title: qsTr("Tag room as:") - - Instantiator { - model: Communities.tagsWithDefault - onObjectAdded: (index, object) => tagsMenu.insertItem(index, object) - onObjectRemoved: (index, object) => tagsMenu.removeItem(object) - - delegate: Platform.MenuItem { - property string t: modelData - - text: { - switch (t) { - case "m.favourite": - return qsTr("Favourite"); - case "m.lowpriority": - return qsTr("Low priority"); - case "m.server_notice": - return qsTr("Server notice"); - default: - return t.substring(2); - } - } - checkable: true - checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t) - onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked) - } - - } - - Platform.MenuItem { - text: qsTr("Create new tag...") - onTriggered: newTag.show() - } - } - - SpaceMenuLevel { - id: rootSpaceMenu - - roomid: roomContextMenu.roomid - position: -1 - title: qsTr("Add or remove from community...") - childMenu: nestedSpaceMenuLevel - } - } - - delegate: ItemDelegate { - id: roomItem - - property color backgroundColor: palette.window - property color importantText: palette.text - property color unimportantText: palette.buttonText - property color bubbleBackground: palette.highlight - property color bubbleText: palette.highlightedText - required property string roomName - required property string roomId - required property string avatarUrl - required property string time - required property string lastMessage - required property var tags - required property bool isInvite - required property bool isSpace - required property int notificationCount - required property bool hasLoudNotification - required property bool hasUnreadMessages - required property bool isDirect - required property string directChatOtherUserId - - Ripple { - color: Qt.rgba(palette.dark.r, palette.dark.g, palette.dark.b, 0.5) - } - - height: avatarSize + 2 * Nheko.paddingMedium - width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0) - state: "normal" - ToolTip.visible: hovered && collapsed - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: roomName - onClicked: { - console.log("tapped " + roomId); - - if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId) - Rooms.setCurrentRoom(roomId); - else - Rooms.resetCurrentRoom(); - } - onPressAndHold: { - if (!isInvite) - roomContextMenu.show(roomId, tags); - - } - states: [ - State { - name: "highlight" - when: roomItem.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId) - - PropertyChanges { - target: roomItem - backgroundColor: palette.dark - importantText: palette.brightText - unimportantText: palette.brightText - bubbleBackground: palette.highlight - bubbleText: palette.highlightedText - } - - }, - State { - name: "selected" - when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId - - PropertyChanges { - target: roomItem - backgroundColor: palette.highlight - importantText: palette.highlightedText - unimportantText: palette.highlightedText - bubbleBackground: palette.highlightedText - bubbleText: palette.highlight - } - - } - ] - - // NOTE(Nico): We want to prevent the touch areas from overlapping. For some reason we need to add 1px of padding for that... - Item { - anchors.fill: parent - anchors.margins: 1 - - TapHandler { - acceptedButtons: Qt.RightButton - onSingleTapped: { - if (!TimelineManager.isInvite) - roomContextMenu.show(roomId, tags); - - } - gesturePolicy: TapHandler.ReleaseWithinBounds - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad - } - - } - - RowLayout { - spacing: Nheko.paddingMedium - anchors.fill: parent - anchors.margins: Nheko.paddingMedium - - Avatar { - id: avatar - - enabled: false - Layout.alignment: Qt.AlignVCenter - height: avatarSize - width: avatarSize - url: avatarUrl.replace("mxc://", "image://MxcImage/") - displayName: roomName - userid: isDirect ? directChatOtherUserId : "" - roomid: roomId - - NotificationBubble { - id: collapsedNotificationBubble - - notificationCount: roomItem.notificationCount - hasLoudNotification: roomItem.hasLoudNotification - bubbleBackgroundColor: roomItem.bubbleBackground - bubbleTextColor: roomItem.bubbleText - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: -Nheko.paddingSmall - mayBeVisible: collapsed && (isSpace ? Settings.spaceNotifications : true) - } - - } - - ColumnLayout { - id: textContent - - visible: !collapsed - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - Layout.minimumWidth: 100 - width: parent.width - avatar.width - Layout.preferredWidth: parent.width - avatar.width - height: avatar.height - spacing: Nheko.paddingSmall - - NotificationBubble { - id: notificationBubble - - parent: isSpace ? titleRow : subtextRow - notificationCount: roomItem.notificationCount - hasLoudNotification: roomItem.hasLoudNotification - bubbleBackgroundColor: roomItem.bubbleBackground - bubbleTextColor: roomItem.bubbleText - Layout.alignment: Qt.AlignRight - Layout.leftMargin: Nheko.paddingSmall - Layout.preferredWidth: implicitWidth - Layout.preferredHeight: implicitHeight - mayBeVisible: !collapsed && (isSpace ? Settings.spaceNotifications : true) - } - - RowLayout { - id: titleRow - - Layout.alignment: Qt.AlignTop - Layout.fillWidth: true - spacing: Nheko.paddingSmall - - ElidedLabel { - id: rN - Layout.alignment: Qt.AlignBaseline - color: roomItem.importantText - elideWidth: width - fullText: TimelineManager.htmlEscape(roomName) - textFormat: Text.RichText - Layout.fillWidth: true - } - - Label { - id: timestamp - - visible: !isInvite && !isSpace - width: visible ? 0 : undefined - Layout.alignment: Qt.AlignRight | Qt.AlignBaseline - font.pixelSize: fontMetrics.font.pixelSize * 0.9 - color: roomItem.unimportantText - text: time - } - - } - - RowLayout { - id: subtextRow - - Layout.fillWidth: true - spacing: 0 - visible: !isSpace - height: visible ? 0 : undefined - Layout.alignment: Qt.AlignBottom - - ElidedLabel { - color: roomItem.unimportantText - font.pixelSize: fontMetrics.font.pixelSize * 0.9 - elideWidth: width - fullText: TimelineManager.htmlEscape(lastMessage) - textFormat: Text.RichText - Layout.fillWidth: true - } - - } - - } - - } - - Rectangle { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - height: parent.height - Nheko.paddingSmall * 2 - width: 3 - color: palette.highlight - visible: hasUnreadMessages - } - - background: Rectangle { - color: backgroundColor - } - - } - - } - background: Rectangle { color: Nheko.theme.sidebarBackground } + footer: ColumnLayout { + spacing: 0 + Rectangle { + Layout.fillWidth: true + color: Nheko.theme.separator + height: 1 + } + Pane { + Layout.alignment: Qt.AlignBottom + Layout.fillWidth: true + Layout.minimumHeight: 40 + horizontalPadding: Nheko.paddingMedium + verticalPadding: 0 + + background: Rectangle { + color: palette.window + } + contentItem: RowLayout { + id: buttonRow + + ImageButton { + Layout.fillWidth: true + Layout.margins: Nheko.paddingMedium + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Start a new chat") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/add-square-button.svg" + width: 22 + + onClicked: roomJoinCreateMenu.open(parent) + + Platform.Menu { + id: roomJoinCreateMenu + + Platform.MenuItem { + text: qsTr("Join a room") + + onTriggered: Nheko.openJoinRoomDialog() + } + Platform.MenuItem { + text: qsTr("Create a new room") + + onTriggered: { + var createRoom = createRoomComponent.createObject(timelineRoot); + createRoom.show(); + timelineRoot.destroyOnClose(createRoom); + } + } + Platform.MenuItem { + text: qsTr("Start a direct chat") + + onTriggered: { + var createDirect = createDirectComponent.createObject(timelineRoot); + createDirect.show(); + timelineRoot.destroyOnClose(createDirect); + } + } + Platform.MenuItem { + text: qsTr("Create a new community") + + onTriggered: { + var createRoom = createRoomComponent.createObject(timelineRoot, { + "space": true + }); + createRoom.show(); + timelineRoot.destroyOnClose(createRoom); + } + } + } + } + ImageButton { + Layout.fillWidth: true + Layout.margins: Nheko.paddingMedium + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Room directory") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/room-directory.svg" + visible: !collapsed + width: 22 + + onClicked: { + var win = roomDirectoryComponent.createObject(timelineRoot); + win.show(); + timelineRoot.destroyOnClose(win); + } + } + ImageButton { + Layout.fillWidth: true + Layout.margins: Nheko.paddingMedium + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Search rooms (Ctrl+K)") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/search.svg" + ripple: false + visible: !collapsed + width: 22 + + onClicked: { + var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml"); + if (component.status == Component.Ready) { + var quickSwitch = component.createObject(timelineRoot); + quickSwitch.open(); + destroyOnClosed(quickSwitch); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + } + ImageButton { + Layout.fillWidth: true + Layout.margins: Nheko.paddingMedium + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("User settings") + ToolTip.visible: hovered + height: 22 + hoverEnabled: true + image: ":/icons/icons/ui/settings.svg" + ripple: false + visible: !collapsed + width: 22 + + onClicked: mainWindow.push(userSettingsPage) + } + } + } + } header: ColumnLayout { spacing: 0 @@ -468,9 +162,11 @@ Page { function openUserProfile() { Nheko.updateUserProfile(); - var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml") + var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml"); if (component.status == Component.Ready) { - var userProfile = component.createObject(timelineRoot, {"profile": Nheko.currentUser}); + var userProfile = component.createObject(timelineRoot, { + "profile": Nheko.currentUser + }); userProfile.show(); timelineRoot.destroyOnClose(userProfile); } else { @@ -478,55 +174,15 @@ Page { } } - - Layout.fillWidth: true Layout.alignment: Qt.AlignBottom + Layout.fillWidth: true + Layout.minimumHeight: 40 //Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium padding: Nheko.paddingMedium - Layout.minimumHeight: 40 - background: Rectangle {color: palette.window} - - InputDialog { - id: statusDialog - - title: qsTr("Status Message") - prompt: qsTr("Enter your status message:") - onAccepted: function(text) { - Nheko.setStatusMessage(text); - } + background: Rectangle { + color: palette.window } - - Platform.Menu { - id: userInfoMenu - - Platform.MenuItem { - text: qsTr("Profile settings") - onTriggered: userInfoPanel.openUserProfile() - } - - Platform.MenuItem { - text: qsTr("Set status message") - onTriggered: statusDialog.show() - } - - } - - TapHandler { - margin: -Nheko.paddingSmall - acceptedButtons: Qt.LeftButton - onSingleTapped: userInfoPanel.openUserProfile() - onLongPressed: userInfoMenu.open() - gesturePolicy: TapHandler.ReleaseWithinBounds - } - - TapHandler { - margin: -Nheko.paddingSmall - acceptedButtons: Qt.RightButton - onSingleTapped: userInfoMenu.open() - gesturePolicy: TapHandler.ReleaseWithinBounds - } - contentItem: RowLayout { id: userInfoGrid @@ -538,91 +194,123 @@ Page { id: avatar Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: fontMetrics.lineSpacing * 2 Layout.preferredHeight: fontMetrics.lineSpacing * 2 - url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/") + Layout.preferredWidth: fontMetrics.lineSpacing * 2 displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : "" - userid: userInfoGrid.profile ? userInfoGrid.profile.userid : "" enabled: false + url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/") + userid: userInfoGrid.profile ? userInfoGrid.profile.userid : "" } - ColumnLayout { id: col - visible: !collapsed Layout.alignment: Qt.AlignLeft Layout.fillWidth: true - width: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2 Layout.preferredWidth: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2 spacing: 0 + visible: !collapsed + width: parent.width - avatar.width - logoutButton.width - Nheko.paddingMedium * 2 ElidedLabel { Layout.alignment: Qt.AlignBottom + elideWidth: col.width font.pointSize: fontMetrics.font.pointSize * 1.1 font.weight: Font.DemiBold fullText: userInfoGrid.profile ? userInfoGrid.profile.displayName : "" - elideWidth: col.width } - ElidedLabel { Layout.alignment: Qt.AlignTop color: palette.buttonText - font.pointSize: fontMetrics.font.pointSize * 0.9 elideWidth: col.width + font.pointSize: fontMetrics.font.pointSize * 0.9 fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : "" } - } - Item { } - ImageButton { id: logoutButton - visible: !collapsed Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: fontMetrics.lineSpacing * 2 Layout.preferredHeight: fontMetrics.lineSpacing * 2 - image: ":/icons/icons/ui/power-off.svg" - ToolTip.visible: hovered + Layout.preferredWidth: fontMetrics.lineSpacing * 2 ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Logout") + ToolTip.visible: hovered + image: ":/icons/icons/ui/power-off.svg" + visible: !collapsed + onClicked: Nheko.openLogoutDialog() } - } - } + InputDialog { + id: statusDialog + prompt: qsTr("Enter your status message:") + title: qsTr("Status Message") + + onAccepted: function (text) { + Nheko.setStatusMessage(text); + } + } + Platform.Menu { + id: userInfoMenu + + Platform.MenuItem { + text: qsTr("Profile settings") + + onTriggered: userInfoPanel.openUserProfile() + } + Platform.MenuItem { + text: qsTr("Set status message") + + onTriggered: statusDialog.show() + } + } + TapHandler { + acceptedButtons: Qt.LeftButton + gesturePolicy: TapHandler.ReleaseWithinBounds + margin: -Nheko.paddingSmall + + onLongPressed: userInfoMenu.open() + onSingleTapped: userInfoPanel.openUserProfile() + } + TapHandler { + acceptedButtons: Qt.RightButton + gesturePolicy: TapHandler.ReleaseWithinBounds + margin: -Nheko.paddingSmall + + onSingleTapped: userInfoMenu.open() + } + } Rectangle { + Layout.fillWidth: true color: Nheko.theme.separator height: 2 - Layout.fillWidth: true } - Rectangle { id: unverifiedStuffBubble - color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1) Layout.fillWidth: true + color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1) implicitHeight: explanation.height + Nheko.paddingMedium * 2 visible: SelfVerificationStatus.status != SelfVerificationStatus.AllVerified RowLayout { id: unverifiedStuffBubbleContainer - width: parent.width height: explanation.height + Nheko.paddingMedium * 2 spacing: 0 + width: parent.width Label { id: explanation + Layout.fillWidth: true Layout.margins: Nheko.paddingMedium Layout.rightMargin: Nheko.paddingSmall color: palette.buttonText - Layout.fillWidth: true text: { switch (SelfVerificationStatus.status) { case SelfVerificationStatus.NoMasterKey: @@ -641,34 +329,32 @@ Page { textFormat: Text.PlainText wrapMode: Text.Wrap } - ImageButton { id: closeUnverifiedBubble - Layout.rightMargin: Nheko.paddingMedium Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - hoverEnabled: true - width: fontMetrics.font.pixelSize - height: fontMetrics.font.pixelSize - image: ":/icons/icons/ui/dismiss.svg" - ToolTip.visible: closeUnverifiedBubble.hovered + Layout.rightMargin: Nheko.paddingMedium ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Close") + ToolTip.visible: closeUnverifiedBubble.hovered + height: fontMetrics.font.pixelSize + hoverEnabled: true + image: ":/icons/icons/ui/dismiss.svg" + width: fontMetrics.font.pixelSize + onClicked: unverifiedStuffBubble.visible = false } - } - HoverHandler { id: verifyButtonHovered - enabled: !closeUnverifiedBubble.hovered acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad - } - - TapHandler { enabled: !closeUnverifiedBubble.hovered + } + TapHandler { acceptedButtons: Qt.LeftButton + enabled: !closeUnverifiedBubble.hovered + onSingleTapped: { if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedDevices) SelfVerificationStatus.verifyUnverifiedDevices(); @@ -676,151 +362,424 @@ Page { SelfVerificationStatus.statusChanged(); } } - } - Rectangle { + Layout.fillWidth: true color: Nheko.theme.separator height: 1 - Layout.fillWidth: true visible: unverifiedStuffBubble.visible } - } - footer: ColumnLayout { - spacing: 0 - - Rectangle { - color: Nheko.theme.separator - height: 1 - Layout.fillWidth: true + // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu + Connections { + function onHideMenu() { + userInfoMenu.close(); + roomContextMenu.close(); } - Pane { - Layout.fillWidth: true - Layout.alignment: Qt.AlignBottom - Layout.minimumHeight: 40 + target: MainWindow + } + Component { + id: roomDirectoryComponent - horizontalPadding: Nheko.paddingMedium - verticalPadding: 0 + RoomDirectory { + } + } + Component { + id: createRoomComponent - background: Rectangle {color: palette.window} - contentItem: RowLayout { - id: buttonRow + CreateRoom { + } + } + Component { + id: createDirectComponent - ImageButton { - Layout.fillWidth: true - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/add-square-button.svg" - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Start a new chat") - Layout.margins: Nheko.paddingMedium - onClicked: roomJoinCreateMenu.open(parent) + CreateDirect { + } + } + ListView { + id: roomlist - Platform.Menu { - id: roomJoinCreateMenu + anchors.left: parent.left + anchors.right: parent.right + height: parent.height + model: Rooms - Platform.MenuItem { - text: qsTr("Join a room") - onTriggered: Nheko.openJoinRoomDialog() - } + //reuseItems: true + ScrollBar.vertical: ScrollBar { + id: scrollbar - Platform.MenuItem { - text: qsTr("Create a new room") - onTriggered: { - var createRoom = createRoomComponent.createObject(timelineRoot); - createRoom.show(); - timelineRoot.destroyOnClose(createRoom); - } - } + parent: !collapsed && Settings.scrollbarsInRoomlist ? roomlist : null + } + delegate: ItemDelegate { + id: roomItem - Platform.MenuItem { - text: qsTr("Start a direct chat") - onTriggered: { - var createDirect = createDirectComponent.createObject(timelineRoot); - createDirect.show(); - timelineRoot.destroyOnClose(createDirect); - } - } + required property string avatarUrl + property color backgroundColor: palette.window + property color bubbleBackground: palette.highlight + property color bubbleText: palette.highlightedText + required property string directChatOtherUserId + required property bool hasLoudNotification + required property bool hasUnreadMessages + property color importantText: palette.text + required property bool isDirect + required property bool isInvite + required property bool isSpace + required property string lastMessage + required property int notificationCount + required property string roomId + required property string roomName + required property var tags + required property string time + property color unimportantText: palette.buttonText - Platform.MenuItem { - text: qsTr("Create a new community") - onTriggered: { - var createRoom = createRoomComponent.createObject(timelineRoot, { "space": true }); - createRoom.show(); - timelineRoot.destroyOnClose(createRoom); - } - } + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: roomName + ToolTip.visible: hovered && collapsed + height: avatarSize + 2 * Nheko.paddingMedium + state: "normal" + width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0) + background: Rectangle { + color: backgroundColor + } + states: [ + State { + name: "highlight" + when: roomItem.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId) + + PropertyChanges { + backgroundColor: palette.dark + bubbleBackground: palette.highlight + bubbleText: palette.highlightedText + importantText: palette.brightText + target: roomItem + unimportantText: palette.brightText } + }, + State { + name: "selected" + when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId - } - - ImageButton { - visible: !collapsed - Layout.fillWidth: true - hoverEnabled: true - width: 22 - height: 22 - image: ":/icons/icons/ui/room-directory.svg" - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Room directory") - Layout.margins: Nheko.paddingMedium - onClicked: { - var win = roomDirectoryComponent.createObject(timelineRoot); - win.show(); - timelineRoot.destroyOnClose(win); + PropertyChanges { + backgroundColor: palette.highlight + bubbleBackground: palette.highlightedText + bubbleText: palette.highlight + importantText: palette.highlightedText + target: roomItem + unimportantText: palette.highlightedText } } + ] - ImageButton { - visible: !collapsed - Layout.fillWidth: true - hoverEnabled: true - ripple: false - width: 22 - height: 22 - image: ":/icons/icons/ui/search.svg" - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Search rooms (Ctrl+K)") - Layout.margins: Nheko.paddingMedium - onClicked: { - var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml") - if (component.status == Component.Ready) { - var quickSwitch = component.createObject(timelineRoot); - quickSwitch.open(); - destroyOnClosed(quickSwitch); - } else { - console.error("Failed to create component: " + component.errorString()); - } - } - } - - ImageButton { - visible: !collapsed - Layout.fillWidth: true - hoverEnabled: true - ripple: false - width: 22 - height: 22 - image: ":/icons/icons/ui/settings.svg" - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("User settings") - Layout.margins: Nheko.paddingMedium - onClicked: mainWindow.push(userSettingsPage); - } - + onClicked: { + console.log("tapped " + roomId); + if (!Rooms.currentRoom || Rooms.currentRoom.roomId !== roomId) + Rooms.setCurrentRoom(roomId); + else + Rooms.resetCurrentRoom(); + } + onPressAndHold: { + if (!isInvite) + roomContextMenu.show(roomId, tags); } + Ripple { + color: Qt.rgba(palette.dark.r, palette.dark.g, palette.dark.b, 0.5) + } + + // NOTE(Nico): We want to prevent the touch areas from overlapping. For some reason we need to add 1px of padding for that... + Item { + anchors.fill: parent + anchors.margins: 1 + + TapHandler { + acceptedButtons: Qt.RightButton + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + gesturePolicy: TapHandler.ReleaseWithinBounds + + onSingleTapped: { + if (!TimelineManager.isInvite) + roomContextMenu.show(roomId, tags); + } + } + } + RowLayout { + anchors.fill: parent + anchors.margins: Nheko.paddingMedium + spacing: Nheko.paddingMedium + + Avatar { + id: avatar + + Layout.alignment: Qt.AlignVCenter + displayName: roomName + enabled: false + height: avatarSize + roomid: roomId + url: avatarUrl.replace("mxc://", "image://MxcImage/") + userid: isDirect ? directChatOtherUserId : "" + width: avatarSize + + NotificationBubble { + id: collapsedNotificationBubble + + anchors.bottom: parent.bottom + anchors.margins: -Nheko.paddingSmall + anchors.right: parent.right + bubbleBackgroundColor: roomItem.bubbleBackground + bubbleTextColor: roomItem.bubbleText + hasLoudNotification: roomItem.hasLoudNotification + mayBeVisible: collapsed && (isSpace ? Settings.spaceNotifications : true) + notificationCount: roomItem.notificationCount + } + } + ColumnLayout { + id: textContent + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + Layout.minimumWidth: 100 + Layout.preferredWidth: parent.width - avatar.width + height: avatar.height + spacing: Nheko.paddingSmall + visible: !collapsed + width: parent.width - avatar.width + + NotificationBubble { + id: notificationBubble + + Layout.alignment: Qt.AlignRight + Layout.leftMargin: Nheko.paddingSmall + Layout.preferredHeight: implicitHeight + Layout.preferredWidth: implicitWidth + bubbleBackgroundColor: roomItem.bubbleBackground + bubbleTextColor: roomItem.bubbleText + hasLoudNotification: roomItem.hasLoudNotification + mayBeVisible: !collapsed && (isSpace ? Settings.spaceNotifications : true) + notificationCount: roomItem.notificationCount + parent: isSpace ? titleRow : subtextRow + } + RowLayout { + id: titleRow + + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + spacing: Nheko.paddingSmall + + ElidedLabel { + id: rN + + Layout.alignment: Qt.AlignBaseline + Layout.fillWidth: true + color: roomItem.importantText + elideWidth: width + fullText: TimelineManager.htmlEscape(roomName) + textFormat: Text.RichText + } + Label { + id: timestamp + + Layout.alignment: Qt.AlignRight | Qt.AlignBaseline + color: roomItem.unimportantText + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + text: time + visible: !isInvite && !isSpace + width: visible ? 0 : undefined + } + } + RowLayout { + id: subtextRow + + Layout.alignment: Qt.AlignBottom + Layout.fillWidth: true + height: visible ? 0 : undefined + spacing: 0 + visible: !isSpace + + ElidedLabel { + Layout.fillWidth: true + color: roomItem.unimportantText + elideWidth: width + font.pixelSize: fontMetrics.font.pixelSize * 0.9 + fullText: TimelineManager.htmlEscape(lastMessage) + textFormat: Text.RichText + } + } + } + } + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + color: palette.highlight + height: parent.height - Nheko.paddingSmall * 2 + visible: hasUnreadMessages + width: 3 + } } - } + Connections { + function onCurrentRoomChanged() { + if (Rooms.currentRoom) + roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain); + } + target: Rooms + } + Component { + id: roomWindowComponent + + ApplicationWindow { + id: roomWindowW + + property var room: null + property var roomPreview: null + + color: palette.window + height: 650 + minimumHeight: 150 + minimumWidth: 150 + title: room.plainRoomName + width: 420 + + Component.onCompleted: { + MainWindow.addPerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW); + Nheko.setTransientParent(roomWindowW, null); + } + Component.onDestruction: MainWindow.removePerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW) + onActiveChanged: { + room.lastReadIdOnWindowFocus(); + } + + //flags: Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowTitleHint + Shortcut { + sequence: StandardKey.Cancel + + onActivated: roomWindowW.close() + } + TimelineView { + id: timeline + + anchors.fill: parent + privacyScreen: privacyScreen + room: roomWindowW.room + roomPreview: roomWindowW.roomPreview.roomid ? roomWindowW.roomPreview : null + } + PrivacyScreen { + id: privacyScreen + + anchors.fill: parent + screenTimeout: Settings.privacyScreenTimeout + timelineRoot: timeline + visible: Settings.privacyScreen + windowTarget: roomWindowW + } + } + } + Component { + id: nestedSpaceMenuLevel + + SpaceMenuLevel { + childMenu: rootSpaceMenu.childMenu + roomid: roomContextMenu.roomid + } + } + Platform.Menu { + id: roomContextMenu + + property string roomid + property var tags + + function show(roomid_, tags_) { + roomid = roomid_; + tags = tags_; + open(); + } + + InputDialog { + id: newTag + + prompt: qsTr("Enter the tag you want to use:") + title: qsTr("New tag") + + onAccepted: function (text) { + Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true); + } + } + Platform.MenuItem { + text: qsTr("Open separately") + + onTriggered: { + var roomWindow = roomWindowComponent.createObject(null, { + "room": Rooms.getRoomById(roomContextMenu.roomid), + "roomPreview": Rooms.getRoomPreviewById(roomContextMenu.roomid) + }); + roomWindow.showNormal(); + destroyOnClose(roomWindow); + } + } + Platform.MenuItem { + text: qsTr("Room settings") + + onTriggered: TimelineManager.openRoomSettings(roomContextMenu.roomid) + } + Platform.MenuItem { + text: qsTr("Leave room") + + onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid) + } + Platform.MenuItem { + text: qsTr("Copy room link") + + onTriggered: Rooms.copyLink(roomContextMenu.roomid) + } + Platform.Menu { + id: tagsMenu + + title: qsTr("Tag room as:") + + Instantiator { + model: Communities.tagsWithDefault + + delegate: Platform.MenuItem { + property string t: modelData + + checkable: true + checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t) + text: { + switch (t) { + case "m.favourite": + return qsTr("Favourite"); + case "m.lowpriority": + return qsTr("Low priority"); + case "m.server_notice": + return qsTr("Server notice"); + default: + return t.substring(2); + } + } + + onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked) + } + + onObjectAdded: (index, object) => tagsMenu.insertItem(index, object) + onObjectRemoved: (index, object) => tagsMenu.removeItem(object) + } + Platform.MenuItem { + text: qsTr("Create new tag...") + + onTriggered: newTag.show() + } + } + SpaceMenuLevel { + id: rootSpaceMenu + + childMenu: nestedSpaceMenuLevel + position: -1 + roomid: roomContextMenu.roomid + title: qsTr("Add or remove from community...") + } + } + } } diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 01fde18e..cb000040 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -20,19 +20,14 @@ import im.nheko.EmojiModel 1.0 Pane { id: timelineRoot - background: null - padding: 0 - - FontMetrics { - id: fontMetrics + function destroyOnClose(obj) { + if (obj.closing != undefined) + obj.closing.connect(() => obj.destroy(1000)); + else if (obj.aboutToHide != undefined) + obj.aboutToHide.connect(() => obj.destroy(1000)); } - - RoomDirectoryModel { - id: publicRooms - } - - UserDirectoryModel { - id: userDirectory + function destroyOnClosed(obj) { + obj.aboutToHide.connect(() => obj.destroy(1000)); } //Timer { @@ -41,54 +36,49 @@ Pane { // running: true // repeat: true //} - function showAliasEditor(settings) { - var component = Qt.createComponent("qrc:/qml/dialogs/AliasEditor.qml") + var component = Qt.createComponent("qrc:/qml/dialogs/AliasEditor.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { - "roomSettings": settings - }); - dialog.show(); - destroyOnClose(dialog); - } else { - console.error("Failed to create component: " + component.errorString()); - } - - } - - function showPLEditor(settings) { - var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelEditor.qml") - if (component.status == Component.Ready) { - var dialog = component.createObject(timelineRoot, { - "roomSettings": settings - }); + "roomSettings": settings + }); dialog.show(); destroyOnClose(dialog); } else { console.error("Failed to create component: " + component.errorString()); } } - - function showSpacePLApplyPrompt(settings, editingModel) { - var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelSpacesApplyDialog.qml") - if (component.status == Component.Ready) { - var dialog = component.createObject(timelineRoot, { - "roomSettings": settings, - "editingModel": editingModel - }); - dialog.show(); - destroyOnClose(dialog); - } else { - console.error("Failed to create component: " + component.errorString()); - } - } - function showAllowedRoomsEditor(settings) { - var component = Qt.createComponent("qrc:/qml/dialogs/AllowedRoomsSettingsDialog.qml") + var component = Qt.createComponent("qrc:/qml/dialogs/AllowedRoomsSettingsDialog.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { - "roomSettings": settings - }); + "roomSettings": settings + }); + dialog.show(); + destroyOnClose(dialog); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + function showPLEditor(settings) { + var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelEditor.qml"); + if (component.status == Component.Ready) { + var dialog = component.createObject(timelineRoot, { + "roomSettings": settings + }); + dialog.show(); + destroyOnClose(dialog); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + function showSpacePLApplyPrompt(settings, editingModel) { + var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelSpacesApplyDialog.qml"); + if (component.status == Component.Ready) { + var dialog = component.createObject(timelineRoot, { + "roomSettings": settings, + "editingModel": editingModel + }); dialog.show(); destroyOnClose(dialog); } else { @@ -96,23 +86,37 @@ Pane { } } + background: null + padding: 0 + + FontMetrics { + id: fontMetrics + + } + RoomDirectoryModel { + id: publicRooms + + } + UserDirectoryModel { + id: userDirectory + + } Component { id: readReceiptsDialog ReadReceipts { } - } - Shortcut { sequence: StandardKey.Quit + onActivated: Qt.quit() } - Shortcut { sequence: "Ctrl+K" + onActivated: { - var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml") + var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml"); if (component.status == Component.Ready) { var quickSwitch = component.createObject(timelineRoot); quickSwitch.open(); @@ -122,37 +126,25 @@ Pane { } } } - Shortcut { // Add alternative shortcut, because sometimes Alt+A is stolen by the TextEdit sequences: ["Alt+A", "Ctrl+Shift+A"] + onActivated: Rooms.nextRoomWithActivity() } - Shortcut { sequence: "Ctrl+Down" + onActivated: Rooms.nextRoom() } - Shortcut { sequence: "Ctrl+Up" + onActivated: Rooms.previousRoom() } - Connections { - function onOpenLogoutDialog() { - var component = Qt.createComponent("qrc:/qml/dialogs/LogoutDialog.qml") - if (component.status == Component.Ready) { - var dialog = component.createObject(timelineRoot); - dialog.open(); - destroyOnClose(dialog); - } else { - console.error("Failed to create component: " + component.errorString()); - } - } - function onOpenJoinRoomDialog() { - var component = Qt.createComponent("qrc:/qml/dialogs/JoinRoomDialog.qml") + var component = Qt.createComponent("qrc:/qml/dialogs/JoinRoomDialog.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot); dialog.show(); @@ -161,11 +153,22 @@ Pane { console.error("Failed to create component: " + component.errorString()); } } - - function onShowRoomJoinPrompt(summary) { - var component = Qt.createComponent("qrc:/qml/dialogs/ConfirmJoinRoomDialog.qml") + function onOpenLogoutDialog() { + var component = Qt.createComponent("qrc:/qml/dialogs/LogoutDialog.qml"); if (component.status == Component.Ready) { - var dialog = component.createObject(timelineRoot, {"summary": summary}); + var dialog = component.createObject(timelineRoot); + dialog.open(); + destroyOnClose(dialog); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + function onShowRoomJoinPrompt(summary) { + var component = Qt.createComponent("qrc:/qml/dialogs/ConfirmJoinRoomDialog.qml"); + if (component.status == Component.Ready) { + var dialog = component.createObject(timelineRoot, { + "summary": summary + }); dialog.show(); destroyOnClose(dialog); } else { @@ -175,12 +178,13 @@ Pane { target: Nheko } - Connections { function onNewDeviceVerificationRequest(flow) { - var component = Qt.createComponent("qrc:/qml/device-verification/DeviceVerification.qml") + var component = Qt.createComponent("qrc:/qml/device-verification/DeviceVerification.qml"); if (component.status == Component.Ready) { - var dialog = component.createObject(timelineRoot, {"flow": flow}); + var dialog = component.createObject(timelineRoot, { + "flow": flow + }); dialog.show(); destroyOnClose(dialog); } else { @@ -190,101 +194,71 @@ Pane { target: VerificationManager } - - function destroyOnClose(obj) { - if (obj.closing != undefined) obj.closing.connect(() => obj.destroy(1000)); - else if (obj.aboutToHide != undefined) obj.aboutToHide.connect(() => obj.destroy(1000)); - } - - function destroyOnClosed(obj) { - obj.aboutToHide.connect(() => obj.destroy(1000)); - } - Connections { - function onOpenProfile(profile) { - var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml") - if (component.status == Component.Ready) { - var userProfile = component.createObject(timelineRoot, {"profile": profile}); - userProfile.show(); - destroyOnClose(userProfile); - } else { - console.error("Failed to create component: " + component.errorString()); - } - } - - function onShowImagePackSettings(room, packlist) { - var component = Qt.createComponent("qrc:/qml/dialogs/ImagePackSettingsDialog.qml") - - if (component.status == Component.Ready) { - var packSet = component.createObject(timelineRoot, { - "room": room, - "packlist": packlist - }); - packSet.show(); - destroyOnClose(packSet); - } else { - console.error("Failed to create component: " + component.errorString()); - } - } - - function onOpenRoomMembersDialog(members, room) { - var component = Qt.createComponent("qrc:/qml/dialogs/RoomMembers.qml") - if (component.status == Component.Ready) { - var membersDialog = component.createObject(timelineRoot, { - "members": members, - "room": room - }); - membersDialog.show(); - destroyOnClose(membersDialog); - } else { - console.error("Failed to create component: " + component.errorString()); - } - - } - - function onOpenRoomSettingsDialog(settings) { - var component = Qt.createComponent("qrc:/qml/dialogs/RoomSettings.qml") - if (component.status == Component.Ready) { - var roomSettings = component.createObject(timelineRoot, { - "roomSettings": settings - }); - roomSettings.show(); - destroyOnClose(roomSettings); - } else { - console.error("Failed to create component: " + component.errorString()); - } - - } - function onOpenInviteUsersDialog(invitees) { - var component = Qt.createComponent("qrc:/qml/dialogs/InviteDialog.qml") + var component = Qt.createComponent("qrc:/qml/dialogs/InviteDialog.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { - "invitees": invitees - }); + "invitees": invitees + }); dialog.show(); destroyOnClose(dialog); } else { console.error("Failed to create component: " + component.errorString()); } } - function onOpenLeaveRoomDialog(roomid, reason) { - var component = Qt.createComponent("qrc:/qml/dialogs/LeaveRoomDialog.qml") + var component = Qt.createComponent("qrc:/qml/dialogs/LeaveRoomDialog.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { - "roomId": roomid, - "reason": reason - }); + "roomId": roomid, + "reason": reason + }); dialog.open(); destroyOnClose(dialog); } else { console.error("Failed to create component: " + component.errorString()); } } - + function onOpenProfile(profile) { + var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml"); + if (component.status == Component.Ready) { + var userProfile = component.createObject(timelineRoot, { + "profile": profile + }); + userProfile.show(); + destroyOnClose(userProfile); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + function onOpenRoomMembersDialog(members, room) { + var component = Qt.createComponent("qrc:/qml/dialogs/RoomMembers.qml"); + if (component.status == Component.Ready) { + var membersDialog = component.createObject(timelineRoot, { + "members": members, + "room": room + }); + membersDialog.show(); + destroyOnClose(membersDialog); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } + function onOpenRoomSettingsDialog(settings) { + var component = Qt.createComponent("qrc:/qml/dialogs/RoomSettings.qml"); + if (component.status == Component.Ready) { + var roomSettings = component.createObject(timelineRoot, { + "roomSettings": settings + }); + roomSettings.show(); + destroyOnClose(roomSettings); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } function onShowImageOverlay(room, eventId, url, originalWidth, proportionalHeight) { - var component = Qt.createComponent("qrc:/qml/dialogs/ImageOverlay.qml") + var component = Qt.createComponent("qrc:/qml/dialogs/ImageOverlay.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { "room": room, @@ -292,22 +266,33 @@ Pane { "url": url, "originalWidth": originalWidth ?? 0, "proportionalHeight": proportionalHeight ?? 0 - } - ); + }); dialog.showFullScreen(); destroyOnClose(dialog); } else { console.error("Failed to create component: " + component.errorString()); } } + function onShowImagePackSettings(room, packlist) { + var component = Qt.createComponent("qrc:/qml/dialogs/ImagePackSettingsDialog.qml"); + if (component.status == Component.Ready) { + var packSet = component.createObject(timelineRoot, { + "room": room, + "packlist": packlist + }); + packSet.show(); + destroyOnClose(packSet); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } target: TimelineManager } - Connections { function onNewInviteState() { if (CallManager.haveCallInvite && Settings.mobileMode) { - var component = Qt.createComponent("qrc:/qml/voip/CallInvite.qml") + var component = Qt.createComponent("qrc:/qml/voip/CallInvite.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot); dialog.open(); @@ -320,141 +305,97 @@ Pane { target: CallManager } - SelfVerificationCheck { } - InputDialog { id: uiaPassPrompt echoMode: TextInput.Password - title: UIA.title prompt: qsTr("Please enter your login password to continue:") - onAccepted: (t) => { + title: UIA.title + + onAccepted: t => { return UIA.continuePassword(t); } } - InputDialog { id: uiaEmailPrompt - title: UIA.title prompt: qsTr("Please enter a valid email address to continue:") - onAccepted: (t) => { + title: UIA.title + + onAccepted: t => { return UIA.continueEmail(t); } } - PhoneNumberInputDialog { id: uiaPhoneNumberPrompt - title: UIA.title prompt: qsTr("Please enter a valid phone number to continue:") + title: UIA.title + onAccepted: (p, t) => { return UIA.continuePhoneNumber(p, t); } } - InputDialog { id: uiaTokenPrompt - title: UIA.title prompt: qsTr("Please enter the token which has been sent to you:") - onAccepted: (t) => { + title: UIA.title + + onAccepted: t => { return UIA.submit3pidToken(t); } } - Platform.MessageDialog { id: uiaErrorDialog buttons: Platform.MessageDialog.Ok } - Platform.MessageDialog { id: uiaConfirmationLinkDialog buttons: Platform.MessageDialog.Ok text: qsTr("Wait for the confirmation link to arrive, then continue.") + onAccepted: UIA.continue3pidReceived() } - Connections { - function onPassword() { - console.log("UIA: password needed"); - uiaPassPrompt.show(); - } - - function onEmail() { - uiaEmailPrompt.show(); - } - - function onPhoneNumber() { - uiaPhoneNumberPrompt.show(); - } - - function onPrompt3pidToken() { - uiaTokenPrompt.show(); - } - function onConfirm3pidToken() { uiaConfirmationLinkDialog.open(); } - + function onEmail() { + uiaEmailPrompt.show(); + } function onError(msg) { uiaErrorDialog.text = msg; uiaErrorDialog.open(); } + function onPassword() { + console.log("UIA: password needed"); + uiaPassPrompt.show(); + } + function onPhoneNumber() { + uiaPhoneNumberPrompt.show(); + } + function onPrompt3pidToken() { + uiaTokenPrompt.show(); + } target: UIA } - StackView { id: mainWindow - anchors.fill: parent - initialItem: welcomePage - - Transition { - id: reducedMotionTransitionExit - PropertyAnimation { - property: "opacity" - from: 1 - to:0 - duration: 200 - } - } - Transition { - id: reducedMotionTransitionEnter - SequentialAnimation { - PropertyAction { property: "opacity"; value: 0 } - PauseAnimation { duration: 200 } - PropertyAnimation { - property: "opacity" - from: 0 - to:1 - duration: 200 - } - } - } + property Transition popEnterOrg + property Transition popExitOrg // for some reason direct bindings to a hidden StackView don't work, so manually store and restore here. property Transition pushEnterOrg property Transition pushExitOrg - property Transition popEnterOrg - property Transition popExitOrg property Transition replaceEnterOrg property Transition replaceExitOrg - Component.onCompleted: { - pushEnterOrg = pushEnter; - popEnterOrg = popEnter; - replaceEnterOrg = replaceEnter; - pushExitOrg = pushExit; - popExitOrg = popExit; - replaceExitOrg = replaceExit; - - updateTrans() - } function updateTrans() { pushEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : pushEnterOrg; @@ -465,65 +406,104 @@ Pane { replaceExit = Settings.reducedMotion ? reducedMotionTransitionExit : replaceExitOrg; } + anchors.fill: parent + initialItem: welcomePage + + Component.onCompleted: { + pushEnterOrg = pushEnter; + popEnterOrg = popEnter; + replaceEnterOrg = replaceEnter; + pushExitOrg = pushExit; + popExitOrg = popExit; + replaceExitOrg = replaceExit; + updateTrans(); + } + + Transition { + id: reducedMotionTransitionExit + + PropertyAnimation { + duration: 200 + from: 1 + property: "opacity" + to: 0 + } + } + Transition { + id: reducedMotionTransitionEnter + + SequentialAnimation { + PropertyAction { + property: "opacity" + value: 0 + } + PauseAnimation { + duration: 200 + } + PropertyAnimation { + duration: 200 + from: 0 + property: "opacity" + to: 1 + } + } + } Connections { - target: Settings function onReducedMotionChanged() { mainWindow.updateTrans(); } + + target: Settings } } - Component { id: welcomePage WelcomePage { } } - Component { id: chatPage ChatPage { } } - Component { id: loginPage LoginPage { } } - Component { id: registerPage RegisterPage { } } - Component { id: userSettingsPage UserSettingsPage { } + } + Snackbar { + id: snackbar } - - - Snackbar { id: snackbar } - Connections { - function onSwitchToChatPage() { - mainWindow.replace(null, chatPage); - } - function onSwitchToLoginPage(error) { - mainWindow.replace(welcomePage, {}, loginPage, {"error": error}, StackView.PopTransition); - } function onShowNotification(msg) { snackbar.showNotification(msg); console.log("New snack: " + msg); } + function onSwitchToChatPage() { + mainWindow.replace(null, chatPage); + } + function onSwitchToLoginPage(error) { + mainWindow.replace(welcomePage, {}, loginPage, { + "error": error + }, StackView.PopTransition); + } + target: MainWindow } - } diff --git a/resources/qml/SelfVerificationCheck.qml b/resources/qml/SelfVerificationCheck.qml index bb7ea5f0..80897ff9 100644 --- a/resources/qml/SelfVerificationCheck.qml +++ b/resources/qml/SelfVerificationCheck.qml @@ -10,22 +10,29 @@ import QtQuick.Layouts 1.3 import im.nheko 1.0 Item { - visible: false enabled: false + visible: false Dialog { id: showRecoverKeyDialog property string recoveryKey: "" - parent: Overlay.overlay anchors.centerIn: parent - height: content.height + implicitFooterHeight + implicitHeaderHeight - width: content.width - padding: 0 - modal: true - standardButtons: Dialog.Ok closePolicy: Popup.NoAutoClose + height: content.height + implicitFooterHeight + implicitHeaderHeight + modal: true + padding: 0 + parent: Overlay.overlay + standardButtons: Dialog.Ok + width: content.width + + background: Rectangle { + border.color: Nheko.theme.separator + border.width: 1 + color: palette.window + radius: Nheko.paddingSmall + } ColumnLayout { id: content @@ -33,45 +40,33 @@ Item { spacing: 0 Label { + Layout.fillWidth: true Layout.margins: Nheko.paddingMedium Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4 - Layout.fillWidth: true - text: qsTr("This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don't share it with anyone and don't lose it! Do not pass go! Do not collect $200!") color: palette.text + text: qsTr("This is your recovery key. You will need it to restore access to your encrypted messages and verification keys. Keep this safe. Don't share it with anyone and don't lose it! Do not pass go! Do not collect $200!") wrapMode: Text.Wrap } - TextEdit { - Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4 Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4 + color: palette.text + font.bold: true horizontalAlignment: TextEdit.AlignHCenter - verticalAlignment: TextEdit.AlignVCenter readOnly: true selectByMouse: true text: showRecoverKeyDialog.recoveryKey - color: palette.text - font.bold: true + verticalAlignment: TextEdit.AlignVCenter wrapMode: TextEdit.Wrap } - } - - background: Rectangle { - color: palette.window - border.color: Nheko.theme.separator - border.width: 1 - radius: Nheko.paddingSmall - } - } - P.MessageDialog { id: successDialog buttons: P.MessageDialog.Ok text: qsTr("Encryption setup successfully") } - P.MessageDialog { id: failureDialog @@ -80,85 +75,86 @@ Item { buttons: P.MessageDialog.Ok text: qsTr("Failed to setup encryption: %1").arg(errorMessage) } - MainWindowDialog { id: bootstrapCrosssigning + background: Rectangle { + border.color: Nheko.theme.separator + border.width: 1 + color: palette.window + radius: Nheko.paddingSmall + } + onAccepted: SelfVerificationStatus.setupCrosssigning(storeSecretsOnline.checked, usePassword.checked ? passwordField.text : "", useOnlineKeyBackup.checked) GridLayout { id: grid - width: bootstrapCrosssigning.useableWidth + columnSpacing: 0 columns: 2 rowSpacing: 0 - columnSpacing: 0 + width: bootstrapCrosssigning.useableWidth z: 1 Label { - Layout.margins: Nheko.paddingMedium Layout.alignment: Qt.AlignHCenter Layout.columnSpan: 2 + Layout.margins: Nheko.paddingMedium + color: palette.text font.pointSize: fontMetrics.font.pointSize * 2 text: qsTr("Setup Encryption") - color: palette.text wrapMode: Text.Wrap } - Label { - Layout.margins: Nheko.paddingMedium Layout.alignment: Qt.AlignLeft Layout.columnSpan: 2 + Layout.margins: Nheko.paddingMedium Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2 - text: qsTr("Hello and welcome to Matrix!\nIt seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!") color: palette.text + text: qsTr("Hello and welcome to Matrix!\nIt seems like you are new. Before you can securely encrypt your messages, we need to setup a few small things. You can either press accept immediately or adjust a few basic options. We also try to explain a few of the basics. You can skip those parts, but they might prove to be helpful!") wrapMode: Text.Wrap } - Label { - Layout.margins: Nheko.paddingMedium Layout.alignment: Qt.AlignLeft Layout.columnSpan: 1 + Layout.margins: Nheko.paddingMedium Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 - text: "Store secrets online.\nYou have a few secrets to make all the encryption magic work. While you can keep them stored only locally, we recommend storing them encrypted on the server. Otherwise it will be painful to recover them. Only disable this if you are paranoid and like losing your data!" color: palette.text + text: "Store secrets online.\nYou have a few secrets to make all the encryption magic work. While you can keep them stored only locally, we recommend storing them encrypted on the server. Otherwise it will be painful to recover them. Only disable this if you are paranoid and like losing your data!" wrapMode: Text.Wrap } - Item { - Layout.margins: Nheko.paddingMedium - Layout.preferredHeight: storeSecretsOnline.height Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.fillWidth: true + Layout.margins: Nheko.paddingMedium + Layout.preferredHeight: storeSecretsOnline.height ToggleButton { id: storeSecretsOnline checked: true + onClicked: console.log("Store secrets toggled: " + checked) } - } - Label { - Layout.margins: Nheko.paddingMedium Layout.alignment: Qt.AlignLeft Layout.columnSpan: 1 - Layout.rowSpan: 2 + Layout.margins: Nheko.paddingMedium Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 - visible: storeSecretsOnline.checked - text: "Set an online backup password.\nWe recommend you DON'T set a password and instead only rely on the recovery key. You will get a recovery key in any case when storing the cross-signing secrets online, but passwords are usually not very random, so they are easier to attack than a completely random recovery key. If you choose to use a password, DON'T make it the same as your login password, otherwise your server can read all your encrypted messages. (You don't want that.)" + Layout.rowSpan: 2 color: palette.text + text: "Set an online backup password.\nWe recommend you DON'T set a password and instead only rely on the recovery key. You will get a recovery key in any case when storing the cross-signing secrets online, but passwords are usually not very random, so they are easier to attack than a completely random recovery key. If you choose to use a password, DON'T make it the same as your login password, otherwise your server can read all your encrypted messages. (You don't want that.)" + visible: storeSecretsOnline.checked wrapMode: Text.Wrap } - Item { - Layout.margins: Nheko.paddingMedium - Layout.topMargin: Nheko.paddingLarge - Layout.preferredHeight: storeSecretsOnline.height Layout.alignment: Qt.AlignLeft | Qt.AlignTop - Layout.rowSpan: usePassword.checked ? 1 : 2 Layout.fillWidth: true + Layout.margins: Nheko.paddingMedium + Layout.preferredHeight: storeSecretsOnline.height + Layout.rowSpan: usePassword.checked ? 1 : 2 + Layout.topMargin: Nheko.paddingLarge visible: storeSecretsOnline.checked ToggleButton { @@ -166,57 +162,43 @@ Item { checked: false } - } - MatrixTextField { id: passwordField - Layout.margins: Nheko.paddingMedium - Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.columnSpan: 1 Layout.fillWidth: true - visible: storeSecretsOnline.checked && usePassword.checked - echoMode: TextInput.Password - } - - Label { Layout.margins: Nheko.paddingMedium + Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 + echoMode: TextInput.Password + visible: storeSecretsOnline.checked && usePassword.checked + } + Label { Layout.alignment: Qt.AlignLeft Layout.columnSpan: 1 + Layout.margins: Nheko.paddingMedium Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 - text: "Use online key backup.\nStore the keys for your messages securely encrypted online. In general you do want this, because it protects your messages from becoming unreadable, if you log out by accident. It does however carry a small security risk, if you ever share your recovery key by accident. Currently this also has some other weaknesses, that might allow the server to insert new keys into your backup. The server will however never be able to read your messages." color: palette.text + text: "Use online key backup.\nStore the keys for your messages securely encrypted online. In general you do want this, because it protects your messages from becoming unreadable, if you log out by accident. It does however carry a small security risk, if you ever share your recovery key by accident. Currently this also has some other weaknesses, that might allow the server to insert new keys into your backup. The server will however never be able to read your messages." wrapMode: Text.Wrap } - Item { - Layout.margins: Nheko.paddingMedium - Layout.preferredHeight: storeSecretsOnline.height Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.fillWidth: true + Layout.margins: Nheko.paddingMedium + Layout.preferredHeight: storeSecretsOnline.height ToggleButton { id: useOnlineKeyBackup checked: true + onClicked: console.log("Online key backup toggled: " + checked) } - } - } - - background: Rectangle { - color: palette.window - border.color: Nheko.theme.separator - border.width: 1 - radius: Nheko.paddingSmall - } - } - MainWindowDialog { id: verifyMasterKey @@ -225,54 +207,61 @@ Item { GridLayout { id: masterGrid - width: verifyMasterKey.useableWidth columns: 1 + width: verifyMasterKey.useableWidth z: 1 Label { - Layout.margins: Nheko.paddingMedium Layout.alignment: Qt.AlignHCenter + Layout.margins: Nheko.paddingMedium + color: palette.text //Layout.columnSpan: 2 font.pointSize: fontMetrics.font.pointSize * 2 text: qsTr("Activate Encryption") - color: palette.text wrapMode: Text.Wrap } - Label { - Layout.margins: Nheko.paddingMedium Layout.alignment: Qt.AlignLeft + Layout.margins: Nheko.paddingMedium //Layout.columnSpan: 2 Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2 - text: qsTr("It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.\nIf you choose verify, you need to have the other device available. If you choose \"enter passphrase\", you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.") color: palette.text + text: qsTr("It seems like you have encryption already configured for this account. To be able to access your encrypted messages and make this device appear as trusted, you can either verify an existing device or (if you have one) enter your recovery passphrase. Please select one of the options below.\nIf you choose verify, you need to have the other device available. If you choose \"enter passphrase\", you will need your recovery key or passphrase. If you click cancel, you can choose to verify yourself at a later point.") wrapMode: Text.Wrap } - FlatButton { Layout.alignment: Qt.AlignHCenter text: qsTr("verify") + onClicked: { SelfVerificationStatus.verifyMasterKey(); verifyMasterKey.close(); } } - FlatButton { - visible: SelfVerificationStatus.hasSSSS Layout.alignment: Qt.AlignHCenter text: qsTr("enter passphrase") + visible: SelfVerificationStatus.hasSSSS + onClicked: { SelfVerificationStatus.verifyMasterKeyWithPassphrase(); verifyMasterKey.close(); } } - } - } - Connections { + function onSetupCompleted() { + successDialog.open(); + } + function onSetupFailed(m) { + failureDialog.errorMessage = m; + failureDialog.open(); + } + function onShowRecoveryKey(key) { + showRecoverKeyDialog.recoveryKey = key; + showRecoverKeyDialog.open(); + } function onStatusChanged() { console.log("STATUS CHANGED: " + SelfVerificationStatus.status); if (SelfVerificationStatus.status == SelfVerificationStatus.NoMasterKey) { @@ -285,21 +274,6 @@ Item { } } - function onShowRecoveryKey(key) { - showRecoverKeyDialog.recoveryKey = key; - showRecoverKeyDialog.open(); - } - - function onSetupCompleted() { - successDialog.open(); - } - - function onSetupFailed(m) { - failureDialog.errorMessage = m; - failureDialog.open(); - } - target: SelfVerificationStatus } - } diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index 862f9d7a..4a305ac5 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -9,15 +9,9 @@ import im.nheko 1.0 ImageButton { id: indicator - required property int status required property string eventId + required property int status - width: 16 - height: 16 - hoverEnabled: true - changeColorOnHover: (status == MtxEvent.Read) - cursor: (status == MtxEvent.Read) ? Qt.PointingHandCursor : Qt.ArrowCursor - ToolTip.visible: hovered && status != MtxEvent.Empty ToolTip.text: { switch (status) { case MtxEvent.Failed: @@ -32,11 +26,11 @@ ImageButton { return ""; } } - onClicked: { - if (status == MtxEvent.Read) - room.showReadReceipts(eventId); - - } + ToolTip.visible: hovered && status != MtxEvent.Empty + changeColorOnHover: (status == MtxEvent.Read) + cursor: (status == MtxEvent.Read) ? Qt.PointingHandCursor : Qt.ArrowCursor + height: 16 + hoverEnabled: true image: { switch (status) { case MtxEvent.Failed: @@ -51,4 +45,10 @@ ImageButton { return ""; } } + width: 16 + + onClicked: { + if (status == MtxEvent.Read) + room.showReadReceipts(eventId); + } } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 07cb5ce2..a064bd15 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -13,72 +13,45 @@ import im.nheko 1.0 AbstractButton { id: r - required property double proportionalHeight - required property int type - required property string typeString - required property int originalWidth required property string blurhash required property string body - required property string formattedBody + required property string callType + required property int duration + required property int encryptionError required property string eventId required property string filename required property string filesize - required property string url - required property string thumbnailUrl - required property bool isOnlyEmoji - required property bool isSender - required property bool isEncrypted + required property string formattedBody + required property int index required property bool isEditable required property bool isEdited + required property bool isEncrypted + required property bool isOnlyEmoji + required property bool isSender required property bool isStateEvent + required property int notificationlevel + required property int originalWidth + required property double proportionalHeight + required property var reactions + required property int relatedEventCacheBuster required property string replyTo + required property string roomName + required property string roomTopic + required property int status required property string threadId + required property string thumbnailUrl + required property var timestamp + required property int trustlevel + required property int type + required property string typeString + required property string url required property string userId required property string userName - required property string roomTopic - required property string roomName - required property string callType - required property var reactions - required property int trustlevel - required property int notificationlevel - required property int encryptionError - required property int duration - required property var timestamp - required property int status - required property int index - required property int relatedEventCacheBuster + height: row.height + (reactionRow.height > 0 ? reactionRow.height - 2 : 0) + unreadRow.height hoverEnabled: true - width: parent.width - height: row.height+(reactionRow.height > 0 ? reactionRow.height-2 : 0 )+unreadRow.height - Rectangle { - color: (Settings.messageHoverHighlight && hovered) ? palette.alternateBase : "transparent" - anchors.fill: parent - // this looks better without margins - TapHandler { - acceptedButtons: Qt.RightButton - onSingleTapped: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) - gesturePolicy: TapHandler.ReleaseWithinBounds - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad - } - } - - - onPressAndHold: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) - onDoubleClicked: room.reply = eventId - - DragHandler { - id: draghandler - yAxis.enabled: false - xAxis.maximum: 100 - xAxis.minimum: -100 - onActiveChanged: { - if(!active && (x < -70 || x > 70)) - room.reply = eventId - } - } states: State { name: "dragging" when: draghandler.active @@ -86,265 +59,292 @@ AbstractButton { transitions: Transition { from: "dragging" to: "" + PropertyAnimation { - target: r - properties: "x" - easing.type: Easing.InOutQuad - to: 0 duration: 100 + easing.type: Easing.InOutQuad + properties: "x" + target: r + to: 0 } } onClicked: { - let link = contentItem.child.linkAt != undefined && contentItem.child.linkAt(pressX-row.x-msg.x, pressY-row.y-msg.y-contentItem.y); + let link = contentItem.child.linkAt != undefined && contentItem.child.linkAt(pressX - row.x - msg.x, pressY - row.y - msg.y - contentItem.y); if (link) { - Nheko.openLink(link) + Nheko.openLink(link); } } + onDoubleClicked: room.reply = eventId + onPressAndHold: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) + Rectangle { + anchors.fill: parent + color: (Settings.messageHoverHighlight && hovered) ? palette.alternateBase : "transparent" + + // this looks better without margins + TapHandler { + acceptedButtons: Qt.RightButton + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + gesturePolicy: TapHandler.ReleaseWithinBounds + + onSingleTapped: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) + } + } + DragHandler { + id: draghandler + + xAxis.maximum: 100 + xAxis.minimum: -100 + yAxis.enabled: false + + onActiveChanged: { + if (!active && (x < -70 || x > 70)) + room.reply = eventId; + } + } AbstractButton { - anchors.leftMargin: Settings.smallAvatars? 0 : (Nheko.avatarSize + 8) // align bubble with section header + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Part of a thread") + ToolTip.visible: hovered anchors.left: parent.left + anchors.leftMargin: Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8) // align bubble with section header + height: parent.height visible: threadId width: 4 - height: parent.height + + onClicked: room.thread = threadId Rectangle { id: threadLine - color: TimelineManager.userColor(threadId, palette.base) anchors.fill: parent + color: TimelineManager.userColor(threadId, palette.base) } - - ToolTip.visible: hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Part of a thread") - onClicked: room.thread = threadId } - Rectangle { id: row - property bool bubbleOnRight : isSender && Settings.bubbles - anchors.leftMargin: (isStateEvent || Settings.smallAvatars? 0 : (Nheko.avatarSize + 8)) + (threadId ? 6 : 0) // align bubble with section header - anchors.left: (isStateEvent || bubbleOnRight) ? undefined : parent.left - anchors.right: (isStateEvent || !bubbleOnRight) ? undefined : parent.right - anchors.horizontalCenter: isStateEvent? parent.horizontalCenter : undefined - property int maxWidth: (parent.width-(Settings.smallAvatars || isStateEvent? 0 : Nheko.avatarSize+8))*(Settings.bubbles && !isStateEvent? 0.9 : 1) - width: Settings.bubbles? Math.min(maxWidth,Math.max(reply.implicitWidth+8,contentItem.implicitWidth+metadata.width+20)) : maxWidth - height: msg.height+msg.anchors.margins*2 - property color userColor: TimelineManager.userColor(userId, palette.base) property color bgColor: palette.base - color: (Settings.bubbles && !isStateEvent) ? Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.2)) : "#00000000" - radius: 4 - border.width: r.notificationlevel == MtxEvent.Highlight ? 1 : 0 + property bool bubbleOnRight: isSender && Settings.bubbles + property int maxWidth: (parent.width - (Settings.smallAvatars || isStateEvent ? 0 : Nheko.avatarSize + 8)) * (Settings.bubbles && !isStateEvent ? 0.9 : 1) + property color userColor: TimelineManager.userColor(userId, palette.base) + + anchors.horizontalCenter: isStateEvent ? parent.horizontalCenter : undefined + anchors.left: (isStateEvent || bubbleOnRight) ? undefined : parent.left + anchors.leftMargin: (isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) + (threadId ? 6 : 0) // align bubble with section header + anchors.right: (isStateEvent || !bubbleOnRight) ? undefined : parent.right border.color: Nheko.theme.red + border.width: r.notificationlevel == MtxEvent.Highlight ? 1 : 0 + color: (Settings.bubbles && !isStateEvent) ? Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.2)) : "#00000000" + height: msg.height + msg.anchors.margins * 2 + radius: 4 + width: Settings.bubbles ? Math.min(maxWidth, Math.max(reply.implicitWidth + 8, contentItem.implicitWidth + metadata.width + 20)) : maxWidth GridLayout { + id: msg + + columnSpacing: 2 + columns: Settings.bubbles ? 1 : 2 + rowSpacing: 0 + rows: Settings.bubbles ? 3 : 2 + anchors { left: parent.left - top: parent.top - right: parent.right - margins: (Settings.bubbles && ! isStateEvent)? 4 : 2 leftMargin: 4 + margins: (Settings.bubbles && !isStateEvent) ? 4 : 2 + right: parent.right rightMargin: 4 + top: parent.top } - id: msg - rowSpacing: 0 - columnSpacing: 2 - columns: Settings.bubbles? 1 : 2 - rows: Settings.bubbles? 3 : 2 // fancy reply, if this is a reply Reply { - Layout.row: 0 - Layout.column: 0 - Layout.fillWidth: true - Layout.maximumWidth: Settings.bubbles? Number.MAX_VALUE : implicitWidth - Layout.bottomMargin: visible? 2 : 0 - Layout.preferredHeight: height id: reply function fromModel(role) { return replyTo != "" ? room.dataById(replyTo, role, r.eventId) : null; } - visible: replyTo - userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, palette.base) + + Layout.bottomMargin: visible ? 2 : 0 + Layout.column: 0 + Layout.fillWidth: true + Layout.maximumWidth: Settings.bubbles ? Number.MAX_VALUE : implicitWidth + Layout.preferredHeight: height + Layout.row: 0 blurhash: r.relatedEventCacheBuster, fromModel(Room.Blurhash) ?? "" body: r.relatedEventCacheBuster, fromModel(Room.Body) ?? "" - formattedBody: r.relatedEventCacheBuster, fromModel(Room.FormattedBody) ?? "" + callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? "" + duration: r.relatedEventCacheBuster, fromModel(Room.Duration) ?? 0 + encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? 0 eventId: fromModel(Room.EventId) ?? "" filename: r.relatedEventCacheBuster, fromModel(Room.Filename) ?? "" filesize: r.relatedEventCacheBuster, fromModel(Room.Filesize) ?? "" + formattedBody: r.relatedEventCacheBuster, fromModel(Room.FormattedBody) ?? "" + isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false + isStateEvent: r.relatedEventCacheBuster, fromModel(Room.IsStateEvent) ?? false + originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0 proportionalHeight: r.relatedEventCacheBuster, fromModel(Room.ProportionalHeight) ?? 1 + relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0 + roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? "" + roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? "" + thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? "" type: r.relatedEventCacheBuster, fromModel(Room.Type) ?? MtxEvent.UnknownMessage typeString: r.relatedEventCacheBuster, fromModel(Room.TypeString) ?? "" url: r.relatedEventCacheBuster, fromModel(Room.Url) ?? "" - originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0 - isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false - isStateEvent: r.relatedEventCacheBuster, fromModel(Room.IsStateEvent) ?? false + userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, palette.base) userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? "" userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? "" - thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? "" - duration: r.relatedEventCacheBuster, fromModel(Room.Duration) ?? 0 - roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? "" - roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? "" - callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? "" - encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? 0 - relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0 + visible: replyTo } // actual message content MessageDelegate { - Layout.row: 1 + id: contentItem + Layout.column: 0 Layout.fillWidth: true Layout.preferredHeight: height - id: contentItem - + Layout.row: 1 blurhash: r.blurhash body: r.body - formattedBody: r.formattedBody + callType: r.callType + duration: r.duration + encryptionError: r.encryptionError eventId: r.eventId filename: r.filename filesize: r.filesize + formattedBody: r.formattedBody + isOnlyEmoji: r.isOnlyEmoji + isReply: false + isStateEvent: r.isStateEvent + metadataWidth: metadata.width + originalWidth: r.originalWidth proportionalHeight: r.proportionalHeight + relatedEventCacheBuster: r.relatedEventCacheBuster + roomName: r.roomName + roomTopic: r.roomTopic + thumbnailUrl: r.thumbnailUrl type: r.type typeString: r.typeString ?? "" url: r.url - thumbnailUrl: r.thumbnailUrl - duration: r.duration - originalWidth: r.originalWidth - isOnlyEmoji: r.isOnlyEmoji - isStateEvent: r.isStateEvent userId: r.userId userName: r.userName - roomTopic: r.roomTopic - roomName: r.roomName - callType: r.callType - encryptionError: r.encryptionError - relatedEventCacheBuster: r.relatedEventCacheBuster - isReply: false - metadataWidth: metadata.width } - Row { id: metadata - Layout.column: Settings.bubbles? 0 : 1 - Layout.row: Settings.bubbles? 2 : 0 - Layout.rowSpan: Settings.bubbles? 1 : 2 - Layout.bottomMargin: -2 - Layout.topMargin: (contentItem.fitsMetadata && Settings.bubbles)? -height-Layout.bottomMargin : 0 + + property int iconSize: Math.floor(fontMetrics.ascent * scaling) + property double scaling: Settings.bubbles ? 0.75 : 1 + Layout.alignment: Qt.AlignTop | Qt.AlignRight + Layout.bottomMargin: -2 + Layout.column: Settings.bubbles ? 0 : 1 Layout.preferredWidth: implicitWidth - visible: !isStateEvent + Layout.row: Settings.bubbles ? 2 : 0 + Layout.rowSpan: Settings.bubbles ? 1 : 2 + Layout.topMargin: (contentItem.fitsMetadata && Settings.bubbles) ? -height - Layout.bottomMargin : 0 spacing: 2 - - property double scaling: Settings.bubbles? 0.75 : 1 - - property int iconSize: Math.floor(fontMetrics.ascent*scaling) + visible: !isStateEvent StatusIndicator { Layout.alignment: Qt.AlignRight | Qt.AlignTop - height: parent.iconSize - width: parent.iconSize - status: r.status - eventId: r.eventId anchors.verticalCenter: ts.verticalCenter - } - - Image { - visible: isEdited || eventId == room.edit - Layout.alignment: Qt.AlignRight | Qt.AlignTop + eventId: r.eventId height: parent.iconSize + status: r.status width: parent.iconSize - sourceSize.width: parent.iconSize * Screen.devicePixelRatio - sourceSize.height: parent.iconSize * Screen.devicePixelRatio - source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == room.edit) ? palette.highlight : palette.buttonText) - ToolTip.visible: editHovered.hovered + } + Image { + Layout.alignment: Qt.AlignRight | Qt.AlignTop ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Edited") + ToolTip.visible: editHovered.hovered anchors.verticalCenter: ts.verticalCenter + height: parent.iconSize + source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == room.edit) ? palette.highlight : palette.buttonText) + sourceSize.height: parent.iconSize * Screen.devicePixelRatio + sourceSize.width: parent.iconSize * Screen.devicePixelRatio + visible: isEdited || eventId == room.edit + width: parent.iconSize HoverHandler { id: editHovered + } - } - ImageButton { - visible: threadId Layout.alignment: Qt.AlignRight | Qt.AlignTop - height: parent.iconSize - width: parent.iconSize - image: ":/icons/icons/ui/thread.svg" - buttonTextColor: TimelineManager.userColor(threadId, palette.base) - ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Part of a thread") + ToolTip.visible: hovered anchors.verticalCenter: ts.verticalCenter + buttonTextColor: TimelineManager.userColor(threadId, palette.base) + height: parent.iconSize + image: ":/icons/icons/ui/thread.svg" + visible: threadId + width: parent.iconSize + onClicked: room.thread = threadId } - EncryptionIndicator { - visible: room.isEncrypted - encrypted: isEncrypted - trust: trustlevel Layout.alignment: Qt.AlignRight | Qt.AlignTop - height: parent.iconSize - width: parent.iconSize - sourceSize.width: parent.iconSize * Screen.devicePixelRatio - sourceSize.height: parent.iconSize * Screen.devicePixelRatio anchors.verticalCenter: ts.verticalCenter + encrypted: isEncrypted + height: parent.iconSize + sourceSize.height: parent.iconSize * Screen.devicePixelRatio + sourceSize.width: parent.iconSize * Screen.devicePixelRatio + trust: trustlevel + visible: room.isEncrypted + width: parent.iconSize } - Label { id: ts + Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredWidth: implicitWidth - text: timestamp.toLocaleTimeString(Locale.ShortFormat) - color: palette.inactive.text - ToolTip.visible: ma.hovered ToolTip.delay: Nheko.tooltipDelay ToolTip.text: Qt.formatDateTime(timestamp, Qt.DefaultLocaleLongDate) - font.pointSize: fontMetrics.font.pointSize*parent.scaling + ToolTip.visible: ma.hovered + color: palette.inactive.text + font.pointSize: fontMetrics.font.pointSize * parent.scaling + text: timestamp.toLocaleTimeString(Locale.ShortFormat) + HoverHandler { id: ma - } + } } } } } - Reactions { - anchors { - top: row.bottom - topMargin: -4 - left: row.bubbleOnRight? undefined : row.left - right: row.bubbleOnRight? row.right : undefined - } - width: row.maxWidth - layoutDirection: row.bubbleOnRight? Qt.RightToLeft : Qt.LeftToRight - id: reactionRow - reactions: r.reactions eventId: r.eventId - } + layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight + reactions: r.reactions + width: row.maxWidth + anchors { + left: row.bubbleOnRight ? undefined : row.left + right: row.bubbleOnRight ? row.right : undefined + top: row.bottom + topMargin: -4 + } + } Rectangle { id: unreadRow + + color: palette.highlight + height: visible ? 3 : 0 + visible: (r.index > 0 && (room.fullyReadEventId == r.eventId)) + anchors { - top: reactionRow.bottom - topMargin: 5 left: parent.left right: parent.right + top: reactionRow.bottom + topMargin: 5 } - color: palette.highlight - - visible: (r.index > 0 && (room.fullyReadEventId == r.eventId)) - height: visible ? 3 : 0 - } } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 8fc567f2..24489d0b 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -20,86 +20,85 @@ import im.nheko.EmojiModel 1.0 Item { id: timelineView + required property PrivacyScreen privacyScreen property var room: null property var roomPreview: null - property bool showBackButton: false property bool shouldEffectsRun: false - required property PrivacyScreen privacyScreen + property bool showBackButton: false + clip: true - onRoomChanged: if (room != null) room.triggerSpecialEffects() + // focus message input on key press, but not on Ctrl-C and such. + Keys.onPressed: event => { + if (event.text && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && !topBar.searchHasFocus) { + TimelineManager.focusMessageInput(); + room.input.setText(room.input.text + event.text); + } + } + onRoomChanged: if (room != null) + room.triggerSpecialEffects() StickerPicker { id: emojiPopup emoji: true } - - // focus message input on key press, but not on Ctrl-C and such. - Keys.onPressed: (event) => { - if (event.text && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && !topBar.searchHasFocus) { - TimelineManager.focusMessageInput(); - room.input.setText(room.input.text + event.text); - } - } - Shortcut { sequence: StandardKey.Close + onActivated: Rooms.resetCurrentRoom() } - Label { - visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid) anchors.centerIn: parent - text: qsTr("No room open") font.pointSize: 24 + text: qsTr("No room open") + visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid) } - Spinner { - visible: TimelineManager.isInitialSync anchors.centerIn: parent foreground: palette.mid - running: TimelineManager.isInitialSync // height is somewhat arbitrary here... don't set width because width scales w/ height height: parent.height / 16 - z: 3 opacity: hh.hovered ? 0.3 : 1 + running: TimelineManager.isInitialSync + visible: TimelineManager.isInitialSync + z: 3 - Behavior on opacity { - NumberAnimation { duration: 100; } + Behavior on opacity { + NumberAnimation { + duration: 100 + } } HoverHandler { id: hh + } } - ColumnLayout { id: timelineLayout - visible: room != null && !room.isSpace - enabled: visible anchors.fill: parent + enabled: visible spacing: 0 + visible: room != null && !room.isSpace TopBar { id: topBar showBackButton: timelineView.showBackButton } - Rectangle { Layout.fillWidth: true + color: Nheko.theme.separator height: 1 z: 3 - color: Nheko.theme.separator } - Rectangle { id: msgView - Layout.fillWidth: true Layout.fillHeight: true + Layout.fillWidth: true color: palette.base ColumnLayout { @@ -118,143 +117,121 @@ Item { target: timelineView } - MessageView { + Layout.fillWidth: true implicitHeight: msgView.height - typingIndicator.height searchString: topBar.searchString - Layout.fillWidth: true } - Loader { source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" + onLoaded: TimelineManager.setVideoCallItem() } - } - TypingIndicator { id: typingIndicator + } - } - } - CallInviteBar { id: callInviteBar Layout.fillWidth: true z: 3 } - ActiveCallBar { Layout.fillWidth: true z: 3 } - Rectangle { Layout.fillWidth: true - z: 3 - height: 1 color: Nheko.theme.separator + height: 1 + z: 3 } - - UploadBox { } - MessageInputWarning { text: qsTr("You are about to notify the whole room") visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom) } - MessageInputWarning { text: qsTr("The command /%1 is not recognized and will be sent as part of your message").arg(room ? room.input.currentCommand : "") visible: room ? room.input.containsInvalidCommand && !room.input.containsIncompleteCommand : false } - MessageInputWarning { + bubbleColor: Nheko.theme.orange text: qsTr("/%1 looks like an incomplete command. To send it anyway, add a space to the end of your message.").arg(room ? room.input.currentCommand : "") visible: room ? room.input.containsIncompleteCommand : false - bubbleColor: Nheko.theme.orange } - ReplyPopup { } - MessageInput { } - } - ColumnLayout { id: preview + property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "") + property string reason: roomPreview ? roomPreview.reason : "" property string roomId: room ? room.roomId : (roomPreview ? roomPreview.roomid : "") property string roomName: room ? room.roomName : (roomPreview ? roomPreview.roomName : "") property string roomTopic: room ? room.roomTopic : (roomPreview ? roomPreview.roomTopic : "") - property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "") - property string reason: roomPreview ? roomPreview.reason : "" - visible: room != null && room.isSpace || roomPreview != null - enabled: visible anchors.fill: parent anchors.margins: Nheko.paddingLarge + enabled: visible spacing: Nheko.paddingLarge + visible: room != null && room.isSpace || roomPreview != null Item { Layout.fillHeight: true } - Avatar { - url: parent.avatarUrl.replace("mxc://", "image://MxcImage/") - roomid: parent.roomId + Layout.alignment: Qt.AlignHCenter displayName: parent.roomName - height: 130 - width: 130 - Layout.alignment: Qt.AlignHCenter enabled: false + height: 130 + roomid: parent.roomId + url: parent.avatarUrl.replace("mxc://", "image://MxcImage/") + width: 130 } - RowLayout { - spacing: Nheko.paddingMedium Layout.alignment: Qt.AlignHCenter + spacing: Nheko.paddingMedium MatrixText { - text: !(roomPreview?.isFetched ?? false) ? qsTr("No preview available") : preview.roomName font.pixelSize: 24 + text: !(roomPreview?.isFetched ?? false) ? qsTr("No preview available") : preview.roomName } - ImageButton { + ToolTip.text: qsTr("Settings") + ToolTip.visible: hovered + hoverEnabled: true image: ":/icons/icons/ui/settings.svg" visible: !!room - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Settings") + onClicked: TimelineManager.openRoomSettings(room.roomId) } - } - RowLayout { - visible: !!room - spacing: Nheko.paddingMedium Layout.alignment: Qt.AlignHCenter + spacing: Nheko.paddingMedium + visible: !!room MatrixText { text: qsTr("%n member(s)", "", room ? room.roomMemberCount : 0) } - ImageButton { - image: ":/icons/icons/ui/people.svg" - hoverEnabled: true - ToolTip.visible: hovered ToolTip.text: qsTr("View members of %1").arg(room ? room.roomName : "") + ToolTip.visible: hovered + hoverEnabled: true + image: ":/icons/icons/ui/people.svg" + onClicked: TimelineManager.openRoomMembers(room) } - } - ScrollView { Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true @@ -262,54 +239,53 @@ Item { Layout.rightMargin: Nheko.paddingLarge TextArea { - text: (roomPreview?.isFetched ?? false) ? TimelineManager.escapeEmoji(preview.roomTopic) : qsTr("This room is possibly inaccessible. If this room is private, you should remove it from this community.") - wrapMode: TextEdit.WordWrap - textFormat: TextEdit.RichText - readOnly: true background: null - selectByMouse: true horizontalAlignment: TextEdit.AlignHCenter + readOnly: true + selectByMouse: true + text: (roomPreview?.isFetched ?? false) ? TimelineManager.escapeEmoji(preview.roomTopic) : qsTr("This room is possibly inaccessible. If this room is private, you should remove it from this community.") + textFormat: TextEdit.RichText + wrapMode: TextEdit.WordWrap + onLinkActivated: Nheko.openLink(link) CursorShape { anchors.fill: parent cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } - } - } - FlatButton { - visible: roomPreview && !roomPreview.isInvite Layout.alignment: Qt.AlignHCenter text: qsTr("join the conversation") + visible: roomPreview && !roomPreview.isInvite + onClicked: Rooms.joinPreview(roomPreview.roomid) } - FlatButton { - visible: roomPreview && roomPreview.isInvite Layout.alignment: Qt.AlignHCenter text: qsTr("accept invite") + visible: roomPreview && roomPreview.isInvite + onClicked: Rooms.acceptInvite(roomPreview.roomid) } - FlatButton { - visible: roomPreview && roomPreview.isInvite Layout.alignment: Qt.AlignHCenter text: qsTr("decline invite") + visible: roomPreview && roomPreview.isInvite + onClicked: Rooms.declineInvite(roomPreview.roomid) } - FlatButton { - visible: !!room Layout.alignment: Qt.AlignHCenter text: qsTr("leave") + visible: !!room + onClicked: TimelineManager.openLeaveRoomDialog(room.roomId) } - ScrollView { id: reasonField + property bool showReason: false Layout.alignment: Qt.AlignHCenter @@ -319,17 +295,15 @@ Item { visible: preview.reason !== "" && showReason TextArea { - text: TimelineManager.escapeEmoji(preview.reason) - wrapMode: TextEdit.WordWrap - textFormat: TextEdit.RichText - readOnly: true background: null - selectByMouse: true horizontalAlignment: TextEdit.AlignHCenter + readOnly: true + selectByMouse: true + text: TimelineManager.escapeEmoji(preview.reason) + textFormat: TextEdit.RichText + wrapMode: TextEdit.WordWrap } - } - Button { id: showReasonButton @@ -337,76 +311,94 @@ Item { //Layout.fillWidth: true Layout.leftMargin: Nheko.paddingLarge Layout.rightMargin: Nheko.paddingLarge - - visible: preview.reason !== "" text: reasonField.showReason ? qsTr("Hide invite reason") : qsTr("Show invite reason") + visible: preview.reason !== "" + onClicked: { reasonField.showReason = !reasonField.showReason; } } - Item { - visible: room != null Layout.preferredHeight: Math.ceil(fontMetrics.lineSpacing * 2) + visible: room != null } - Item { Layout.fillHeight: true } - } - ImageButton { id: backToRoomsButton - anchors.top: parent.top + ToolTip.text: qsTr("Back to room list") + ToolTip.visible: hovered anchors.left: parent.left anchors.margins: Nheko.paddingMedium - width: Nheko.avatarSize - height: Nheko.avatarSize - visible: (room == null || room.isSpace) && showBackButton + anchors.top: parent.top enabled: visible + height: Nheko.avatarSize image: ":/icons/icons/ui/angle-arrow-left.svg" - ToolTip.visible: hovered - ToolTip.text: qsTr("Back to room list") + visible: (room == null || room.isSpace) && showBackButton + width: Nheko.avatarSize + onClicked: Rooms.resetCurrentRoom() } - TimelineEffects { id: timelineEffects anchors.fill: parent } - NhekoDropArea { anchors.fill: parent roomid: room ? room.roomId : "" } - Timer { id: effectsTimer - onTriggered: shouldEffectsRun = false; + interval: timelineEffects.maxLifespan repeat: false running: false - } + onTriggered: shouldEffectsRun = false + } Connections { + function onConfetti() { + if (!Settings.fancyEffects) + return; + shouldEffectsRun = true; + timelineEffects.pulseConfetti(); + room.markSpecialEffectsDone(); + } + function onConfettiDone() { + if (!Settings.fancyEffects) + return; + effectsTimer.restart(); + } function onOpenReadReceiptsDialog(rr) { var dialog = readReceiptsDialog.createObject(timelineRoot, { - "readReceipts": rr, - "room": room - }); + "readReceipts": rr, + "room": room + }); dialog.show(); timelineRoot.destroyOnClose(dialog); } - + function onRainfall() { + if (!Settings.fancyEffects) + return; + shouldEffectsRun = true; + timelineEffects.pulseRainfall(); + room.markSpecialEffectsDone(); + } + function onRainfallDone() { + if (!Settings.fancyEffects) + return; + effectsTimer.restart(); + } function onShowRawMessageDialog(rawMessage) { - var component = Qt.createComponent("qrc:/qml/dialogs/RawMessageDialog.qml") + var component = Qt.createComponent("qrc:/qml/dialogs/RawMessageDialog.qml"); if (component.status == Component.Ready) { var dialog = component.createObject(timelineRoot, { - "rawMessage": rawMessage - }); + "rawMessage": rawMessage + }); dialog.show(); timelineRoot.destroyOnClose(dialog); } else { @@ -414,43 +406,6 @@ Item { } } - function onConfetti() - { - if (!Settings.fancyEffects) - return - - shouldEffectsRun = true; - timelineEffects.pulseConfetti() - room.markSpecialEffectsDone() - } - - function onConfettiDone() - { - if (!Settings.fancyEffects) - return - - effectsTimer.restart(); - } - - function onRainfall() - { - if (!Settings.fancyEffects) - return - - shouldEffectsRun = true; - timelineEffects.pulseRainfall() - room.markSpecialEffectsDone() - } - - function onRainfallDone() - { - if (!Settings.fancyEffects) - return - - effectsTimer.restart(); - } - target: room } - } diff --git a/resources/qml/ToggleButton.qml b/resources/qml/ToggleButton.qml index 66902bfd..f3bd5cce 100644 --- a/resources/qml/ToggleButton.qml +++ b/resources/qml/ToggleButton.qml @@ -11,17 +11,44 @@ Switch { id: toggleButton implicitWidth: indicatorItem.width - state: checked ? "on" : "off" + + indicator: Item { + id: indicatorItem + + implicitHeight: 24 + implicitWidth: 48 + y: parent.height / 2 - height / 2 + + Rectangle { + id: track + + color: Qt.rgba(border.color.r, border.color.g, border.color.b, 0.6) + height: parent.height * 0.6 + radius: height / 2 + width: parent.width - height + x: radius + y: parent.height / 2 - height / 2 + } + Rectangle { + id: handle + + border.color: "#767676" + color: palette.button + height: width + radius: width / 2 + width: parent.height * 0.9 + y: parent.height / 2 - height / 2 + } + } states: [ State { name: "off" PropertyChanges { - target: track border.color: "#767676" + target: track } - PropertyChanges { target: handle x: 0 @@ -31,10 +58,9 @@ Switch { name: "on" PropertyChanges { - target: track border.color: palette.highlight + target: track } - PropertyChanges { target: handle x: indicatorItem.width - handle.width @@ -43,55 +69,22 @@ Switch { ] transitions: [ Transition { - to: "off" reversible: true + to: "off" ParallelAnimation { NumberAnimation { - target: handle - property: "x" duration: 200 easing.type: Easing.InOutQuad + property: "x" + target: handle } - ColorAnimation { - target: track - properties: "color,border.color" duration: 200 + properties: "color,border.color" + target: track } } } ] - - indicator: Item { - id: indicatorItem - - implicitWidth: 48 - implicitHeight: 24 - y: parent.height / 2 - height / 2 - - Rectangle { - id: track - - height: parent.height * 0.6 - radius: height / 2 - width: parent.width - height - x: radius - y: parent.height / 2 - height / 2 - color: Qt.rgba(border.color.r, border.color.g, border.color.b, 0.6) - } - - Rectangle { - id: handle - - y: parent.height / 2 - height / 2 - width: parent.height * 0.9 - height: width - radius: width / 2 - color: palette.button - border.color: "#767676" - } - - } - } diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index a54c5ed7..3f2d8d2a 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -8,212 +8,142 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.2 import QtQuick.Window 2.15 import im.nheko 1.0 - import "./delegates" Pane { id: topBar - property bool showBackButton: false - property string roomName: room ? room.roomName : qsTr("No room selected") - property string roomId: room ? room.roomId : "" property string avatarUrl: room ? room.roomAvatarUrl : "" - property string roomTopic: room ? room.roomTopic : "" - property bool isEncrypted: room ? room.isEncrypted : false - property int trustlevel: room ? room.trustlevel : Crypto.Unverified - property bool isDirect: room ? room.isDirect : false property string directChatOtherUserId: room ? room.directChatOtherUserId : "" - + property bool isDirect: room ? room.isDirect : false + property bool isEncrypted: room ? room.isEncrypted : false + property string roomId: room ? room.roomId : "" + property string roomName: room ? room.roomName : qsTr("No room selected") + property string roomTopic: room ? room.roomTopic : "" property bool searchHasFocus: searchField.focus && searchField.enabled - property string searchString: "" - - // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu - Connections { - function onHideMenu() { - roomOptionsMenu.close() - } - target: MainWindow - } - - onRoomIdChanged: { - searchString = ""; - searchButton.searchActive = false; - searchField.text = "" - } - - Shortcut { - sequence: StandardKey.Find - onActivated: searchButton.searchActive = !searchButton.searchActive - } + property bool showBackButton: false + property int trustlevel: room ? room.trustlevel : Crypto.Unverified Layout.fillWidth: true implicitHeight: topLayout.height + Nheko.paddingMedium * 2 + padding: 0 z: 3 - padding: 0 background: Rectangle { color: palette.window } - - TapHandler { - onSingleTapped: { - if (eventPoint.position.y > topBar.height - (pinnedMessages.visible ? pinnedMessages.height : 0) - (widgets.visible ? widgets.height : 0)) { - eventPoint.accepted = true - return; - } - if (showBackButton && eventPoint.position.x < Nheko.paddingMedium + backToRoomsButton.width) { - eventPoint.accepted = true - return; - } - if (eventPoint.position.x > topBar.width - Nheko.paddingMedium - roomOptionsButton.width) { - eventPoint.accepted = true - return; - } - - if (communityLabel.visible && eventPoint.position.y < communityAvatar.height + Nheko.paddingMedium + Nheko.paddingSmall/2) { - if (!Communities.trySwitchToSpace(room.parentSpace.roomid)) - room.parentSpace.promptJoin(); - eventPoint.accepted = true - return; - } - - if (room) { - let p = topBar.mapToItem(roomTopicC, eventPoint.position.x, eventPoint.position.y); - let link = roomTopicC.linkAt(p.x, p.y); - - if (link) { - Nheko.openLink(link); - } else { - TimelineManager.openRoomSettings(room.roomId); - } - } - - eventPoint.accepted = true; - } - gesturePolicy: TapHandler.ReleaseWithinBounds - } - - HoverHandler { - grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything - } - contentItem: Item { GridLayout { id: topLayout anchors.left: parent.left - anchors.right: parent.right anchors.margins: Nheko.paddingMedium + anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter columnSpacing: Nheko.paddingSmall rowSpacing: Nheko.paddingSmall - Avatar { id: communityAvatar - visible: roomid && room.parentSpace.isLoaded && ("space:"+room.parentSpace.roomid != Communities.currentTagId) - property string avatarUrl: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomAvatarUrl) || "" property string communityId: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomid) || "" property string communityName: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomName) || "" + Layout.alignment: Qt.AlignRight Layout.column: 1 Layout.row: 0 - Layout.alignment: Qt.AlignRight - width: fontMetrics.lineSpacing - height: fontMetrics.lineSpacing - url: avatarUrl.replace("mxc://", "image://MxcImage/") - roomid: communityId displayName: communityName enabled: false + height: fontMetrics.lineSpacing + roomid: communityId + url: avatarUrl.replace("mxc://", "image://MxcImage/") + visible: roomid && room.parentSpace.isLoaded && ("space:" + room.parentSpace.roomid != Communities.currentTagId) + width: fontMetrics.lineSpacing } - Label { id: communityLabel - visible: communityAvatar.visible Layout.column: 2 - Layout.row: 0 Layout.fillWidth: true + Layout.row: 0 color: palette.text - text: qsTr("In %1").arg(communityAvatar.displayName) - maximumLineCount: 1 elide: Text.ElideRight + maximumLineCount: 1 + text: qsTr("In %1").arg(communityAvatar.displayName) textFormat: Text.RichText + visible: communityAvatar.visible } - ImageButton { id: backToRoomsButton - Layout.column: 0 - Layout.row: 1 - Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter + Layout.column: 0 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium - visible: showBackButton - image: ":/icons/icons/ui/angle-arrow-left.svg" - ToolTip.visible: hovered + Layout.row: 1 + Layout.rowSpan: 2 ToolTip.text: qsTr("Back to room list") + ToolTip.visible: hovered + image: ":/icons/icons/ui/angle-arrow-left.svg" + visible: showBackButton + onClicked: Rooms.resetCurrentRoom() } - Avatar { + Layout.alignment: Qt.AlignVCenter Layout.column: 1 Layout.row: 1 Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - width: Nheko.avatarSize - height: Nheko.avatarSize - url: avatarUrl.replace("mxc://", "image://MxcImage/") - roomid: roomId - userid: isDirect ? directChatOtherUserId : "" displayName: roomName enabled: false + height: Nheko.avatarSize + roomid: roomId + url: avatarUrl.replace("mxc://", "image://MxcImage/") + userid: isDirect ? directChatOtherUserId : "" + width: Nheko.avatarSize } - Label { - Layout.fillWidth: true Layout.column: 2 + Layout.fillWidth: true Layout.row: 1 color: palette.text - font.pointSize: fontMetrics.font.pointSize * 1.1 - font.bold: true - text: roomName - maximumLineCount: 1 elide: Text.ElideRight + font.bold: true + font.pointSize: fontMetrics.font.pointSize * 1.1 + maximumLineCount: 1 + text: roomName textFormat: Text.RichText } - MatrixText { id: roomTopicC - Layout.fillWidth: true + Layout.column: 2 - Layout.row: 2 + Layout.fillWidth: true Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines - selectByMouse: false - enabled: false + Layout.row: 2 clip: true + enabled: false + selectByMouse: false text: roomTopic } - ImageButton { id: pinButton property bool pinsShown: !Settings.hiddenPins.includes(roomId) - visible: !!room && room.pinnedMessages.length > 0 - Layout.column: 3 - Layout.row: 1 - Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter + Layout.column: 3 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium - image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg" - ToolTip.visible: hovered + Layout.row: 1 + Layout.rowSpan: 2 ToolTip.text: qsTr("Show or hide pinned messages") + ToolTip.visible: hovered + image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg" + visible: !!room && room.pinnedMessages.length > 0 + onClicked: { var ps = Settings.hiddenPins; if (pinsShown) { @@ -226,242 +156,280 @@ Pane { } Settings.hiddenPins = ps; } - } - AbstractButton { Layout.column: 4 - Layout.row: 1 - Layout.rowSpan: 2 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium + Layout.row: 1 + Layout.rowSpan: 2 + background: null contentItem: EncryptionIndicator { - encrypted: isEncrypted - trust: trustlevel - enabled: false - unencryptedIcon: ":/icons/icons/ui/people.svg" - unencryptedColor: palette.buttonText - unencryptedHoverColor: palette.highlight - hovered: parent.hovered - ToolTip.delay: Nheko.tooltipDelay ToolTip.text: { if (!isEncrypted) - return qsTr("Show room members."); - + return qsTr("Show room members."); switch (trustlevel) { - case Crypto.Verified: + case Crypto.Verified: return qsTr("This room contains only verified devices."); - case Crypto.TOFU: + case Crypto.TOFU: return qsTr("This room contains verified devices and devices which have never changed their master key."); - default: + default: return qsTr("This room contains unverified devices!"); } } + enabled: false + encrypted: isEncrypted + hovered: parent.hovered + trust: trustlevel + unencryptedColor: palette.buttonText + unencryptedHoverColor: palette.highlight + unencryptedIcon: ":/icons/icons/ui/people.svg" } - background: null onClicked: TimelineManager.openRoomMembers(room) } - ImageButton { id: searchButton property bool searchActive: false - visible: !!room - Layout.column: 5 - Layout.row: 1 - Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter + Layout.column: 5 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium - image: ":/icons/icons/ui/search.svg" - ToolTip.visible: hovered + Layout.row: 1 + Layout.rowSpan: 2 ToolTip.text: qsTr("Search this room") - onClicked: searchActive = !searchActive + ToolTip.visible: hovered + image: ":/icons/icons/ui/search.svg" + visible: !!room + onClicked: searchActive = !searchActive onSearchActiveChanged: { if (searchActive) { searchField.forceActiveFocus(); - } - else { + } else { searchField.clear(); topBar.searchString = ""; } } } - ImageButton { id: roomOptionsButton - visible: !!room - Layout.column: 6 - Layout.row: 1 - Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter + Layout.column: 6 Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium - image: ":/icons/icons/ui/options.svg" - ToolTip.visible: hovered + Layout.row: 1 + Layout.rowSpan: 2 ToolTip.text: qsTr("Room options") + ToolTip.visible: hovered + image: ":/icons/icons/ui/options.svg" + visible: !!room + onClicked: roomOptionsMenu.open(roomOptionsButton) Platform.Menu { id: roomOptionsMenu Platform.MenuItem { - visible: room ? room.permissions.canInvite() : false text: qsTr("Invite users") + visible: room ? room.permissions.canInvite() : false + onTriggered: TimelineManager.openInviteUsers(roomId) } - Platform.MenuItem { text: qsTr("Members") + onTriggered: TimelineManager.openRoomMembers(room) } - Platform.MenuItem { text: qsTr("Leave room") + onTriggered: TimelineManager.openLeaveRoomDialog(roomId) } - Platform.MenuItem { text: qsTr("Settings") + onTriggered: TimelineManager.openRoomSettings(roomId) } - } - } - ScrollView { id: pinnedMessages - Layout.row: 3 Layout.column: 2 Layout.columnSpan: 4 - Layout.fillWidth: true Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4) - - visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId) - clip: true - + Layout.row: 3 ScrollBar.horizontal.visible: false + clip: true + visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId) ListView { - - spacing: Nheko.paddingSmall model: room ? room.pinnedMessages : undefined + spacing: Nheko.paddingSmall + delegate: RowLayout { required property string modelData - width: ListView.view.width height: implicitHeight + width: ListView.view.width Reply { id: reply + property var e: room ? room.getDump(modelData, "pins") : {} - Connections { - function onPinnedMessagesChanged() { reply.e = room.getDump(modelData, "pins") } - target: room - } + Layout.fillWidth: true Layout.preferredHeight: height - - userColor: TimelineManager.userColor(e.userId, palette.window) blurhash: e.blurhash ?? "" body: e.body ?? "" - formattedBody: e.formattedBody ?? "" + encryptionError: e.encryptionError ?? 0 eventId: e.eventId ?? "" filename: e.filename ?? "" filesize: e.filesize ?? "" + formattedBody: e.formattedBody ?? "" + isOnlyEmoji: e.isOnlyEmoji ?? false + keepFullText: true + originalWidth: e.originalWidth ?? 0 proportionalHeight: e.proportionalHeight ?? 1 type: e.type ?? MtxEvent.UnknownMessage typeString: e.typeString ?? "" url: e.url ?? "" - originalWidth: e.originalWidth ?? 0 - isOnlyEmoji: e.isOnlyEmoji ?? false + userColor: TimelineManager.userColor(e.userId, palette.window) userId: e.userId ?? "" userName: e.userName ?? "" - encryptionError: e.encryptionError ?? 0 - keepFullText: true - } + Connections { + function onPinnedMessagesChanged() { + reply.e = room.getDump(modelData, "pins"); + } + + target: room + } + } ImageButton { id: deletePinButton + Layout.alignment: Qt.AlignTop | Qt.AlignLeft Layout.preferredHeight: 16 Layout.preferredWidth: 16 - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - visible: room.permissions.canChange(MtxEvent.PinnedEvents) - + ToolTip.text: qsTr("Unpin") + ToolTip.visible: hovered hoverEnabled: true image: ":/icons/icons/ui/dismiss.svg" - ToolTip.visible: hovered - ToolTip.text: qsTr("Unpin") + visible: room.permissions.canChange(MtxEvent.PinnedEvents) onClicked: room.unpin(modelData) } } - - } } - ScrollView { id: widgets - Layout.row: 4 Layout.column: 2 Layout.columnSpan: 4 - Layout.fillWidth: true Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 1.5) - - visible: !!room && room.widgetLinks.length > 0 && !Settings.hiddenWidgets.includes(roomId) - clip: true - + Layout.row: 4 ScrollBar.horizontal.visible: false + clip: true + visible: !!room && room.widgetLinks.length > 0 && !Settings.hiddenWidgets.includes(roomId) ListView { - - spacing: Nheko.paddingSmall model: room ? room.widgetLinks : undefined + spacing: Nheko.paddingSmall + delegate: MatrixText { required property var modelData color: palette.text text: modelData } - - } } - MatrixTextField { id: searchField - visible: searchButton.searchActive - enabled: visible - hasClear: true - Layout.row: 5 Layout.column: 2 Layout.columnSpan: 4 - Layout.fillWidth: true - + Layout.row: 5 + enabled: visible + hasClear: true placeholderText: qsTr("Enter search query") + visible: searchButton.searchActive + onAccepted: topBar.searchString = text } } - CursorShape { - anchors.fill: parent anchors.bottomMargin: (pinnedMessages.visible ? pinnedMessages.height : 0) + (widgets.visible ? widgets.height : 0) + anchors.fill: parent cursorShape: Qt.PointingHandCursor } } + + onRoomIdChanged: { + searchString = ""; + searchButton.searchActive = false; + searchField.text = ""; + } + + // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu + Connections { + function onHideMenu() { + roomOptionsMenu.close(); + } + + target: MainWindow + } + Shortcut { + sequence: StandardKey.Find + + onActivated: searchButton.searchActive = !searchButton.searchActive + } + TapHandler { + gesturePolicy: TapHandler.ReleaseWithinBounds + + onSingleTapped: { + if (eventPoint.position.y > topBar.height - (pinnedMessages.visible ? pinnedMessages.height : 0) - (widgets.visible ? widgets.height : 0)) { + eventPoint.accepted = true; + return; + } + if (showBackButton && eventPoint.position.x < Nheko.paddingMedium + backToRoomsButton.width) { + eventPoint.accepted = true; + return; + } + if (eventPoint.position.x > topBar.width - Nheko.paddingMedium - roomOptionsButton.width) { + eventPoint.accepted = true; + return; + } + if (communityLabel.visible && eventPoint.position.y < communityAvatar.height + Nheko.paddingMedium + Nheko.paddingSmall / 2) { + if (!Communities.trySwitchToSpace(room.parentSpace.roomid)) + room.parentSpace.promptJoin(); + eventPoint.accepted = true; + return; + } + if (room) { + let p = topBar.mapToItem(roomTopicC, eventPoint.position.x, eventPoint.position.y); + let link = roomTopicC.linkAt(p.x, p.y); + if (link) { + Nheko.openLink(link); + } else { + TimelineManager.openRoomSettings(room.roomId); + } + } + eventPoint.accepted = true; + } + } + HoverHandler { + grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything + } } diff --git a/resources/qml/TypingIndicator.qml b/resources/qml/TypingIndicator.qml index 704fe8ef..b6c502d8 100644 --- a/resources/qml/TypingIndicator.qml +++ b/resources/qml/TypingIndicator.qml @@ -8,30 +8,28 @@ import QtQuick.Layouts 1.2 import im.nheko 1.0 Item { - implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height) Layout.fillWidth: true + implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height) Rectangle { id: typingRect - visible: (room && room.typingUsers.length > 0) - color: palette.base anchors.fill: parent + color: palette.base + visible: (room && room.typingUsers.length > 0) z: 3 Label { id: typingDisplay + anchors.bottom: parent.bottom anchors.left: parent.left anchors.leftMargin: 10 anchors.right: parent.right anchors.rightMargin: 10 - anchors.bottom: parent.bottom color: palette.text text: room ? room.formatTypingUsers(room.typingUsers, palette.base) : "" textFormat: Text.RichText } - } - } diff --git a/resources/qml/UploadBox.qml b/resources/qml/UploadBox.qml index ccec6131..54007163 100644 --- a/resources/qml/UploadBox.qml +++ b/resources/qml/UploadBox.qml @@ -4,7 +4,6 @@ import "./components" import "./ui" - import QtQuick 2.9 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.3 @@ -12,31 +11,33 @@ import im.nheko 1.0 Page { id: uploadPopup - visible: room && room.input.uploads.length > 0 - Layout.preferredHeight: 200 - clip: true Layout.fillWidth: true - + Layout.preferredHeight: 200 + clip: true padding: Nheko.paddingMedium + visible: room && room.input.uploads.length > 0 + background: Rectangle { + color: palette.base + } contentItem: ListView { id: uploadsList + anchors.horizontalCenter: parent.horizontalCenter boundsBehavior: Flickable.StopAtBounds + model: room ? room.input.uploads : undefined + orientation: ListView.Horizontal + spacing: Nheko.paddingMedium + width: Math.min(contentWidth, parent.availableWidth) ScrollBar.horizontal: ScrollBar { id: scr + } - - orientation: ListView.Horizontal - width: Math.min(contentWidth, parent.availableWidth) - model: room ? room.input.uploads : undefined - spacing: Nheko.paddingMedium - delegate: Pane { + height: uploadPopup.availableHeight - buttons.height - (scr.visible ? scr.height : 0) padding: Nheko.paddingSmall - height: uploadPopup.availableHeight - buttons.height - (scr.visible? scr.height : 0) width: uploadPopup.availableHeight - buttons.height background: Rectangle { @@ -45,46 +46,48 @@ Page { } contentItem: ColumnLayout { Image { + property string typeStr: switch (modelData.mediaType) { + case MediaUpload.Video: + return "video-file"; + case MediaUpload.Audio: + return "music"; + case MediaUpload.Image: + return "image"; + default: + return "zip"; + } + Layout.fillHeight: true Layout.fillWidth: true - + fillMode: Image.PreserveAspectFit + mipmap: true + smooth: true + source: (modelData.thumbnail != "") ? modelData.thumbnail : ("image://colorimage/:/icons/icons/ui/" + typeStr + ".svg?" + palette.buttonText) sourceSize.height: parent.availableHeight - namefield.height sourceSize.width: parent.availableWidth - fillMode: Image.PreserveAspectFit - smooth: true - mipmap: true - - property string typeStr: switch(modelData.mediaType) { - case MediaUpload.Video: return "video-file"; - case MediaUpload.Audio: return "music"; - case MediaUpload.Image: return "image"; - default: return "zip"; - } - source: (modelData.thumbnail != "") ? modelData.thumbnail : ("image://colorimage/:/icons/icons/ui/"+typeStr+".svg?" + palette.buttonText) } MatrixTextField { id: namefield + Layout.fillWidth: true text: modelData.filename + onTextEdited: modelData.filename = text } } } } - footer: DialogButtonBox { id: buttons standardButtons: DialogButtonBox.Cancel - Button { - text: qsTr("Upload %n file(s)", "", (room ? room.input.uploads.length : 0)) - DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole - } + onAccepted: room.input.acceptUploads() onRejected: room.input.declineUploads() - } - background: Rectangle { - color: palette.base + Button { + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + text: qsTr("Upload %n file(s)", "", (room ? room.input.uploads.length : 0)) + } } }