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 string url
- property string userid
- property string roomid
+ property alias color: bg.color
+ property bool crop: true
property string displayName
+ property string roomid
property alias textColor: label.color
- property bool crop: true
- property alias color: bg.color
+ property string url
+ property string userid
- 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,195 +41,180 @@ Page {
ScrollBar.vertical: ScrollBar {
id: scrollbar
- parent: !collapsed && Settings.scrollbarsInRoomlist ? communitiesList : null
- }
-
- Platform.Menu {
- id: communityContextMenu
-
- property string tagId
- property bool hidden
- property bool muted
-
- function show(id_, hidden_, muted_) {
- tagId = id_;
- hidden = hidden_;
- muted = muted_;
- open();
- }
-
- Platform.MenuItem {
- text: qsTr("Do not show notification counts for this community or tag.")
- checkable: true
- checked: communityContextMenu.muted
- 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
- onTriggered: Communities.toggleTagId(communityContextMenu.tagId)
- }
+ parent: !collapsed && Settings.scrollbarsInRoomlist ? communitiesList : null
}
-
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
+ 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
- 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)
+ 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 {
- target: communityItem
backgroundColor: palette.dark
- importantText: palette.brightText
- unimportantText: palette.brightText
bubbleBackground: palette.highlight
bubbleText: palette.highlightedText
+ importantText: palette.brightText
+ target: communityItem
+ unimportantText: palette.brightText
}
-
},
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
+ 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
- onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted)
- gesturePolicy: TapHandler.ReleaseWithinBounds
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
- }
+ gesturePolicy: TapHandler.ReleaseWithinBounds
+ onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted)
+ }
}
-
RowLayout {
id: r
- spacing: Nheko.paddingMedium
+
anchors.fill: parent
- anchors.margins: Nheko.paddingMedium
anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * model.depth))
+ anchors.margins: Nheko.paddingMedium
+ spacing: Nheko.paddingMedium
ImageButton {
- visible: !communitySidebar.collapsed && model.collapsible
+ Layout.alignment: Qt.AlignVCenter
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")
+ 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
- enabled: false
Layout.alignment: Qt.AlignVCenter
+ color: communityItem.backgroundColor
+ displayName: model.displayName
+ enabled: false
height: avatarSize
- width: 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;
}
- roomid: model.id
- displayName: model.displayName
- color: communityItem.backgroundColor
+ width: avatarSize
NotificationBubble {
- notificationCount: model.unreadMessages
- hasLoudNotification: model.hasLoudNotification
+ 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
- anchors.right: avatar.right
- anchors.bottom: avatar.bottom
- anchors.margins: -Nheko.paddingSmall
+ notificationCount: model.unreadMessages
}
-
}
-
ElidedLabel {
- visible: !communitySidebar.collapsed
Layout.alignment: Qt.AlignVCenter
- color: communityItem.importantText
Layout.fillWidth: true
+ color: communityItem.importantText
elideWidth: width
fullText: model.displayName
textFormat: Text.PlainText
+ visible: !communitySidebar.collapsed
}
-
Item {
Layout.fillWidth: true
}
-
NotificationBubble {
- notificationCount: model.unreadMessages
- hasLoudNotification: model.hasLoudNotification
+ Layout.alignment: Qt.AlignRight
+ Layout.leftMargin: Nheko.paddingSmall
bubbleBackgroundColor: communityItem.bubbleBackground
bubbleTextColor: communityItem.bubbleText
+ hasLoudNotification: model.hasLoudNotification
mayBeVisible: !communitySidebar.collapsed && !model.muted && Settings.spaceNotifications
- Layout.alignment: Qt.AlignRight
- Layout.leftMargin: Nheko.paddingSmall
+ notificationCount: model.unreadMessages
}
-
}
+ }
- background: Rectangle {
- color: communityItem.backgroundColor
+ Platform.Menu {
+ id: communityContextMenu
+
+ property bool hidden
+ property bool muted
+ property string tagId
+
+ function show(id_, hidden_, muted_) {
+ tagId = id_;
+ hidden = hidden_;
+ muted = muted_;
+ open();
}
- }
+ Platform.MenuItem {
+ checkable: true
+ checked: communityContextMenu.muted
+ text: qsTr("Do not show notification counts for this community or tag.")
- }
+ onTriggered: Communities.toggleTagMute(communityContextMenu.tagId)
+ }
+ Platform.MenuItem {
+ checkable: true
+ checked: communityContextMenu.hidden
+ text: qsTr("Hide rooms with this tag or from this community by default.")
- background: Rectangle {
- color: Nheko.theme.sidebarBackground
+ onTriggered: Communities.toggleTagId(communityContextMenu.tagId)
+ }
+ }
}
-
}
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_();
+ function changeCompleter() {
+ if (completerName) {
+ completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : (popup.roomId != "" ? popup.roomId : room.roomId));
+ completer.setSearchString("");
+ } else {
+ completer = undefined;
+ }
+ currentIndex = -1;
+ }
+ function currentCompletion() {
+ if (currentIndex > -1 && currentIndex < listView.count)
+ return completer.completionAt(currentIndex);
else
- up_();
+ return null;
}
-
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 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
- background: Rectangle {
- color: palette.base
- border.color: palette.mid
+ interval: 50
+ }
}
+ 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
-
- Timer {
- id: timer
- interval: 350
- onTriggered: editingFinished()
- }
-
- onTextChanged: timer.restart()
+ property alias text: input.text
+ property alias textPadding: input.padding
- 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
+ onTextChanged: timer.restart()
+
+ Timer {
+ id: timer
+
+ interval: 350
+
+ onTriggered: editingFinished()
+ }
Item {
+ Layout.bottomMargin: Nheko.paddingSmall
Layout.fillWidth: true
- Layout.preferredHeight: labelC.contentHeight
Layout.margins: input.padding
- Layout.bottomMargin: Nheko.paddingSmall
+ 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
- properties: "y"
+ alwaysRunToEnd: true
duration: 210
easing.type: Easing.InCubic
- alwaysRunToEnd: true
+ properties: "y"
+ 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
+ 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
+ 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"
- ToolTip.visible: hovered
- ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call"))
- Layout.margins: 8
+ 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
+ Layout.margins: 8
+ ToolTip.text: qsTr("Send a file")
+ ToolTip.visible: hovered
height: 22
+ hoverEnabled: true
image: ":/icons/icons/ui/attach.svg"
- Layout.margins: 8
+ visible: showAllButtons
+ width: 22
+
onClicked: room.input.openFileSelection()
- ToolTip.visible: hovered
- ToolTip.text: qsTr("Send a file")
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
+ selectByMouse: true
+ topPadding: 8
verticalAlignment: TextEdit.AlignVCenter
+ width: textInput.width
wrapMode: TextEdit.Wrap
- padding: 0
- 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 ;
-
- 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
- x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x
- y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height
-
background: null
padding: 0
-
- Completer {
- anchors.fill: parent
- id: completer
- rowMargin: 2
- rowSpacing: 0
- }
+ x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x
+ y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height
enter: Transition {
NumberAnimation {
- property: "opacity"
+ duration: 100
from: 0
+ property: "opacity"
to: 1
- duration: 100
}
-
}
-
exit: Transition {
NumberAnimation {
- property: "opacity"
+ duration: 100
from: 1
+ property: "opacity"
to: 0
- duration: 100
}
}
- }
+ Completer {
+ id: completer
+
+ anchors.fill: parent
+ rowMargin: 2
+ rowSpacing: 0
+ }
+ }
Connections {
function onTextChanged(newText) {
messageInput.text = newText;
@@ -380,16 +357,13 @@ Rectangle {
ignoreUnknownSignals: true
target: room ? room.input : null
}
-
Connections {
- function onReplyChanged() {
+ function onEditChanged() {
messageInput.forceActiveFocus();
}
-
- function onEditChanged() {
+ 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
+ ToolTip.text: qsTr("Stickers")
+ ToolTip.visible: hovered
height: 22
+ hoverEnabled: true
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();
- })
+ 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
+ ToolTip.text: qsTr("Emoji")
+ ToolTip.visible: hovered
height: 22
+ hoverEnabled: true
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();
- })
+ 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
+ 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)
- border.width: 1
- border.color: bubbleColor
radius: 3
- anchors.fill: parent
- anchors.margins: visible ? Nheko.paddingSmall : 0
+ 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
+
+ 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
+ }
+ }
+
+ Window.onActiveChanged: readTimer.running = Window.active
onCountChanged: {
// Mark timeline as read
- if (atYEnd && room) model.currentIndex = 0;
+ if (atYEnd && room)
+ model.currentIndex = 0;
}
- ScrollBar.vertical: scrollbar
-
- 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
+ implicitWidth: showImage ? 16 : buttonText.implicitWidth
+ width: showImage ? 16 : buttonText.implicitWidth
+
+ onClicked: {
+ room.input.reaction(row.model.eventId, modelData);
+ TimelineManager.focusMessageInput();
+ }
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
+ 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
- fillMode: Image.PreserveAspectFit
}
-
CursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
-
Ripple {
color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5)
}
-
- onClicked: {
- room.input.reaction(row.model.eventId, modelData);
- TimelineManager.focusMessageInput();
- }
}
-
}
-
ImageButton {
- visible: !!row.model && row.model.isEditable
+ ToolTip.delay: Nheko.tooltipDelay
+ ToolTip.text: qsTr("Edit")
+ ToolTip.visible: hovered
buttonTextColor: palette.buttonText
- width: 16
hoverEnabled: true
image: ":/icons/icons/ui/edit.svg"
- ToolTip.visible: hovered
- ToolTip.delay: Nheko.tooltipDelay
- ToolTip.text: qsTr("Edit")
+ 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();
- })
- }
+ 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 {
- 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
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread")
+ 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
+
onClicked: room.thread = (row.model.threadId || row.model.eventId)
}
-
ImageButton {
- visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
- width: 16
- hoverEnabled: true
- image: ":/icons/icons/ui/reply.svg"
- ToolTip.visible: hovered
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
+ ToolTip.delay: Nheko.tooltipDelay
+ ToolTip.text: qsTr("Go to message")
+ ToolTip.visible: hovered
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")
+ 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/")
+ 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)
- ToolTip.visible: messageUserAvatar.hovered
- ToolTip.delay: Nheko.tooltipDelay
- ToolTip.text: 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
+
+ ToolTip.delay: Nheko.tooltipDelay
+ ToolTip.text: userId
+ ToolTip.visible: hovered
+ leftInset: 0
+ leftPadding: 0
+ rightInset: 0
+ rightPadding: 0
+
contentItem: ElidedLabel {
id: userName_
- fullText: 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
- 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)
- leftInset: 0
- rightInset: 0
- leftPadding: 0
- rightPadding: 0
CursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
-
}
-
Label {
id: statusMsg
+
+ property string userStatus: Presence.userStatus(userId)
+
+ ToolTip.delay: Nheko.tooltipDelay
+ ToolTip.text: qsTr("%1's status message").arg(userName)
+ ToolTip.visible: statusMsgHoverHandler.hovered
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)
- ToolTip.text: qsTr("%1's status message").arg(userName)
- ToolTip.visible: statusMsgHoverHandler.hovered
- ToolTip.delay: Nheko.tooltipDelay
+ 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);
- }
- }
- }
-
- }
-
- }
-
- }
-
- 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;
- }
- }
- }
- 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()
+ if (id == userId)
+ statusMsg.userStatus = Presence.userStatus(userId);
}
+ target: Presence
}
-
}
-
}
}
-
- 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 string link
- property string text
property int eventType
- property bool isEncrypted
property bool isEditable
+ property bool isEncrypted
property bool isSender
+ property string link
+ property string text
+ 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
+ property: "visible"
+ target: screenSaver
}
-
NumberAnimation {
- target: screenSaver
- property: "opacity"
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
- x: Math.round(parent.width / 2 - contentWidth / 2)
- y: Math.round(parent.height / 4)
- modal: true
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)
+
+ 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
+ ToolTip.text: qsTr("Cancel Thread")
+ ToolTip.visible: closeThreadButton.hovered
anchors.margins: 8
+ anchors.right: parent.right
anchors.top: parent.top
- hoverEnabled: true
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
- height: 22
- ToolTip.visible: closeThreadButton.hovered
- ToolTip.text: qsTr("Cancel Thread")
+
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,264 +18,431 @@ 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 {
- }
+ background: Rectangle {
+ color: Nheko.theme.sidebarBackground
}
+ footer: ColumnLayout {
+ spacing: 0
- Component {
- id: createDirectComponent
-
- CreateDirect {
+ Rectangle {
+ Layout.fillWidth: true
+ color: Nheko.theme.separator
+ height: 1
}
- }
-
- ListView {
- id: roomlist
+ Pane {
+ Layout.alignment: Qt.AlignBottom
+ Layout.fillWidth: true
+ Layout.minimumHeight: 40
+ horizontalPadding: Nheko.paddingMedium
+ verticalPadding: 0
- anchors.left: parent.left
- anchors.right: parent.right
- height: parent.height
- model: Rooms
- //reuseItems: true
+ background: Rectangle {
+ color: palette.window
+ }
+ contentItem: RowLayout {
+ id: buttonRow
- ScrollBar.vertical: ScrollBar {
- id: scrollbar
- parent: !collapsed && Settings.scrollbarsInRoomlist ? roomlist : null
- }
+ 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
- Connections {
- function onCurrentRoomChanged() {
- if (Rooms.currentRoom)
- roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
+ onClicked: roomJoinCreateMenu.open(parent)
- }
+ Platform.Menu {
+ id: roomJoinCreateMenu
- target: Rooms
- }
+ Platform.MenuItem {
+ text: qsTr("Join a room")
- Component {
- id: roomWindowComponent
+ onTriggered: Nheko.openJoinRoomDialog()
+ }
+ Platform.MenuItem {
+ text: qsTr("Create a new room")
- ApplicationWindow {
- id: roomWindowW
+ onTriggered: {
+ var createRoom = createRoomComponent.createObject(timelineRoot);
+ createRoom.show();
+ timelineRoot.destroyOnClose(createRoom);
+ }
+ }
+ Platform.MenuItem {
+ text: qsTr("Start a direct chat")
- property var room: null
- property var roomPreview: null
+ onTriggered: {
+ var createDirect = createDirectComponent.createObject(timelineRoot);
+ createDirect.show();
+ timelineRoot.destroyOnClose(createDirect);
+ }
+ }
+ Platform.MenuItem {
+ text: qsTr("Create a new community")
- Component.onCompleted: {
- MainWindow.addPerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW);
- Nheko.setTransientParent(roomWindowW, null);
+ onTriggered: {
+ var createRoom = createRoomComponent.createObject(timelineRoot, {
+ "space": true
+ });
+ createRoom.show();
+ timelineRoot.destroyOnClose(createRoom);
+ }
+ }
+ }
}
- 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
+ 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
- Shortcut {
- sequence: StandardKey.Cancel
- onActivated: roomWindowW.close()
+ 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
- TimelineView {
- id: timeline
+ 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
- privacyScreen: privacyScreen
- anchors.fill: parent
- room: roomWindowW.room
- roomPreview: roomWindowW.roomPreview.roomid ? roomWindowW.roomPreview : null
+ onClicked: mainWindow.push(userSettingsPage)
}
+ }
+ }
+ }
+ header: ColumnLayout {
+ spacing: 0
- PrivacyScreen {
- id: privacyScreen
+ Pane {
+ id: userInfoPanel
- anchors.fill: parent
- visible: Settings.privacyScreen
- screenTimeout: Settings.privacyScreenTimeout
- timelineRoot: timeline
- windowTarget: roomWindowW
+ function openUserProfile() {
+ Nheko.updateUserProfile();
+ var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml");
+ if (component.status == Component.Ready) {
+ var userProfile = component.createObject(timelineRoot, {
+ "profile": Nheko.currentUser
+ });
+ userProfile.show();
+ timelineRoot.destroyOnClose(userProfile);
+ } else {
+ console.error("Failed to create component: " + component.errorString());
}
+ }
- onActiveChanged: { room.lastReadIdOnWindowFocus(); }
+ Layout.alignment: Qt.AlignBottom
+ Layout.fillWidth: true
+ Layout.minimumHeight: 40
+ //Layout.preferredHeight: userInfoGrid.implicitHeight + 2 * Nheko.paddingMedium
+ padding: Nheko.paddingMedium
+
+ background: Rectangle {
+ color: palette.window
}
+ contentItem: RowLayout {
+ id: userInfoGrid
- }
+ property var profile: Nheko.currentUser
+ spacing: Nheko.paddingMedium
- Component {
- id: nestedSpaceMenuLevel
+ Avatar {
+ id: avatar
- SpaceMenuLevel {
- roomid: roomContextMenu.roomid
- childMenu: rootSpaceMenu.childMenu
- }
- }
+ Layout.alignment: Qt.AlignVCenter
+ Layout.preferredHeight: fontMetrics.lineSpacing * 2
+ Layout.preferredWidth: fontMetrics.lineSpacing * 2
+ displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
+ enabled: false
+ url: (userInfoGrid.profile ? userInfoGrid.profile.avatarUrl : "").replace("mxc://", "image://MxcImage/")
+ userid: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
+ }
+ ColumnLayout {
+ id: col
+ Layout.alignment: Qt.AlignLeft
+ Layout.fillWidth: true
+ 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
- Platform.Menu {
- id: roomContextMenu
+ 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 : ""
+ }
+ ElidedLabel {
+ Layout.alignment: Qt.AlignTop
+ color: palette.buttonText
+ elideWidth: col.width
+ font.pointSize: fontMetrics.font.pointSize * 0.9
+ fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
+ }
+ }
+ Item {
+ }
+ ImageButton {
+ id: logoutButton
- property string roomid
- property var tags
+ Layout.alignment: Qt.AlignVCenter
+ Layout.preferredHeight: fontMetrics.lineSpacing * 2
+ 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
- function show(roomid_, tags_) {
- roomid = roomid_;
- tags = tags_;
- open();
+ onClicked: Nheko.openLogoutDialog()
+ }
}
InputDialog {
- id: newTag
+ id: statusDialog
- title: qsTr("New tag")
- prompt: qsTr("Enter the tag you want to use:")
- onAccepted: function(text) {
- Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true);
+ prompt: qsTr("Enter your status message:")
+ title: qsTr("Status Message")
+
+ onAccepted: function (text) {
+ Nheko.setStatusMessage(text);
}
}
+ Platform.Menu {
+ id: userInfoMenu
- 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("Profile settings")
+
+ onTriggered: userInfoPanel.openUserProfile()
}
- }
+ Platform.MenuItem {
+ text: qsTr("Set status message")
- Platform.MenuItem {
- text: qsTr("Room settings")
- onTriggered: TimelineManager.openRoomSettings(roomContextMenu.roomid)
+ onTriggered: statusDialog.show()
+ }
}
+ TapHandler {
+ acceptedButtons: Qt.LeftButton
+ gesturePolicy: TapHandler.ReleaseWithinBounds
+ margin: -Nheko.paddingSmall
- Platform.MenuItem {
- text: qsTr("Leave room")
- onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid)
+ onLongPressed: userInfoMenu.open()
+ onSingleTapped: userInfoPanel.openUserProfile()
}
+ TapHandler {
+ acceptedButtons: Qt.RightButton
+ gesturePolicy: TapHandler.ReleaseWithinBounds
+ margin: -Nheko.paddingSmall
- Platform.MenuItem {
- text: qsTr("Copy room link")
- onTriggered: Rooms.copyLink(roomContextMenu.roomid)
+ onSingleTapped: userInfoMenu.open()
}
+ }
+ Rectangle {
+ Layout.fillWidth: true
+ color: Nheko.theme.separator
+ height: 2
+ }
+ Rectangle {
+ id: unverifiedStuffBubble
- Platform.Menu {
- id: tagsMenu
- title: qsTr("Tag room as:")
+ 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
- Instantiator {
- model: Communities.tagsWithDefault
- onObjectAdded: (index, object) => tagsMenu.insertItem(index, object)
- onObjectRemoved: (index, object) => tagsMenu.removeItem(object)
+ RowLayout {
+ id: unverifiedStuffBubbleContainer
- delegate: Platform.MenuItem {
- property string t: modelData
+ height: explanation.height + Nheko.paddingMedium * 2
+ spacing: 0
+ width: parent.width
- 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);
- }
+ Label {
+ id: explanation
+
+ Layout.fillWidth: true
+ Layout.margins: Nheko.paddingMedium
+ Layout.rightMargin: Nheko.paddingSmall
+ color: palette.buttonText
+ text: {
+ switch (SelfVerificationStatus.status) {
+ case SelfVerificationStatus.NoMasterKey:
+ //: Cross-signing setup has not run yet.
+ return qsTr("Encryption not set up");
+ case SelfVerificationStatus.UnverifiedMasterKey:
+ //: The user just signed in with this device and hasn't verified their master key.
+ return qsTr("Unverified login");
+ case SelfVerificationStatus.UnverifiedDevices:
+ //: There are unverified devices signed in to this account.
+ return qsTr("Please verify your other devices");
+ default:
+ return "";
}
- checkable: true
- checked: roomContextMenu.tags !== undefined && roomContextMenu.tags.includes(t)
- onTriggered: Rooms.toggleTag(roomContextMenu.roomid, t, checked)
}
-
+ textFormat: Text.PlainText
+ wrapMode: Text.Wrap
}
+ ImageButton {
+ id: closeUnverifiedBubble
- Platform.MenuItem {
- text: qsTr("Create new tag...")
- onTriggered: newTag.show()
+ Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+ 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
- SpaceMenuLevel {
- id: rootSpaceMenu
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
+ enabled: !closeUnverifiedBubble.hovered
+ }
+ TapHandler {
+ acceptedButtons: Qt.LeftButton
+ enabled: !closeUnverifiedBubble.hovered
- roomid: roomContextMenu.roomid
- position: -1
- title: qsTr("Add or remove from community...")
- childMenu: nestedSpaceMenuLevel
+ onSingleTapped: {
+ if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedDevices)
+ SelfVerificationStatus.verifyUnverifiedDevices();
+ else
+ SelfVerificationStatus.statusChanged();
+ }
}
}
+ Rectangle {
+ Layout.fillWidth: true
+ color: Nheko.theme.separator
+ height: 1
+ visible: unverifiedStuffBubble.visible
+ }
+ }
+
+ // 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
+ }
delegate: ItemDelegate {
id: roomItem
+ required property string avatarUrl
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 string directChatOtherUserId
required property bool hasLoudNotification
required property bool hasUnreadMessages
+ property color importantText: palette.text
required property bool isDirect
- required property string directChatOtherUserId
-
- Ripple {
- color: Qt.rgba(palette.dark.r, palette.dark.g, palette.dark.b, 0.5)
- }
+ 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
- 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);
+ 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 {
@@ -283,31 +450,45 @@ Page {
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
+ importantText: palette.brightText
+ target: roomItem
+ unimportantText: palette.brightText
}
-
},
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
+ importantText: palette.highlightedText
+ target: roomItem
+ unimportantText: palette.highlightedText
}
-
}
]
+ 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
@@ -315,76 +496,71 @@ Page {
TapHandler {
acceptedButtons: Qt.RightButton
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
+ gesturePolicy: TapHandler.ReleaseWithinBounds
+
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
+ spacing: Nheko.paddingMedium
Avatar {
id: avatar
- enabled: false
Layout.alignment: Qt.AlignVCenter
+ displayName: roomName
+ enabled: false
height: avatarSize
- width: avatarSize
+ roomid: roomId
url: avatarUrl.replace("mxc://", "image://MxcImage/")
- displayName: roomName
userid: isDirect ? directChatOtherUserId : ""
- roomid: roomId
+ width: avatarSize
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
+ 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
- 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
+ visible: !collapsed
+ width: parent.width - avatar.width
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
+ 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
@@ -394,433 +570,216 @@ Page {
ElidedLabel {
id: rN
+
Layout.alignment: Qt.AlignBaseline
+ Layout.fillWidth: true
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
+ 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
- height: visible ? 0 : undefined
- Layout.alignment: Qt.AlignBottom
ElidedLabel {
+ Layout.fillWidth: true
color: roomItem.unimportantText
- font.pixelSize: fontMetrics.font.pixelSize * 0.9
elideWidth: width
+ font.pixelSize: fontMetrics.font.pixelSize * 0.9
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
+ height: parent.height - Nheko.paddingSmall * 2
visible: hasUnreadMessages
+ width: 3
}
-
- background: Rectangle {
- color: backgroundColor
- }
-
}
- }
-
- background: Rectangle {
- color: Nheko.theme.sidebarBackground
- }
-
- header: ColumnLayout {
- spacing: 0
-
- Pane {
- id: userInfoPanel
-
- function openUserProfile() {
- Nheko.updateUserProfile();
- var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml")
- if (component.status == Component.Ready) {
- var userProfile = component.createObject(timelineRoot, {"profile": Nheko.currentUser});
- userProfile.show();
- timelineRoot.destroyOnClose(userProfile);
- } else {
- console.error("Failed to create component: " + component.errorString());
- }
+ Connections {
+ function onCurrentRoomChanged() {
+ if (Rooms.currentRoom)
+ roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
}
+ target: Rooms
+ }
+ Component {
+ id: roomWindowComponent
- Layout.fillWidth: true
- Layout.alignment: Qt.AlignBottom
- //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);
- }
- }
+ ApplicationWindow {
+ id: roomWindowW
- Platform.Menu {
- id: userInfoMenu
+ property var room: null
+ property var roomPreview: null
- Platform.MenuItem {
- text: qsTr("Profile settings")
- onTriggered: userInfoPanel.openUserProfile()
- }
+ color: palette.window
+ height: 650
+ minimumHeight: 150
+ minimumWidth: 150
+ title: room.plainRoomName
+ width: 420
- Platform.MenuItem {
- text: qsTr("Set status message")
- onTriggered: statusDialog.show()
+ Component.onCompleted: {
+ MainWindow.addPerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW);
+ Nheko.setTransientParent(roomWindowW, null);
}
-
- }
-
- 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
-
- property var profile: Nheko.currentUser
-
- spacing: Nheko.paddingMedium
-
- Avatar {
- 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/")
- displayName: userInfoGrid.profile ? userInfoGrid.profile.displayName : ""
- userid: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
- enabled: false
+ Component.onDestruction: MainWindow.removePerRoomWindow(room.roomId || roomPreview.roomid, roomWindowW)
+ onActiveChanged: {
+ room.lastReadIdOnWindowFocus();
}
- 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
-
- ElidedLabel {
- Layout.alignment: Qt.AlignBottom
- 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
- fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
- }
+ //flags: Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowTitleHint
+ Shortcut {
+ sequence: StandardKey.Cancel
+ onActivated: roomWindowW.close()
}
+ TimelineView {
+ id: timeline
- Item {
+ anchors.fill: parent
+ privacyScreen: privacyScreen
+ room: roomWindowW.room
+ roomPreview: roomWindowW.roomPreview.roomid ? roomWindowW.roomPreview : null
}
+ PrivacyScreen {
+ id: privacyScreen
- 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
- ToolTip.delay: Nheko.tooltipDelay
- ToolTip.text: qsTr("Logout")
- onClicked: Nheko.openLogoutDialog()
+ anchors.fill: parent
+ screenTimeout: Settings.privacyScreenTimeout
+ timelineRoot: timeline
+ visible: Settings.privacyScreen
+ windowTarget: roomWindowW
}
-
}
-
}
+ Component {
+ id: nestedSpaceMenuLevel
- Rectangle {
- color: Nheko.theme.separator
- height: 2
- Layout.fillWidth: true
+ SpaceMenuLevel {
+ childMenu: rootSpaceMenu.childMenu
+ roomid: roomContextMenu.roomid
+ }
}
+ Platform.Menu {
+ id: roomContextMenu
- Rectangle {
- id: unverifiedStuffBubble
-
- color: Qt.lighter(Nheko.theme.orange, verifyButtonHovered.hovered ? 1.2 : 1)
- Layout.fillWidth: true
- implicitHeight: explanation.height + Nheko.paddingMedium * 2
- visible: SelfVerificationStatus.status != SelfVerificationStatus.AllVerified
+ property string roomid
+ property var tags
- RowLayout {
- id: unverifiedStuffBubbleContainer
+ function show(roomid_, tags_) {
+ roomid = roomid_;
+ tags = tags_;
+ open();
+ }
- width: parent.width
- height: explanation.height + Nheko.paddingMedium * 2
- spacing: 0
+ InputDialog {
+ id: newTag
- Label {
- id: explanation
+ prompt: qsTr("Enter the tag you want to use:")
+ title: qsTr("New tag")
- Layout.margins: Nheko.paddingMedium
- Layout.rightMargin: Nheko.paddingSmall
- color: palette.buttonText
- Layout.fillWidth: true
- text: {
- switch (SelfVerificationStatus.status) {
- case SelfVerificationStatus.NoMasterKey:
- //: Cross-signing setup has not run yet.
- return qsTr("Encryption not set up");
- case SelfVerificationStatus.UnverifiedMasterKey:
- //: The user just signed in with this device and hasn't verified their master key.
- return qsTr("Unverified login");
- case SelfVerificationStatus.UnverifiedDevices:
- //: There are unverified devices signed in to this account.
- return qsTr("Please verify your other devices");
- default:
- return "";
- }
- }
- textFormat: Text.PlainText
- wrapMode: Text.Wrap
+ onAccepted: function (text) {
+ Rooms.toggleTag(roomContextMenu.roomid, "u." + text, true);
}
+ }
+ Platform.MenuItem {
+ text: qsTr("Open separately")
- 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
- ToolTip.delay: Nheko.tooltipDelay
- ToolTip.text: qsTr("Close")
- onClicked: unverifiedStuffBubble.visible = false
+ 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")
- HoverHandler {
- id: verifyButtonHovered
-
- enabled: !closeUnverifiedBubble.hovered
- acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
+ onTriggered: TimelineManager.openRoomSettings(roomContextMenu.roomid)
}
+ Platform.MenuItem {
+ text: qsTr("Leave room")
- TapHandler {
- enabled: !closeUnverifiedBubble.hovered
- acceptedButtons: Qt.LeftButton
- onSingleTapped: {
- if (SelfVerificationStatus.status == SelfVerificationStatus.UnverifiedDevices)
- SelfVerificationStatus.verifyUnverifiedDevices();
- else
- SelfVerificationStatus.statusChanged();
- }
+ onTriggered: TimelineManager.openLeaveRoomDialog(roomContextMenu.roomid)
}
+ Platform.MenuItem {
+ text: qsTr("Copy room link")
- }
-
- Rectangle {
- 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
- }
-
- Pane {
- Layout.fillWidth: true
- Layout.alignment: Qt.AlignBottom
- Layout.minimumHeight: 40
-
- horizontalPadding: Nheko.paddingMedium
- verticalPadding: 0
-
- background: Rectangle {color: palette.window}
- contentItem: RowLayout {
- id: buttonRow
-
- 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)
-
- Platform.Menu {
- id: roomJoinCreateMenu
+ onTriggered: Rooms.copyLink(roomContextMenu.roomid)
+ }
+ Platform.Menu {
+ id: tagsMenu
- Platform.MenuItem {
- text: qsTr("Join a room")
- onTriggered: Nheko.openJoinRoomDialog()
- }
+ title: qsTr("Tag room as:")
- Platform.MenuItem {
- text: qsTr("Create a new room")
- onTriggered: {
- var createRoom = createRoomComponent.createObject(timelineRoot);
- createRoom.show();
- timelineRoot.destroyOnClose(createRoom);
- }
- }
+ Instantiator {
+ model: Communities.tagsWithDefault
- Platform.MenuItem {
- text: qsTr("Start a direct chat")
- onTriggered: {
- var createDirect = createDirectComponent.createObject(timelineRoot);
- createDirect.show();
- timelineRoot.destroyOnClose(createDirect);
- }
- }
+ delegate: Platform.MenuItem {
+ property string t: modelData
- Platform.MenuItem {
- text: qsTr("Create a new community")
- onTriggered: {
- var createRoom = createRoomComponent.createObject(timelineRoot, { "space": true });
- createRoom.show();
- timelineRoot.destroyOnClose(createRoom);
+ 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...")
- 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);
- }
- }
-
- 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);
+ 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
- }
-
- RoomDirectoryModel {
- id: publicRooms
+ function destroyOnClose(obj) {
+ if (obj.closing != undefined)
+ obj.closing.connect(() => obj.destroy(1000));
+ else if (obj.aboutToHide != undefined)
+ obj.aboutToHide.connect(() => obj.destroy(1000));
}
-
- 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
- });
+ "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")
+ function showAllowedRoomsEditor(settings) {
+ 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 showSpacePLApplyPrompt(settings, editingModel) {
- var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelSpacesApplyDialog.qml")
+ function showPLEditor(settings) {
+ var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelEditor.qml");
if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, {
- "roomSettings": settings,
- "editingModel": editingModel
- });
+ "roomSettings": settings
+ });
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")
+ 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
- });
+ "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,50 +126,49 @@ 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")
+ function onOpenJoinRoomDialog() {
+ var component = Qt.createComponent("qrc:/qml/dialogs/JoinRoomDialog.qml");
if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot);
- dialog.open();
+ dialog.show();
destroyOnClose(dialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
-
- function onOpenJoinRoomDialog() {
- var component = Qt.createComponent("qrc:/qml/dialogs/JoinRoomDialog.qml")
+ function onOpenLogoutDialog() {
+ var component = Qt.createComponent("qrc:/qml/dialogs/LogoutDialog.qml");
if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot);
- dialog.show();
+ 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")
+ var component = Qt.createComponent("qrc:/qml/dialogs/ConfirmJoinRoomDialog.qml");
if (component.status == Component.Ready) {
- var dialog = component.createObject(timelineRoot, {"summary": summary});
+ 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")
+ function onOpenInviteUsersDialog(invitees) {
+ var component = Qt.createComponent("qrc:/qml/dialogs/InviteDialog.qml");
if (component.status == Component.Ready) {
- var userProfile = component.createObject(timelineRoot, {"profile": profile});
- userProfile.show();
- destroyOnClose(userProfile);
+ var dialog = component.createObject(timelineRoot, {
+ "invitees": invitees
+ });
+ dialog.show();
+ destroyOnClose(dialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
-
- function onShowImagePackSettings(room, packlist) {
- var component = Qt.createComponent("qrc:/qml/dialogs/ImagePackSettingsDialog.qml")
-
+ function onOpenLeaveRoomDialog(roomid, reason) {
+ var component = Qt.createComponent("qrc:/qml/dialogs/LeaveRoomDialog.qml");
if (component.status == Component.Ready) {
- var packSet = component.createObject(timelineRoot, {
- "room": room,
- "packlist": packlist
- });
- packSet.show();
- destroyOnClose(packSet);
+ var dialog = component.createObject(timelineRoot, {
+ "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")
+ var component = Qt.createComponent("qrc:/qml/dialogs/RoomMembers.qml");
if (component.status == Component.Ready) {
var membersDialog = component.createObject(timelineRoot, {
- "members": members,
- "room": room
- });
+ "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")
+ var component = Qt.createComponent("qrc:/qml/dialogs/RoomSettings.qml");
if (component.status == Component.Ready) {
var roomSettings = component.createObject(timelineRoot, {
- "roomSettings": settings
- });
+ "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")
- if (component.status == Component.Ready) {
- var dialog = component.createObject(timelineRoot, {
- "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")
- if (component.status == Component.Ready) {
- var dialog = component.createObject(timelineRoot, {
- "roomId": roomid,
- "reason": reason
- });
- dialog.open();
- destroyOnClose(dialog);
- } 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,131 +305,110 @@ 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 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();
}
- function onConfirm3pidToken() {
- uiaConfirmationLinkDialog.open();
- }
-
- function onError(msg) {
- uiaErrorDialog.text = msg;
- uiaErrorDialog.open();
- }
-
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
+
+ function updateTrans() {
+ pushEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : pushEnterOrg;
+ pushExit = Settings.reducedMotion ? reducedMotionTransitionExit : pushExitOrg;
+ popEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : popEnterOrg;
+ popExit = Settings.reducedMotion ? reducedMotionTransitionExit : popExitOrg;
+ replaceEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : replaceEnterOrg;
+ replaceExit = Settings.reducedMotion ? reducedMotionTransitionExit : replaceExitOrg;
+ }
+
+ anchors.fill: parent
+ initialItem: welcomePage
+
Component.onCompleted: {
pushEnterOrg = pushEnter;
popEnterOrg = popEnter;
@@ -452,78 +416,94 @@ Pane {
pushExitOrg = pushExit;
popExitOrg = popExit;
replaceExitOrg = replaceExit;
-
- updateTrans()
+ updateTrans();
}
- function updateTrans() {
- pushEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : pushEnterOrg;
- pushExit = Settings.reducedMotion ? reducedMotionTransitionExit : pushExitOrg;
- popEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : popEnterOrg;
- popExit = Settings.reducedMotion ? reducedMotionTransitionExit : popExitOrg;
- replaceEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : replaceEnterOrg;
- replaceExit = Settings.reducedMotion ? reducedMotionTransitionExit : replaceExitOrg;
+ 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 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);
- }
- function onShowNotification(msg) {
- snackbar.showNotification(msg);
- console.log("New snack: " + msg);
+ 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
+ closePolicy: Popup.NoAutoClose
height: content.height + implicitFooterHeight + implicitHeaderHeight
- width: content.width
- padding: 0
modal: true
+ padding: 0
+ parent: Overlay.overlay
standardButtons: Dialog.Ok
- closePolicy: Popup.NoAutoClose
+ 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.alignment: Qt.AlignLeft | Qt.AlignTop
+ Layout.fillWidth: true
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.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
+ Layout.margins: Nheko.paddingMedium
+ Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
echoMode: TextInput.Password
+ visible: storeSecretsOnline.checked && usePassword.checked
}
-
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: "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"
+ duration: 100
easing.type: Easing.InOutQuad
+ properties: "x"
+ target: r
to: 0
- duration: 100
}
}
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
+ 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
- border.width: r.notificationlevel == MtxEvent.Highlight ? 1 : 0
- border.color: Nheko.theme.red
+ 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
+ anchors.verticalCenter: ts.verticalCenter
+ eventId: r.eventId
height: parent.iconSize
- width: parent.iconSize
status: r.status
- eventId: r.eventId
- anchors.verticalCenter: ts.verticalCenter
+ width: parent.iconSize
}
-
Image {
- visible: isEdited || eventId == room.edit
Layout.alignment: Qt.AlignRight | Qt.AlignTop
- height: parent.iconSize
- 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
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
+ anchors.verticalCenter: ts.verticalCenter
+ encrypted: isEncrypted
height: parent.iconSize
- width: parent.iconSize
- sourceSize.width: parent.iconSize * Screen.devicePixelRatio
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
- anchors.verticalCenter: ts.verticalCenter
+ 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 {
+ id: reactionRow
+
+ 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
- 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
}
-
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
- clip: true
-
- onRoomChanged: if (room != null) room.triggerSpecialEffects()
-
- StickerPicker {
- id: emojiPopup
+ property bool showBackButton: false
- emoji: true
- }
+ clip: true
// focus message input on key press, but not on Ctrl-C and such.
- Keys.onPressed: (event) => {
+ 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
+ }
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
+ enabled: false
height: 130
+ roomid: parent.roomId
+ url: parent.avatarUrl.replace("mxc://", "image://MxcImage/")
width: 130
- Layout.alignment: Qt.AlignHCenter
- enabled: false
}
-
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
+ displayName: roomName
+ enabled: false
height: Nheko.avatarSize
- url: avatarUrl.replace("mxc://", "image://MxcImage/")
roomid: roomId
+ url: avatarUrl.replace("mxc://", "image://MxcImage/")
userid: isDirect ? directChatOtherUserId : ""
- displayName: roomName
- enabled: false
+ 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
+ elide: Text.ElideRight
font.bold: true
- text: roomName
+ font.pointSize: fontMetrics.font.pointSize * 1.1
maximumLineCount: 1
- elide: Text.ElideRight
+ 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
-
- 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)
+ 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
}
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))
+ }
}
}
|