Format qml

This commit is contained in:
Nicolas Werner 2023-06-02 01:45:24 +02:00
parent de8522a185
commit 5aee8d609a
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
28 changed files with 2623 additions and 2954 deletions

7
.qmlformat.ini Normal file
View file

@ -0,0 +1,7 @@
[General]
FunctionsSpacing=
IndentWidth=4
NewlineType=native
NormalizeOrder=true
ObjectsSpacing=
UseTabs=false

View file

@ -11,45 +11,44 @@ import im.nheko 1.0
AbstractButton { AbstractButton {
id: avatar id: avatar
property alias color: bg.color
property bool crop: true
property string displayName
property string roomid
property alias textColor: label.color
property string url property string url
property string userid property string userid
property string roomid
property string displayName
property alias textColor: label.color
property bool crop: true
property alias color: bg.color
width: 48
height: 48 height: 48
width: 48
background: Rectangle { background: Rectangle {
id: bg id: bg
radius: Settings.avatarCircles ? height / 2 : height / 8
color: palette.alternateBase color: palette.alternateBase
radius: Settings.avatarCircles ? height / 2 : height / 8
} }
Label { Label {
id: label id: label
enabled: false
anchors.fill: parent 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)) : "") text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "")
textFormat: Text.RichText textFormat: Text.RichText
font.pixelSize: avatar.height / 2
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
visible: img.status != Image.Ready && !Settings.useIdenticon visible: img.status != Image.Ready && !Settings.useIdenticon
color: palette.text
} }
Image { Image {
id: identicon id: identicon
anchors.fill: parent anchors.fill: parent
visible: Settings.useIdenticon && img.status != Image.Ready
source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : "" source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : ""
visible: Settings.useIdenticon && img.status != Image.Ready
} }
Image { Image {
id: img id: img
@ -58,8 +57,6 @@ AbstractButton {
fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit fillMode: avatar.crop ? Image.PreserveAspectCrop : Image.PreserveAspectFit
mipmap: true mipmap: true
smooth: true smooth: true
sourceSize.width: avatar.width * Screen.devicePixelRatio
sourceSize.height: avatar.height * Screen.devicePixelRatio
source: if (avatar.url.startsWith('image://')) { source: if (avatar.url.startsWith('image://')) {
return avatar.url + "?radius=" + (Settings.avatarCircles ? 100 : 25) + ((avatar.crop) ? "" : "&scale"); return avatar.url + "?radius=" + (Settings.avatarCircles ? 100 : 25) + ((avatar.crop) ? "" : "&scale");
} else if (avatar.url.startsWith(':/')) { } else if (avatar.url.startsWith(':/')) {
@ -67,20 +64,12 @@ AbstractButton {
} else { } else {
return ""; return "";
} }
sourceSize.height: avatar.height * Screen.devicePixelRatio
sourceSize.width: avatar.width * Screen.devicePixelRatio
} }
Rectangle { Rectangle {
id: onlineIndicator 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() { function updatePresence() {
switch (Presence.userPresence(userid)) { switch (Presence.userPresence(userid)) {
case "online": case "online":
@ -94,22 +83,28 @@ AbstractButton {
} }
} }
Connections { anchors.bottom: avatar.bottom
target: Presence 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) { function onPresenceChanged(id) {
if (id == userid) onlineIndicator.color = onlineIndicator.updatePresence(); if (id == userid)
onlineIndicator.color = onlineIndicator.updatePresence();
} }
target: Presence
} }
} }
CursorShape { CursorShape {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
Ripple { Ripple {
color: Qt.rgba(palette.alternateBase.r, palette.alternateBase.g, palette.alternateBase.b, 0.5) color: Qt.rgba(palette.alternateBase.r, palette.alternateBase.g, palette.alternateBase.b, 0.5)
} }
} }

View file

@ -17,16 +17,16 @@ Rectangle {
color: palette.window color: palette.window
ColumnLayout { ColumnLayout {
spacing: 0
anchors.fill: parent anchors.fill: parent
spacing: 0
Rectangle { Rectangle {
id: offlineIndicator id: offlineIndicator
Layout.fillWidth: true
Layout.preferredHeight: offlineLabel.height + Nheko.paddingMedium
color: Nheko.theme.error color: Nheko.theme.error
visible: !TimelineManager.isConnected visible: !TimelineManager.isConnected
Layout.preferredHeight: offlineLabel.height + Nheko.paddingMedium
Layout.fillWidth: true
z: 1 z: 1
Label { Label {
@ -36,18 +36,9 @@ Rectangle {
text: qsTr("No network connection") text: qsTr("No network connection")
} }
} }
AdaptiveLayout { AdaptiveLayout {
id: adaptiveView 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() { function initializePageIndex() {
if (!singlePageMode) if (!singlePageMode)
adaptiveView.pageIndex = 0; adaptiveView.pageIndex = 0;
@ -57,67 +48,67 @@ Rectangle {
adaptiveView.pageIndex = 1; 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 { Connections {
target: Rooms
function onCurrentRoomChanged() { function onCurrentRoomChanged() {
adaptiveView.initializePageIndex(); adaptiveView.initializePageIndex();
} }
}
target: Rooms
}
AdaptiveLayoutElement { AdaptiveLayoutElement {
id: communityListC id: communityListC
visible: Settings.groupView
minimumWidth: communitiesList.avatarSize * 4 + Nheko.paddingMedium * 2
collapsedWidth: communitiesList.avatarSize + 2 * Nheko.paddingMedium collapsedWidth: communitiesList.avatarSize + 2 * Nheko.paddingMedium
preferredWidth: Settings.communityListWidth >= minimumWidth ? Settings.communityListWidth : collapsedWidth
maximumWidth: communitiesList.avatarSize * 10 + 2 * Nheko.paddingMedium 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 { CommunitiesList {
id: communitiesList id: communitiesList
collapsed: parent.collapsed collapsed: parent.collapsed
} }
Binding { Binding {
target: Settings delayed: true
property: 'communityListWidth' property: 'communityListWidth'
restoreMode: Binding.RestoreBindingOrValue
target: Settings
value: communityListC.preferredWidth value: communityListC.preferredWidth
when: !adaptiveView.singlePageMode when: !adaptiveView.singlePageMode
delayed: true
restoreMode: Binding.RestoreBindingOrValue
} }
} }
AdaptiveLayoutElement { AdaptiveLayoutElement {
id: roomListC 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 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 { RoomList {
id: roomlist id: roomlist
height: adaptiveView.height
collapsed: parent.collapsed collapsed: parent.collapsed
height: adaptiveView.height
} }
Binding { Binding {
target: Settings delayed: true
property: 'roomListWidth' property: 'roomListWidth'
restoreMode: Binding.RestoreBindingOrValue
target: Settings
value: roomListC.preferredWidth value: roomListC.preferredWidth
when: !adaptiveView.singlePageMode when: !adaptiveView.singlePageMode
delayed: true
restoreMode: Binding.RestoreBindingOrValue
} }
} }
AdaptiveLayoutElement { AdaptiveLayoutElement {
id: timlineViewC id: timlineViewC
@ -127,25 +118,20 @@ Rectangle {
id: timeline id: timeline
privacyScreen: privacyScreen privacyScreen: privacyScreen
showBackButton: adaptiveView.singlePageMode
room: Rooms.currentRoom room: Rooms.currentRoom
roomPreview: Rooms.currentRoomPreview.roomid ? Rooms.currentRoomPreview : null roomPreview: Rooms.currentRoomPreview.roomid ? Rooms.currentRoomPreview : null
showBackButton: adaptiveView.singlePageMode
} }
} }
} }
} }
PrivacyScreen { PrivacyScreen {
id: privacyScreen id: privacyScreen
anchors.fill: parent anchors.fill: parent
visible: Settings.privacyScreen
screenTimeout: Settings.privacyScreenTimeout screenTimeout: Settings.privacyScreenTimeout
timelineRoot: adaptiveView timelineRoot: adaptiveView
visible: Settings.privacyScreen
windowTarget: MainWindow windowTarget: MainWindow
} }
} }

View file

@ -13,19 +13,24 @@ import im.nheko 1.0
Page { Page {
id: communitySidebar id: communitySidebar
//leftPadding: Nheko.paddingSmall //leftPadding: Nheko.paddingSmall
//rightPadding: Nheko.paddingSmall //rightPadding: Nheko.paddingSmall
property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 1.6) property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 1.6)
property bool collapsed: false property bool collapsed: false
background: Rectangle {
color: Nheko.theme.sidebarBackground
}
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
Connections { Connections {
function onHideMenu() { function onHideMenu() {
communityContextMenu.close() communityContextMenu.close();
} }
target: MainWindow target: MainWindow
} }
ListView { ListView {
id: communitiesList id: communitiesList
@ -36,15 +41,158 @@ Page {
ScrollBar.vertical: ScrollBar { ScrollBar.vertical: ScrollBar {
id: scrollbar id: scrollbar
parent: !collapsed && Settings.scrollbarsInRoomlist ? communitiesList : null parent: !collapsed && Settings.scrollbarsInRoomlist ? communitiesList : null
} }
delegate: ItemDelegate {
id: communityItem
property color backgroundColor: palette.window
property color bubbleBackground: palette.highlight
property color bubbleText: palette.highlightedText
property color importantText: palette.text
required property var model
property color unimportantText: palette.buttonText
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: model.tooltip
ToolTip.visible: hovered && collapsed
height: avatarSize + 2 * Nheko.paddingMedium
state: "normal"
width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0)
background: Rectangle {
color: communityItem.backgroundColor
}
states: [
State {
name: "highlight"
when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId === model.id)
PropertyChanges {
backgroundColor: palette.dark
bubbleBackground: palette.highlight
bubbleText: palette.highlightedText
importantText: palette.brightText
target: communityItem
unimportantText: palette.brightText
}
},
State {
name: "selected"
when: Communities.currentTagId == model.id
PropertyChanges {
backgroundColor: palette.highlight
bubbleBackground: palette.highlightedText
bubbleText: palette.highlight
importantText: palette.highlightedText
target: communityItem
unimportantText: palette.highlightedText
}
}
]
onClicked: Communities.setCurrentTagId(model.id)
onPressAndHold: communityContextMenu.show(model.id, model.hidden, model.muted)
Item {
anchors.fill: parent
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
gesturePolicy: TapHandler.ReleaseWithinBounds
onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted)
}
}
RowLayout {
id: r
anchors.fill: parent
anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * model.depth))
anchors.margins: Nheko.paddingMedium
spacing: Nheko.paddingMedium
ImageButton {
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: fontMetrics.lineSpacing
Layout.preferredWidth: fontMetrics.lineSpacing
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: model.collapsed ? qsTr("Expand") : qsTr("Collapse")
ToolTip.visible: hovered
height: fontMetrics.lineSpacing
hoverEnabled: true
image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg"
visible: !communitySidebar.collapsed && model.collapsible
width: fontMetrics.lineSpacing
onClicked: model.collapsed = !model.collapsed
}
Item {
Layout.preferredWidth: fontMetrics.lineSpacing
visible: !communitySidebar.collapsed && !model.collapsible && Communities.containsSubspaces
}
Avatar {
id: avatar
Layout.alignment: Qt.AlignVCenter
color: communityItem.backgroundColor
displayName: model.displayName
enabled: false
height: avatarSize
roomid: model.id
url: {
if (model.avatarUrl.startsWith("mxc://"))
return model.avatarUrl.replace("mxc://", "image://MxcImage/");
else
return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText;
}
width: avatarSize
NotificationBubble {
anchors.bottom: avatar.bottom
anchors.margins: -Nheko.paddingSmall
anchors.right: avatar.right
bubbleBackgroundColor: communityItem.bubbleBackground
bubbleTextColor: communityItem.bubbleText
font.pixelSize: fontMetrics.font.pixelSize * 0.6
hasLoudNotification: model.hasLoudNotification
mayBeVisible: communitySidebar.collapsed && !model.muted && Settings.spaceNotifications
notificationCount: model.unreadMessages
}
}
ElidedLabel {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
color: communityItem.importantText
elideWidth: width
fullText: model.displayName
textFormat: Text.PlainText
visible: !communitySidebar.collapsed
}
Item {
Layout.fillWidth: true
}
NotificationBubble {
Layout.alignment: Qt.AlignRight
Layout.leftMargin: Nheko.paddingSmall
bubbleBackgroundColor: communityItem.bubbleBackground
bubbleTextColor: communityItem.bubbleText
hasLoudNotification: model.hasLoudNotification
mayBeVisible: !communitySidebar.collapsed && !model.muted && Settings.spaceNotifications
notificationCount: model.unreadMessages
}
}
}
Platform.Menu { Platform.Menu {
id: communityContextMenu id: communityContextMenu
property string tagId
property bool hidden property bool hidden
property bool muted property bool muted
property string tagId
function show(id_, hidden_, muted_) { function show(id_, hidden_, muted_) {
tagId = id_; tagId = id_;
@ -54,177 +202,19 @@ Page {
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Do not show notification counts for this community or tag.")
checkable: true checkable: true
checked: communityContextMenu.muted checked: communityContextMenu.muted
text: qsTr("Do not show notification counts for this community or tag.")
onTriggered: Communities.toggleTagMute(communityContextMenu.tagId) onTriggered: Communities.toggleTagMute(communityContextMenu.tagId)
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Hide rooms with this tag or from this community by default.")
checkable: true checkable: true
checked: communityContextMenu.hidden checked: communityContextMenu.hidden
text: qsTr("Hide rooms with this tag or from this community by default.")
onTriggered: Communities.toggleTagId(communityContextMenu.tagId) onTriggered: Communities.toggleTagId(communityContextMenu.tagId)
} }
} }
delegate: ItemDelegate {
id: communityItem
property color backgroundColor: palette.window
property color importantText: palette.text
property color unimportantText: palette.buttonText
property color bubbleBackground: palette.highlight
property color bubbleText: palette.highlightedText
required property var model
height: avatarSize + 2 * Nheko.paddingMedium
width: ListView.view.width - ((scrollbar.interactive && scrollbar.visible && scrollbar.parent) ? scrollbar.width : 0)
state: "normal"
ToolTip.visible: hovered && collapsed
ToolTip.text: model.tooltip
ToolTip.delay: Nheko.tooltipDelay
onClicked: Communities.setCurrentTagId(model.id)
onPressAndHold: communityContextMenu.show(model.id, model.hidden, model.muted)
states: [
State {
name: "highlight"
when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId === model.id)
PropertyChanges {
target: communityItem
backgroundColor: palette.dark
importantText: palette.brightText
unimportantText: palette.brightText
bubbleBackground: palette.highlight
bubbleText: palette.highlightedText
}
},
State {
name: "selected"
when: Communities.currentTagId == model.id
PropertyChanges {
target: communityItem
backgroundColor: palette.highlight
importantText: palette.highlightedText
unimportantText: palette.highlightedText
bubbleBackground: palette.highlightedText
bubbleText: palette.highlight
}
}
]
Item {
anchors.fill: parent
TapHandler {
acceptedButtons: Qt.RightButton
onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted)
gesturePolicy: TapHandler.ReleaseWithinBounds
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
}
}
RowLayout {
id: r
spacing: Nheko.paddingMedium
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * model.depth))
ImageButton {
visible: !communitySidebar.collapsed && model.collapsible
Layout.preferredHeight: fontMetrics.lineSpacing
Layout.preferredWidth: fontMetrics.lineSpacing
Layout.alignment: Qt.AlignVCenter
height: fontMetrics.lineSpacing
width: fontMetrics.lineSpacing
image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg"
ToolTip.visible: hovered
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: model.collapsed ? qsTr("Expand") : qsTr("Collapse")
hoverEnabled: true
onClicked: model.collapsed = !model.collapsed
}
Item {
Layout.preferredWidth: fontMetrics.lineSpacing
visible: !communitySidebar.collapsed && !model.collapsible && Communities.containsSubspaces
}
Avatar {
id: avatar
enabled: false
Layout.alignment: Qt.AlignVCenter
height: avatarSize
width: avatarSize
url: {
if (model.avatarUrl.startsWith("mxc://"))
return model.avatarUrl.replace("mxc://", "image://MxcImage/");
else
return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText;
}
roomid: model.id
displayName: model.displayName
color: communityItem.backgroundColor
NotificationBubble {
notificationCount: model.unreadMessages
hasLoudNotification: model.hasLoudNotification
bubbleBackgroundColor: communityItem.bubbleBackground
bubbleTextColor: communityItem.bubbleText
font.pixelSize: fontMetrics.font.pixelSize * 0.6
mayBeVisible: communitySidebar.collapsed && !model.muted && Settings.spaceNotifications
anchors.right: avatar.right
anchors.bottom: avatar.bottom
anchors.margins: -Nheko.paddingSmall
}
}
ElidedLabel {
visible: !communitySidebar.collapsed
Layout.alignment: Qt.AlignVCenter
color: communityItem.importantText
Layout.fillWidth: true
elideWidth: width
fullText: model.displayName
textFormat: Text.PlainText
}
Item {
Layout.fillWidth: true
}
NotificationBubble {
notificationCount: model.unreadMessages
hasLoudNotification: model.hasLoudNotification
bubbleBackgroundColor: communityItem.bubbleBackground
bubbleTextColor: communityItem.bubbleText
mayBeVisible: !communitySidebar.collapsed && !model.muted && Settings.spaceNotifications
Layout.alignment: Qt.AlignRight
Layout.leftMargin: Nheko.paddingSmall
}
}
background: Rectangle {
color: communityItem.backgroundColor
}
}
} }
background: Rectangle {
color: Nheko.theme.sidebarBackground
}
} }

View file

@ -11,117 +11,102 @@ import im.nheko 1.0
Control { Control {
id: popup 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 avatarHeight: 24
property int avatarWidth: 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 rowMargin: 0
property int rowSpacing: Nheko.paddingSmall property int rowSpacing: Nheko.paddingSmall
property alias count: listView.count
signal completionClicked(string completion) signal completionClicked(string completion)
signal completionSelected(string id) signal completionSelected(string id)
function up() { function changeCompleter() {
if (bottomToTop) if (completerName) {
down_(); completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : (popup.roomId != "" ? popup.roomId : room.roomId));
else completer.setSearchString("");
up_(); } else {
completer = undefined;
}
currentIndex = -1;
} }
function down() {
if (bottomToTop)
up_();
else
down_();
}
function up_() {
currentIndex = currentIndex - 1;
if (currentIndex == -2)
currentIndex = listView.count - 1;
}
function down_() {
currentIndex = currentIndex + 1;
if (currentIndex >= listView.count)
currentIndex = -1;
}
function currentCompletion() { function currentCompletion() {
if (currentIndex > -1 && currentIndex < listView.count) if (currentIndex > -1 && currentIndex < listView.count)
return completer.completionAt(currentIndex); return completer.completionAt(currentIndex);
else else
return null; return null;
} }
function down() {
if (bottomToTop)
up_();
else
down_();
}
function down_() {
currentIndex = currentIndex + 1;
if (currentIndex >= listView.count)
currentIndex = -1;
}
function finishCompletion() { function finishCompletion() {
if (popup.completerName == "room") if (popup.completerName == "room")
popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.roomid); popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.roomid);
else if (popup.completerName == "user") else if (popup.completerName == "user")
popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.userid); popup.completionSelected(listView.itemAtIndex(currentIndex).modelData.userid);
} }
function up() {
function changeCompleter() { if (bottomToTop)
if (completerName) { down_();
completer = TimelineManager.completerFor(completerName, completerName == "room" ? "" : (popup.roomId != "" ? popup.roomId : room.roomId)); else
completer.setSearchString(""); up_();
} else { }
completer = undefined; function up_() {
} currentIndex = currentIndex - 1;
currentIndex = -1 if (currentIndex == -2)
currentIndex = listView.count - 1;
} }
onCompleterNameChanged: changeCompleter()
onRoomIdChanged: changeCompleter()
bottomPadding: 1 bottomPadding: 1
leftPadding: 1 leftPadding: 1
topPadding: 1
rightPadding: 1 rightPadding: 1
topPadding: 1
background: Rectangle {
border.color: palette.mid
color: palette.base
}
contentItem: ListView { contentItem: ListView {
id: listView id: listView
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. // 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 // 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 // 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 // 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! // 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)) implicitHeight: Math.min(contentHeight, 6 * rowSpacing + 7 * (popup.avatarHeight + 2 * rowMargin))
clip: true
Timer {
id: deadTimer
interval: 50
}
onContentYChanged: deadTimer.restart()
// Broken, see https://bugreports.qt.io/browse/QTBUG-102811 // Broken, see https://bugreports.qt.io/browse/QTBUG-102811
//reuseItems: true //reuseItems: true
implicitWidth: listView.contentItem.childrenRect.width implicitWidth: listView.contentItem.childrenRect.width
model: completer model: completer
verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom
spacing: rowSpacing
pixelAligned: true pixelAligned: true
highlightFollowsCurrentItem: true spacing: rowSpacing
verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom
displayMarginBeginning: height / 2
displayMarginEnd: height / 2
delegate: Rectangle { delegate: Rectangle {
property variant modelData: model property variant modelData: model
ListView.delayRemove: true ListView.delayRemove: true
color: model.index == popup.currentIndex ? palette.highlight : palette.base color: model.index == popup.currentIndex ? palette.highlight : palette.base
height: (chooser.child?.implicitHeight ?? 0) + 2 * popup.rowMargin height: (chooser.child?.implicitHeight ?? 0) + 2 * popup.rowMargin
implicitWidth: fullWidth ? ListView.view.width : chooser.child.implicitWidth + 4 implicitWidth: fullWidth ? ListView.view.width : chooser.child.implicitWidth + 4
@ -131,26 +116,27 @@ Control {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onPositionChanged: if (!listView.moving && !deadTimer.running) popup.currentIndex = model.index
onClicked: { onClicked: {
popup.completionClicked(completer.completionAt(model.index)); popup.completionClicked(completer.completionAt(model.index));
if (popup.completerName == "room") if (popup.completerName == "room")
popup.completionSelected(model.roomid); popup.completionSelected(model.roomid);
else if (popup.completerName == "user") else if (popup.completerName == "user")
popup.completionSelected(model.userid); popup.completionSelected(model.userid);
} }
onPositionChanged: if (!listView.moving && !deadTimer.running)
popup.currentIndex = model.index
} }
Ripple { Ripple {
color: Qt.rgba(palette.base.r, palette.base.g, palette.base.b, 0.5) color: Qt.rgba(palette.base.r, palette.base.g, palette.base.b, 0.5)
} }
DelegateChooser { DelegateChooser {
id: chooser id: chooser
roleValue: popup.completerName
anchors.fill: parent anchors.fill: parent
anchors.margins: popup.rowMargin anchors.margins: popup.rowMargin
enabled: false enabled: false
roleValue: popup.completerName
DelegateChoice { DelegateChoice {
roleValue: "user" roleValue: "user"
@ -162,28 +148,23 @@ Control {
spacing: rowSpacing spacing: rowSpacing
Avatar { Avatar {
height: popup.avatarHeight
width: popup.avatarWidth
displayName: model.displayName displayName: model.displayName
userid: model.userid
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
enabled: false enabled: false
height: popup.avatarHeight
url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
userid: model.userid
width: popup.avatarWidth
} }
Label { Label {
text: model.displayName
color: model.index == popup.currentIndex ? palette.highlightedText : palette.text color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
text: model.displayName
} }
Label { Label {
text: "(" + model.userid + ")"
color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText
text: "(" + model.userid + ")"
} }
} }
} }
DelegateChoice { DelegateChoice {
roleValue: "emoji" roleValue: "emoji"
@ -194,39 +175,33 @@ Control {
spacing: rowSpacing spacing: rowSpacing
Label { Label {
visible: !!model.unicode
text: model.unicode
color: model.index == popup.currentIndex ? palette.highlightedText : palette.text color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
font: Settings.emojiFont font: Settings.emojiFont
text: model.unicode
visible: !!model.unicode
} }
Avatar { Avatar {
visible: !model.unicode crop: false
height: popup.avatarHeight
width: popup.avatarWidth
displayName: model.shortcode displayName: model.shortcode
enabled: false
height: popup.avatarHeight
//userid: model.shortcode //userid: model.shortcode
url: (model.url ? model.url : "").replace("mxc://", "image://MxcImage/") url: (model.url ? model.url : "").replace("mxc://", "image://MxcImage/")
enabled: false visible: !model.unicode
crop: false width: popup.avatarWidth
} }
Label { Label {
Layout.leftMargin: Nheko.paddingSmall Layout.leftMargin: Nheko.paddingSmall
Layout.rightMargin: Nheko.paddingSmall Layout.rightMargin: Nheko.paddingSmall
text: model.shortcode
color: model.index == popup.currentIndex ? palette.highlightedText : palette.text color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
text: model.shortcode
} }
Label { Label {
text: "(" + model.packname + ")"
color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText
text: "(" + model.packname + ")"
} }
} }
} }
DelegateChoice { DelegateChoice {
roleValue: "command" roleValue: "command"
@ -237,20 +212,16 @@ Control {
spacing: rowSpacing spacing: rowSpacing
Label { Label {
text: model.name
color: model.index == popup.currentIndex ? palette.highlightedText : palette.text color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
font.bold: true font.bold: true
text: model.name
} }
Label { Label {
text: model.description
color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText
text: model.description
} }
} }
} }
DelegateChoice { DelegateChoice {
roleValue: "room" roleValue: "room"
@ -261,26 +232,22 @@ Control {
spacing: rowSpacing spacing: rowSpacing
Avatar { Avatar {
height: popup.avatarHeight
width: popup.avatarWidth
displayName: model.roomName displayName: model.roomName
enabled: false
height: popup.avatarHeight
roomid: model.roomid roomid: model.roomid
url: model.avatarUrl.replace("mxc://", "image://MxcImage/") url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
enabled: false width: popup.avatarWidth
} }
Label { Label {
text: model.roomName
font.pixelSize: popup.avatarHeight * 0.5
color: model.index == popup.currentIndex ? palette.highlightedText : palette.text color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
font.italic: model.isTombstoned font.italic: model.isTombstoned
font.pixelSize: popup.avatarHeight * 0.5
text: model.roomName
textFormat: Text.RichText textFormat: Text.RichText
} }
} }
} }
DelegateChoice { DelegateChoice {
roleValue: "roomAliases" roleValue: "roomAliases"
@ -291,41 +258,38 @@ Control {
spacing: rowSpacing spacing: rowSpacing
Avatar { Avatar {
height: popup.avatarHeight
width: popup.avatarWidth
displayName: model.roomName displayName: model.roomName
enabled: false
height: popup.avatarHeight
roomid: model.roomid roomid: model.roomid
url: model.avatarUrl.replace("mxc://", "image://MxcImage/") url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
enabled: false width: popup.avatarWidth
} }
Label { Label {
text: model.roomName
color: model.index == popup.currentIndex ? palette.highlightedText : palette.text color: model.index == popup.currentIndex ? palette.highlightedText : palette.text
font.italic: model.isTombstoned font.italic: model.isTombstoned
text: model.roomName
textFormat: Text.RichText textFormat: Text.RichText
} }
Label { Label {
text: "(" + model.roomAlias + ")"
color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText color: model.index == popup.currentIndex ? palette.highlightedText : palette.buttonText
text: "(" + model.roomAlias + ")"
textFormat: Text.RichText textFormat: Text.RichText
} }
} }
} }
} }
} }
onContentYChanged: deadTimer.restart()
Timer {
id: deadTimer
interval: 50
}
} }
onCompleterNameChanged: changeCompleter()
background: Rectangle { onRoomIdChanged: changeCompleter()
color: palette.base
border.color: palette.mid
}
} }

View file

@ -9,21 +9,20 @@ import im.nheko 1.0
Label { Label {
id: root id: root
property alias fullText: metrics.text
property alias elideWidth: metrics.elideWidth property alias elideWidth: metrics.elideWidth
property alias fullText: metrics.text
property int fullTextWidth: Math.ceil(metrics.advanceWidth) property int fullTextWidth: Math.ceil(metrics.advanceWidth)
color: palette.text color: palette.text
text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(metrics.elidedText)
maximumLineCount: 1
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1
text: (textFormat == Text.PlainText) ? metrics.elidedText : TimelineManager.escapeEmoji(metrics.elidedText)
textFormat: Text.PlainText textFormat: Text.PlainText
TextMetrics { TextMetrics {
id: metrics id: metrics
font.pointSize: root.font.pointSize
elide: Text.ElideRight elide: Text.ElideRight
font.pointSize: root.font.pointSize
} }
} }

View file

@ -11,32 +11,40 @@ Image {
id: stateImg id: stateImg
property bool encrypted: false 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 bool hovered: ma.hovered
property string sourceUrl: { property string sourceUrl: {
if (!encrypted) if (!encrypted)
return "image://colorimage/" + unencryptedIcon + "?"; return "image://colorimage/" + unencryptedIcon + "?";
switch (trust) { switch (trust) {
case Crypto.Verified: case Crypto.Verified:
return "image://colorimage/:/icons/icons/ui/shield-filled-checkmark.svg?"; 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?"; 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?"; return "image://colorimage/:/icons/icons/ui/shield-filled-exclamation-mark.svg?";
default: default:
return "image://colorimage/:/icons/icons/ui/shield-filled-cross.svg?"; 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 height: 16
sourceSize.height: height
sourceSize.width: width
source: { source: {
if (encrypted) { if (encrypted) {
switch (trust) { switch (trust) {
@ -51,23 +59,12 @@ Image {
return sourceUrl + (stateImg.hovered ? unencryptedHoverColor : unencryptedColor); return sourceUrl + (stateImg.hovered ? unencryptedHoverColor : unencryptedColor);
} }
} }
ToolTip.visible: stateImg.hovered sourceSize.height: height
ToolTip.text: { sourceSize.width: width
if (!encrypted) width: 16
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.");
}
}
HoverHandler { HoverHandler {
id: ma id: ma
}
}
} }

View file

@ -16,13 +16,21 @@ Popup {
mid = mid_in; mid = mid_in;
} }
x: Math.round(parent.width / 2 - width / 2) leftPadding: 10
y: Math.round(parent.height / 4)
modal: true modal: true
parent: Overlay.overlay parent: Overlay.overlay
width: timelineRoot.width * 0.8
leftPadding: 10
rightPadding: 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: { onOpened: {
roomTextInput.forceActiveFocus(); roomTextInput.forceActiveFocus();
} }
@ -35,46 +43,40 @@ Popup {
Label { Label {
id: titleLabel id: titleLabel
text: qsTr("Forward Message")
font.bold: true
bottomPadding: 10 bottomPadding: 10
color: palette.text color: palette.text
font.bold: true
text: qsTr("Forward Message")
} }
Reply { Reply {
id: replyPreview 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 ?? "" blurhash: modelData.blurhash ?? ""
body: modelData.body ?? "" body: modelData.body ?? ""
formattedBody: modelData.formattedBody ?? "" encryptionError: modelData.encryptionError ?? ""
eventId: modelData.eventId ?? "" eventId: modelData.eventId ?? ""
filename: modelData.filename ?? "" filename: modelData.filename ?? ""
filesize: modelData.filesize ?? "" filesize: modelData.filesize ?? ""
formattedBody: modelData.formattedBody ?? ""
isOnlyEmoji: modelData.isOnlyEmoji ?? false
originalWidth: modelData.originalWidth ?? 0
proportionalHeight: modelData.proportionalHeight ?? 1 proportionalHeight: modelData.proportionalHeight ?? 1
type: modelData.type ?? MtxEvent.UnknownMessage type: modelData.type ?? MtxEvent.UnknownMessage
typeString: modelData.typeString ?? "" typeString: modelData.typeString ?? ""
url: modelData.url ?? "" url: modelData.url ?? ""
originalWidth: modelData.originalWidth ?? 0 userColor: TimelineManager.userColor(modelData.userId, palette.window)
isOnlyEmoji: modelData.isOnlyEmoji ?? false
userId: modelData.userId ?? "" userId: modelData.userId ?? ""
userName: modelData.userName ?? "" userName: modelData.userName ?? ""
encryptionError: modelData.encryptionError ?? "" width: parent.width
} }
MatrixTextField { MatrixTextField {
id: roomTextInput id: roomTextInput
width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
color: palette.text color: palette.text
onTextEdited: { width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
completerPopup.completer.searchString = text;
}
Keys.onPressed: { Keys.onPressed: {
if (event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) { if (event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) {
event.accepted = true; event.accepted = true;
@ -90,43 +92,32 @@ Popup {
event.accepted = true; event.accepted = true;
} }
} }
onTextEdited: {
completerPopup.completer.searchString = text;
}
} }
Completer { Completer {
id: completerPopup id: completerPopup
width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
completerName: "room"
fullWidth: true
centerRowContent: false
avatarHeight: 24 avatarHeight: 24
avatarWidth: 24 avatarWidth: 24
bottomToTop: false bottomToTop: false
centerRowContent: false
completerName: "room"
fullWidth: true
width: forwardMessagePopup.width - forwardMessagePopup.leftPadding * 2
} }
} }
Connections { Connections {
function onCompletionSelected(id) { function onCompletionSelected(id) {
room.forwardMessage(messageContextMenu.eventId, id); room.forwardMessage(messageContextMenu.eventId, id);
forwardMessagePopup.close(); forwardMessagePopup.close();
} }
function onCountChanged() { function onCountChanged() {
if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count)) if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count))
completerPopup.currentIndex = 0; completerPopup.currentIndex = 0;
} }
target: completerPopup target: completerPopup
} }
background: Rectangle {
color: palette.window
}
Overlay.modal: Rectangle {
color: Qt.rgba(palette.window.r, palette.window.g, palette.window.b, 0.7)
}
} }

View file

@ -10,38 +10,35 @@ import im.nheko 1.0 // for cursor shape
AbstractButton { AbstractButton {
id: button id: button
property alias cursor: mouseArea.cursorShape
property string image: undefined
property color highlightColor: palette.highlight
property color buttonTextColor: palette.buttonText property color buttonTextColor: palette.buttonText
property bool changeColorOnHover: true property bool changeColorOnHover: true
property alias cursor: mouseArea.cursorShape
property color highlightColor: palette.highlight
property string image: undefined
property bool ripple: true property bool ripple: true
focusPolicy: Qt.NoFocus focusPolicy: Qt.NoFocus
width: 16
height: 16 height: 16
width: 16
Image { Image {
id: buttonImg id: buttonImg
// Workaround, can't get icon.source working for now... // Workaround, can't get icon.source working for now...
anchors.fill: parent anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : "" source: image != "" ? ("image://colorimage/" + image + "?" + ((button.hovered && changeColorOnHover) ? highlightColor : buttonTextColor)) : ""
sourceSize.height: button.height sourceSize.height: button.height
sourceSize.width: button.width sourceSize.width: button.width
fillMode: Image.PreserveAspectFit
} }
CursorShape { CursorShape {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
Ripple { Ripple {
enabled: button.ripple
color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5) color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5)
enabled: button.ripple
} }
} }

View file

@ -11,22 +11,23 @@ TextEdit {
property alias cursorShape: cs.cursorShape property alias cursorShape: cs.cursorShape
textFormat: TextEdit.RichText ToolTip.text: hoveredLink
readOnly: true ToolTip.visible: hoveredLink || false
focus: false
wrapMode: Text.Wrap
selectByMouse: !Settings.mobileMode
// this always has to be enabled, otherwise you can't click links anymore! // this always has to be enabled, otherwise you can't click links anymore!
//enabled: selectByMouse //enabled: selectByMouse
color: palette.text color: palette.text
onLinkActivated: Nheko.openLink(link) focus: false
ToolTip.visible: hoveredLink || false readOnly: true
ToolTip.text: hoveredLink selectByMouse: !Settings.mobileMode
textFormat: TextEdit.RichText
wrapMode: Text.Wrap
// Setting a tooltip delay makes the hover text empty .-. // Setting a tooltip delay makes the hover text empty .-.
//ToolTip.delay: Nheko.tooltipDelay //ToolTip.delay: Nheko.tooltipDelay
Component.onCompleted: { Component.onCompleted: {
TimelineManager.fixImageRendering(r.textDocument, r); TimelineManager.fixImageRendering(r.textDocument, r);
} }
onLinkActivated: Nheko.openLink(link)
CursorShape { CursorShape {
id: cs id: cs
@ -34,5 +35,4 @@ TextEdit {
anchors.fill: parent anchors.fill: parent
cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
} }
} }

View file

@ -7,67 +7,63 @@ import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import im.nheko 1.0 import im.nheko 1.0
ColumnLayout { ColumnLayout {
id: c id: c
property color backgroundColor: palette.base property color backgroundColor: palette.base
property alias color: labelC.color property alias color: labelC.color
property alias textPadding: input.padding property alias echoMode: input.echoMode
property alias text: input.text property alias font: input.font
property var hasClear: false
property alias label: labelC.text property alias label: labelC.text
property alias placeholderText: input.placeholderText property alias placeholderText: input.placeholderText
property alias font: input.font
property alias echoMode: input.echoMode
property alias selectByMouse: input.selectByMouse property alias selectByMouse: input.selectByMouse
property var hasClear: false property alias text: input.text
property alias textPadding: input.padding
Timer {
id: timer
interval: 350
onTriggered: editingFinished()
}
onTextChanged: timer.restart()
signal textEdited
signal accepted signal accepted
signal editingFinished signal editingFinished
signal textEdited
function forceActiveFocus() {
input.forceActiveFocus();
}
function clear() { function clear() {
input.clear(); input.clear();
} }
function forceActiveFocus() {
input.forceActiveFocus();
}
ToolTip.delay: Nheko.tooltipDelay ToolTip.delay: Nheko.tooltipDelay
ToolTip.visible: hover.hovered ToolTip.visible: hover.hovered
spacing: 0 spacing: 0
Item { onTextChanged: timer.restart()
Layout.fillWidth: true
Layout.preferredHeight: labelC.contentHeight
Layout.margins: input.padding
Layout.bottomMargin: Nheko.paddingSmall
visible: labelC.text
Timer {
id: timer
interval: 350
onTriggered: editingFinished()
}
Item {
Layout.bottomMargin: Nheko.paddingSmall
Layout.fillWidth: true
Layout.margins: input.padding
Layout.preferredHeight: labelC.contentHeight
visible: labelC.text
z: 1 z: 1
Label { Label {
id: labelC id: labelC
y: contentHeight + input.padding + Nheko.paddingSmall
enabled: false
color: palette.text color: palette.text
enabled: false
font.letterSpacing: input.font.pixelSize * 0.02
font.pixelSize: input.font.pixelSize font.pixelSize: input.font.pixelSize
font.weight: Font.DemiBold font.weight: Font.DemiBold
font.letterSpacing: input.font.pixelSize * 0.02
width: parent.width
state: labelC.text && (input.activeFocus == true || input.text) ? "focused" : "" state: labelC.text && (input.activeFocus == true || input.text) ? "focused" : ""
width: parent.width
y: contentHeight + input.padding + Nheko.paddingSmall
states: State { states: State {
name: "focused" name: "focused"
@ -76,50 +72,40 @@ ColumnLayout {
target: labelC target: labelC
y: 0 y: 0
} }
PropertyChanges { PropertyChanges {
target: input
opacity: 1 opacity: 1
target: input
} }
} }
transitions: Transition { transitions: Transition {
from: "" from: ""
to: "focused"
reversible: true reversible: true
to: "focused"
NumberAnimation { NumberAnimation {
target: labelC alwaysRunToEnd: true
duration: 210
easing.type: Easing.InCubic
properties: "y" properties: "y"
duration: 210 target: labelC
easing.type: Easing.InCubic
alwaysRunToEnd: true
} }
NumberAnimation { NumberAnimation {
target: input alwaysRunToEnd: true
properties: "opacity"
duration: 210 duration: 210
easing.type: Easing.InCubic easing.type: Easing.InCubic
alwaysRunToEnd: true properties: "opacity"
target: input
} }
} }
} }
} }
TextField { TextField {
id: input id: input
Layout.fillWidth: true Layout.fillWidth: true
color: labelC.color color: labelC.color
opacity: labelC.text ? 0 : 1
focus: true focus: true
opacity: labelC.text ? 0 : 1
onTextEdited: c.textEdited()
onAccepted: c.accepted()
onEditingFinished: c.editingFinished()
background: Rectangle { background: Rectangle {
id: backgroundRect id: backgroundRect
@ -127,44 +113,46 @@ ColumnLayout {
color: labelC.text ? "transparent" : backgroundColor color: labelC.text ? "transparent" : backgroundColor
} }
onAccepted: c.accepted()
onEditingFinished: c.editingFinished()
onTextEdited: c.textEdited()
ImageButton { ImageButton {
id: clearText id: clearText
focusPolicy: Qt.NoFocus
hoverEnabled: true
image: ":/icons/icons/ui/round-remove-button.svg"
visible: c.hasClear && searchField.text !== '' visible: c.hasClear && searchField.text !== ''
image: ":/icons/icons/ui/round-remove-button.svg"
focusPolicy: Qt.NoFocus
onClicked: { onClicked: {
searchField.clear() searchField.clear();
topBar.searchString = ""; topBar.searchString = "";
} }
hoverEnabled: true
anchors { anchors {
top: parent.top
bottom: parent.bottom bottom: parent.bottom
right: parent.right right: parent.right
rightMargin: Nheko.paddingSmall rightMargin: Nheko.paddingSmall
top: parent.top
} }
} }
} }
Rectangle { Rectangle {
id: blueBar id: blueBar
Layout.fillWidth: true Layout.fillWidth: true
color: palette.highlight color: palette.highlight
height: 1 height: 1
Rectangle { Rectangle {
id: blackBar id: blackBar
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
height: parent.height*2 anchors.top: parent.top
width: 0
color: palette.text color: palette.text
height: parent.height * 2
width: 0
states: State { states: State {
name: "focused" name: "focused"
@ -174,31 +162,25 @@ ColumnLayout {
target: blackBar target: blackBar
width: blueBar.width width: blueBar.width
} }
} }
transitions: Transition { transitions: Transition {
from: "" from: ""
to: "focused"
reversible: true reversible: true
to: "focused"
NumberAnimation { NumberAnimation {
target: blackBar alwaysRunToEnd: true
properties: "width"
duration: 310 duration: 310
easing.type: Easing.InCubic easing.type: Easing.InCubic
alwaysRunToEnd: true properties: "width"
target: blackBar
} }
} }
} }
} }
HoverHandler { HoverHandler {
id: hover id: hover
enabled: c.ToolTip.text enabled: c.ToolTip.text
} }
} }

View file

@ -14,60 +14,54 @@ import im.nheko 1.0
Rectangle { Rectangle {
id: inputBar id: inputBar
property bool showAllButtons: width > 450 || (messageInput.length == 0 && !messageInput.inputMethodComposing)
readonly property string text: messageInput.text readonly property string text: messageInput.text
color: palette.window
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: row.implicitHeight
Layout.minimumHeight: 40 Layout.minimumHeight: 40
property bool showAllButtons: width > 450 || (messageInput.length == 0 && !messageInput.inputMethodComposing) Layout.preferredHeight: row.implicitHeight
color: palette.window
Component { Component {
id: placeCallDialog id: placeCallDialog
PlaceCall { PlaceCall {
} }
} }
Component { Component {
id: screenShareDialog id: screenShareDialog
ScreenShare { ScreenShare {
} }
} }
RowLayout { RowLayout {
id: row id: row
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
ImageButton { ImageButton {
visible: CallManager.callsSupported && showAllButtons
opacity: (CallManager.haveCallInvite || CallManager.isOnCallOnOtherDevice) ? 0.3 : 1
Layout.alignment: Qt.AlignBottom Layout.alignment: Qt.AlignBottom
hoverEnabled: true
width: 22
height: 22
image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.svg" : ":/icons/icons/ui/place-call.svg"
ToolTip.visible: hovered
ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call"))
Layout.margins: 8 Layout.margins: 8
ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : (CallManager.isOnCallOnOtherDevice ? qsTr("Already on a call") : qsTr("Place a call"))
ToolTip.visible: hovered
height: 22
hoverEnabled: true
image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.svg" : ":/icons/icons/ui/place-call.svg"
opacity: (CallManager.haveCallInvite || CallManager.isOnCallOnOtherDevice) ? 0.3 : 1
visible: CallManager.callsSupported && showAllButtons
width: 22
onClicked: { onClicked: {
if (room) { if (room) {
if (CallManager.haveCallInvite) { if (CallManager.haveCallInvite) {
return ; return;
} else if (CallManager.isOnCall) { } else if (CallManager.isOnCall) {
CallManager.hangUp(); CallManager.hangUp();
} } else if (CallManager.isOnCallOnOtherDevice) {
else if(CallManager.isOnCallOnOtherDevice) {
return; return;
} } else {
else {
var dialog = placeCallDialog.createObject(timelineRoot); var dialog = placeCallDialog.createObject(timelineRoot);
dialog.open(); dialog.open();
timelineRoot.destroyOnClose(dialog); timelineRoot.destroyOnClose(dialog);
@ -75,18 +69,18 @@ Rectangle {
} }
} }
} }
ImageButton { ImageButton {
visible: showAllButtons
Layout.alignment: Qt.AlignBottom Layout.alignment: Qt.AlignBottom
hoverEnabled: true
width: 22
height: 22
image: ":/icons/icons/ui/attach.svg"
Layout.margins: 8 Layout.margins: 8
onClicked: room.input.openFileSelection()
ToolTip.visible: hovered
ToolTip.text: qsTr("Send a file") ToolTip.text: qsTr("Send a file")
ToolTip.visible: hovered
height: 22
hoverEnabled: true
image: ":/icons/icons/ui/attach.svg"
visible: showAllButtons
width: 22
onClicked: room.input.openFileSelection()
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@ -98,112 +92,67 @@ Rectangle {
height: parent.height / 2 height: parent.height / 2
running: parent.visible running: parent.visible
} }
} }
} }
ScrollView { ScrollView {
id: textInput id: textInput
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.maximumHeight: Window.height / 4 Layout.maximumHeight: Window.height / 4
Layout.minimumHeight: fontMetrics.lineSpacing Layout.minimumHeight: fontMetrics.lineSpacing
Layout.preferredHeight: contentHeight Layout.preferredHeight: contentHeight
Layout.fillWidth: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentWidth: availableWidth contentWidth: availableWidth
TextArea { TextArea {
id: messageInput id: messageInput
property int completerTriggeredAt: 0 property int completerTriggeredAt: 0
property string lastChar
function insertCompletion(completion) { function insertCompletion(completion) {
messageInput.remove(completerTriggeredAt, cursorPosition); messageInput.remove(completerTriggeredAt, cursorPosition);
messageInput.insert(cursorPosition, completion); messageInput.insert(cursorPosition, completion);
} }
function openCompleter(pos, type) { function openCompleter(pos, type) {
if (popup.opened) return; if (popup.opened)
return;
completerTriggeredAt = pos; completerTriggeredAt = pos;
completer.completerName = type; completer.completerName = type;
popup.open(); popup.open();
completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition)+messageInput.preeditText); completer.completer.setSearchString(messageInput.getText(completerTriggeredAt, cursorPosition) + messageInput.preeditText);
} }
function positionCursorAtEnd() { function positionCursorAtEnd() {
cursorPosition = messageInput.length; cursorPosition = messageInput.length;
} }
function positionCursorAtStart() { function positionCursorAtStart() {
cursorPosition = 0; 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...") placeholderText: qsTr("Write a message...")
placeholderTextColor: palette.buttonText placeholderTextColor: palette.buttonText
color: palette.text selectByMouse: true
width: textInput.width
verticalAlignment: TextEdit.AlignVCenter
wrapMode: TextEdit.Wrap
padding: 0
topPadding: 8 topPadding: 8
bottomPadding: 8 verticalAlignment: TextEdit.AlignVCenter
leftPadding: inputBar.showAllButtons? 0 : 8 width: textInput.width
focus: true wrapMode: TextEdit.Wrap
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); Keys.onPressed: event => {
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) => {
if (event.matches(StandardKey.Paste)) { if (event.matches(StandardKey.Paste)) {
event.accepted = room.input.tryPasteAttachment(false); event.accepted = room.input.tryPasteAttachment(false);
} else if (event.key == Qt.Key_Space) { } else if (event.key == Qt.Key_Space) {
// close popup if user enters space after colon // close popup if user enters space after colon
if (cursorPosition == completerTriggeredAt + 1) if (cursorPosition == completerTriggeredAt + 1)
popup.close(); popup.close();
if (popup.opened && completer.count <= 0) if (popup.opened && completer.count <= 0)
popup.close(); popup.close();
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) { } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) {
messageInput.clear(); messageInput.clear();
} else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) { } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) {
@ -218,8 +167,8 @@ Rectangle {
completer.completerName = ""; completer.completerName = "";
popup.close(); popup.close();
} else if (event.matches(StandardKey.InsertLineSeparator)) { } 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")) { if (Settings.invertEnterKey && (!Qt.inputMethod.visible || Qt.platform.os === "windows")) {
room.input.send(); room.input.send();
event.accepted = true; event.accepted = true;
@ -253,16 +202,16 @@ Rectangle {
console.log('"' + t + '"'); console.log('"' + t + '"');
if (t == '@') { if (t == '@') {
messageInput.openCompleter(pos, "user"); messageInput.openCompleter(pos, "user");
return ; return;
} else if (t == ' ' || t == '\t') { } else if (t == ' ' || t == '\t') {
messageInput.openCompleter(pos + 1, "user"); messageInput.openCompleter(pos + 1, "user");
return ; return;
} else if (t == ':') { } else if (t == ':') {
messageInput.openCompleter(pos, "emoji"); messageInput.openCompleter(pos, "emoji");
return ; return;
} else if (t == '~') { } else if (t == '~') {
messageInput.openCompleter(pos, "customEmoji"); messageInput.openCompleter(pos, "customEmoji");
return ; return;
} }
pos = pos - 1; 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 { Connections {
function onRoomChanged() { function onRoomChanged() {
messageInput.clear(); messageInput.clear();
if (room) if (room)
messageInput.append(room.input.text); messageInput.append(room.input.text);
completer.completerName = ""; completer.completerName = "";
messageInput.forceActiveFocus(); messageInput.forceActiveFocus();
} }
target: timelineView target: timelineView
} }
Connections { Connections {
function onCompletionClicked(completion) { function onCompletionClicked(completion) {
messageInput.insertCompletion(completion); messageInput.insertCompletion(completion);
@ -334,43 +315,39 @@ Rectangle {
target: completer target: completer
} }
Popup { Popup {
id: popup id: popup
background: null
padding: 0
x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x x: messageInput.positionToRectangle(messageInput.completerTriggeredAt).x
y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height y: messageInput.positionToRectangle(messageInput.completerTriggeredAt).y - height
background: null enter: Transition {
padding: 0 NumberAnimation {
duration: 100
from: 0
property: "opacity"
to: 1
}
}
exit: Transition {
NumberAnimation {
duration: 100
from: 1
property: "opacity"
to: 0
}
}
Completer { Completer {
anchors.fill: parent
id: completer id: completer
anchors.fill: parent
rowMargin: 2 rowMargin: 2
rowSpacing: 0 rowSpacing: 0
} }
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: 100
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: 100
}
}
} }
Connections { Connections {
function onTextChanged(newText) { function onTextChanged(newText) {
messageInput.text = newText; messageInput.text = newText;
@ -380,16 +357,13 @@ Rectangle {
ignoreUnknownSignals: true ignoreUnknownSignals: true
target: room ? room.input : null target: room ? room.input : null
} }
Connections { Connections {
function onReplyChanged() {
messageInput.forceActiveFocus();
}
function onEditChanged() { function onEditChanged() {
messageInput.forceActiveFocus(); messageInput.forceActiveFocus();
} }
function onReplyChanged() {
messageInput.forceActiveFocus();
}
function onThreadChanged() { function onThreadChanged() {
messageInput.forceActiveFocus(); messageInput.forceActiveFocus();
} }
@ -397,7 +371,6 @@ Rectangle {
ignoreUnknownSignals: true ignoreUnknownSignals: true
target: room target: room
} }
Connections { Connections {
function onFocusInput() { function onFocusInput() {
messageInput.forceActiveFocus(); messageInput.forceActiveFocus();
@ -405,59 +378,56 @@ Rectangle {
target: TimelineManager target: TimelineManager
} }
MouseArea { MouseArea {
acceptedButtons: Qt.MiddleButton
// workaround for wrong cursor shape on some platforms // workaround for wrong cursor shape on some platforms
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.MiddleButton
cursorShape: Qt.IBeamCursor cursorShape: Qt.IBeamCursor
onPressed: (mouse) => mouse.accepted = room.input.tryPasteAttachment(true)
onPressed: mouse => mouse.accepted = room.input.tryPasteAttachment(true)
} }
} }
} }
ImageButton { ImageButton {
id: stickerButton id: stickerButton
visible: showAllButtons
Layout.alignment: Qt.AlignRight | Qt.AlignBottom Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.margins: 8 Layout.margins: 8
hoverEnabled: true
width: 22
height: 22
image: ":/icons/icons/ui/sticky-note-solid.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Stickers") ToolTip.text: qsTr("Stickers")
onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function(row) { ToolTip.visible: hovered
room.input.sticker(row); height: 22
TimelineManager.focusMessageInput(); hoverEnabled: true
}) image: ":/icons/icons/ui/sticky-note-solid.svg"
visible: showAllButtons
width: 22
onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function (row) {
room.input.sticker(row);
TimelineManager.focusMessageInput();
})
StickerPicker { StickerPicker {
id: stickerPopup id: stickerPopup
emoji: false emoji: false
} }
} }
ImageButton { ImageButton {
id: emojiButton id: emojiButton
Layout.alignment: Qt.AlignRight | Qt.AlignBottom Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.margins: 8 Layout.margins: 8
hoverEnabled: true
width: 22
height: 22
image: ":/icons/icons/ui/smile.svg"
ToolTip.visible: hovered
ToolTip.text: qsTr("Emoji") ToolTip.text: qsTr("Emoji")
onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, room.roomId, function(plaintext, markdown) { ToolTip.visible: hovered
messageInput.insert(messageInput.cursorPosition, markdown); height: 22
TimelineManager.focusMessageInput(); hoverEnabled: true
}) image: ":/icons/icons/ui/smile.svg"
width: 22
onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, room.roomId, function (plaintext, markdown) {
messageInput.insert(messageInput.cursorPosition, markdown);
TimelineManager.focusMessageInput();
})
StickerPicker { StickerPicker {
id: emojiPopup id: emojiPopup
@ -465,28 +435,25 @@ Rectangle {
emoji: true emoji: true
} }
} }
ImageButton { ImageButton {
Layout.alignment: Qt.AlignRight | Qt.AlignBottom Layout.alignment: Qt.AlignRight | Qt.AlignBottom
Layout.margins: 8 Layout.margins: 8
hoverEnabled: true
width: 22
height: 22
image: ":/icons/icons/ui/send.svg"
Layout.rightMargin: 8 Layout.rightMargin: 8
ToolTip.visible: hovered
ToolTip.text: qsTr("Send") ToolTip.text: qsTr("Send")
ToolTip.visible: hovered
height: 22
hoverEnabled: true
image: ":/icons/icons/ui/send.svg"
width: 22
onClicked: { onClicked: {
room.input.send(); room.input.send();
} }
} }
} }
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false
text: qsTr("You don't have permission to send messages in this room") text: qsTr("You don't have permission to send messages in this room")
visible: room ? (!room.permissions.canSend(MtxEvent.TextMessage)) : false
} }
} }

View file

@ -10,37 +10,35 @@ import im.nheko 1.0
Rectangle { Rectangle {
id: warningRoot id: warningRoot
required property string text
property color bubbleColor: Nheko.theme.error property color bubbleColor: Nheko.theme.error
required property string text
implicitHeight: visible ? warningDisplay.implicitHeight + 4 * Nheko.paddingSmall : 0
height: implicitHeight
Layout.fillWidth: true Layout.fillWidth: true
color: palette.window // required to hide the timeline behind this warning color: palette.window // required to hide the timeline behind this warning
height: implicitHeight
implicitHeight: visible ? warningDisplay.implicitHeight + 4 * Nheko.paddingSmall : 0
Rectangle { Rectangle {
id: warningRect id: warningRect
visible: warningRoot.visible
// TODO: Qt.alpha() would make more sense but it wasn't working...
color: Qt.rgba(bubbleColor.r, bubbleColor.g, bubbleColor.b, 0.3)
border.width: 1
border.color: bubbleColor
radius: 3
anchors.fill: parent anchors.fill: parent
anchors.margins: visible ? Nheko.paddingSmall : 0 anchors.margins: visible ? Nheko.paddingSmall : 0
border.color: bubbleColor
border.width: 1
// TODO: Qt.alpha() would make more sense but it wasn't working...
color: Qt.rgba(bubbleColor.r, bubbleColor.g, bubbleColor.b, 0.3)
radius: 3
visible: warningRoot.visible
z: 3 z: 3
Label { Label {
id: warningDisplay id: warningDisplay
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Nheko.paddingSmall anchors.margins: Nheko.paddingSmall
anchors.verticalCenter: parent.verticalCenter
text: warningRoot.text text: warningRoot.text
textFormat: Text.PlainText textFormat: Text.PlainText
} }
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -11,9 +11,8 @@ Item {
id: privacyScreen id: privacyScreen
readonly property bool active: Settings.privacyScreen && screenSaver.state === "Visible" readonly property bool active: Settings.privacyScreen && screenSaver.state === "Visible"
property var timelineRoot
property int screenTimeout property int screenTimeout
property var timelineRoot
required property var windowTarget required property var windowTarget
Connections { Connections {
@ -24,29 +23,28 @@ Item {
} else { } else {
if (timelineRoot.visible) if (timelineRoot.visible)
screenSaverTimer.start(); screenSaverTimer.start();
} }
} }
target: windowTarget target: windowTarget
} }
Timer { Timer {
id: screenSaverTimer id: screenSaverTimer
interval: screenTimeout * 1000 interval: screenTimeout * 1000
running: !windowTarget.active running: !windowTarget.active
onTriggered: { onTriggered: {
screenSaver.state = "Visible"; screenSaver.state = "Visible";
} }
} }
Item { Item {
id: screenSaver id: screenSaver
state: "Invisible"
anchors.fill: parent anchors.fill: parent
state: "Invisible"
visible: false visible: false
states: [ states: [
State { State {
name: "Visible" name: "Visible"
@ -55,20 +53,18 @@ Item {
target: screenSaver target: screenSaver
visible: true visible: true
} }
PropertyChanges { PropertyChanges {
target: screenSaver
opacity: 1 opacity: 1
target: screenSaver
} }
}, },
State { State {
name: "Invisible" name: "Invisible"
PropertyChanges { PropertyChanges {
target: screenSaver
opacity: 0 opacity: 0
target: screenSaver
} }
PropertyChanges { PropertyChanges {
target: screenSaver target: screenSaver
visible: false visible: false
@ -78,39 +74,33 @@ Item {
transitions: [ transitions: [
Transition { Transition {
from: "Invisible" from: "Invisible"
to: "Visible"
reversible: true reversible: true
to: "Visible"
SequentialAnimation { SequentialAnimation {
NumberAnimation { NumberAnimation {
target: screenSaver
property: "visible"
duration: 0 duration: 0
} property: "visible"
NumberAnimation {
target: screenSaver target: screenSaver
property: "opacity" }
NumberAnimation {
duration: 300 duration: 300
easing.type: Easing.Linear easing.type: Easing.Linear
property: "opacity"
target: screenSaver
} }
} }
} }
] ]
MultiEffect { MultiEffect {
id: blur id: blur
blurEnabled: true
anchors.fill: parent anchors.fill: parent
source: timelineRoot
blur: 1.0 blur: 1.0
blurEnabled: true
blurMax: 32 blurMax: 32
source: timelineRoot
} }
} }
} }

View file

@ -11,33 +11,36 @@ Popup {
id: quickSwitcher id: quickSwitcher
property int textHeight: Math.round(Qt.application.font.pixelSize * 2.4) property int textHeight: Math.round(Qt.application.font.pixelSize * 2.4)
property int textMargin: Nheko.paddingSmall
background: null background: null
width: Math.min(Math.max(Math.round(parent.width / 2),450),parent.width) // limiting width to parent.width/2 can be a bit narrow closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
modal: true
parent: Overlay.overlay
width: Math.min(Math.max(Math.round(parent.width / 2), 450), parent.width) // limiting width to parent.width/2 can be a bit narrow
x: Math.round(parent.width / 2 - contentWidth / 2) x: Math.round(parent.width / 2 - contentWidth / 2)
y: Math.round(parent.height / 4) y: Math.round(parent.height / 4)
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside Overlay.modal: Rectangle {
parent: Overlay.overlay color: "#aa1E1E1E"
}
onClosed: TimelineManager.focusMessageInput()
onOpened: { onOpened: {
roomTextInput.forceActiveFocus(); roomTextInput.forceActiveFocus();
} }
onClosed: TimelineManager.focusMessageInput()
property int textMargin: Nheko.paddingSmall
Column{ Column {
anchors.fill: parent anchors.fill: parent
spacing: 1 spacing: 1
MatrixTextField { MatrixTextField {
id: roomTextInput id: roomTextInput
width: parent.width
font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6)
color: palette.text color: palette.text
onTextEdited: { font.pixelSize: Math.ceil(quickSwitcher.textHeight * 0.6)
completerPopup.completer.searchString = text; width: parent.width
}
Keys.onPressed: { Keys.onPressed: {
if (event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) { if (event.key == Qt.Key_Up || event.key == Qt.Key_Backtab) {
event.accepted = true; event.accepted = true;
@ -45,49 +48,43 @@ Popup {
} else if (event.key == Qt.Key_Down || event.key == Qt.Key_Tab) { } else if (event.key == Qt.Key_Down || event.key == Qt.Key_Tab) {
event.accepted = true; event.accepted = true;
if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier)) if (event.key == Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))
completerPopup.up(); completerPopup.up();
else else
completerPopup.down(); completerPopup.down();
} else if (event.matches(StandardKey.InsertParagraphSeparator)) { } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
completerPopup.finishCompletion(); completerPopup.finishCompletion();
event.accepted = true; event.accepted = true;
} }
} }
onTextEdited: {
completerPopup.completer.searchString = text;
}
} }
Completer { Completer {
id: completerPopup id: completerPopup
visible: roomTextInput.text.length > 0
width: parent.width
completerName: "room"
bottomToTop: false
fullWidth: true
avatarHeight: quickSwitcher.textHeight avatarHeight: quickSwitcher.textHeight
avatarWidth: quickSwitcher.textHeight avatarWidth: quickSwitcher.textHeight
bottomToTop: false
centerRowContent: false centerRowContent: false
completerName: "room"
fullWidth: true
rowMargin: Math.round(quickSwitcher.textMargin / 2) rowMargin: Math.round(quickSwitcher.textMargin / 2)
rowSpacing: quickSwitcher.textMargin rowSpacing: quickSwitcher.textMargin
visible: roomTextInput.text.length > 0
width: parent.width
} }
} }
Connections { Connections {
function onCompletionSelected(id) { function onCompletionSelected(id) {
Rooms.setCurrentRoom(id); Rooms.setCurrentRoom(id);
quickSwitcher.close(); quickSwitcher.close();
} }
function onCountChanged() { function onCountChanged() {
if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count)) if (completerPopup.count > 0 && (completerPopup.currentIndex < 0 || completerPopup.currentIndex >= completerPopup.count))
completerPopup.currentIndex = 0; completerPopup.currentIndex = 0;
} }
target: completerPopup target: completerPopup
} }
Overlay.modal: Rectangle {
color: "#aa1E1E1E"
}
} }

View file

@ -11,10 +11,11 @@ import im.nheko 1.0
Flow { Flow {
id: reactionFlow id: reactionFlow
property string eventId
// lower-contrast colors to avoid distracting from text & to enhance hover effect // 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 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 color gentleText: Qt.hsla(palette.text.hslHue, palette.text.hslSaturation, palette.text.hslLightness, 0.6)
property string eventId
property alias reactions: repeater.model property alias reactions: repeater.model
spacing: 4 spacing: 4
@ -25,40 +26,39 @@ Flow {
delegate: AbstractButton { delegate: AbstractButton {
id: reaction id: reaction
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.delay: Nheko.tooltipDelay ToolTip.delay: Nheko.tooltipDelay
onClicked: { ToolTip.visible: hovered
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); hoverEnabled: true
room.input.reaction(reactionFlow.eventId, modelData.key); leftPadding: textMetrics.height / 2
} rightPadding: textMetrics.height / 2
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
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 { contentItem: Row {
spacing: textMetrics.height / 4 spacing: textMetrics.height / 4
TextMetrics { TextMetrics {
id: textMetrics id: textMetrics
font.family: Settings.emojiFont
elide: Text.ElideRight elide: Text.ElideRight
elideWidth: 150 elideWidth: 150
font.family: Settings.emojiFont
text: modelData.displayKey text: modelData.displayKey
} }
Text { Text {
id: reactionText id: reactionText
anchors.baseline: reactionCounter.baseline anchors.baseline: reactionCounter.baseline
color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText : palette.text
font.family: Settings.emojiFont
maximumLineCount: 1
text: { text: {
// When an emoji font is selected that doesn't have , it is dropped from elidedText. So we add it back. // 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) { if (textMetrics.elidedText !== modelData.displayKey) {
@ -68,51 +68,45 @@ Flow {
} }
return textMetrics.elidedText; return textMetrics.elidedText;
} }
font.family: Settings.emojiFont
color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText: palette.text
maximumLineCount: 1
visible: !modelData.key.startsWith("mxc://") visible: !modelData.key.startsWith("mxc://")
} }
Image { Image {
anchors.verticalCenter: divider.verticalCenter anchors.verticalCenter: divider.verticalCenter
fillMode: Image.PreserveAspectFit
height: textMetrics.height height: textMetrics.height
width: textMetrics.height
source: modelData.key.startsWith("mxc://") ? (modelData.key.replace("mxc://", "image://MxcImage/") + "?scale") : "" source: modelData.key.startsWith("mxc://") ? (modelData.key.replace("mxc://", "image://MxcImage/") + "?scale") : ""
visible: modelData.key.startsWith("mxc://") visible: modelData.key.startsWith("mxc://")
fillMode: Image.PreserveAspectFit width: textMetrics.height
} }
Rectangle { Rectangle {
id: divider id: divider
color: reaction.hovered ? palette.text : gentleText
height: Math.floor(reactionCounter.implicitHeight * 1.4) height: Math.floor(reactionCounter.implicitHeight * 1.4)
width: 1 width: 1
color: reaction.hovered ? palette.text: gentleText
} }
Text { Text {
id: reactionCounter id: reactionCounter
anchors.verticalCenter: divider.verticalCenter anchors.verticalCenter: divider.verticalCenter
text: modelData.count color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText : palette.windowText
font: reaction.font font: reaction.font
color: (reaction.hovered || modelData.selfReactedEvent !== '') ? palette.highlightedText: palette.windowText text: modelData.count
} }
} }
background: Rectangle { Component.onCompleted: {
anchors.centerIn: parent ToolTip.text = Qt.binding(function () {
implicitWidth: reaction.implicitWidth if (textMetrics.elidedText === textMetrics.text) {
implicitHeight: reaction.implicitHeight return modelData.users;
border.color: reaction.hovered ? palette.text: gentleText }
color: reaction.hovered ? palette.highlight : (modelData.selfReactedEvent !== '' ? gentleHighlight : palette.window) return modelData.displayKey + "\n" + modelData.users;
border.width: 1 });
radius: reaction.height / 2 }
onClicked: {
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent);
room.input.reaction(reactionFlow.eventId, modelData.key);
} }
} }
} }
} }

View file

@ -12,91 +12,89 @@ Rectangle {
id: replyPopup id: replyPopup
Layout.fillWidth: true Layout.fillWidth: true
visible: room && (room.reply || room.edit || room.thread) color: palette.window
// Height of child, plus margins, plus border // Height of child, plus margins, plus border
implicitHeight: (room && room.reply ? replyPreview.height : Math.max(closeEditButton.height, closeThreadButton.height)) + Nheko.paddingSmall 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 z: 3
Reply { Reply {
id: replyPreview 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.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.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.top: parent.top
anchors.topMargin: Nheko.paddingSmall anchors.topMargin: Nheko.paddingSmall
userColor: TimelineManager.userColor(modelData.userId, palette.window)
blurhash: modelData.blurhash ?? "" blurhash: modelData.blurhash ?? ""
body: modelData.body ?? "" body: modelData.body ?? ""
formattedBody: modelData.formattedBody ?? "" encryptionError: modelData.encryptionError ?? 0
eventId: modelData.eventId ?? "" eventId: modelData.eventId ?? ""
filename: modelData.filename ?? "" filename: modelData.filename ?? ""
filesize: modelData.filesize ?? "" filesize: modelData.filesize ?? ""
formattedBody: modelData.formattedBody ?? ""
isOnlyEmoji: modelData.isOnlyEmoji ?? false
originalWidth: modelData.originalWidth ?? 0
proportionalHeight: modelData.proportionalHeight ?? 1 proportionalHeight: modelData.proportionalHeight ?? 1
type: modelData.type ?? MtxEvent.UnknownMessage type: modelData.type ?? MtxEvent.UnknownMessage
typeString: modelData.typeString ?? "" typeString: modelData.typeString ?? ""
url: modelData.url ?? "" url: modelData.url ?? ""
originalWidth: modelData.originalWidth ?? 0 userColor: TimelineManager.userColor(modelData.userId, palette.window)
isOnlyEmoji: modelData.isOnlyEmoji ?? false
userId: modelData.userId ?? "" userId: modelData.userId ?? ""
userName: modelData.userName ?? "" userName: modelData.userName ?? ""
encryptionError: modelData.encryptionError ?? 0 visible: room && room.reply
width: parent.width width: parent.width
} }
ImageButton { ImageButton {
id: closeReplyButton id: closeReplyButton
visible: room && room.reply ToolTip.text: qsTr("Close")
ToolTip.visible: closeReplyButton.hovered
anchors.margins: Nheko.paddingSmall
anchors.right: replyPreview.right anchors.right: replyPreview.right
anchors.top: replyPreview.top anchors.top: replyPreview.top
anchors.margins: Nheko.paddingSmall
hoverEnabled: true
width: 16
height: 16 height: 16
hoverEnabled: true
image: ":/icons/icons/ui/dismiss.svg" image: ":/icons/icons/ui/dismiss.svg"
ToolTip.visible: closeReplyButton.hovered visible: room && room.reply
ToolTip.text: qsTr("Close") width: 16
onClicked: room.reply = undefined onClicked: room.reply = undefined
} }
ImageButton { ImageButton {
id: closeEditButton id: closeEditButton
visible: room && room.edit ToolTip.text: qsTr("Cancel Edit")
anchors.right: closeThreadButton.left ToolTip.visible: closeEditButton.hovered
anchors.margins: 8 anchors.margins: 8
anchors.right: closeThreadButton.left
anchors.top: parent.top anchors.top: parent.top
height: 22
hoverEnabled: true hoverEnabled: true
image: ":/icons/icons/ui/dismiss_edit.svg" image: ":/icons/icons/ui/dismiss_edit.svg"
visible: room && room.edit
width: 22 width: 22
height: 22
ToolTip.visible: closeEditButton.hovered
ToolTip.text: qsTr("Cancel Edit")
onClicked: room.edit = undefined onClicked: room.edit = undefined
} }
ImageButton { ImageButton {
id: closeThreadButton id: closeThreadButton
visible: room && room.thread
anchors.right: parent.right
anchors.margins: 8
anchors.top: parent.top
hoverEnabled: true
buttonTextColor: room ? TimelineManager.userColor(room.thread, palette.base) : palette.buttonText
image: ":/icons/icons/ui/dismiss_thread.svg"
width: 22
height: 22
ToolTip.visible: closeThreadButton.hovered
ToolTip.text: qsTr("Cancel Thread") ToolTip.text: qsTr("Cancel Thread")
ToolTip.visible: closeThreadButton.hovered
anchors.margins: 8
anchors.right: parent.right
anchors.top: parent.top
buttonTextColor: room ? TimelineManager.userColor(room.thread, palette.base) : palette.buttonText
height: 22
hoverEnabled: true
image: ":/icons/icons/ui/dismiss_thread.svg"
visible: room && room.thread
width: 22
onClicked: room.thread = undefined onClicked: room.thread = undefined
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -20,19 +20,14 @@ import im.nheko.EmojiModel 1.0
Pane { Pane {
id: timelineRoot id: timelineRoot
background: null function destroyOnClose(obj) {
padding: 0 if (obj.closing != undefined)
obj.closing.connect(() => obj.destroy(1000));
FontMetrics { else if (obj.aboutToHide != undefined)
id: fontMetrics obj.aboutToHide.connect(() => obj.destroy(1000));
} }
function destroyOnClosed(obj) {
RoomDirectoryModel { obj.aboutToHide.connect(() => obj.destroy(1000));
id: publicRooms
}
UserDirectoryModel {
id: userDirectory
} }
//Timer { //Timer {
@ -41,54 +36,49 @@ Pane {
// running: true // running: true
// repeat: true // repeat: true
//} //}
function showAliasEditor(settings) { 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) { if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, { var dialog = component.createObject(timelineRoot, {
"roomSettings": settings "roomSettings": settings
}); });
dialog.show();
destroyOnClose(dialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function showPLEditor(settings) {
var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelEditor.qml")
if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, {
"roomSettings": settings
});
dialog.show(); dialog.show();
destroyOnClose(dialog); destroyOnClose(dialog);
} else { } else {
console.error("Failed to create component: " + component.errorString()); console.error("Failed to create component: " + component.errorString());
} }
} }
function showSpacePLApplyPrompt(settings, editingModel) {
var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelSpacesApplyDialog.qml")
if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, {
"roomSettings": settings,
"editingModel": editingModel
});
dialog.show();
destroyOnClose(dialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function showAllowedRoomsEditor(settings) { function showAllowedRoomsEditor(settings) {
var component = Qt.createComponent("qrc:/qml/dialogs/AllowedRoomsSettingsDialog.qml") var component = Qt.createComponent("qrc:/qml/dialogs/AllowedRoomsSettingsDialog.qml");
if (component.status == Component.Ready) { if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, { var dialog = component.createObject(timelineRoot, {
"roomSettings": settings "roomSettings": settings
}); });
dialog.show();
destroyOnClose(dialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function showPLEditor(settings) {
var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelEditor.qml");
if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, {
"roomSettings": settings
});
dialog.show();
destroyOnClose(dialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function showSpacePLApplyPrompt(settings, editingModel) {
var component = Qt.createComponent("qrc:/qml/dialogs/PowerLevelSpacesApplyDialog.qml");
if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, {
"roomSettings": settings,
"editingModel": editingModel
});
dialog.show(); dialog.show();
destroyOnClose(dialog); destroyOnClose(dialog);
} else { } else {
@ -96,23 +86,37 @@ Pane {
} }
} }
background: null
padding: 0
FontMetrics {
id: fontMetrics
}
RoomDirectoryModel {
id: publicRooms
}
UserDirectoryModel {
id: userDirectory
}
Component { Component {
id: readReceiptsDialog id: readReceiptsDialog
ReadReceipts { ReadReceipts {
} }
} }
Shortcut { Shortcut {
sequence: StandardKey.Quit sequence: StandardKey.Quit
onActivated: Qt.quit() onActivated: Qt.quit()
} }
Shortcut { Shortcut {
sequence: "Ctrl+K" sequence: "Ctrl+K"
onActivated: { onActivated: {
var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml") var component = Qt.createComponent("qrc:/qml/QuickSwitcher.qml");
if (component.status == Component.Ready) { if (component.status == Component.Ready) {
var quickSwitch = component.createObject(timelineRoot); var quickSwitch = component.createObject(timelineRoot);
quickSwitch.open(); quickSwitch.open();
@ -122,37 +126,25 @@ Pane {
} }
} }
} }
Shortcut { Shortcut {
// Add alternative shortcut, because sometimes Alt+A is stolen by the TextEdit // Add alternative shortcut, because sometimes Alt+A is stolen by the TextEdit
sequences: ["Alt+A", "Ctrl+Shift+A"] sequences: ["Alt+A", "Ctrl+Shift+A"]
onActivated: Rooms.nextRoomWithActivity() onActivated: Rooms.nextRoomWithActivity()
} }
Shortcut { Shortcut {
sequence: "Ctrl+Down" sequence: "Ctrl+Down"
onActivated: Rooms.nextRoom() onActivated: Rooms.nextRoom()
} }
Shortcut { Shortcut {
sequence: "Ctrl+Up" sequence: "Ctrl+Up"
onActivated: Rooms.previousRoom() onActivated: Rooms.previousRoom()
} }
Connections { Connections {
function onOpenLogoutDialog() {
var component = Qt.createComponent("qrc:/qml/dialogs/LogoutDialog.qml")
if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot);
dialog.open();
destroyOnClose(dialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function onOpenJoinRoomDialog() { function onOpenJoinRoomDialog() {
var component = Qt.createComponent("qrc:/qml/dialogs/JoinRoomDialog.qml") var component = Qt.createComponent("qrc:/qml/dialogs/JoinRoomDialog.qml");
if (component.status == Component.Ready) { if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot); var dialog = component.createObject(timelineRoot);
dialog.show(); dialog.show();
@ -161,11 +153,22 @@ Pane {
console.error("Failed to create component: " + component.errorString()); console.error("Failed to create component: " + component.errorString());
} }
} }
function onOpenLogoutDialog() {
function onShowRoomJoinPrompt(summary) { var component = Qt.createComponent("qrc:/qml/dialogs/LogoutDialog.qml");
var component = Qt.createComponent("qrc:/qml/dialogs/ConfirmJoinRoomDialog.qml")
if (component.status == Component.Ready) { if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, {"summary": summary}); var dialog = component.createObject(timelineRoot);
dialog.open();
destroyOnClose(dialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function onShowRoomJoinPrompt(summary) {
var component = Qt.createComponent("qrc:/qml/dialogs/ConfirmJoinRoomDialog.qml");
if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, {
"summary": summary
});
dialog.show(); dialog.show();
destroyOnClose(dialog); destroyOnClose(dialog);
} else { } else {
@ -175,12 +178,13 @@ Pane {
target: Nheko target: Nheko
} }
Connections { Connections {
function onNewDeviceVerificationRequest(flow) { 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) { if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, {"flow": flow}); var dialog = component.createObject(timelineRoot, {
"flow": flow
});
dialog.show(); dialog.show();
destroyOnClose(dialog); destroyOnClose(dialog);
} else { } else {
@ -190,101 +194,71 @@ Pane {
target: VerificationManager 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 { Connections {
function onOpenProfile(profile) {
var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml")
if (component.status == Component.Ready) {
var userProfile = component.createObject(timelineRoot, {"profile": profile});
userProfile.show();
destroyOnClose(userProfile);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function onShowImagePackSettings(room, packlist) {
var component = Qt.createComponent("qrc:/qml/dialogs/ImagePackSettingsDialog.qml")
if (component.status == Component.Ready) {
var packSet = component.createObject(timelineRoot, {
"room": room,
"packlist": packlist
});
packSet.show();
destroyOnClose(packSet);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function onOpenRoomMembersDialog(members, room) {
var component = Qt.createComponent("qrc:/qml/dialogs/RoomMembers.qml")
if (component.status == Component.Ready) {
var membersDialog = component.createObject(timelineRoot, {
"members": members,
"room": room
});
membersDialog.show();
destroyOnClose(membersDialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function onOpenRoomSettingsDialog(settings) {
var component = Qt.createComponent("qrc:/qml/dialogs/RoomSettings.qml")
if (component.status == Component.Ready) {
var roomSettings = component.createObject(timelineRoot, {
"roomSettings": settings
});
roomSettings.show();
destroyOnClose(roomSettings);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function onOpenInviteUsersDialog(invitees) { function onOpenInviteUsersDialog(invitees) {
var component = Qt.createComponent("qrc:/qml/dialogs/InviteDialog.qml") var component = Qt.createComponent("qrc:/qml/dialogs/InviteDialog.qml");
if (component.status == Component.Ready) { if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, { var dialog = component.createObject(timelineRoot, {
"invitees": invitees "invitees": invitees
}); });
dialog.show(); dialog.show();
destroyOnClose(dialog); destroyOnClose(dialog);
} else { } else {
console.error("Failed to create component: " + component.errorString()); console.error("Failed to create component: " + component.errorString());
} }
} }
function onOpenLeaveRoomDialog(roomid, reason) { function onOpenLeaveRoomDialog(roomid, reason) {
var component = Qt.createComponent("qrc:/qml/dialogs/LeaveRoomDialog.qml") var component = Qt.createComponent("qrc:/qml/dialogs/LeaveRoomDialog.qml");
if (component.status == Component.Ready) { if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, { var dialog = component.createObject(timelineRoot, {
"roomId": roomid, "roomId": roomid,
"reason": reason "reason": reason
}); });
dialog.open(); dialog.open();
destroyOnClose(dialog); destroyOnClose(dialog);
} else { } else {
console.error("Failed to create component: " + component.errorString()); console.error("Failed to create component: " + component.errorString());
} }
} }
function onOpenProfile(profile) {
var component = Qt.createComponent("qrc:/qml/dialogs/UserProfile.qml");
if (component.status == Component.Ready) {
var userProfile = component.createObject(timelineRoot, {
"profile": profile
});
userProfile.show();
destroyOnClose(userProfile);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function onOpenRoomMembersDialog(members, room) {
var component = Qt.createComponent("qrc:/qml/dialogs/RoomMembers.qml");
if (component.status == Component.Ready) {
var membersDialog = component.createObject(timelineRoot, {
"members": members,
"room": room
});
membersDialog.show();
destroyOnClose(membersDialog);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function onOpenRoomSettingsDialog(settings) {
var component = Qt.createComponent("qrc:/qml/dialogs/RoomSettings.qml");
if (component.status == Component.Ready) {
var roomSettings = component.createObject(timelineRoot, {
"roomSettings": settings
});
roomSettings.show();
destroyOnClose(roomSettings);
} else {
console.error("Failed to create component: " + component.errorString());
}
}
function onShowImageOverlay(room, eventId, url, originalWidth, proportionalHeight) { 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) { if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, { var dialog = component.createObject(timelineRoot, {
"room": room, "room": room,
@ -292,22 +266,33 @@ Pane {
"url": url, "url": url,
"originalWidth": originalWidth ?? 0, "originalWidth": originalWidth ?? 0,
"proportionalHeight": proportionalHeight ?? 0 "proportionalHeight": proportionalHeight ?? 0
} });
);
dialog.showFullScreen(); dialog.showFullScreen();
destroyOnClose(dialog); destroyOnClose(dialog);
} else { } else {
console.error("Failed to create component: " + component.errorString()); 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 target: TimelineManager
} }
Connections { Connections {
function onNewInviteState() { function onNewInviteState() {
if (CallManager.haveCallInvite && Settings.mobileMode) { 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) { if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot); var dialog = component.createObject(timelineRoot);
dialog.open(); dialog.open();
@ -320,141 +305,97 @@ Pane {
target: CallManager target: CallManager
} }
SelfVerificationCheck { SelfVerificationCheck {
} }
InputDialog { InputDialog {
id: uiaPassPrompt id: uiaPassPrompt
echoMode: TextInput.Password echoMode: TextInput.Password
title: UIA.title
prompt: qsTr("Please enter your login password to continue:") prompt: qsTr("Please enter your login password to continue:")
onAccepted: (t) => { title: UIA.title
onAccepted: t => {
return UIA.continuePassword(t); return UIA.continuePassword(t);
} }
} }
InputDialog { InputDialog {
id: uiaEmailPrompt id: uiaEmailPrompt
title: UIA.title
prompt: qsTr("Please enter a valid email address to continue:") prompt: qsTr("Please enter a valid email address to continue:")
onAccepted: (t) => { title: UIA.title
onAccepted: t => {
return UIA.continueEmail(t); return UIA.continueEmail(t);
} }
} }
PhoneNumberInputDialog { PhoneNumberInputDialog {
id: uiaPhoneNumberPrompt id: uiaPhoneNumberPrompt
title: UIA.title
prompt: qsTr("Please enter a valid phone number to continue:") prompt: qsTr("Please enter a valid phone number to continue:")
title: UIA.title
onAccepted: (p, t) => { onAccepted: (p, t) => {
return UIA.continuePhoneNumber(p, t); return UIA.continuePhoneNumber(p, t);
} }
} }
InputDialog { InputDialog {
id: uiaTokenPrompt id: uiaTokenPrompt
title: UIA.title
prompt: qsTr("Please enter the token which has been sent to you:") prompt: qsTr("Please enter the token which has been sent to you:")
onAccepted: (t) => { title: UIA.title
onAccepted: t => {
return UIA.submit3pidToken(t); return UIA.submit3pidToken(t);
} }
} }
Platform.MessageDialog { Platform.MessageDialog {
id: uiaErrorDialog id: uiaErrorDialog
buttons: Platform.MessageDialog.Ok buttons: Platform.MessageDialog.Ok
} }
Platform.MessageDialog { Platform.MessageDialog {
id: uiaConfirmationLinkDialog id: uiaConfirmationLinkDialog
buttons: Platform.MessageDialog.Ok buttons: Platform.MessageDialog.Ok
text: qsTr("Wait for the confirmation link to arrive, then continue.") text: qsTr("Wait for the confirmation link to arrive, then continue.")
onAccepted: UIA.continue3pidReceived() onAccepted: UIA.continue3pidReceived()
} }
Connections { Connections {
function onPassword() {
console.log("UIA: password needed");
uiaPassPrompt.show();
}
function onEmail() {
uiaEmailPrompt.show();
}
function onPhoneNumber() {
uiaPhoneNumberPrompt.show();
}
function onPrompt3pidToken() {
uiaTokenPrompt.show();
}
function onConfirm3pidToken() { function onConfirm3pidToken() {
uiaConfirmationLinkDialog.open(); uiaConfirmationLinkDialog.open();
} }
function onEmail() {
uiaEmailPrompt.show();
}
function onError(msg) { function onError(msg) {
uiaErrorDialog.text = msg; uiaErrorDialog.text = msg;
uiaErrorDialog.open(); uiaErrorDialog.open();
} }
function onPassword() {
console.log("UIA: password needed");
uiaPassPrompt.show();
}
function onPhoneNumber() {
uiaPhoneNumberPrompt.show();
}
function onPrompt3pidToken() {
uiaTokenPrompt.show();
}
target: UIA target: UIA
} }
StackView { StackView {
id: mainWindow id: mainWindow
anchors.fill: parent property Transition popEnterOrg
initialItem: welcomePage property Transition popExitOrg
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
}
}
}
// for some reason direct bindings to a hidden StackView don't work, so manually store and restore here. // for some reason direct bindings to a hidden StackView don't work, so manually store and restore here.
property Transition pushEnterOrg property Transition pushEnterOrg
property Transition pushExitOrg property Transition pushExitOrg
property Transition popEnterOrg
property Transition popExitOrg
property Transition replaceEnterOrg property Transition replaceEnterOrg
property Transition replaceExitOrg property Transition replaceExitOrg
Component.onCompleted: {
pushEnterOrg = pushEnter;
popEnterOrg = popEnter;
replaceEnterOrg = replaceEnter;
pushExitOrg = pushExit;
popExitOrg = popExit;
replaceExitOrg = replaceExit;
updateTrans()
}
function updateTrans() { function updateTrans() {
pushEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : pushEnterOrg; pushEnter = Settings.reducedMotion ? reducedMotionTransitionEnter : pushEnterOrg;
@ -465,65 +406,104 @@ Pane {
replaceExit = Settings.reducedMotion ? reducedMotionTransitionExit : replaceExitOrg; replaceExit = Settings.reducedMotion ? reducedMotionTransitionExit : replaceExitOrg;
} }
anchors.fill: parent
initialItem: welcomePage
Component.onCompleted: {
pushEnterOrg = pushEnter;
popEnterOrg = popEnter;
replaceEnterOrg = replaceEnter;
pushExitOrg = pushExit;
popExitOrg = popExit;
replaceExitOrg = replaceExit;
updateTrans();
}
Transition {
id: reducedMotionTransitionExit
PropertyAnimation {
duration: 200
from: 1
property: "opacity"
to: 0
}
}
Transition {
id: reducedMotionTransitionEnter
SequentialAnimation {
PropertyAction {
property: "opacity"
value: 0
}
PauseAnimation {
duration: 200
}
PropertyAnimation {
duration: 200
from: 0
property: "opacity"
to: 1
}
}
}
Connections { Connections {
target: Settings
function onReducedMotionChanged() { function onReducedMotionChanged() {
mainWindow.updateTrans(); mainWindow.updateTrans();
} }
target: Settings
} }
} }
Component { Component {
id: welcomePage id: welcomePage
WelcomePage { WelcomePage {
} }
} }
Component { Component {
id: chatPage id: chatPage
ChatPage { ChatPage {
} }
} }
Component { Component {
id: loginPage id: loginPage
LoginPage { LoginPage {
} }
} }
Component { Component {
id: registerPage id: registerPage
RegisterPage { RegisterPage {
} }
} }
Component { Component {
id: userSettingsPage id: userSettingsPage
UserSettingsPage { UserSettingsPage {
} }
}
Snackbar {
id: snackbar
} }
Snackbar { id: snackbar }
Connections { Connections {
function onSwitchToChatPage() {
mainWindow.replace(null, chatPage);
}
function onSwitchToLoginPage(error) {
mainWindow.replace(welcomePage, {}, loginPage, {"error": error}, StackView.PopTransition);
}
function onShowNotification(msg) { function onShowNotification(msg) {
snackbar.showNotification(msg); snackbar.showNotification(msg);
console.log("New snack: " + msg); console.log("New snack: " + msg);
} }
function onSwitchToChatPage() {
mainWindow.replace(null, chatPage);
}
function onSwitchToLoginPage(error) {
mainWindow.replace(welcomePage, {}, loginPage, {
"error": error
}, StackView.PopTransition);
}
target: MainWindow target: MainWindow
} }
} }

View file

@ -10,22 +10,29 @@ import QtQuick.Layouts 1.3
import im.nheko 1.0 import im.nheko 1.0
Item { Item {
visible: false
enabled: false enabled: false
visible: false
Dialog { Dialog {
id: showRecoverKeyDialog id: showRecoverKeyDialog
property string recoveryKey: "" property string recoveryKey: ""
parent: Overlay.overlay
anchors.centerIn: parent anchors.centerIn: parent
height: content.height + implicitFooterHeight + implicitHeaderHeight
width: content.width
padding: 0
modal: true
standardButtons: Dialog.Ok
closePolicy: Popup.NoAutoClose closePolicy: Popup.NoAutoClose
height: content.height + implicitFooterHeight + implicitHeaderHeight
modal: true
padding: 0
parent: Overlay.overlay
standardButtons: Dialog.Ok
width: content.width
background: Rectangle {
border.color: Nheko.theme.separator
border.width: 1
color: palette.window
radius: Nheko.paddingSmall
}
ColumnLayout { ColumnLayout {
id: content id: content
@ -33,45 +40,33 @@ Item {
spacing: 0 spacing: 0
Label { Label {
Layout.fillWidth: true
Layout.margins: Nheko.paddingMedium Layout.margins: Nheko.paddingMedium
Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4 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 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 wrapMode: Text.Wrap
} }
TextEdit { TextEdit {
Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: (Overlay.overlay ? Overlay.overlay.width : 400) - Nheko.paddingMedium * 4
color: palette.text
font.bold: true
horizontalAlignment: TextEdit.AlignHCenter horizontalAlignment: TextEdit.AlignHCenter
verticalAlignment: TextEdit.AlignVCenter
readOnly: true readOnly: true
selectByMouse: true selectByMouse: true
text: showRecoverKeyDialog.recoveryKey text: showRecoverKeyDialog.recoveryKey
color: palette.text verticalAlignment: TextEdit.AlignVCenter
font.bold: true
wrapMode: TextEdit.Wrap wrapMode: TextEdit.Wrap
} }
} }
background: Rectangle {
color: palette.window
border.color: Nheko.theme.separator
border.width: 1
radius: Nheko.paddingSmall
}
} }
P.MessageDialog { P.MessageDialog {
id: successDialog id: successDialog
buttons: P.MessageDialog.Ok buttons: P.MessageDialog.Ok
text: qsTr("Encryption setup successfully") text: qsTr("Encryption setup successfully")
} }
P.MessageDialog { P.MessageDialog {
id: failureDialog id: failureDialog
@ -80,85 +75,86 @@ Item {
buttons: P.MessageDialog.Ok buttons: P.MessageDialog.Ok
text: qsTr("Failed to setup encryption: %1").arg(errorMessage) text: qsTr("Failed to setup encryption: %1").arg(errorMessage)
} }
MainWindowDialog { MainWindowDialog {
id: bootstrapCrosssigning 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) onAccepted: SelfVerificationStatus.setupCrosssigning(storeSecretsOnline.checked, usePassword.checked ? passwordField.text : "", useOnlineKeyBackup.checked)
GridLayout { GridLayout {
id: grid id: grid
width: bootstrapCrosssigning.useableWidth columnSpacing: 0
columns: 2 columns: 2
rowSpacing: 0 rowSpacing: 0
columnSpacing: 0 width: bootstrapCrosssigning.useableWidth
z: 1 z: 1
Label { Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.margins: Nheko.paddingMedium
color: palette.text
font.pointSize: fontMetrics.font.pointSize * 2 font.pointSize: fontMetrics.font.pointSize * 2
text: qsTr("Setup Encryption") text: qsTr("Setup Encryption")
color: palette.text
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
Label { Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.margins: Nheko.paddingMedium
Layout.maximumWidth: grid.width - Nheko.paddingMedium * 2 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 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 wrapMode: Text.Wrap
} }
Label { Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 1 Layout.columnSpan: 1
Layout.margins: Nheko.paddingMedium
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 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 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 wrapMode: Text.Wrap
} }
Item { Item {
Layout.margins: Nheko.paddingMedium
Layout.preferredHeight: storeSecretsOnline.height
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: Nheko.paddingMedium
Layout.preferredHeight: storeSecretsOnline.height
ToggleButton { ToggleButton {
id: storeSecretsOnline id: storeSecretsOnline
checked: true checked: true
onClicked: console.log("Store secrets toggled: " + checked) onClicked: console.log("Store secrets toggled: " + checked)
} }
} }
Label { Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 1 Layout.columnSpan: 1
Layout.rowSpan: 2 Layout.margins: Nheko.paddingMedium
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
visible: storeSecretsOnline.checked Layout.rowSpan: 2
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.)"
color: palette.text 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 wrapMode: Text.Wrap
} }
Item { Item {
Layout.margins: Nheko.paddingMedium
Layout.topMargin: Nheko.paddingLarge
Layout.preferredHeight: storeSecretsOnline.height
Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.rowSpan: usePassword.checked ? 1 : 2
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: Nheko.paddingMedium
Layout.preferredHeight: storeSecretsOnline.height
Layout.rowSpan: usePassword.checked ? 1 : 2
Layout.topMargin: Nheko.paddingLarge
visible: storeSecretsOnline.checked visible: storeSecretsOnline.checked
ToggleButton { ToggleButton {
@ -166,57 +162,43 @@ Item {
checked: false checked: false
} }
} }
MatrixTextField { MatrixTextField {
id: passwordField id: passwordField
Layout.margins: Nheko.paddingMedium
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.columnSpan: 1 Layout.columnSpan: 1
Layout.fillWidth: true Layout.fillWidth: true
visible: storeSecretsOnline.checked && usePassword.checked
echoMode: TextInput.Password
}
Label {
Layout.margins: Nheko.paddingMedium Layout.margins: Nheko.paddingMedium
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2
echoMode: TextInput.Password
visible: storeSecretsOnline.checked && usePassword.checked
}
Label {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.columnSpan: 1 Layout.columnSpan: 1
Layout.margins: Nheko.paddingMedium
Layout.maximumWidth: Math.floor(grid.width / 2) - Nheko.paddingMedium * 2 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 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 wrapMode: Text.Wrap
} }
Item { Item {
Layout.margins: Nheko.paddingMedium
Layout.preferredHeight: storeSecretsOnline.height
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
Layout.margins: Nheko.paddingMedium
Layout.preferredHeight: storeSecretsOnline.height
ToggleButton { ToggleButton {
id: useOnlineKeyBackup id: useOnlineKeyBackup
checked: true checked: true
onClicked: console.log("Online key backup toggled: " + checked) 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 { MainWindowDialog {
id: verifyMasterKey id: verifyMasterKey
@ -225,54 +207,61 @@ Item {
GridLayout { GridLayout {
id: masterGrid id: masterGrid
width: verifyMasterKey.useableWidth
columns: 1 columns: 1
width: verifyMasterKey.useableWidth
z: 1 z: 1
Label { Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.margins: Nheko.paddingMedium
color: palette.text
//Layout.columnSpan: 2 //Layout.columnSpan: 2
font.pointSize: fontMetrics.font.pointSize * 2 font.pointSize: fontMetrics.font.pointSize * 2
text: qsTr("Activate Encryption") text: qsTr("Activate Encryption")
color: palette.text
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
Label { Label {
Layout.margins: Nheko.paddingMedium
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.margins: Nheko.paddingMedium
//Layout.columnSpan: 2 //Layout.columnSpan: 2
Layout.maximumWidth: grid.width - Nheko.paddingMedium * 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 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 wrapMode: Text.Wrap
} }
FlatButton { FlatButton {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: qsTr("verify") text: qsTr("verify")
onClicked: { onClicked: {
SelfVerificationStatus.verifyMasterKey(); SelfVerificationStatus.verifyMasterKey();
verifyMasterKey.close(); verifyMasterKey.close();
} }
} }
FlatButton { FlatButton {
visible: SelfVerificationStatus.hasSSSS
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: qsTr("enter passphrase") text: qsTr("enter passphrase")
visible: SelfVerificationStatus.hasSSSS
onClicked: { onClicked: {
SelfVerificationStatus.verifyMasterKeyWithPassphrase(); SelfVerificationStatus.verifyMasterKeyWithPassphrase();
verifyMasterKey.close(); verifyMasterKey.close();
} }
} }
} }
} }
Connections { Connections {
function onSetupCompleted() {
successDialog.open();
}
function onSetupFailed(m) {
failureDialog.errorMessage = m;
failureDialog.open();
}
function onShowRecoveryKey(key) {
showRecoverKeyDialog.recoveryKey = key;
showRecoverKeyDialog.open();
}
function onStatusChanged() { function onStatusChanged() {
console.log("STATUS CHANGED: " + SelfVerificationStatus.status); console.log("STATUS CHANGED: " + SelfVerificationStatus.status);
if (SelfVerificationStatus.status == SelfVerificationStatus.NoMasterKey) { 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 target: SelfVerificationStatus
} }
} }

View file

@ -9,15 +9,9 @@ import im.nheko 1.0
ImageButton { ImageButton {
id: indicator id: indicator
required property int status
required property string eventId 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: { ToolTip.text: {
switch (status) { switch (status) {
case MtxEvent.Failed: case MtxEvent.Failed:
@ -32,11 +26,11 @@ ImageButton {
return ""; return "";
} }
} }
onClicked: { ToolTip.visible: hovered && status != MtxEvent.Empty
if (status == MtxEvent.Read) changeColorOnHover: (status == MtxEvent.Read)
room.showReadReceipts(eventId); cursor: (status == MtxEvent.Read) ? Qt.PointingHandCursor : Qt.ArrowCursor
height: 16
} hoverEnabled: true
image: { image: {
switch (status) { switch (status) {
case MtxEvent.Failed: case MtxEvent.Failed:
@ -51,4 +45,10 @@ ImageButton {
return ""; return "";
} }
} }
width: 16
onClicked: {
if (status == MtxEvent.Read)
room.showReadReceipts(eventId);
}
} }

View file

@ -13,72 +13,45 @@ import im.nheko 1.0
AbstractButton { AbstractButton {
id: r 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 blurhash
required property string body 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 eventId
required property string filename required property string filename
required property string filesize required property string filesize
required property string url required property string formattedBody
required property string thumbnailUrl required property int index
required property bool isOnlyEmoji
required property bool isSender
required property bool isEncrypted
required property bool isEditable required property bool isEditable
required property bool isEdited required property bool isEdited
required property bool isEncrypted
required property bool isOnlyEmoji
required property bool isSender
required property bool isStateEvent 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 replyTo
required property string roomName
required property string roomTopic
required property int status
required property string threadId 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 userId
required property string userName 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 hoverEnabled: true
width: parent.width 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 { states: State {
name: "dragging" name: "dragging"
when: draghandler.active when: draghandler.active
@ -86,265 +59,292 @@ AbstractButton {
transitions: Transition { transitions: Transition {
from: "dragging" from: "dragging"
to: "" to: ""
PropertyAnimation { PropertyAnimation {
target: r
properties: "x"
easing.type: Easing.InOutQuad
to: 0
duration: 100 duration: 100
easing.type: Easing.InOutQuad
properties: "x"
target: r
to: 0
} }
} }
onClicked: { 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) { 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 { 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.left: parent.left
anchors.leftMargin: Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8) // align bubble with section header
height: parent.height
visible: threadId visible: threadId
width: 4 width: 4
height: parent.height
onClicked: room.thread = threadId
Rectangle { Rectangle {
id: threadLine id: threadLine
color: TimelineManager.userColor(threadId, palette.base)
anchors.fill: parent 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 { Rectangle {
id: row 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 color bgColor: palette.base
color: (Settings.bubbles && !isStateEvent) ? Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.2)) : "#00000000" property bool bubbleOnRight: isSender && Settings.bubbles
radius: 4 property int maxWidth: (parent.width - (Settings.smallAvatars || isStateEvent ? 0 : Nheko.avatarSize + 8)) * (Settings.bubbles && !isStateEvent ? 0.9 : 1)
border.width: r.notificationlevel == MtxEvent.Highlight ? 1 : 0 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.color: Nheko.theme.red
border.width: r.notificationlevel == MtxEvent.Highlight ? 1 : 0
color: (Settings.bubbles && !isStateEvent) ? Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.2)) : "#00000000"
height: msg.height + msg.anchors.margins * 2
radius: 4
width: Settings.bubbles ? Math.min(maxWidth, Math.max(reply.implicitWidth + 8, contentItem.implicitWidth + metadata.width + 20)) : maxWidth
GridLayout { GridLayout {
id: msg
columnSpacing: 2
columns: Settings.bubbles ? 1 : 2
rowSpacing: 0
rows: Settings.bubbles ? 3 : 2
anchors { anchors {
left: parent.left left: parent.left
top: parent.top
right: parent.right
margins: (Settings.bubbles && ! isStateEvent)? 4 : 2
leftMargin: 4 leftMargin: 4
margins: (Settings.bubbles && !isStateEvent) ? 4 : 2
right: parent.right
rightMargin: 4 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 // fancy reply, if this is a reply
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 id: reply
function fromModel(role) { function fromModel(role) {
return replyTo != "" ? room.dataById(replyTo, role, r.eventId) : null; 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) ?? "" blurhash: r.relatedEventCacheBuster, fromModel(Room.Blurhash) ?? ""
body: r.relatedEventCacheBuster, fromModel(Room.Body) ?? "" 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) ?? "" eventId: fromModel(Room.EventId) ?? ""
filename: r.relatedEventCacheBuster, fromModel(Room.Filename) ?? "" filename: r.relatedEventCacheBuster, fromModel(Room.Filename) ?? ""
filesize: r.relatedEventCacheBuster, fromModel(Room.Filesize) ?? "" 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 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 type: r.relatedEventCacheBuster, fromModel(Room.Type) ?? MtxEvent.UnknownMessage
typeString: r.relatedEventCacheBuster, fromModel(Room.TypeString) ?? "" typeString: r.relatedEventCacheBuster, fromModel(Room.TypeString) ?? ""
url: r.relatedEventCacheBuster, fromModel(Room.Url) ?? "" url: r.relatedEventCacheBuster, fromModel(Room.Url) ?? ""
originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0 userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, palette.base)
isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false
isStateEvent: r.relatedEventCacheBuster, fromModel(Room.IsStateEvent) ?? false
userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? "" userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? ""
userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? "" userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? ""
thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? "" visible: replyTo
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
} }
// actual message content // actual message content
MessageDelegate { MessageDelegate {
Layout.row: 1 id: contentItem
Layout.column: 0 Layout.column: 0
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: height Layout.preferredHeight: height
id: contentItem Layout.row: 1
blurhash: r.blurhash blurhash: r.blurhash
body: r.body body: r.body
formattedBody: r.formattedBody callType: r.callType
duration: r.duration
encryptionError: r.encryptionError
eventId: r.eventId eventId: r.eventId
filename: r.filename filename: r.filename
filesize: r.filesize filesize: r.filesize
formattedBody: r.formattedBody
isOnlyEmoji: r.isOnlyEmoji
isReply: false
isStateEvent: r.isStateEvent
metadataWidth: metadata.width
originalWidth: r.originalWidth
proportionalHeight: r.proportionalHeight proportionalHeight: r.proportionalHeight
relatedEventCacheBuster: r.relatedEventCacheBuster
roomName: r.roomName
roomTopic: r.roomTopic
thumbnailUrl: r.thumbnailUrl
type: r.type type: r.type
typeString: r.typeString ?? "" typeString: r.typeString ?? ""
url: r.url url: r.url
thumbnailUrl: r.thumbnailUrl
duration: r.duration
originalWidth: r.originalWidth
isOnlyEmoji: r.isOnlyEmoji
isStateEvent: r.isStateEvent
userId: r.userId userId: r.userId
userName: r.userName userName: r.userName
roomTopic: r.roomTopic
roomName: r.roomName
callType: r.callType
encryptionError: r.encryptionError
relatedEventCacheBuster: r.relatedEventCacheBuster
isReply: false
metadataWidth: metadata.width
} }
Row { Row {
id: metadata id: metadata
Layout.column: Settings.bubbles? 0 : 1
Layout.row: Settings.bubbles? 2 : 0 property int iconSize: Math.floor(fontMetrics.ascent * scaling)
Layout.rowSpan: Settings.bubbles? 1 : 2 property double scaling: Settings.bubbles ? 0.75 : 1
Layout.bottomMargin: -2
Layout.topMargin: (contentItem.fitsMetadata && Settings.bubbles)? -height-Layout.bottomMargin : 0
Layout.alignment: Qt.AlignTop | Qt.AlignRight Layout.alignment: Qt.AlignTop | Qt.AlignRight
Layout.bottomMargin: -2
Layout.column: Settings.bubbles ? 0 : 1
Layout.preferredWidth: implicitWidth 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 spacing: 2
visible: !isStateEvent
property double scaling: Settings.bubbles? 0.75 : 1
property int iconSize: Math.floor(fontMetrics.ascent*scaling)
StatusIndicator { StatusIndicator {
Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.alignment: Qt.AlignRight | Qt.AlignTop
height: parent.iconSize
width: parent.iconSize
status: r.status
eventId: r.eventId
anchors.verticalCenter: ts.verticalCenter anchors.verticalCenter: ts.verticalCenter
} eventId: r.eventId
Image {
visible: isEdited || eventId == room.edit
Layout.alignment: Qt.AlignRight | Qt.AlignTop
height: parent.iconSize height: parent.iconSize
status: r.status
width: parent.iconSize width: parent.iconSize
sourceSize.width: parent.iconSize * Screen.devicePixelRatio }
sourceSize.height: parent.iconSize * Screen.devicePixelRatio Image {
source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == room.edit) ? palette.highlight : palette.buttonText) Layout.alignment: Qt.AlignRight | Qt.AlignTop
ToolTip.visible: editHovered.hovered
ToolTip.delay: Nheko.tooltipDelay ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Edited") ToolTip.text: qsTr("Edited")
ToolTip.visible: editHovered.hovered
anchors.verticalCenter: ts.verticalCenter 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 { HoverHandler {
id: editHovered id: editHovered
} }
} }
ImageButton { ImageButton {
visible: threadId
Layout.alignment: Qt.AlignRight | Qt.AlignTop 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.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Part of a thread") ToolTip.text: qsTr("Part of a thread")
ToolTip.visible: hovered
anchors.verticalCenter: ts.verticalCenter 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 onClicked: room.thread = threadId
} }
EncryptionIndicator { EncryptionIndicator {
visible: room.isEncrypted
encrypted: isEncrypted
trust: trustlevel
Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.alignment: Qt.AlignRight | Qt.AlignTop
height: parent.iconSize
width: parent.iconSize
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
anchors.verticalCenter: ts.verticalCenter anchors.verticalCenter: ts.verticalCenter
encrypted: isEncrypted
height: parent.iconSize
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
trust: trustlevel
visible: room.isEncrypted
width: parent.iconSize
} }
Label { Label {
id: ts id: ts
Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.alignment: Qt.AlignRight | Qt.AlignTop
Layout.preferredWidth: implicitWidth Layout.preferredWidth: implicitWidth
text: timestamp.toLocaleTimeString(Locale.ShortFormat)
color: palette.inactive.text
ToolTip.visible: ma.hovered
ToolTip.delay: Nheko.tooltipDelay ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: Qt.formatDateTime(timestamp, Qt.DefaultLocaleLongDate) 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 { HoverHandler {
id: ma id: ma
}
}
} }
} }
} }
} }
Reactions { Reactions {
anchors {
top: row.bottom
topMargin: -4
left: row.bubbleOnRight? undefined : row.left
right: row.bubbleOnRight? row.right : undefined
}
width: row.maxWidth
layoutDirection: row.bubbleOnRight? Qt.RightToLeft : Qt.LeftToRight
id: reactionRow id: reactionRow
reactions: r.reactions
eventId: r.eventId eventId: r.eventId
} layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight
reactions: r.reactions
width: row.maxWidth
anchors {
left: row.bubbleOnRight ? undefined : row.left
right: row.bubbleOnRight ? row.right : undefined
top: row.bottom
topMargin: -4
}
}
Rectangle { Rectangle {
id: unreadRow id: unreadRow
color: palette.highlight
height: visible ? 3 : 0
visible: (r.index > 0 && (room.fullyReadEventId == r.eventId))
anchors { anchors {
top: reactionRow.bottom
topMargin: 5
left: parent.left left: parent.left
right: parent.right right: parent.right
top: reactionRow.bottom
topMargin: 5
} }
color: palette.highlight
visible: (r.index > 0 && (room.fullyReadEventId == r.eventId))
height: visible ? 3 : 0
} }
} }

View file

@ -20,86 +20,85 @@ import im.nheko.EmojiModel 1.0
Item { Item {
id: timelineView id: timelineView
required property PrivacyScreen privacyScreen
property var room: null property var room: null
property var roomPreview: null property var roomPreview: null
property bool showBackButton: false
property bool shouldEffectsRun: false property bool shouldEffectsRun: false
required property PrivacyScreen privacyScreen property bool showBackButton: false
clip: true clip: true
onRoomChanged: if (room != null) room.triggerSpecialEffects() // focus message input on key press, but not on Ctrl-C and such.
Keys.onPressed: event => {
if (event.text && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && !topBar.searchHasFocus) {
TimelineManager.focusMessageInput();
room.input.setText(room.input.text + event.text);
}
}
onRoomChanged: if (room != null)
room.triggerSpecialEffects()
StickerPicker { StickerPicker {
id: emojiPopup id: emojiPopup
emoji: true emoji: true
} }
// focus message input on key press, but not on Ctrl-C and such.
Keys.onPressed: (event) => {
if (event.text && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && !topBar.searchHasFocus) {
TimelineManager.focusMessageInput();
room.input.setText(room.input.text + event.text);
}
}
Shortcut { Shortcut {
sequence: StandardKey.Close sequence: StandardKey.Close
onActivated: Rooms.resetCurrentRoom() onActivated: Rooms.resetCurrentRoom()
} }
Label { Label {
visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid)
anchors.centerIn: parent anchors.centerIn: parent
text: qsTr("No room open")
font.pointSize: 24 font.pointSize: 24
text: qsTr("No room open")
visible: !room && !TimelineManager.isInitialSync && (!roomPreview || !roomPreview.roomid)
} }
Spinner { Spinner {
visible: TimelineManager.isInitialSync
anchors.centerIn: parent anchors.centerIn: parent
foreground: palette.mid foreground: palette.mid
running: TimelineManager.isInitialSync
// height is somewhat arbitrary here... don't set width because width scales w/ height // height is somewhat arbitrary here... don't set width because width scales w/ height
height: parent.height / 16 height: parent.height / 16
z: 3
opacity: hh.hovered ? 0.3 : 1 opacity: hh.hovered ? 0.3 : 1
running: TimelineManager.isInitialSync
visible: TimelineManager.isInitialSync
z: 3
Behavior on opacity { Behavior on opacity {
NumberAnimation { duration: 100; } NumberAnimation {
duration: 100
}
} }
HoverHandler { HoverHandler {
id: hh id: hh
} }
} }
ColumnLayout { ColumnLayout {
id: timelineLayout id: timelineLayout
visible: room != null && !room.isSpace
enabled: visible
anchors.fill: parent anchors.fill: parent
enabled: visible
spacing: 0 spacing: 0
visible: room != null && !room.isSpace
TopBar { TopBar {
id: topBar id: topBar
showBackButton: timelineView.showBackButton showBackButton: timelineView.showBackButton
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
color: Nheko.theme.separator
height: 1 height: 1
z: 3 z: 3
color: Nheko.theme.separator
} }
Rectangle { Rectangle {
id: msgView id: msgView
Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true
color: palette.base color: palette.base
ColumnLayout { ColumnLayout {
@ -118,143 +117,121 @@ Item {
target: timelineView target: timelineView
} }
MessageView { MessageView {
Layout.fillWidth: true
implicitHeight: msgView.height - typingIndicator.height implicitHeight: msgView.height - typingIndicator.height
searchString: topBar.searchString searchString: topBar.searchString
Layout.fillWidth: true
} }
Loader { Loader {
source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : ""
onLoaded: TimelineManager.setVideoCallItem() onLoaded: TimelineManager.setVideoCallItem()
} }
} }
TypingIndicator { TypingIndicator {
id: typingIndicator id: typingIndicator
} }
} }
} }
CallInviteBar { CallInviteBar {
id: callInviteBar id: callInviteBar
Layout.fillWidth: true Layout.fillWidth: true
z: 3 z: 3
} }
ActiveCallBar { ActiveCallBar {
Layout.fillWidth: true Layout.fillWidth: true
z: 3 z: 3
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
z: 3
height: 1
color: Nheko.theme.separator color: Nheko.theme.separator
height: 1
z: 3
} }
UploadBox { UploadBox {
} }
MessageInputWarning { MessageInputWarning {
text: qsTr("You are about to notify the whole room") text: qsTr("You are about to notify the whole room")
visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom) visible: (room && room.permissions.canPingRoom() && room.input.containsAtRoom)
} }
MessageInputWarning { MessageInputWarning {
text: qsTr("The command /%1 is not recognized and will be sent as part of your message").arg(room ? room.input.currentCommand : "") 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 visible: room ? room.input.containsInvalidCommand && !room.input.containsIncompleteCommand : false
} }
MessageInputWarning { 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 : "") 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 visible: room ? room.input.containsIncompleteCommand : false
bubbleColor: Nheko.theme.orange
} }
ReplyPopup { ReplyPopup {
} }
MessageInput { MessageInput {
} }
} }
ColumnLayout { ColumnLayout {
id: preview 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 roomId: room ? room.roomId : (roomPreview ? roomPreview.roomid : "")
property string roomName: room ? room.roomName : (roomPreview ? roomPreview.roomName : "") property string roomName: room ? room.roomName : (roomPreview ? roomPreview.roomName : "")
property string roomTopic: room ? room.roomTopic : (roomPreview ? roomPreview.roomTopic : "") 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.fill: parent
anchors.margins: Nheko.paddingLarge anchors.margins: Nheko.paddingLarge
enabled: visible
spacing: Nheko.paddingLarge spacing: Nheko.paddingLarge
visible: room != null && room.isSpace || roomPreview != null
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
} }
Avatar { Avatar {
url: parent.avatarUrl.replace("mxc://", "image://MxcImage/") Layout.alignment: Qt.AlignHCenter
roomid: parent.roomId
displayName: parent.roomName displayName: parent.roomName
height: 130
width: 130
Layout.alignment: Qt.AlignHCenter
enabled: false enabled: false
height: 130
roomid: parent.roomId
url: parent.avatarUrl.replace("mxc://", "image://MxcImage/")
width: 130
} }
RowLayout { RowLayout {
spacing: Nheko.paddingMedium
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
spacing: Nheko.paddingMedium
MatrixText { MatrixText {
text: !(roomPreview?.isFetched ?? false) ? qsTr("No preview available") : preview.roomName
font.pixelSize: 24 font.pixelSize: 24
text: !(roomPreview?.isFetched ?? false) ? qsTr("No preview available") : preview.roomName
} }
ImageButton { ImageButton {
ToolTip.text: qsTr("Settings")
ToolTip.visible: hovered
hoverEnabled: true
image: ":/icons/icons/ui/settings.svg" image: ":/icons/icons/ui/settings.svg"
visible: !!room visible: !!room
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("Settings")
onClicked: TimelineManager.openRoomSettings(room.roomId) onClicked: TimelineManager.openRoomSettings(room.roomId)
} }
} }
RowLayout { RowLayout {
visible: !!room
spacing: Nheko.paddingMedium
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
spacing: Nheko.paddingMedium
visible: !!room
MatrixText { MatrixText {
text: qsTr("%n member(s)", "", room ? room.roomMemberCount : 0) text: qsTr("%n member(s)", "", room ? room.roomMemberCount : 0)
} }
ImageButton { ImageButton {
image: ":/icons/icons/ui/people.svg"
hoverEnabled: true
ToolTip.visible: hovered
ToolTip.text: qsTr("View members of %1").arg(room ? room.roomName : "") 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) onClicked: TimelineManager.openRoomMembers(room)
} }
} }
ScrollView { ScrollView {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
@ -262,54 +239,53 @@ Item {
Layout.rightMargin: Nheko.paddingLarge Layout.rightMargin: Nheko.paddingLarge
TextArea { 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 background: null
selectByMouse: true
horizontalAlignment: TextEdit.AlignHCenter 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) onLinkActivated: Nheko.openLink(link)
CursorShape { CursorShape {
anchors.fill: parent anchors.fill: parent
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
} }
} }
} }
FlatButton { FlatButton {
visible: roomPreview && !roomPreview.isInvite
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: qsTr("join the conversation") text: qsTr("join the conversation")
visible: roomPreview && !roomPreview.isInvite
onClicked: Rooms.joinPreview(roomPreview.roomid) onClicked: Rooms.joinPreview(roomPreview.roomid)
} }
FlatButton { FlatButton {
visible: roomPreview && roomPreview.isInvite
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: qsTr("accept invite") text: qsTr("accept invite")
visible: roomPreview && roomPreview.isInvite
onClicked: Rooms.acceptInvite(roomPreview.roomid) onClicked: Rooms.acceptInvite(roomPreview.roomid)
} }
FlatButton { FlatButton {
visible: roomPreview && roomPreview.isInvite
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: qsTr("decline invite") text: qsTr("decline invite")
visible: roomPreview && roomPreview.isInvite
onClicked: Rooms.declineInvite(roomPreview.roomid) onClicked: Rooms.declineInvite(roomPreview.roomid)
} }
FlatButton { FlatButton {
visible: !!room
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: qsTr("leave") text: qsTr("leave")
visible: !!room
onClicked: TimelineManager.openLeaveRoomDialog(room.roomId) onClicked: TimelineManager.openLeaveRoomDialog(room.roomId)
} }
ScrollView { ScrollView {
id: reasonField id: reasonField
property bool showReason: false property bool showReason: false
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@ -319,17 +295,15 @@ Item {
visible: preview.reason !== "" && showReason visible: preview.reason !== "" && showReason
TextArea { TextArea {
text: TimelineManager.escapeEmoji(preview.reason)
wrapMode: TextEdit.WordWrap
textFormat: TextEdit.RichText
readOnly: true
background: null background: null
selectByMouse: true
horizontalAlignment: TextEdit.AlignHCenter horizontalAlignment: TextEdit.AlignHCenter
readOnly: true
selectByMouse: true
text: TimelineManager.escapeEmoji(preview.reason)
textFormat: TextEdit.RichText
wrapMode: TextEdit.WordWrap
} }
} }
Button { Button {
id: showReasonButton id: showReasonButton
@ -337,76 +311,94 @@ Item {
//Layout.fillWidth: true //Layout.fillWidth: true
Layout.leftMargin: Nheko.paddingLarge Layout.leftMargin: Nheko.paddingLarge
Layout.rightMargin: Nheko.paddingLarge Layout.rightMargin: Nheko.paddingLarge
visible: preview.reason !== ""
text: reasonField.showReason ? qsTr("Hide invite reason") : qsTr("Show invite reason") text: reasonField.showReason ? qsTr("Hide invite reason") : qsTr("Show invite reason")
visible: preview.reason !== ""
onClicked: { onClicked: {
reasonField.showReason = !reasonField.showReason; reasonField.showReason = !reasonField.showReason;
} }
} }
Item { Item {
visible: room != null
Layout.preferredHeight: Math.ceil(fontMetrics.lineSpacing * 2) Layout.preferredHeight: Math.ceil(fontMetrics.lineSpacing * 2)
visible: room != null
} }
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
} }
} }
ImageButton { ImageButton {
id: backToRoomsButton id: backToRoomsButton
anchors.top: parent.top ToolTip.text: qsTr("Back to room list")
ToolTip.visible: hovered
anchors.left: parent.left anchors.left: parent.left
anchors.margins: Nheko.paddingMedium anchors.margins: Nheko.paddingMedium
width: Nheko.avatarSize anchors.top: parent.top
height: Nheko.avatarSize
visible: (room == null || room.isSpace) && showBackButton
enabled: visible enabled: visible
height: Nheko.avatarSize
image: ":/icons/icons/ui/angle-arrow-left.svg" image: ":/icons/icons/ui/angle-arrow-left.svg"
ToolTip.visible: hovered visible: (room == null || room.isSpace) && showBackButton
ToolTip.text: qsTr("Back to room list") width: Nheko.avatarSize
onClicked: Rooms.resetCurrentRoom() onClicked: Rooms.resetCurrentRoom()
} }
TimelineEffects { TimelineEffects {
id: timelineEffects id: timelineEffects
anchors.fill: parent anchors.fill: parent
} }
NhekoDropArea { NhekoDropArea {
anchors.fill: parent anchors.fill: parent
roomid: room ? room.roomId : "" roomid: room ? room.roomId : ""
} }
Timer { Timer {
id: effectsTimer id: effectsTimer
onTriggered: shouldEffectsRun = false;
interval: timelineEffects.maxLifespan interval: timelineEffects.maxLifespan
repeat: false repeat: false
running: false running: false
}
onTriggered: shouldEffectsRun = false
}
Connections { Connections {
function onConfetti() {
if (!Settings.fancyEffects)
return;
shouldEffectsRun = true;
timelineEffects.pulseConfetti();
room.markSpecialEffectsDone();
}
function onConfettiDone() {
if (!Settings.fancyEffects)
return;
effectsTimer.restart();
}
function onOpenReadReceiptsDialog(rr) { function onOpenReadReceiptsDialog(rr) {
var dialog = readReceiptsDialog.createObject(timelineRoot, { var dialog = readReceiptsDialog.createObject(timelineRoot, {
"readReceipts": rr, "readReceipts": rr,
"room": room "room": room
}); });
dialog.show(); dialog.show();
timelineRoot.destroyOnClose(dialog); 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) { 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) { if (component.status == Component.Ready) {
var dialog = component.createObject(timelineRoot, { var dialog = component.createObject(timelineRoot, {
"rawMessage": rawMessage "rawMessage": rawMessage
}); });
dialog.show(); dialog.show();
timelineRoot.destroyOnClose(dialog); timelineRoot.destroyOnClose(dialog);
} else { } 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 target: room
} }
} }

View file

@ -11,17 +11,44 @@ Switch {
id: toggleButton id: toggleButton
implicitWidth: indicatorItem.width implicitWidth: indicatorItem.width
state: checked ? "on" : "off" 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: [ states: [
State { State {
name: "off" name: "off"
PropertyChanges { PropertyChanges {
target: track
border.color: "#767676" border.color: "#767676"
target: track
} }
PropertyChanges { PropertyChanges {
target: handle target: handle
x: 0 x: 0
@ -31,10 +58,9 @@ Switch {
name: "on" name: "on"
PropertyChanges { PropertyChanges {
target: track
border.color: palette.highlight border.color: palette.highlight
target: track
} }
PropertyChanges { PropertyChanges {
target: handle target: handle
x: indicatorItem.width - handle.width x: indicatorItem.width - handle.width
@ -43,55 +69,22 @@ Switch {
] ]
transitions: [ transitions: [
Transition { Transition {
to: "off"
reversible: true reversible: true
to: "off"
ParallelAnimation { ParallelAnimation {
NumberAnimation { NumberAnimation {
target: handle
property: "x"
duration: 200 duration: 200
easing.type: Easing.InOutQuad easing.type: Easing.InOutQuad
property: "x"
target: handle
} }
ColorAnimation { ColorAnimation {
target: track
properties: "color,border.color"
duration: 200 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"
}
}
} }

View file

@ -8,212 +8,142 @@ import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import QtQuick.Window 2.15 import QtQuick.Window 2.15
import im.nheko 1.0 import im.nheko 1.0
import "./delegates" import "./delegates"
Pane { Pane {
id: topBar 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 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 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 bool searchHasFocus: searchField.focus && searchField.enabled
property string searchString: "" property string searchString: ""
property bool showBackButton: false
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu property int trustlevel: room ? room.trustlevel : Crypto.Unverified
Connections {
function onHideMenu() {
roomOptionsMenu.close()
}
target: MainWindow
}
onRoomIdChanged: {
searchString = "";
searchButton.searchActive = false;
searchField.text = ""
}
Shortcut {
sequence: StandardKey.Find
onActivated: searchButton.searchActive = !searchButton.searchActive
}
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: topLayout.height + Nheko.paddingMedium * 2 implicitHeight: topLayout.height + Nheko.paddingMedium * 2
padding: 0
z: 3 z: 3
padding: 0
background: Rectangle { background: Rectangle {
color: palette.window 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 { contentItem: Item {
GridLayout { GridLayout {
id: topLayout id: topLayout
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Nheko.paddingMedium anchors.margins: Nheko.paddingMedium
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
columnSpacing: Nheko.paddingSmall columnSpacing: Nheko.paddingSmall
rowSpacing: Nheko.paddingSmall rowSpacing: Nheko.paddingSmall
Avatar { Avatar {
id: communityAvatar 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 avatarUrl: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomAvatarUrl) || ""
property string communityId: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomid) || "" property string communityId: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomid) || ""
property string communityName: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomName) || "" property string communityName: (Settings.groupView && room && room.parentSpace && room.parentSpace.roomName) || ""
Layout.alignment: Qt.AlignRight
Layout.column: 1 Layout.column: 1
Layout.row: 0 Layout.row: 0
Layout.alignment: Qt.AlignRight
width: fontMetrics.lineSpacing
height: fontMetrics.lineSpacing
url: avatarUrl.replace("mxc://", "image://MxcImage/")
roomid: communityId
displayName: communityName displayName: communityName
enabled: false 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 { Label {
id: communityLabel id: communityLabel
visible: communityAvatar.visible
Layout.column: 2 Layout.column: 2
Layout.row: 0
Layout.fillWidth: true Layout.fillWidth: true
Layout.row: 0
color: palette.text color: palette.text
text: qsTr("In %1").arg(communityAvatar.displayName)
maximumLineCount: 1
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1
text: qsTr("In %1").arg(communityAvatar.displayName)
textFormat: Text.RichText textFormat: Text.RichText
visible: communityAvatar.visible
} }
ImageButton { ImageButton {
id: backToRoomsButton id: backToRoomsButton
Layout.column: 0
Layout.row: 1
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.column: 0
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
visible: showBackButton Layout.row: 1
image: ":/icons/icons/ui/angle-arrow-left.svg" Layout.rowSpan: 2
ToolTip.visible: hovered
ToolTip.text: qsTr("Back to room list") ToolTip.text: qsTr("Back to room list")
ToolTip.visible: hovered
image: ":/icons/icons/ui/angle-arrow-left.svg"
visible: showBackButton
onClicked: Rooms.resetCurrentRoom() onClicked: Rooms.resetCurrentRoom()
} }
Avatar { Avatar {
Layout.alignment: Qt.AlignVCenter
Layout.column: 1 Layout.column: 1
Layout.row: 1 Layout.row: 1
Layout.rowSpan: 2 Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter
width: Nheko.avatarSize
height: Nheko.avatarSize
url: avatarUrl.replace("mxc://", "image://MxcImage/")
roomid: roomId
userid: isDirect ? directChatOtherUserId : ""
displayName: roomName displayName: roomName
enabled: false enabled: false
height: Nheko.avatarSize
roomid: roomId
url: avatarUrl.replace("mxc://", "image://MxcImage/")
userid: isDirect ? directChatOtherUserId : ""
width: Nheko.avatarSize
} }
Label { Label {
Layout.fillWidth: true
Layout.column: 2 Layout.column: 2
Layout.fillWidth: true
Layout.row: 1 Layout.row: 1
color: palette.text color: palette.text
font.pointSize: fontMetrics.font.pointSize * 1.1
font.bold: true
text: roomName
maximumLineCount: 1
elide: Text.ElideRight elide: Text.ElideRight
font.bold: true
font.pointSize: fontMetrics.font.pointSize * 1.1
maximumLineCount: 1
text: roomName
textFormat: Text.RichText textFormat: Text.RichText
} }
MatrixText { MatrixText {
id: roomTopicC id: roomTopicC
Layout.fillWidth: true
Layout.column: 2 Layout.column: 2
Layout.row: 2 Layout.fillWidth: true
Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
selectByMouse: false Layout.row: 2
enabled: false
clip: true clip: true
enabled: false
selectByMouse: false
text: roomTopic text: roomTopic
} }
ImageButton { ImageButton {
id: pinButton id: pinButton
property bool pinsShown: !Settings.hiddenPins.includes(roomId) 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.alignment: Qt.AlignVCenter
Layout.column: 3
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg" Layout.row: 1
ToolTip.visible: hovered Layout.rowSpan: 2
ToolTip.text: qsTr("Show or hide pinned messages") 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: { onClicked: {
var ps = Settings.hiddenPins; var ps = Settings.hiddenPins;
if (pinsShown) { if (pinsShown) {
@ -226,242 +156,280 @@ Pane {
} }
Settings.hiddenPins = ps; Settings.hiddenPins = ps;
} }
} }
AbstractButton { AbstractButton {
Layout.column: 4 Layout.column: 4
Layout.row: 1
Layout.rowSpan: 2
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
Layout.row: 1
Layout.rowSpan: 2
background: null
contentItem: EncryptionIndicator { 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.delay: Nheko.tooltipDelay
ToolTip.text: { ToolTip.text: {
if (!isEncrypted) if (!isEncrypted)
return qsTr("Show room members."); return qsTr("Show room members.");
switch (trustlevel) { switch (trustlevel) {
case Crypto.Verified: case Crypto.Verified:
return qsTr("This room contains only verified devices."); 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."); 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!"); 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) onClicked: TimelineManager.openRoomMembers(room)
} }
ImageButton { ImageButton {
id: searchButton id: searchButton
property bool searchActive: false property bool searchActive: false
visible: !!room
Layout.column: 5
Layout.row: 1
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.column: 5
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
image: ":/icons/icons/ui/search.svg" Layout.row: 1
ToolTip.visible: hovered Layout.rowSpan: 2
ToolTip.text: qsTr("Search this room") ToolTip.text: qsTr("Search this room")
onClicked: searchActive = !searchActive ToolTip.visible: hovered
image: ":/icons/icons/ui/search.svg"
visible: !!room
onClicked: searchActive = !searchActive
onSearchActiveChanged: { onSearchActiveChanged: {
if (searchActive) { if (searchActive) {
searchField.forceActiveFocus(); searchField.forceActiveFocus();
} } else {
else {
searchField.clear(); searchField.clear();
topBar.searchString = ""; topBar.searchString = "";
} }
} }
} }
ImageButton { ImageButton {
id: roomOptionsButton id: roomOptionsButton
visible: !!room
Layout.column: 6
Layout.row: 1
Layout.rowSpan: 2
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.column: 6
Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
image: ":/icons/icons/ui/options.svg" Layout.row: 1
ToolTip.visible: hovered Layout.rowSpan: 2
ToolTip.text: qsTr("Room options") ToolTip.text: qsTr("Room options")
ToolTip.visible: hovered
image: ":/icons/icons/ui/options.svg"
visible: !!room
onClicked: roomOptionsMenu.open(roomOptionsButton) onClicked: roomOptionsMenu.open(roomOptionsButton)
Platform.Menu { Platform.Menu {
id: roomOptionsMenu id: roomOptionsMenu
Platform.MenuItem { Platform.MenuItem {
visible: room ? room.permissions.canInvite() : false
text: qsTr("Invite users") text: qsTr("Invite users")
visible: room ? room.permissions.canInvite() : false
onTriggered: TimelineManager.openInviteUsers(roomId) onTriggered: TimelineManager.openInviteUsers(roomId)
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Members") text: qsTr("Members")
onTriggered: TimelineManager.openRoomMembers(room) onTriggered: TimelineManager.openRoomMembers(room)
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Leave room") text: qsTr("Leave room")
onTriggered: TimelineManager.openLeaveRoomDialog(roomId) onTriggered: TimelineManager.openLeaveRoomDialog(roomId)
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Settings") text: qsTr("Settings")
onTriggered: TimelineManager.openRoomSettings(roomId) onTriggered: TimelineManager.openRoomSettings(roomId)
} }
} }
} }
ScrollView { ScrollView {
id: pinnedMessages id: pinnedMessages
Layout.row: 3
Layout.column: 2 Layout.column: 2
Layout.columnSpan: 4 Layout.columnSpan: 4
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4) Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4)
Layout.row: 3
visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId)
clip: true
ScrollBar.horizontal.visible: false ScrollBar.horizontal.visible: false
clip: true
visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId)
ListView { ListView {
spacing: Nheko.paddingSmall
model: room ? room.pinnedMessages : undefined model: room ? room.pinnedMessages : undefined
spacing: Nheko.paddingSmall
delegate: RowLayout { delegate: RowLayout {
required property string modelData required property string modelData
width: ListView.view.width
height: implicitHeight height: implicitHeight
width: ListView.view.width
Reply { Reply {
id: reply id: reply
property var e: room ? room.getDump(modelData, "pins") : {} property var e: room ? room.getDump(modelData, "pins") : {}
Connections {
function onPinnedMessagesChanged() { reply.e = room.getDump(modelData, "pins") }
target: room
}
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: height Layout.preferredHeight: height
userColor: TimelineManager.userColor(e.userId, palette.window)
blurhash: e.blurhash ?? "" blurhash: e.blurhash ?? ""
body: e.body ?? "" body: e.body ?? ""
formattedBody: e.formattedBody ?? "" encryptionError: e.encryptionError ?? 0
eventId: e.eventId ?? "" eventId: e.eventId ?? ""
filename: e.filename ?? "" filename: e.filename ?? ""
filesize: e.filesize ?? "" filesize: e.filesize ?? ""
formattedBody: e.formattedBody ?? ""
isOnlyEmoji: e.isOnlyEmoji ?? false
keepFullText: true
originalWidth: e.originalWidth ?? 0
proportionalHeight: e.proportionalHeight ?? 1 proportionalHeight: e.proportionalHeight ?? 1
type: e.type ?? MtxEvent.UnknownMessage type: e.type ?? MtxEvent.UnknownMessage
typeString: e.typeString ?? "" typeString: e.typeString ?? ""
url: e.url ?? "" url: e.url ?? ""
originalWidth: e.originalWidth ?? 0 userColor: TimelineManager.userColor(e.userId, palette.window)
isOnlyEmoji: e.isOnlyEmoji ?? false
userId: e.userId ?? "" userId: e.userId ?? ""
userName: e.userName ?? "" userName: e.userName ?? ""
encryptionError: e.encryptionError ?? 0
keepFullText: true
}
Connections {
function onPinnedMessagesChanged() {
reply.e = room.getDump(modelData, "pins");
}
target: room
}
}
ImageButton { ImageButton {
id: deletePinButton id: deletePinButton
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.preferredHeight: 16 Layout.preferredHeight: 16
Layout.preferredWidth: 16 Layout.preferredWidth: 16
Layout.alignment: Qt.AlignTop | Qt.AlignLeft ToolTip.text: qsTr("Unpin")
visible: room.permissions.canChange(MtxEvent.PinnedEvents) ToolTip.visible: hovered
hoverEnabled: true hoverEnabled: true
image: ":/icons/icons/ui/dismiss.svg" image: ":/icons/icons/ui/dismiss.svg"
ToolTip.visible: hovered visible: room.permissions.canChange(MtxEvent.PinnedEvents)
ToolTip.text: qsTr("Unpin")
onClicked: room.unpin(modelData) onClicked: room.unpin(modelData)
} }
} }
} }
} }
ScrollView { ScrollView {
id: widgets id: widgets
Layout.row: 4
Layout.column: 2 Layout.column: 2
Layout.columnSpan: 4 Layout.columnSpan: 4
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 1.5) Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 1.5)
Layout.row: 4
visible: !!room && room.widgetLinks.length > 0 && !Settings.hiddenWidgets.includes(roomId)
clip: true
ScrollBar.horizontal.visible: false ScrollBar.horizontal.visible: false
clip: true
visible: !!room && room.widgetLinks.length > 0 && !Settings.hiddenWidgets.includes(roomId)
ListView { ListView {
spacing: Nheko.paddingSmall
model: room ? room.widgetLinks : undefined model: room ? room.widgetLinks : undefined
spacing: Nheko.paddingSmall
delegate: MatrixText { delegate: MatrixText {
required property var modelData required property var modelData
color: palette.text color: palette.text
text: modelData text: modelData
} }
} }
} }
MatrixTextField { MatrixTextField {
id: searchField id: searchField
visible: searchButton.searchActive
enabled: visible
hasClear: true
Layout.row: 5
Layout.column: 2 Layout.column: 2
Layout.columnSpan: 4 Layout.columnSpan: 4
Layout.fillWidth: true Layout.fillWidth: true
Layout.row: 5
enabled: visible
hasClear: true
placeholderText: qsTr("Enter search query") placeholderText: qsTr("Enter search query")
visible: searchButton.searchActive
onAccepted: topBar.searchString = text onAccepted: topBar.searchString = text
} }
} }
CursorShape { CursorShape {
anchors.fill: parent
anchors.bottomMargin: (pinnedMessages.visible ? pinnedMessages.height : 0) + (widgets.visible ? widgets.height : 0) anchors.bottomMargin: (pinnedMessages.visible ? pinnedMessages.height : 0) + (widgets.visible ? widgets.height : 0)
anchors.fill: parent
cursorShape: Qt.PointingHandCursor 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
}
} }

View file

@ -8,30 +8,28 @@ import QtQuick.Layouts 1.2
import im.nheko 1.0 import im.nheko 1.0
Item { Item {
implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: Math.max(fontMetrics.height * 1.2, typingDisplay.height)
Rectangle { Rectangle {
id: typingRect id: typingRect
visible: (room && room.typingUsers.length > 0)
color: palette.base
anchors.fill: parent anchors.fill: parent
color: palette.base
visible: (room && room.typingUsers.length > 0)
z: 3 z: 3
Label { Label {
id: typingDisplay id: typingDisplay
anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 10 anchors.leftMargin: 10
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 10 anchors.rightMargin: 10
anchors.bottom: parent.bottom
color: palette.text color: palette.text
text: room ? room.formatTypingUsers(room.typingUsers, palette.base) : "" text: room ? room.formatTypingUsers(room.typingUsers, palette.base) : ""
textFormat: Text.RichText textFormat: Text.RichText
} }
} }
} }

View file

@ -4,7 +4,6 @@
import "./components" import "./components"
import "./ui" import "./ui"
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.5 import QtQuick.Controls 2.5
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
@ -12,31 +11,33 @@ import im.nheko 1.0
Page { Page {
id: uploadPopup id: uploadPopup
visible: room && room.input.uploads.length > 0
Layout.preferredHeight: 200
clip: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 200
clip: true
padding: Nheko.paddingMedium padding: Nheko.paddingMedium
visible: room && room.input.uploads.length > 0
background: Rectangle {
color: palette.base
}
contentItem: ListView { contentItem: ListView {
id: uploadsList id: uploadsList
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
model: room ? room.input.uploads : undefined
orientation: ListView.Horizontal
spacing: Nheko.paddingMedium
width: Math.min(contentWidth, parent.availableWidth)
ScrollBar.horizontal: ScrollBar { ScrollBar.horizontal: ScrollBar {
id: scr id: scr
} }
orientation: ListView.Horizontal
width: Math.min(contentWidth, parent.availableWidth)
model: room ? room.input.uploads : undefined
spacing: Nheko.paddingMedium
delegate: Pane { delegate: Pane {
height: uploadPopup.availableHeight - buttons.height - (scr.visible ? scr.height : 0)
padding: Nheko.paddingSmall padding: Nheko.paddingSmall
height: uploadPopup.availableHeight - buttons.height - (scr.visible? scr.height : 0)
width: uploadPopup.availableHeight - buttons.height width: uploadPopup.availableHeight - buttons.height
background: Rectangle { background: Rectangle {
@ -45,46 +46,48 @@ Page {
} }
contentItem: ColumnLayout { contentItem: ColumnLayout {
Image { 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.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
fillMode: Image.PreserveAspectFit
mipmap: true
smooth: true
source: (modelData.thumbnail != "") ? modelData.thumbnail : ("image://colorimage/:/icons/icons/ui/" + typeStr + ".svg?" + palette.buttonText)
sourceSize.height: parent.availableHeight - namefield.height sourceSize.height: parent.availableHeight - namefield.height
sourceSize.width: parent.availableWidth sourceSize.width: parent.availableWidth
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: true
property string typeStr: switch(modelData.mediaType) {
case MediaUpload.Video: return "video-file";
case MediaUpload.Audio: return "music";
case MediaUpload.Image: return "image";
default: return "zip";
}
source: (modelData.thumbnail != "") ? modelData.thumbnail : ("image://colorimage/:/icons/icons/ui/"+typeStr+".svg?" + palette.buttonText)
} }
MatrixTextField { MatrixTextField {
id: namefield id: namefield
Layout.fillWidth: true Layout.fillWidth: true
text: modelData.filename text: modelData.filename
onTextEdited: modelData.filename = text onTextEdited: modelData.filename = text
} }
} }
} }
} }
footer: DialogButtonBox { footer: DialogButtonBox {
id: buttons id: buttons
standardButtons: DialogButtonBox.Cancel standardButtons: DialogButtonBox.Cancel
Button {
text: qsTr("Upload %n file(s)", "", (room ? room.input.uploads.length : 0))
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
}
onAccepted: room.input.acceptUploads() onAccepted: room.input.acceptUploads()
onRejected: room.input.declineUploads() onRejected: room.input.declineUploads()
}
background: Rectangle { Button {
color: palette.base DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
text: qsTr("Upload %n file(s)", "", (room ? room.input.uploads.length : 0))
}
} }
} }