diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml index 61484625..9344738e 100644 --- a/resources/qml/ActiveCallBar.qml +++ b/resources/qml/ActiveCallBar.qml @@ -1,111 +1,113 @@ import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 - import im.nheko 1.0 Rectangle { - id: activeCallBar - visible: TimelineManager.callState != WebRTCState.DISCONNECTED - color: "#2ECC71" - implicitHeight: rowLayout.height + 8 + id: activeCallBar - RowLayout { - id: rowLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 8 + visible: TimelineManager.callState != WebRTCState.DISCONNECTED + color: "#2ECC71" + implicitHeight: rowLayout.height + 8 - Avatar { - width: avatarSize - height: avatarSize + RowLayout { + id: rowLayout - url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") - displayName: TimelineManager.callPartyName - } + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 8 - Label { - font.pointSize: fontMetrics.font.pointSize * 1.1 - text: " " + TimelineManager.callPartyName + " " - } + Avatar { + width: avatarSize + height: avatarSize + url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") + displayName: TimelineManager.callPartyName + } - Image { - Layout.preferredWidth: 24 - Layout.preferredHeight: 24 - source: "qrc:/icons/icons/ui/place-call.png" - } + Label { + font.pointSize: fontMetrics.font.pointSize * 1.1 + text: " " + TimelineManager.callPartyName + " " + } - Label { - id: callStateLabel - font.pointSize: fontMetrics.font.pointSize * 1.1 - } + Image { + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + source: "qrc:/icons/icons/ui/place-call.png" + } - Connections { - target: TimelineManager - function onCallStateChanged(state) { - switch (state) { - case WebRTCState.INITIATING: - callStateLabel.text = qsTr("Initiating...") - break; - case WebRTCState.OFFERSENT: - callStateLabel.text = qsTr("Calling...") - break; - case WebRTCState.CONNECTING: - callStateLabel.text = qsTr("Connecting...") - break; - case WebRTCState.CONNECTED: - callStateLabel.text = "00:00" - var d = new Date() - callTimer.startTime = Math.floor(d.getTime() / 1000) - break; - case WebRTCState.DISCONNECTED: - callStateLabel.text = "" - } - } - } + Label { + id: callStateLabel - Timer { - id: callTimer - property int startTime - interval: 1000 - running: TimelineManager.callState == WebRTCState.CONNECTED - repeat: true - onTriggered: { - var d = new Date() - let seconds = Math.floor(d.getTime() / 1000 - startTime) - let s = Math.floor(seconds % 60) - let m = Math.floor(seconds / 60) % 60 - let h = Math.floor(seconds / 3600) - callStateLabel.text = (h ? (pad(h) + ":") : "") + pad(m) + ":" + pad(s) - } + font.pointSize: fontMetrics.font.pointSize * 1.1 + } - function pad(n) { - return (n < 10) ? ("0" + n) : n - } - } + Connections { + function onCallStateChanged(state) { + switch (state) { + case WebRTCState.INITIATING: + callStateLabel.text = qsTr("Initiating..."); + break; + case WebRTCState.OFFERSENT: + callStateLabel.text = qsTr("Calling..."); + break; + case WebRTCState.CONNECTING: + callStateLabel.text = qsTr("Connecting..."); + break; + case WebRTCState.CONNECTED: + callStateLabel.text = "00:00"; + var d = new Date(); + callTimer.startTime = Math.floor(d.getTime() / 1000); + break; + case WebRTCState.DISCONNECTED: + callStateLabel.text = ""; + } + } - Item { - Layout.fillWidth: true - } + target: TimelineManager + } - ImageButton { - width: 24 - height: 24 - buttonTextColor: "#000000" - image: TimelineManager.isMicMuted ? - ":/icons/icons/ui/microphone-unmute.png" : - ":/icons/icons/ui/microphone-mute.png" + Timer { + id: callTimer - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic") + property int startTime - onClicked: TimelineManager.toggleMicMute() - } + function pad(n) { + return (n < 10) ? ("0" + n) : n; + } + + interval: 1000 + running: TimelineManager.callState == WebRTCState.CONNECTED + repeat: true + onTriggered: { + var d = new Date(); + let seconds = Math.floor(d.getTime() / 1000 - startTime); + let s = Math.floor(seconds % 60); + let m = Math.floor(seconds / 60) % 60; + let h = Math.floor(seconds / 3600); + callStateLabel.text = (h ? (pad(h) + ":") : "") + pad(m) + ":" + pad(s); + } + } + + Item { + Layout.fillWidth: true + } + + ImageButton { + width: 24 + height: 24 + buttonTextColor: "#000000" + image: TimelineManager.isMicMuted ? ":/icons/icons/ui/microphone-unmute.png" : ":/icons/icons/ui/microphone-mute.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic") + onClicked: TimelineManager.toggleMicMute() + } + + Item { + implicitWidth: 16 + } + + } - Item { - implicitWidth: 16 - } - } } diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index df3dd08e..a247bffe 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -1,69 +1,75 @@ +import QtGraphicalEffects 1.0 import QtQuick 2.6 import QtQuick.Controls 2.3 -import QtGraphicalEffects 1.0 - import im.nheko 1.0 Rectangle { - id: avatar - width: 48 - height: 48 - radius: Settings.avatarCircles ? height/2 : 3 + id: avatar - property alias url: img.source - property string userid - property string displayName + property alias url: img.source + property string userid + property string displayName - Label { - anchors.fill: parent - text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") - textFormat: Text.RichText - font.pixelSize: avatar.height/2 - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - visible: img.status != Image.Ready - color: colors.text - } + width: 48 + height: 48 + radius: Settings.avatarCircles ? height / 2 : 3 + color: colors.base - Image { - id: img - anchors.fill: parent - asynchronous: true - fillMode: Image.PreserveAspectCrop - mipmap: true - smooth: false + Label { + anchors.fill: parent + text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") + textFormat: Text.RichText + font.pixelSize: avatar.height / 2 + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + visible: img.status != Image.Ready + color: colors.text + } - sourceSize.width: avatar.width - sourceSize.height: avatar.height + Image { + id: img - layer.enabled: true - layer.effect: OpacityMask { - maskSource: Rectangle { - anchors.fill: parent - width: avatar.width - height: avatar.height - radius: Settings.avatarCircles ? height/2 : 3 - } - } + anchors.fill: parent + asynchronous: true + fillMode: Image.PreserveAspectCrop + mipmap: true + smooth: false + sourceSize.width: avatar.width + sourceSize.height: avatar.height + layer.enabled: true - } + layer.effect: OpacityMask { - Rectangle { - anchors.bottom: avatar.bottom - anchors.right: avatar.right + maskSource: Rectangle { + anchors.fill: parent + width: avatar.width + height: avatar.height + radius: Settings.avatarCircles ? height / 2 : 3 + } - visible: !!userid + } - height: avatar.height / 6 - width: height - radius: Settings.avatarCircles ? height / 2 : height / 4 - color: switch (TimelineManager.userPresence(userid)) { - case "online": return "#00cc66" - case "unavailable": return "#ff9933" - case "offline": // return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled - default: "transparent" - } - } + } + + Rectangle { + anchors.bottom: avatar.bottom + anchors.right: avatar.right + visible: !!userid + height: avatar.height / 6 + width: height + radius: Settings.avatarCircles ? height / 2 : height / 4 + color: { + switch (TimelineManager.userPresence(userid)) { + case "online": + return "#00cc66"; + case "unavailable": + return "#ff9933"; + case "offline": + default: + // return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled + "transparent"; + } + } + } - color: colors.base } diff --git a/resources/qml/EncryptionIndicator.qml b/resources/qml/EncryptionIndicator.qml index 428c2fae..46ca62c5 100644 --- a/resources/qml/EncryptionIndicator.qml +++ b/resources/qml/EncryptionIndicator.qml @@ -3,39 +3,42 @@ import QtQuick.Controls 2.1 import im.nheko 1.0 Rectangle { - property bool encrypted: false - id: indicator - color: "transparent" - width: 16 - height: 16 + id: indicator - ToolTip.visible: ma.containsMouse && indicator.visible - ToolTip.text: getEncryptionTooltip() + property bool encrypted: false - MouseArea{ - id: ma - anchors.fill: parent - hoverEnabled: true - } + function getEncryptionImage() { + if (encrypted) + return "image://colorimage/:/icons/icons/ui/lock.png?" + colors.buttonText; + else + return "image://colorimage/:/icons/icons/ui/unlock.png?#dd3d3d"; + } - Image { - id: stateImg - anchors.fill: parent - source: getEncryptionImage() - } + function getEncryptionTooltip() { + if (encrypted) + return qsTr("Encrypted"); + else + return qsTr("This message is not encrypted!"); + } - function getEncryptionImage() { - if (encrypted) - return "image://colorimage/:/icons/icons/ui/lock.png?"+colors.buttonText - else - return "image://colorimage/:/icons/icons/ui/unlock.png?#dd3d3d" - } + color: "transparent" + width: 16 + height: 16 + ToolTip.visible: ma.containsMouse && indicator.visible + ToolTip.text: getEncryptionTooltip() + + MouseArea { + id: ma + + anchors.fill: parent + hoverEnabled: true + } + + Image { + id: stateImg + + anchors.fill: parent + source: getEncryptionImage() + } - function getEncryptionTooltip() { - if (encrypted) - return qsTr("Encrypted") - else - return qsTr("This message is not encrypted!") - } } - diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml index 54399ae7..49ddf671 100644 --- a/resources/qml/ImageButton.qml +++ b/resources/qml/ImageButton.qml @@ -2,25 +2,29 @@ import QtQuick 2.3 import QtQuick.Controls 2.3 AbstractButton { - property string image: undefined - property color highlightColor: colors.highlight - property color buttonTextColor: colors.buttonText - width: 16 - height: 16 - id: button + id: button - Image { - id: buttonImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: "image://colorimage/" + image + "?" + (button.hovered ? highlightColor : buttonTextColor) - } + property string image: undefined + property color highlightColor: colors.highlight + property color buttonTextColor: colors.buttonText + + width: 16 + height: 16 + + Image { + id: buttonImg + + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: "image://colorimage/" + image + "?" + (button.hovered ? highlightColor : buttonTextColor) + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + onPressed: mouse.accepted = false + cursorShape: Qt.PointingHandCursor + } - MouseArea - { - id: mouseArea - anchors.fill: parent - onPressed: mouse.accepted = false - cursorShape: Qt.PointingHandCursor - } } diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index 29214168..6c96a539 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -1,35 +1,37 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 - import im.nheko 1.0 TextEdit { - textFormat: TextEdit.RichText - readOnly: true - wrapMode: Text.Wrap - selectByMouse: true - activeFocusOnPress: false - color: colors.text + textFormat: TextEdit.RichText + readOnly: true + wrapMode: Text.Wrap + selectByMouse: true + activeFocusOnPress: false + color: colors.text + onLinkActivated: { + if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) { + chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]); + } else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) { + TimelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]); + } else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { + var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link); + TimelineManager.setHistoryView(match[1]); + chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain); + } else { + TimelineManager.openLink(link); + } + } + ToolTip.visible: hoveredLink + ToolTip.text: hoveredLink - onLinkActivated: { - if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]) - else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) TimelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]) - else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { - var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link) - TimelineManager.setHistoryView(match[1]) - chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain) - } - else TimelineManager.openLink(link) - } - MouseArea - { - id: ma - anchors.fill: parent - propagateComposedEvents: true - acceptedButtons: Qt.NoButton - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor - } + MouseArea { + id: ma + + anchors.fill: parent + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } - ToolTip.visible: hoveredLink - ToolTip.text: hoveredLink } diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml index ec46f7e0..6487f512 100644 --- a/resources/qml/Reactions.qml +++ b/resources/qml/Reactions.qml @@ -1,94 +1,95 @@ import QtQuick 2.6 import QtQuick.Controls 2.2 - import im.nheko 1.0 // This class is for showing Reactions in the timeline row, not for // adding new reactions via the emoji picker Flow { - id: reactionFlow + id: reactionFlow - // highlight colors for selfReactedEvent background - property real highlightHue: colors.highlight.hslHue - property real highlightSat: colors.highlight.hslSaturation - property real highlightLight: colors.highlight.hslLightness + // highlight colors for selfReactedEvent background + property real highlightHue: colors.highlight.hslHue + property real highlightSat: colors.highlight.hslSaturation + property real highlightLight: colors.highlight.hslLightness + property string eventId + property alias reactions: repeater.model - property string eventId + anchors.left: parent.left + anchors.right: parent.right + spacing: 4 - anchors.left: parent.left - anchors.right: parent.right - spacing: 4 + Repeater { + id: repeater - property alias reactions: repeater.model + delegate: AbstractButton { + id: reaction - Repeater { - id: repeater + hoverEnabled: true + implicitWidth: contentItem.childrenRect.width + contentItem.leftPadding * 2 + implicitHeight: contentItem.childrenRect.height + ToolTip.visible: hovered + ToolTip.text: modelData.users + onClicked: { + console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); + TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key); + } - delegate: AbstractButton { - id: reaction - hoverEnabled: true - implicitWidth: contentItem.childrenRect.width + contentItem.leftPadding*2 - implicitHeight: contentItem.childrenRect.height + contentItem: Row { + anchors.centerIn: parent + spacing: reactionText.implicitHeight / 4 + leftPadding: reactionText.implicitHeight / 2 + rightPadding: reactionText.implicitHeight / 2 - ToolTip.visible: hovered - ToolTip.text: modelData.users + TextMetrics { + id: textMetrics - onClicked: { - console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent) - TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key) - } + font.family: Settings.emojiFont + elide: Text.ElideRight + elideWidth: 150 + text: modelData.key + } + Text { + id: reactionText - contentItem: Row { - anchors.centerIn: parent - spacing: reactionText.implicitHeight/4 - leftPadding: reactionText.implicitHeight / 2 - rightPadding: reactionText.implicitHeight / 2 + anchors.baseline: reactionCounter.baseline + text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…") + font.family: Settings.emojiFont + color: reaction.hovered ? colors.highlight : colors.text + maximumLineCount: 1 + } - TextMetrics { - id: textMetrics - font.family: Settings.emojiFont - elide: Text.ElideRight - elideWidth: 150 - text: modelData.key - } + Rectangle { + id: divider - Text { - anchors.baseline: reactionCounter.baseline - id: reactionText - text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…") - font.family: Settings.emojiFont - color: reaction.hovered ? colors.highlight : colors.text - maximumLineCount: 1 - } + height: Math.floor(reactionCounter.implicitHeight * 1.4) + width: 1 + color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text + } - Rectangle { - id: divider - height: Math.floor(reactionCounter.implicitHeight * 1.4) - width: 1 - color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text - } + Text { + id: reactionCounter - Text { - anchors.verticalCenter: divider.verticalCenter - id: reactionCounter - text: modelData.count - font: reaction.font - color: reaction.hovered ? colors.highlight : colors.text - } - } + anchors.verticalCenter: divider.verticalCenter + text: modelData.count + font: reaction.font + color: reaction.hovered ? colors.highlight : colors.text + } - background: Rectangle { - anchors.centerIn: parent + } + + background: Rectangle { + anchors.centerIn: parent + implicitWidth: reaction.implicitWidth + implicitHeight: reaction.implicitHeight + border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text + color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : colors.base + border.width: 1 + radius: reaction.height / 2 + } + + } + + } - implicitWidth: reaction.implicitWidth - implicitHeight: reaction.implicitHeight - border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text - color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.20) : colors.base - border.width: 1 - radius: reaction.height / 2.0 - } - } - } } - diff --git a/resources/qml/ScrollHelper.qml b/resources/qml/ScrollHelper.qml index ba7c2648..7dc31464 100644 --- a/resources/qml/ScrollHelper.qml +++ b/resources/qml/ScrollHelper.qml @@ -16,10 +16,6 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - -import QtQuick 2.9 -import QtQuick.Controls 2.3 - /* * Shamelessly stolen from: * https://cgit.kde.org/kube.git/tree/framework/qml/ScrollHelper.qml @@ -31,81 +27,82 @@ import QtQuick.Controls 2.3 * ScrollView.qml in qtquickcontrols * qquickwheelarea.cpp in qtquickcontrols */ + +import QtQuick 2.9 +import QtQuick.Controls 2.3 + MouseArea { + // console.warn("Delta: ", wheel.pixelDelta.y); + // console.warn("Old position: ", flickable.contentY); + // console.warn("New position: ", newPos); + id: root - propagateComposedEvents: true property Flickable flickable property alias enabled: root.enabled - //Place the mouse area under the flickable - z: -1 - onFlickableChanged: { - if (enabled) { - flickable.maximumFlickVelocity = 100000 - flickable.boundsBehavior = Flickable.StopAtBounds - root.parent = flickable - } - } - - acceptedButtons: Qt.NoButton - function calculateNewPosition(flickableItem, wheel) { //Nothing to scroll - if (flickableItem.contentHeight < flickableItem.height) { + if (flickableItem.contentHeight < flickableItem.height) return flickableItem.contentY; - } + //Ignore 0 events (happens at least with Christians trackpad) - if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0) { + if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0) return flickableItem.contentY; - } + //pixelDelta seems to be the same as angleDelta/8 - var pixelDelta = 0 + var pixelDelta = 0; //The pixelDelta is a smaller number if both are provided, so pixelDelta can be 0 while angleDelta is still something. So we check the angleDelta if (wheel.angleDelta.y) { - var wheelScrollLines = 3 //Default value of QApplication wheelScrollLines property - var pixelPerLine = 20 //Default value in Qt, originally comes from QTextEdit - var ticks = (wheel.angleDelta.y / 8) / 15.0 //Divide by 8 gives us pixels typically come in 15pixel steps. - pixelDelta = ticks * pixelPerLine * wheelScrollLines + var wheelScrollLines = 3; //Default value of QApplication wheelScrollLines property + var pixelPerLine = 20; //Default value in Qt, originally comes from QTextEdit + var ticks = (wheel.angleDelta.y / 8) / 15; //Divide by 8 gives us pixels typically come in 15pixel steps. + pixelDelta = ticks * pixelPerLine * wheelScrollLines; } else { - pixelDelta = wheel.pixelDelta.y + pixelDelta = wheel.pixelDelta.y; } - - pixelDelta = Math.round(pixelDelta) - - if (!pixelDelta) { + pixelDelta = Math.round(pixelDelta); + if (!pixelDelta) return flickableItem.contentY; - } var minYExtent = flickableItem.originY + flickableItem.topMargin; var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height; - - if (typeof(flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) { - minYExtent += flickableItem.headerItem.height - } + if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) + minYExtent += flickableItem.headerItem.height; //Avoid overscrolling return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta)); } + propagateComposedEvents: true + //Place the mouse area under the flickable + z: -1 + onFlickableChanged: { + if (enabled) { + flickable.maximumFlickVelocity = 100000; + flickable.boundsBehavior = Flickable.StopAtBounds; + root.parent = flickable; + } + } + acceptedButtons: Qt.NoButton onWheel: { var newPos = calculateNewPosition(flickable, wheel); - // console.warn("Delta: ", wheel.pixelDelta.y); - // console.warn("Old position: ", flickable.contentY); - // console.warn("New position: ", newPos); - // Show the scrollbars flickable.flick(0, 0); flickable.contentY = newPos; - cancelFlickStateTimer.start() + cancelFlickStateTimer.start(); } - Timer { id: cancelFlickStateTimer + //How long the scrollbar will remain visible interval: 500 // Hide the scrollbars - onTriggered: { flickable.cancelFlick(); flickable.movementEnded(); } + onTriggered: { + flickable.cancelFlick(); + flickable.movementEnded(); + } } + } diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml index ec82ed49..0b18b888 100644 --- a/resources/qml/StatusIndicator.qml +++ b/resources/qml/StatusIndicator.qml @@ -3,37 +3,55 @@ import QtQuick.Controls 2.1 import im.nheko 1.0 Rectangle { - id: indicator - property int state: 0 - color: "transparent" - width: 16 - height: 16 + id: indicator - ToolTip.visible: ma.containsMouse && state != MtxEvent.Empty - ToolTip.text: switch (state) { - case MtxEvent.Failed: return qsTr("Failed") - case MtxEvent.Sent: return qsTr("Sent") - case MtxEvent.Received: return qsTr("Received") - case MtxEvent.Read: return qsTr("Read") - default: return "" - } - MouseArea{ - id: ma - anchors.fill: parent - hoverEnabled: true - } + property int state: 0 + + color: "transparent" + width: 16 + height: 16 + ToolTip.visible: ma.containsMouse && state != MtxEvent.Empty + ToolTip.text: { + switch (state) { + case MtxEvent.Failed: + return qsTr("Failed"); + case MtxEvent.Sent: + return qsTr("Sent"); + case MtxEvent.Received: + return qsTr("Received"); + case MtxEvent.Read: + return qsTr("Read"); + default: + return ""; + } + } + + MouseArea { + id: ma + + anchors.fill: parent + hoverEnabled: true + } + + Image { + id: stateImg + + // Workaround, can't get icon.source working for now... + anchors.fill: parent + source: { + switch (indicator.state) { + case MtxEvent.Failed: + return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText; + case MtxEvent.Sent: + return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText; + case MtxEvent.Received: + return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText; + case MtxEvent.Read: + return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText; + default: + return ""; + } + } + } - Image { - id: stateImg - // Workaround, can't get icon.source working for now... - anchors.fill: parent - source: switch (indicator.state) { - case MtxEvent.Failed: return "image://colorimage/:/icons/icons/ui/remove-symbol.png?" + colors.buttonText - case MtxEvent.Sent: return "image://colorimage/:/icons/icons/ui/clock.png?" + colors.buttonText - case MtxEvent.Received: return "image://colorimage/:/icons/icons/ui/checkmark.png?" + colors.buttonText - case MtxEvent.Read: return "image://colorimage/:/icons/icons/ui/double-tick-indicator.png?" + colors.buttonText - default: return "" - } - } } - diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index c026d828..38597673 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -1,146 +1,148 @@ +import "./delegates" +import "./emoji" import QtQuick 2.6 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 - import im.nheko 1.0 -import "./delegates" -import "./emoji" - Item { - anchors.left: parent.left - anchors.right: parent.right - height: row.height + anchors.left: parent.left + anchors.right: parent.right + height: row.height - MouseArea { - anchors.fill: parent - propagateComposedEvents: true - preventStealing: true - hoverEnabled: true + MouseArea { + anchors.fill: parent + propagateComposedEvents: true + preventStealing: true + hoverEnabled: true + acceptedButtons: Qt.AllButtons + onClicked: { + if (mouse.button === Qt.RightButton) + messageContextMenu.show(model.id, model.type, model.isEncrypted, row); - acceptedButtons: Qt.AllButtons - onClicked: { - if (mouse.button === Qt.RightButton) - messageContextMenu.show(model.id, model.type, model.isEncrypted, row) - } - onPressAndHold: { - messageContextMenu.show(model.id, model.type, model.isEncrypted, row, mapToItem(timelineRoot, mouse.x, mouse.y)) - } - } - Rectangle { - color: (Settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent" - anchors.fill: row - } - RowLayout { - id: row + } + onPressAndHold: { + messageContextMenu.show(model.id, model.type, model.isEncrypted, row, mapToItem(timelineRoot, mouse.x, mouse.y)); + } + } - anchors.leftMargin: avatarSize + 16 - anchors.left: parent.left - anchors.right: parent.right + Rectangle { + color: (Settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent" + anchors.fill: row + } + RowLayout { + id: row - Column { - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: 4 + anchors.leftMargin: avatarSize + 16 + anchors.left: parent.left + anchors.right: parent.right - // fancy reply, if this is a reply - Reply { - visible: model.replyTo - modelData: chat.model.getDump(model.replyTo,model.id) - userColor: TimelineManager.userColor(modelData.userId, colors.window) - } + Column { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: 4 - // actual message content - MessageDelegate { - id: contentItem + // fancy reply, if this is a reply + Reply { + visible: model.replyTo + modelData: chat.model.getDump(model.replyTo, model.id) + userColor: TimelineManager.userColor(modelData.userId, colors.window) + } - width: parent.width + // actual message content + MessageDelegate { + id: contentItem - modelData: model - } + width: parent.width + modelData: model + } - Reactions { - id: reactionRow - reactions: model.reactions - eventId: model.id - } - } + Reactions { + id: reactionRow - StatusIndicator { - state: model.state - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - width: 16 - } + reactions: model.reactions + eventId: model.id + } - EncryptionIndicator { - visible: model.isRoomEncrypted - encrypted: model.isEncrypted - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - width: 16 - } - EmojiButton { - visible: Settings.buttonsInTimeline - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - width: 16 - id: reactButton - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("React") - emojiPicker: emojiPopup - event_id: model.id - } - ImageButton { - visible: Settings.buttonsInTimeline - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - width: 16 - id: replyButton - hoverEnabled: true + } + StatusIndicator { + state: model.state + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + } - image: ":/icons/icons/ui/mail-reply.png" + EncryptionIndicator { + visible: model.isRoomEncrypted + encrypted: model.isEncrypted + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + } - ToolTip.visible: hovered - ToolTip.text: qsTr("Reply") + EmojiButton { + id: reactButton - onClicked: chat.model.replyAction(model.id) - } - ImageButton { - visible: Settings.buttonsInTimeline - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - width: 16 - id: optionsButton - hoverEnabled: true + visible: Settings.buttonsInTimeline + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("React") + emojiPicker: emojiPopup + event_id: model.id + } - image: ":/icons/icons/ui/vertical-ellipsis.png" + ImageButton { + id: replyButton - ToolTip.visible: hovered - ToolTip.text: qsTr("Options") + visible: Settings.buttonsInTimeline + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + hoverEnabled: true + image: ":/icons/icons/ui/mail-reply.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Reply") + onClicked: chat.model.replyAction(model.id) + } - onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, optionsButton) - } + ImageButton { + id: optionsButton - Label { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - text: model.timestamp.toLocaleTimeString("HH:mm") - width: Math.max(implicitWidth, text.length*fontMetrics.maximumCharacterWidth) - color: inactiveColors.text + visible: Settings.buttonsInTimeline + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + hoverEnabled: true + image: ":/icons/icons/ui/vertical-ellipsis.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Options") + onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, optionsButton) + } - MouseArea{ - id: ma - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - } + Label { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + text: model.timestamp.toLocaleTimeString("HH:mm") + width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth) + color: inactiveColors.text + ToolTip.visible: ma.containsMouse + ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) + + MouseArea { + id: ma + + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + } + + } + + } - ToolTip.visible: ma.containsMouse - ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) - } - } } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 5c9ca348..ab0148e9 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -1,526 +1,582 @@ +import "./delegates" +import "./device-verification" +import "./emoji" +import QtGraphicalEffects 1.0 import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 -import QtGraphicalEffects 1.0 import QtQuick.Window 2.2 - import im.nheko 1.0 import im.nheko.EmojiModel 1.0 -import "./delegates" -import "./emoji" -import "./device-verification" - Page { - id: timelineRoot - - property var colors: currentActivePalette - property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled } - property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive - property int avatarSize: 40 - property real highlightHue: colors.highlight.hslHue - property real highlightSat: colors.highlight.hslSaturation - property real highlightLight: colors.highlight.hslLightness - - palette: colors - - FontMetrics { - id: fontMetrics - } - - EmojiPicker { - id: emojiPopup - width: 7 * 52 + 20 - height: 6 * 52 - colors: palette - model: EmojiProxyModel { - category: EmojiCategory.People - sourceModel: EmojiModel {} - } - } - - Menu { - id: messageContextMenu - modal: true - - function show(eventId_, eventType_, isEncrypted_, showAt_, position) { - eventId = eventId_ - eventType = eventType_ - isEncrypted = isEncrypted_ - - if (position) - popup(position, showAt_) - else - popup(showAt_) - } - - property string eventId - property int eventType - property bool isEncrypted - - MenuItem { - text: qsTr("React") - onClicked: emojiPopup.show(messageContextMenu.parent, messageContextMenu.eventId) - } - MenuItem { - text: qsTr("Reply") - onClicked: chat.model.replyAction(messageContextMenu.eventId) - } - MenuItem { - text: qsTr("Read receipts") - onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId) - } - MenuItem { - text: qsTr("Mark as read") - } - MenuItem { - text: qsTr("View raw message") - onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId) - } - MenuItem { - visible: messageContextMenu.isEncrypted - height: visible ? implicitHeight : 0 - text: qsTr("View decrypted raw message") - onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId) - } - MenuItem { - text: qsTr("Redact message") - onTriggered: chat.model.redactEvent(messageContextMenu.eventId) - } - MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker - height: visible ? implicitHeight : 0 - text: qsTr("Save as") - onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId) - } - } - - Rectangle { - anchors.fill: parent - color: colors.window - - Component { - id: deviceVerificationDialog - DeviceVerification {} - } - Connections { - target: TimelineManager - function onNewDeviceVerificationRequest(flow,transactionId,userId,deviceId,isRequest) { - var dialog = deviceVerificationDialog.createObject(timelineRoot, {flow: flow}); - dialog.show(); - } - } - Connections { - target: TimelineManager.timeline - function onOpenProfile(profile) { - var userProfile = userProfileComponent.createObject(timelineRoot,{profile: profile}); - userProfile.show(); - } - } - - Label { - visible: !TimelineManager.timeline && !TimelineManager.isInitialSync - anchors.centerIn: parent - text: qsTr("No room open") - font.pointSize: 24 - color: colors.text - } - - BusyIndicator { - visible: running - anchors.centerIn: parent - running: TimelineManager.isInitialSync - height: 200 - width: 200 - z: 3 - } - - ColumnLayout { - anchors.fill: parent - Rectangle { - id: topBar - - Layout.fillWidth: true - implicitHeight: topLayout.height + 16 - z: 3 - - color: colors.base - - MouseArea { - anchors.fill: parent - onClicked: TimelineManager.openRoomSettings(); - } - - GridLayout { - id: topLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 8 - anchors.verticalCenter: parent.verticalCenter - - //Layout.margins: 8 - - ImageButton { - id: backToRoomsButton - - Layout.column: 0 - Layout.row: 0 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - - visible: TimelineManager.isNarrowView - - image: ":/icons/icons/ui/angle-pointing-to-left.png" - - ToolTip.visible: hovered - ToolTip.text: qsTr("Back to room list") - - onClicked: TimelineManager.backToRooms() - } - - Avatar { - Layout.column: 1 - Layout.row: 0 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - - width: avatarSize - height: avatarSize - - url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" - displayName: chat.model ? chat.model.roomName : qsTr("No room selected") - - MouseArea { - anchors.fill: parent - onClicked: TimelineManager.openRoomSettings(); - } - } - - Label { - Layout.fillWidth: true - Layout.column: 2 - Layout.row: 0 - color: colors.text - - font.pointSize: fontMetrics.font.pointSize * 1.1 - - text: chat.model ? chat.model.roomName : qsTr("No room selected") - - MouseArea { - anchors.fill: parent - onClicked: TimelineManager.openRoomSettings(); - } - } - MatrixText { - Layout.fillWidth: true - Layout.column: 2 - Layout.row: 1 - Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines - clip: true - - text: chat.model ? chat.model.roomTopic : "" - } - - ImageButton { - id: roomOptionsButton - - Layout.column: 3 - Layout.row: 0 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - - image: ":/icons/icons/ui/vertical-ellipsis.png" - - ToolTip.visible: hovered - ToolTip.text: qsTr("Room options") - - onClicked: roomOptionsMenu.popup(roomOptionsButton) - - Menu { - id: roomOptionsMenu - MenuItem { - text: qsTr("Invite users") - onTriggered: TimelineManager.openInviteUsersDialog(); - } - MenuItem { - text: qsTr("Members") - onTriggered: TimelineManager.openMemberListDialog(); - } - MenuItem { - text: qsTr("Leave room") - onTriggered: TimelineManager.openLeaveRoomDialog(); - } - MenuItem { - text: qsTr("Settings") - onTriggered: TimelineManager.openRoomSettings(); - } - } - } - } - } - - ListView { - id: chat - - visible: TimelineManager.timeline != null - - cacheBuffer: 400 - - Layout.fillWidth: true - Layout.fillHeight: true - - model: TimelineManager.timeline - - boundsBehavior: Flickable.StopAtBounds - - ScrollHelper { - flickable: parent - anchors.fill: parent - } - - pixelAligned: true - - Shortcut { - sequence: StandardKey.MoveToPreviousPage - onActivated: { chat.contentY = chat.contentY - chat.height / 2; chat.returnToBounds(); } - } - Shortcut { - sequence: StandardKey.MoveToNextPage - onActivated: { chat.contentY = chat.contentY + chat.height / 2; chat.returnToBounds(); } - } - Shortcut { - sequence: StandardKey.Cancel - onActivated: chat.model.reply = undefined - } - Shortcut { - sequence: "Alt+Up" - onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply? chat.model.idToIndex(chat.model.reply) + 1 : 0) - } - Shortcut { - sequence: "Alt+Down" - onActivated: { - var idx = chat.model.reply? chat.model.idToIndex(chat.model.reply) - 1 : -1 - chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined - } - } - - ScrollBar.vertical: ScrollBar { - id: scrollbar - } - - spacing: 4 - verticalLayoutDirection: ListView.BottomToTop - - onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom - - property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width*2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width*2) - - delegate: Item { - // This would normally be previousSection, but our model's order is inverted. - property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1 - - id: wrapper - property Item section - anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - width: chat.delegateMaxWidth - height: section ? section.height + timelinerow.height : timelinerow.height - - TimelineRow { - id: timelinerow - y: section ? section.y + section.height : 0 - } - - onSectionBoundaryChanged: { - if (sectionBoundary) { - var properties = { - 'modelData': model.dump, - 'section': ListView.section, - 'nextSection': ListView.nextSection - } - section = sectionHeader.createObject(wrapper, properties) - } else { - section.destroy() - section = null - } - } - - Connections { - target: chat - function onMovementEnded() { - if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) - chat.model.currentIndex = index; - } - } - } - - Component{ - id: userProfileComponent - UserProfile{} - } - - section { - property: "section" - } - Component { - id: sectionHeader - Column { - property var modelData - property string section - property string nextSection - - topPadding: 4 - bottomPadding: 4 - spacing: 8 - - visible: !!modelData - - width: parent.width - height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 - - Label { - id: dateBubble - anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - visible: section.includes(" ") - text: chat.model.formatDateSeparator(modelData.timestamp) - color: colors.text - - height: fontMetrics.height * 1.4 - width: contentWidth * 1.2 - - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - background: Rectangle { - radius: parent.height / 2 - color: colors.base - } - } - - Row { - height: userName.height - spacing: 8 - - Avatar { - width: avatarSize - height: avatarSize - url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") - displayName: modelData.userName - userid: modelData.userId - - MouseArea { - anchors.fill: parent - onClicked: chat.model.openUserProfile(modelData.userId) - cursorShape: Qt.PointingHandCursor - propagateComposedEvents: true - } - } - - Label { - id: userName - text: TimelineManager.escapeEmoji(modelData.userName) - color: TimelineManager.userColor(modelData.userId, colors.window) - textFormat: Text.RichText - - MouseArea { - anchors.fill: parent - Layout.alignment: Qt.AlignHCenter - onClicked: chat.model.openUserProfile(modelData.userId) - cursorShape: Qt.PointingHandCursor - propagateComposedEvents: true - } - } - } - } - } - - footer: BusyIndicator { - anchors.horizontalCenter: parent.horizontalCenter - running: chat.model && chat.model.paginationInProgress - height: 50 - width: 50 - z: 3 - } - } - - Item { - id: chatFooter - - implicitHeight: Math.max(fontMetrics.height * 1.2, footerContent.height) - Layout.fillWidth: true - z: 3 - - Column { - id: footerContent - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - Rectangle { - id: typingRect + id: timelineRoot + + property var colors: currentActivePalette + property var systemInactive + property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive + property int avatarSize: 40 + property real highlightHue: colors.highlight.hslHue + property real highlightSat: colors.highlight.hslSaturation + property real highlightLight: colors.highlight.hslLightness + + palette: colors + + FontMetrics { + id: fontMetrics + } + + EmojiPicker { + id: emojiPopup + + width: 7 * 52 + 20 + height: 6 * 52 + colors: palette + + model: EmojiProxyModel { + category: EmojiCategory.People + + sourceModel: EmojiModel { + } + + } + + } + + Menu { + id: messageContextMenu + + property string eventId + property int eventType + property bool isEncrypted + + function show(eventId_, eventType_, isEncrypted_, showAt_, position) { + eventId = eventId_; + eventType = eventType_; + isEncrypted = isEncrypted_; + if (position) + popup(position, showAt_); + else + popup(showAt_); + } + + modal: true + + MenuItem { + text: qsTr("React") + onClicked: emojiPopup.show(messageContextMenu.parent, messageContextMenu.eventId) + } + + MenuItem { + text: qsTr("Reply") + onClicked: chat.model.replyAction(messageContextMenu.eventId) + } + + MenuItem { + text: qsTr("Read receipts") + onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId) + } + + MenuItem { + text: qsTr("Mark as read") + } + + MenuItem { + text: qsTr("View raw message") + onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId) + } + + MenuItem { + visible: messageContextMenu.isEncrypted + height: visible ? implicitHeight : 0 + text: qsTr("View decrypted raw message") + onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId) + } + + MenuItem { + text: qsTr("Redact message") + onTriggered: chat.model.redactEvent(messageContextMenu.eventId) + } + + MenuItem { + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + height: visible ? implicitHeight : 0 + text: qsTr("Save as") + onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId) + } + + } + + Rectangle { + anchors.fill: parent + color: colors.window + + Component { + id: deviceVerificationDialog + + DeviceVerification { + } + + } + + Connections { + function onNewDeviceVerificationRequest(flow, transactionId, userId, deviceId, isRequest) { + var dialog = deviceVerificationDialog.createObject(timelineRoot, { + "flow": flow + }); + dialog.show(); + } + + target: TimelineManager + } + + Connections { + function onOpenProfile(profile) { + var userProfile = userProfileComponent.createObject(timelineRoot, { + "profile": profile + }); + userProfile.show(); + } + + target: TimelineManager.timeline + } + + Label { + visible: !TimelineManager.timeline && !TimelineManager.isInitialSync + anchors.centerIn: parent + text: qsTr("No room open") + font.pointSize: 24 + color: colors.text + } + + BusyIndicator { + visible: running + anchors.centerIn: parent + running: TimelineManager.isInitialSync + height: 200 + width: 200 + z: 3 + } + + ColumnLayout { + anchors.fill: parent + + Rectangle { + id: topBar + + Layout.fillWidth: true + implicitHeight: topLayout.height + 16 + z: 3 + color: colors.base + + MouseArea { + anchors.fill: parent + onClicked: TimelineManager.openRoomSettings() + } + + GridLayout { + //Layout.margins: 8 + + id: topLayout + anchors.left: parent.left anchors.right: parent.right - color: (chat.model && chat.model.typingUsers.length > 0) ? colors.window : "transparent" - height: typingDisplay.height + anchors.margins: 8 + anchors.verticalCenter: parent.verticalCenter + + ImageButton { + id: backToRoomsButton + + Layout.column: 0 + Layout.row: 0 + Layout.rowSpan: 2 + Layout.alignment: Qt.AlignVCenter + visible: TimelineManager.isNarrowView + image: ":/icons/icons/ui/angle-pointing-to-left.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Back to room list") + onClicked: TimelineManager.backToRooms() + } + + Avatar { + Layout.column: 1 + Layout.row: 0 + Layout.rowSpan: 2 + Layout.alignment: Qt.AlignVCenter + width: avatarSize + height: avatarSize + url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" + displayName: chat.model ? chat.model.roomName : qsTr("No room selected") + + MouseArea { + anchors.fill: parent + onClicked: TimelineManager.openRoomSettings() + } + + } + Label { - id: typingDisplay - anchors.left: parent.left - anchors.leftMargin: 10 - anchors.right: parent.right - anchors.rightMargin: 10 + Layout.fillWidth: true + Layout.column: 2 + Layout.row: 0 color: colors.text - text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : "" - textFormat: Text.RichText + font.pointSize: fontMetrics.font.pointSize * 1.1 + text: chat.model ? chat.model.roomName : qsTr("No room selected") + + MouseArea { + anchors.fill: parent + onClicked: TimelineManager.openRoomSettings() + } + + } + + MatrixText { + Layout.fillWidth: true + Layout.column: 2 + Layout.row: 1 + Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines + clip: true + text: chat.model ? chat.model.roomTopic : "" + } + + ImageButton { + id: roomOptionsButton + + Layout.column: 3 + Layout.row: 0 + Layout.rowSpan: 2 + Layout.alignment: Qt.AlignVCenter + image: ":/icons/icons/ui/vertical-ellipsis.png" + ToolTip.visible: hovered + ToolTip.text: qsTr("Room options") + onClicked: roomOptionsMenu.popup(roomOptionsButton) + + Menu { + id: roomOptionsMenu + + MenuItem { + text: qsTr("Invite users") + onTriggered: TimelineManager.openInviteUsersDialog() + } + + MenuItem { + text: qsTr("Members") + onTriggered: TimelineManager.openMemberListDialog() + } + + MenuItem { + text: qsTr("Leave room") + onTriggered: TimelineManager.openLeaveRoomDialog() + } + + MenuItem { + text: qsTr("Settings") + onTriggered: TimelineManager.openRoomSettings() + } + + } + + } + + } + + } + + ListView { + id: chat + + property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2) + + visible: TimelineManager.timeline != null + cacheBuffer: 400 + Layout.fillWidth: true + Layout.fillHeight: true + model: TimelineManager.timeline + boundsBehavior: Flickable.StopAtBounds + pixelAligned: true + spacing: 4 + verticalLayoutDirection: ListView.BottomToTop + onCountChanged: { + if (atYEnd) + model.currentIndex = 0; + + } // Mark last event as read, since we are at the bottom + + ScrollHelper { + flickable: parent + anchors.fill: parent + } + + Shortcut { + sequence: StandardKey.MoveToPreviousPage + onActivated: { + chat.contentY = chat.contentY - chat.height / 2; + chat.returnToBounds(); } } - Rectangle { - anchors.left: parent.left - anchors.right: parent.right + Shortcut { + sequence: StandardKey.MoveToNextPage + onActivated: { + chat.contentY = chat.contentY + chat.height / 2; + chat.returnToBounds(); + } + } - id: replyPopup + Shortcut { + sequence: StandardKey.Cancel + onActivated: chat.model.reply = undefined + } - visible: chat.model && chat.model.reply - // Height of child, plus margins, plus border - height: replyPreview.height + 10 - color: colors.base + Shortcut { + sequence: "Alt+Up" + onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0) + } + Shortcut { + sequence: "Alt+Down" + onActivated: { + var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1; + chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined; + } + } - Reply { - id: replyPreview + Component { + id: userProfileComponent - anchors.left: parent.left - anchors.leftMargin: 10 - anchors.right: closeReplyButton.left - anchors.rightMargin: 20 - anchors.bottom: parent.bottom + UserProfile { + } - modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {} - userColor: TimelineManager.userColor(modelData.userId, colors.window) - } + } - ImageButton { - id: closeReplyButton + section { + property: "section" + } - anchors.right: parent.right - anchors.rightMargin: 15 - anchors.top: replyPreview.top - hoverEnabled: true - width: 16 - height: 16 + Component { + id: sectionHeader - image: ":/icons/icons/ui/remove-symbol.png" - ToolTip.visible: closeReplyButton.hovered - ToolTip.text: qsTr("Close") + Column { + property var modelData + property string section + property string nextSection - onClicked: chat.model.reply = undefined - } - } - } - } + topPadding: 4 + bottomPadding: 4 + spacing: 8 + visible: !!modelData + width: parent.width + height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 + + Label { + id: dateBubble + + anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined + visible: section.includes(" ") + text: chat.model.formatDateSeparator(modelData.timestamp) + color: colors.text + height: fontMetrics.height * 1.4 + width: contentWidth * 1.2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + background: Rectangle { + radius: parent.height / 2 + color: colors.base + } + + } + + Row { + height: userName.height + spacing: 8 + + Avatar { + width: avatarSize + height: avatarSize + url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") + displayName: modelData.userName + userid: modelData.userId + + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(modelData.userId) + cursorShape: Qt.PointingHandCursor + propagateComposedEvents: true + } + + } + + Label { + id: userName + + text: TimelineManager.escapeEmoji(modelData.userName) + color: TimelineManager.userColor(modelData.userId, colors.window) + textFormat: Text.RichText + + MouseArea { + anchors.fill: parent + Layout.alignment: Qt.AlignHCenter + onClicked: chat.model.openUserProfile(modelData.userId) + cursorShape: Qt.PointingHandCursor + propagateComposedEvents: true + } + + } + + } + + } + + } + + ScrollBar.vertical: ScrollBar { + id: scrollbar + } + + delegate: Item { + id: wrapper + + // This would normally be previousSection, but our model's order is inverted. + property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1 + property Item section + + anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined + width: chat.delegateMaxWidth + height: section ? section.height + timelinerow.height : timelinerow.height + onSectionBoundaryChanged: { + if (sectionBoundary) { + var properties = { + "modelData": model.dump, + "section": ListView.section, + "nextSection": ListView.nextSection + }; + section = sectionHeader.createObject(wrapper, properties); + } else { + section.destroy(); + section = null; + } + } + + TimelineRow { + id: timelinerow + + y: section ? section.y + section.height : 0 + } + + Connections { + function onMovementEnded() { + if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) + chat.model.currentIndex = index; + + } + + target: chat + } + + } + + footer: BusyIndicator { + anchors.horizontalCenter: parent.horizontalCenter + running: chat.model && chat.model.paginationInProgress + height: 50 + width: 50 + z: 3 + } + + } + + Item { + id: chatFooter + + implicitHeight: Math.max(fontMetrics.height * 1.2, footerContent.height) + Layout.fillWidth: true + z: 3 + + Column { + id: footerContent + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + Rectangle { + id: typingRect + + anchors.left: parent.left + anchors.right: parent.right + color: (chat.model && chat.model.typingUsers.length > 0) ? colors.window : "transparent" + height: typingDisplay.height + + Label { + id: typingDisplay + + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.right: parent.right + anchors.rightMargin: 10 + color: colors.text + text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : "" + textFormat: Text.RichText + } + + } + + Rectangle { + id: replyPopup + + anchors.left: parent.left + anchors.right: parent.right + visible: chat.model && chat.model.reply + // Height of child, plus margins, plus border + height: replyPreview.height + 10 + color: colors.base + + Reply { + id: replyPreview + + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.right: closeReplyButton.left + anchors.rightMargin: 20 + anchors.bottom: parent.bottom + modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : { + } + userColor: TimelineManager.userColor(modelData.userId, colors.window) + } + + ImageButton { + id: closeReplyButton + + anchors.right: parent.right + anchors.rightMargin: 15 + anchors.top: replyPreview.top + hoverEnabled: true + width: 16 + height: 16 + image: ":/icons/icons/ui/remove-symbol.png" + ToolTip.visible: closeReplyButton.hovered + ToolTip.text: qsTr("Close") + onClicked: chat.model.reply = undefined + } + + } + + } + + } + + ActiveCallBar { + Layout.fillWidth: true + z: 3 + } + + } + + } + + systemInactive: SystemPalette { + colorGroup: SystemPalette.Disabled + } - ActiveCallBar { - Layout.fillWidth: true - z: 3 - } - } - } } diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml index 562dd4f9..2e6758ae 100644 --- a/resources/qml/UserProfile.qml +++ b/resources/qml/UserProfile.qml @@ -1,172 +1,175 @@ +import "./device-verification" import QtQuick 2.9 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.3 - import im.nheko 1.0 -import "./device-verification" +ApplicationWindow { + id: userProfileDialog -ApplicationWindow{ - property var profile + property var profile - id: userProfileDialog - height: 650 - width: 420 - minimumHeight: 420 + height: 650 + width: 420 + minimumHeight: 420 + palette: colors - palette: colors + Component { + id: deviceVerificationDialog - Component { - id: deviceVerificationDialog - DeviceVerification {} - } + DeviceVerification { + } - ColumnLayout{ - id: contentL + } - anchors.fill: parent - anchors.margins: 10 + ColumnLayout { + id: contentL - spacing: 10 + anchors.fill: parent + anchors.margins: 10 + spacing: 10 - Avatar { - url: profile.avatarUrl.replace("mxc://", "image://MxcImage/") - height: 130 - width: 130 - displayName: profile.displayName - userid: profile.userid - Layout.alignment: Qt.AlignHCenter - } + Avatar { + url: profile.avatarUrl.replace("mxc://", "image://MxcImage/") + height: 130 + width: 130 + displayName: profile.displayName + userid: profile.userid + Layout.alignment: Qt.AlignHCenter + } - Label { - text: profile.displayName - fontSizeMode: Text.HorizontalFit - font.pixelSize: 20 - color: TimelineManager.userColor(profile.userid, colors.window) - font.bold: true - Layout.alignment: Qt.AlignHCenter - } + Label { + text: profile.displayName + fontSizeMode: Text.HorizontalFit + font.pixelSize: 20 + color: TimelineManager.userColor(profile.userid, colors.window) + font.bold: true + Layout.alignment: Qt.AlignHCenter + } - MatrixText { - text: profile.userid - font.pixelSize: 15 - Layout.alignment: Qt.AlignHCenter - } + MatrixText { + text: profile.userid + font.pixelSize: 15 + Layout.alignment: Qt.AlignHCenter + } - Button { - id: verifyUserButton - text: qsTr("Verify") - Layout.alignment: Qt.AlignHCenter - enabled: !profile.isUserVerified - visible: !profile.isUserVerified + Button { + id: verifyUserButton - onClicked: profile.verify() - } + text: qsTr("Verify") + Layout.alignment: Qt.AlignHCenter + enabled: !profile.isUserVerified + visible: !profile.isUserVerified + onClicked: profile.verify() + } - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 8 + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 8 - ImageButton { - image:":/icons/icons/ui/do-not-disturb-rounded-sign.png" - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Ban the user") - onClicked: profile.banUser() - } - // ImageButton{ - // image:":/icons/icons/ui/volume-off-indicator.png" - // Layout.margins: { - // left: 5 - // right: 5 - // } - // ToolTip.visible: hovered - // ToolTip.text: qsTr("Ignore messages from this user") - // onClicked : { - // profile.ignoreUser() - // } - // } - ImageButton{ - image:":/icons/icons/ui/black-bubble-speech.png" - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Start a private chat") - onClicked: profile.startChat() - } - ImageButton{ - image:":/icons/icons/ui/round-remove-button.png" - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Kick the user") - onClicked: profile.kickUser() - } - } + ImageButton { + image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Ban the user") + onClicked: profile.banUser() + } + // ImageButton{ - ListView{ - id: devicelist + // image:":/icons/icons/ui/volume-off-indicator.png" + // Layout.margins: { + // left: 5 + // right: 5 + // } + // ToolTip.visible: hovered + // ToolTip.text: qsTr("Ignore messages from this user") + // onClicked : { + // profile.ignoreUser() + // } + // } + ImageButton { + image: ":/icons/icons/ui/black-bubble-speech.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Start a private chat") + onClicked: profile.startChat() + } - Layout.fillHeight: true - Layout.minimumHeight: 200 - Layout.fillWidth: true + ImageButton { + image: ":/icons/icons/ui/round-remove-button.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Kick the user") + onClicked: profile.kickUser() + } - clip: true - spacing: 8 - boundsBehavior: Flickable.StopAtBounds + } - model: profile.deviceList + ListView { + id: devicelist - delegate: RowLayout{ - width: devicelist.width - spacing: 4 + Layout.fillHeight: true + Layout.minimumHeight: 200 + Layout.fillWidth: true + clip: true + spacing: 8 + boundsBehavior: Flickable.StopAtBounds + model: profile.deviceList - ColumnLayout{ - spacing: 0 - Text{ - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft + delegate: RowLayout { + width: devicelist.width + spacing: 4 - elide: Text.ElideRight - font.bold: true - color: colors.text - text: model.deviceId - } - Text{ - Layout.fillWidth: true - Layout.alignment: Qt.AlignRight + ColumnLayout { + spacing: 0 - elide: Text.ElideRight - color: colors.text - text: model.deviceName - } - } + Text { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + elide: Text.ElideRight + font.bold: true + color: colors.text + text: model.deviceId + } - Image{ - Layout.preferredHeight: 16 - Layout.preferredWidth: 16 + Text { + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + elide: Text.ElideRight + color: colors.text + text: model.deviceName + } - source: ((model.verificationStatus == VerificationStatus.VERIFIED)?"image://colorimage/:/icons/icons/ui/lock.png?green": - ((model.verificationStatus == VerificationStatus.UNVERIFIED)?"image://colorimage/:/icons/icons/ui/unlock.png?yellow": - "image://colorimage/:/icons/icons/ui/unlock.png?red")) - } - Button{ - id: verifyButton - text: (model.verificationStatus != VerificationStatus.VERIFIED)?"Verify":"Unverify" - onClicked: { - if(model.verificationStatus == VerificationStatus.VERIFIED){ - profile.unverify(model.deviceId) - }else{ - profile.verify(model.deviceId); - } - } - } - } - } - } + } - footer: DialogButtonBox { - standardButtons: DialogButtonBox.Ok + Image { + Layout.preferredHeight: 16 + Layout.preferredWidth: 16 + source: ((model.verificationStatus == VerificationStatus.VERIFIED) ? "image://colorimage/:/icons/icons/ui/lock.png?green" : ((model.verificationStatus == VerificationStatus.UNVERIFIED) ? "image://colorimage/:/icons/icons/ui/unlock.png?yellow" : "image://colorimage/:/icons/icons/ui/unlock.png?red")) + } + + Button { + id: verifyButton + + text: (model.verificationStatus != VerificationStatus.VERIFIED) ? "Verify" : "Unverify" + onClicked: { + if (model.verificationStatus == VerificationStatus.VERIFIED) + profile.unverify(model.deviceId); + else + profile.verify(model.deviceId); + } + } + + } + + } + + } + + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok + onAccepted: userProfileDialog.close() + } - onAccepted: userProfileDialog.close() - } } diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 7a2588f3..c6f213ee 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -1,68 +1,75 @@ import QtQuick 2.6 import QtQuick.Layouts 1.2 - import im.nheko 1.0 Item { - height: row.height + 24 - width: parent ? parent.width : undefined + height: row.height + 24 + width: parent ? parent.width : undefined - RowLayout { - id: row + RowLayout { + id: row - anchors.centerIn: parent - width: parent.width - 24 + anchors.centerIn: parent + width: parent.width - 24 + spacing: 15 - spacing: 15 + Rectangle { + id: button - Rectangle { - id: button - color: colors.light - radius: 22 - height: 44 - width: 44 - Image { - id: img - anchors.centerIn: parent + color: colors.light + radius: 22 + height: 44 + width: 44 - source: "qrc:/icons/icons/ui/arrow-pointing-down.png" - fillMode: Image.Pad + Image { + id: img - } - MouseArea { - anchors.fill: parent - onClicked: TimelineManager.timeline.saveMedia(model.data.id) - cursorShape: Qt.PointingHandCursor - } - } - ColumnLayout { - id: col + anchors.centerIn: parent + source: "qrc:/icons/icons/ui/arrow-pointing-down.png" + fillMode: Image.Pad + } - Text { - id: filename - Layout.fillWidth: true - text: model.data.filename - textFormat: Text.PlainText - elide: Text.ElideRight - color: colors.text - } - Text { - id: filesize - Layout.fillWidth: true - text: model.data.filesize - textFormat: Text.PlainText - elide: Text.ElideRight - color: colors.text - } - } - } + MouseArea { + anchors.fill: parent + onClicked: TimelineManager.timeline.saveMedia(model.data.id) + cursorShape: Qt.PointingHandCursor + } - Rectangle { - color: colors.dark - z: -1 - radius: 10 - height: row.height + 24 - width: 44 + 24 + 24 + Math.max(Math.min(filesize.width, filesize.implicitWidth), Math.min(filename.width, filename.implicitWidth)) - } + } + + ColumnLayout { + id: col + + Text { + id: filename + + Layout.fillWidth: true + text: model.data.filename + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + + Text { + id: filesize + + Layout.fillWidth: true + text: model.data.filesize + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + + } + + } + + Rectangle { + color: colors.dark + z: -1 + radius: 10 + height: row.height + 24 + width: 44 + 24 + 24 + Math.max(Math.min(filesize.width, filesize.implicitWidth), Math.min(filename.width, filename.implicitWidth)) + } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index b5c51c2c..e2c78fbe 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -1,42 +1,41 @@ import QtQuick 2.6 - import im.nheko 1.0 Item { - property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width) - property double tempHeight: tempWidth * model.data.proportionalHeight + property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width) + property double tempHeight: tempWidth * model.data.proportionalHeight + property double divisor: model.isReply ? 4 : 2 + property bool tooHigh: tempHeight > timelineRoot.height / divisor - property double divisor: model.isReply ? 4 : 2 - property bool tooHigh: tempHeight > timelineRoot.height / divisor + height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight) + width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth) - height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight) - width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth) + Image { + id: blurhash - Image { - id: blurhash - anchors.fill: parent - visible: img.status != Image.Ready + anchors.fill: parent + visible: img.status != Image.Ready + source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + colors.buttonText) + asynchronous: true + fillMode: Image.PreserveAspectFit + sourceSize.width: parent.width + sourceSize.height: parent.height + } - source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?"+colors.buttonText) - asynchronous: true - fillMode: Image.PreserveAspectFit + Image { + id: img - sourceSize.width: parent.width - sourceSize.height: parent.height - } + anchors.fill: parent + source: model.data.url.replace("mxc://", "image://MxcImage/") + asynchronous: true + fillMode: Image.PreserveAspectFit - Image { - id: img - anchors.fill: parent + MouseArea { + enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready + anchors.fill: parent + onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id) + } - source: model.data.url.replace("mxc://", "image://MxcImage/") - asynchronous: true - fillMode: Image.PreserveAspectFit + } - MouseArea { - enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready - anchors.fill: parent - onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id) - } - } } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index a5cb2ae7..cb5d8a95 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -2,215 +2,334 @@ import QtQuick 2.6 import im.nheko 1.0 Item { - // Workaround to have an assignable global property - Item { - id: model - property var data; - property bool isReply: false - } - - property alias modelData: model.data - property alias isReply: model.isReply + property alias modelData: model.data + property alias isReply: model.isReply + property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width - height: chooser.childrenRect.height - property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width + height: chooser.childrenRect.height - DelegateChooser { - id: chooser - //role: "type" //< not supported in our custom implementation, have to use roleValue - roleValue: model.data.type - anchors.fill: parent + // Workaround to have an assignable global property + Item { + id: model + + property var data + property bool isReply: false + } + + DelegateChooser { + id: chooser + + //role: "type" //< not supported in our custom implementation, have to use roleValue + roleValue: model.data.type + anchors.fill: parent + + DelegateChoice { + roleValue: MtxEvent.UnknownMessage + + Placeholder { + text: "Unretrieved event" + } + + } + + DelegateChoice { + roleValue: MtxEvent.TextMessage + + TextMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.NoticeMessage + + NoticeMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.EmoteMessage + + NoticeMessage { + formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody + color: TimelineManager.userColor(modelData.userId, colors.window) + } + + } + + DelegateChoice { + roleValue: MtxEvent.ImageMessage + + ImageMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.Sticker + + ImageMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.FileMessage + + FileMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.VideoMessage + + PlayableMediaMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.AudioMessage + + PlayableMediaMessage { + } + + } + + DelegateChoice { + roleValue: MtxEvent.Redacted + + Pill { + text: qsTr("redacted") + } + + } + + DelegateChoice { + roleValue: MtxEvent.Redaction + + Pill { + text: qsTr("redacted") + } + + } + + DelegateChoice { + roleValue: MtxEvent.Encryption + + Pill { + text: qsTr("Encryption enabled") + } + + } + + DelegateChoice { + roleValue: MtxEvent.Name + + NoticeMessage { + text: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name") + } + + } + + DelegateChoice { + roleValue: MtxEvent.Topic + + NoticeMessage { + text: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic") + } + + } + + DelegateChoice { + roleValue: MtxEvent.RoomCreate + + NoticeMessage { + text: qsTr("%1 created and configured room: %2").arg(model.data.userName).arg(model.data.roomId) + } + + } + + DelegateChoice { + roleValue: MtxEvent.CallInvite + + NoticeMessage { + text: { + switch (model.data.callType) { + case "voice": + return qsTr("%1 placed a voice call.").arg(model.data.userName); + case "video": + return qsTr("%1 placed a video call.").arg(model.data.userName); + default: + return qsTr("%1 placed a call.").arg(model.data.userName); + } + } + } + + } + + DelegateChoice { + roleValue: MtxEvent.CallAnswer + + NoticeMessage { + text: qsTr("%1 answered the call.").arg(model.data.userName) + } + + } + + DelegateChoice { + roleValue: MtxEvent.CallHangUp + + NoticeMessage { + text: qsTr("%1 ended the call.").arg(model.data.userName) + } + + } + + DelegateChoice { + roleValue: MtxEvent.CallCandidates + + NoticeMessage { + text: qsTr("Negotiating call...") + } + + } + + DelegateChoice { + // TODO: make a more complex formatter for the power levels. + roleValue: MtxEvent.PowerLevels + + NoticeMessage { + text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id) + } + + } + + DelegateChoice { + roleValue: MtxEvent.RoomJoinRules + + NoticeMessage { + text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id) + } + + } + + DelegateChoice { + roleValue: MtxEvent.RoomHistoryVisibility + + NoticeMessage { + text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id) + } + + } + + DelegateChoice { + roleValue: MtxEvent.RoomGuestAccess + + NoticeMessage { + text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id) + } + + } + + DelegateChoice { + roleValue: MtxEvent.Member + + NoticeMessage { + text: TimelineManager.timeline.formatMemberEvent(model.data.id) + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationRequest + + NoticeMessage { + text: "KeyVerificationRequest" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationStart + + NoticeMessage { + text: "KeyVerificationStart" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationReady + + NoticeMessage { + text: "KeyVerificationReady" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationCancel + + NoticeMessage { + text: "KeyVerificationCancel" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationKey + + NoticeMessage { + text: "KeyVerificationKey" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationMac + + NoticeMessage { + text: "KeyVerificationMac" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationDone + + NoticeMessage { + text: "KeyVerificationDone" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationDone + + NoticeMessage { + text: "KeyVerificationDone" + } + + } + + DelegateChoice { + roleValue: MtxEvent.KeyVerificationAccept + + NoticeMessage { + text: "KeyVerificationAccept" + } + + } + + DelegateChoice { + Placeholder { + } + + } + + } - DelegateChoice { - roleValue: MtxEvent.UnknownMessage - Placeholder { text: "Unretrieved event" } - } - DelegateChoice { - roleValue: MtxEvent.TextMessage - TextMessage {} - } - DelegateChoice { - roleValue: MtxEvent.NoticeMessage - NoticeMessage {} - } - DelegateChoice { - roleValue: MtxEvent.EmoteMessage - NoticeMessage { - formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody - color: TimelineManager.userColor(modelData.userId, colors.window) - } - } - DelegateChoice { - roleValue: MtxEvent.ImageMessage - ImageMessage {} - } - DelegateChoice { - roleValue: MtxEvent.Sticker - ImageMessage {} - } - DelegateChoice { - roleValue: MtxEvent.FileMessage - FileMessage {} - } - DelegateChoice { - roleValue: MtxEvent.VideoMessage - PlayableMediaMessage {} - } - DelegateChoice { - roleValue: MtxEvent.AudioMessage - PlayableMediaMessage {} - } - DelegateChoice { - roleValue: MtxEvent.Redacted - Pill { - text: qsTr("redacted") - } - } - DelegateChoice { - roleValue: MtxEvent.Redaction - Pill { - text: qsTr("redacted") - } - } - DelegateChoice { - roleValue: MtxEvent.Encryption - Pill { - text: qsTr("Encryption enabled") - } - } - DelegateChoice { - roleValue: MtxEvent.Name - NoticeMessage { - text: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name") - } - } - DelegateChoice { - roleValue: MtxEvent.Topic - NoticeMessage { - text: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic") - } - } - DelegateChoice { - roleValue: MtxEvent.RoomCreate - NoticeMessage { - text: qsTr("%1 created and configured room: %2").arg(model.data.userName).arg(model.data.roomId) - } - } - DelegateChoice { - roleValue: MtxEvent.CallInvite - NoticeMessage { - text: switch(model.data.callType) { - case "voice": return qsTr("%1 placed a voice call.").arg(model.data.userName) - case "video": return qsTr("%1 placed a video call.").arg(model.data.userName) - default: return qsTr("%1 placed a call.").arg(model.data.userName) - } - } - } - DelegateChoice { - roleValue: MtxEvent.CallAnswer - NoticeMessage { - text: qsTr("%1 answered the call.").arg(model.data.userName) - } - } - DelegateChoice { - roleValue: MtxEvent.CallHangUp - NoticeMessage { - text: qsTr("%1 ended the call.").arg(model.data.userName) - } - } - DelegateChoice { - roleValue: MtxEvent.CallCandidates - NoticeMessage { - text: qsTr("Negotiating call...") - } - } - DelegateChoice { - // TODO: make a more complex formatter for the power levels. - roleValue: MtxEvent.PowerLevels - NoticeMessage { - text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id) - } - } - DelegateChoice { - roleValue: MtxEvent.RoomJoinRules - NoticeMessage { - text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id) - } - } - DelegateChoice { - roleValue: MtxEvent.RoomHistoryVisibility - NoticeMessage { - text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id) - } - } - DelegateChoice { - roleValue: MtxEvent.RoomGuestAccess - NoticeMessage { - text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id) - } - } - DelegateChoice { - roleValue: MtxEvent.Member - NoticeMessage { - text: TimelineManager.timeline.formatMemberEvent(model.data.id); - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationRequest - NoticeMessage { - text: "KeyVerificationRequest"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationStart - NoticeMessage { - text: "KeyVerificationStart"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationReady - NoticeMessage { - text: "KeyVerificationReady"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationCancel - NoticeMessage { - text: "KeyVerificationCancel"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationKey - NoticeMessage { - text: "KeyVerificationKey"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationMac - NoticeMessage { - text: "KeyVerificationMac"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationDone - NoticeMessage { - text: "KeyVerificationDone"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationDone - NoticeMessage { - text: "KeyVerificationDone"; - } - } - DelegateChoice { - roleValue: MtxEvent.KeyVerificationAccept - NoticeMessage { - text: "KeyVerificationAccept"; - } - } - DelegateChoice { - Placeholder {} - } - } } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index be348329..d9a7a3e7 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -1,6 +1,6 @@ TextMessage { - font.italic: true - color: colors.buttonText - height: isReply ? Math.min(chat.height / 8, implicitHeight) : undefined - clip: true + font.italic: true + color: colors.buttonText + height: isReply ? Math.min(chat.height / 8, implicitHeight) : undefined + clip: true } diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml index 27985b58..4acf2bef 100644 --- a/resources/qml/delegates/Pill.qml +++ b/resources/qml/delegates/Pill.qml @@ -2,13 +2,14 @@ import QtQuick 2.5 import QtQuick.Controls 2.1 Label { - color: colors.brightText - horizontalAlignment: Text.AlignHCenter + color: colors.brightText + horizontalAlignment: Text.AlignHCenter + height: contentHeight * 1.2 + width: contentWidth * 1.2 + + background: Rectangle { + radius: parent.height / 2 + color: colors.dark + } - height: contentHeight * 1.2 - width: contentWidth * 1.2 - background: Rectangle { - radius: parent.height / 2 - color: colors.dark - } } diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml index 26de2067..db023d8a 100644 --- a/resources/qml/delegates/Placeholder.qml +++ b/resources/qml/delegates/Placeholder.qml @@ -1,7 +1,7 @@ import ".." MatrixText { - text: qsTr("unimplemented event: ") + model.data.typeString - width: parent ? parent.width : undefined - color: inactiveColors.text + text: qsTr("unimplemented event: ") + model.data.typeString + width: parent ? parent.width : undefined + color: inactiveColors.text } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 893325b6..9ad115c7 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -1,173 +1,216 @@ -import QtQuick 2.6 -import QtQuick.Layouts 1.2 -import QtQuick.Controls 2.1 import QtMultimedia 5.6 - +import QtQuick 2.6 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.2 import im.nheko 1.0 Rectangle { - id: bg - radius: 10 - color: colors.dark - height: Math.round(content.height + 24) - width: parent ? parent.width : undefined + id: bg - Column { - id: content - width: parent.width - 24 - anchors.centerIn: parent + radius: 10 + color: colors.dark + height: Math.round(content.height + 24) + width: parent ? parent.width : undefined - Rectangle { - id: videoContainer - visible: model.data.type == MtxEvent.VideoMessage - property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width) - property double tempHeight: tempWidth * model.data.proportionalHeight + Column { + id: content - property double divisor: model.isReply ? 4 : 2 - property bool tooHigh: tempHeight > timelineRoot.height / divisor + width: parent.width - 24 + anchors.centerIn: parent - height: tooHigh ? timelineRoot.height / divisor : tempHeight - width: tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth - Image { - anchors.fill: parent - source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/") - asynchronous: true - fillMode: Image.PreserveAspectFit + Rectangle { + id: videoContainer - VideoOutput { - anchors.fill: parent - fillMode: VideoOutput.PreserveAspectFit - source: media - } - } - } + property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width) + property double tempHeight: tempWidth * model.data.proportionalHeight + property double divisor: model.isReply ? 4 : 2 + property bool tooHigh: tempHeight > timelineRoot.height / divisor - RowLayout { - width: parent.width - Text { - id: positionText - text: "--:--:--" - color: colors.text - } - Slider { - Layout.fillWidth: true - id: progress - value: media.position - from: 0 - to: media.duration + visible: model.data.type == MtxEvent.VideoMessage + height: tooHigh ? timelineRoot.height / divisor : tempHeight + width: tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth - onMoved: media.seek(value) - //indeterminate: true - function updatePositionTexts() { - function formatTime(date) { - var hh = date.getUTCHours(); - var mm = date.getUTCMinutes(); - var ss = date.getSeconds(); - if (hh < 10) {hh = "0"+hh;} - if (mm < 10) {mm = "0"+mm;} - if (ss < 10) {ss = "0"+ss;} - return hh+":"+mm+":"+ss; - } - positionText.text = formatTime(new Date(media.position)) - durationText.text = formatTime(new Date(media.duration)) - } - onValueChanged: updatePositionTexts() + Image { + anchors.fill: parent + source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/") + asynchronous: true + fillMode: Image.PreserveAspectFit - palette: colors - } - Text { - id: durationText - text: "--:--:--" - color: colors.text - } - } + VideoOutput { + anchors.fill: parent + fillMode: VideoOutput.PreserveAspectFit + source: media + } - RowLayout { - width: parent.width + } - spacing: 15 + } - Rectangle { - id: button - color: colors.window - radius: 22 - height: 44 - width: 44 - Image { - id: img - anchors.centerIn: parent - z: 3 + RowLayout { + width: parent.width - source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+colors.text - fillMode: Image.Pad + Text { + id: positionText - } - MouseArea { - anchors.fill: parent - onClicked: { - switch (button.state) { - case "": TimelineManager.timeline.cacheMedia(model.data.id); break; - case "stopped": - media.play(); console.log("play"); - button.state = "playing" - break - case "playing": - media.pause(); console.log("pause"); - button.state = "stopped" - break - } - } - cursorShape: Qt.PointingHandCursor - } - MediaPlayer { - id: media - onError: console.log(errorString) - onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts() - onStopped: button.state = "stopped" - } + text: "--:--:--" + color: colors.text + } - Connections { - target: TimelineManager.timeline - onMediaCached: { - if (mxcUrl == model.data.url) { - media.source = "file://" + cacheUrl - button.state = "stopped" - console.log("media loaded: " + mxcUrl + " at " + cacheUrl) - } - console.log("media cached: " + mxcUrl + " at " + cacheUrl) - } - } + Slider { + id: progress - states: [ - State { - name: "stopped" - PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/play-sign.png?"+colors.text } - }, - State { - name: "playing" - PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+colors.text } - } - ] - } - ColumnLayout { - id: col + //indeterminate: true + function updatePositionTexts() { + function formatTime(date) { + var hh = date.getUTCHours(); + var mm = date.getUTCMinutes(); + var ss = date.getSeconds(); + if (hh < 10) + hh = "0" + hh; + + if (mm < 10) + mm = "0" + mm; + + if (ss < 10) + ss = "0" + ss; + + return hh + ":" + mm + ":" + ss; + } + + positionText.text = formatTime(new Date(media.position)); + durationText.text = formatTime(new Date(media.duration)); + } + + Layout.fillWidth: true + value: media.position + from: 0 + to: media.duration + onMoved: media.seek(value) + onValueChanged: updatePositionTexts() + palette: colors + } + + Text { + id: durationText + + text: "--:--:--" + color: colors.text + } + + } + + RowLayout { + width: parent.width + spacing: 15 + + Rectangle { + id: button + + color: colors.window + radius: 22 + height: 44 + width: 44 + states: [ + State { + name: "stopped" + + PropertyChanges { + target: img + source: "image://colorimage/:/icons/icons/ui/play-sign.png?" + colors.text + } + + }, + State { + name: "playing" + + PropertyChanges { + target: img + source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?" + colors.text + } + + } + ] + + Image { + id: img + + anchors.centerIn: parent + z: 3 + source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + colors.text + fillMode: Image.Pad + } + + MouseArea { + anchors.fill: parent + onClicked: { + switch (button.state) { + case "": + TimelineManager.timeline.cacheMedia(model.data.id); + break; + case "stopped": + media.play(); + console.log("play"); + button.state = "playing"; + break; + case "playing": + media.pause(); + console.log("pause"); + button.state = "stopped"; + break; + } + } + cursorShape: Qt.PointingHandCursor + } + + MediaPlayer { + id: media + + onError: console.log(errorString) + onStatusChanged: { + if (status == MediaPlayer.Loaded) + progress.updatePositionTexts(); + + } + onStopped: button.state = "stopped" + } + + Connections { + target: TimelineManager.timeline + onMediaCached: { + if (mxcUrl == model.data.url) { + media.source = "file://" + cacheUrl; + button.state = "stopped"; + console.log("media loaded: " + mxcUrl + " at " + cacheUrl); + } + console.log("media cached: " + mxcUrl + " at " + cacheUrl); + } + } + + } + + ColumnLayout { + id: col + + Text { + Layout.fillWidth: true + text: model.data.body + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + + Text { + Layout.fillWidth: true + text: model.data.filesize + textFormat: Text.PlainText + elide: Text.ElideRight + color: colors.text + } + + } + + } + + } - Text { - Layout.fillWidth: true - text: model.data.body - textFormat: Text.PlainText - elide: Text.ElideRight - color: colors.text - } - Text { - Layout.fillWidth: true - text: model.data.filesize - textFormat: Text.PlainText - elide: Text.ElideRight - color: colors.text - } - } - } - } } - diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 43fc2814..1471dbdf 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -2,66 +2,71 @@ import QtQuick 2.6 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 - import im.nheko 1.0 Item { - id: replyComponent + id: replyComponent - property alias modelData: reply.modelData - property color userColor: "red" + property alias modelData: reply.modelData + property color userColor: "red" - width: parent.width - height: replyContainer.height + width: parent.width + height: replyContainer.height - MouseArea { - anchors.fill: parent - preventStealing: true - onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain) - cursorShape: Qt.PointingHandCursor - } + MouseArea { + anchors.fill: parent + preventStealing: true + onClicked: chat.positionViewAtIndex(chat.model.idToIndex(modelData.id), ListView.Contain) + cursorShape: Qt.PointingHandCursor + } - Rectangle { - id: colorLine + Rectangle { + id: colorLine - anchors.top: replyContainer.top - anchors.bottom: replyContainer.bottom - width: 4 + anchors.top: replyContainer.top + anchors.bottom: replyContainer.bottom + width: 4 + color: TimelineManager.userColor(reply.modelData.userId, colors.window) + } - color: TimelineManager.userColor(reply.modelData.userId, colors.window) - } + Column { + id: replyContainer - Column { - id: replyContainer - anchors.left: colorLine.right - anchors.leftMargin: 4 - width: parent.width - 8 + anchors.left: colorLine.right + anchors.leftMargin: 4 + width: parent.width - 8 - Text { - id: userName - text: TimelineManager.escapeEmoji(reply.modelData.userName) - color: replyComponent.userColor - textFormat: Text.RichText + Text { + id: userName - MouseArea { - anchors.fill: parent - onClicked: chat.model.openUserProfile(reply.modelData.userId) - cursorShape: Qt.PointingHandCursor - } - } + text: TimelineManager.escapeEmoji(reply.modelData.userName) + color: replyComponent.userColor + textFormat: Text.RichText - MessageDelegate { - id: reply - width: parent.width - isReply: true - } - } + MouseArea { + anchors.fill: parent + onClicked: chat.model.openUserProfile(reply.modelData.userId) + cursorShape: Qt.PointingHandCursor + } + + } + + MessageDelegate { + id: reply + + width: parent.width + isReply: true + } + + } + + Rectangle { + id: backgroundItem + + z: -1 + height: replyContainer.height + width: Math.min(Math.max(reply.implicitWidth, userName.implicitWidth) + 8 + 4, parent.width) + color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.2) + } - Rectangle { - id: backgroundItem - z: -1 - height: replyContainer.height - width: Math.min(Math.max(reply.implicitWidth, userName.implicitWidth) + 8 + 4, parent.width) - color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.2) - } } diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 99ff9329..69f2f0e3 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -1,12 +1,12 @@ import ".." - import im.nheko 1.0 MatrixText { - property string formatted: model.data.formattedBody - text: "" + formatted.replace("
", "
")
-	width: parent ? parent.width : undefined
-	height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
-	clip: true
-	font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
+    property string formatted: model.data.formattedBody
+
+    text: "" + formatted.replace("
", "
")
+    width: parent ? parent.width : undefined
+    height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
+    clip: true
+    font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
 }
diff --git a/resources/qml/device-verification/AwaitingVerificationConfirmation.qml b/resources/qml/device-verification/AwaitingVerificationConfirmation.qml
index cd8ccfd9..42bd68df 100644
--- a/resources/qml/device-verification/AwaitingVerificationConfirmation.qml
+++ b/resources/qml/device-verification/AwaitingVerificationConfirmation.qml
@@ -1,39 +1,46 @@
 import QtQuick 2.3
 import QtQuick.Controls 2.10
 import QtQuick.Layouts 1.10
-
 import im.nheko 1.0
 
 Pane {
-	property string title: qsTr("Awaiting Confirmation")
-	ColumnLayout {
-		spacing: 16
-		Label {
-			Layout.maximumWidth: 400
-			Layout.fillHeight: true
-			Layout.fillWidth: true
-			wrapMode: Text.Wrap
-			id: content
-			text: qsTr("Waiting for other side to complete verification.")
-			color:colors.text
-			verticalAlignment: Text.AlignVCenter
-		}
-		BusyIndicator {
-			Layout.alignment: Qt.AlignHCenter
-		}
-		RowLayout {
-			Button {
-				Layout.alignment: Qt.AlignLeft
-				text: qsTr("Cancel")
+    property string title: qsTr("Awaiting Confirmation")
+
+    ColumnLayout {
+        spacing: 16
+
+        Label {
+            id: content
+
+            Layout.maximumWidth: 400
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            wrapMode: Text.Wrap
+            text: qsTr("Waiting for other side to complete verification.")
+            color: colors.text
+            verticalAlignment: Text.AlignVCenter
+        }
+
+        BusyIndicator {
+            Layout.alignment: Qt.AlignHCenter
+        }
+
+        RowLayout {
+            Button {
+                Layout.alignment: Qt.AlignLeft
+                text: qsTr("Cancel")
+                onClicked: {
+                    flow.cancel();
+                    dialog.close();
+                }
+            }
+
+            Item {
+                Layout.fillWidth: true
+            }
+
+        }
+
+    }
 
-				onClicked: { 
-					flow.cancel();
-					dialog.close();
-				}
-			}
-			Item {
-				Layout.fillWidth: true
-			}
-		}
-	}
 }
diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml
index 2e8f7504..5009cc3a 100644
--- a/resources/qml/device-verification/DeviceVerification.qml
+++ b/resources/qml/device-verification/DeviceVerification.qml
@@ -1,97 +1,144 @@
 import QtQuick 2.10
 import QtQuick.Controls 2.10
 import QtQuick.Window 2.10
-
 import im.nheko 1.0
 
 ApplicationWindow {
-	property var flow
+    id: dialog
 
-	onClosing: TimelineManager.removeVerificationFlow(flow)
+    property var flow
 
-	title: stack.currentItem.title
-	id: dialog
+    onClosing: TimelineManager.removeVerificationFlow(flow)
+    title: stack.currentItem.title
+    flags: Qt.Dialog
+    palette: colors
+    height: stack.implicitHeight
+    width: stack.implicitWidth
 
-	flags: Qt.Dialog
+    StackView {
+        id: stack
 
-	palette: colors
+        initialItem: newVerificationRequest
+        implicitWidth: currentItem.implicitWidth
+        implicitHeight: currentItem.implicitHeight
+    }
 
-	height: stack.implicitHeight
-	width: stack.implicitWidth
+    Component {
+        id: newVerificationRequest
 
-	StackView {
-		id: stack
-		initialItem: newVerificationRequest
-		implicitWidth: currentItem.implicitWidth
-		implicitHeight: currentItem.implicitHeight
-	}
+        NewVerificationRequest {
+        }
 
-	Component{
-		id: newVerificationRequest
-		NewVerificationRequest {}
-	}
+    }
 
-	Component {
-		id: waiting
-		Waiting {}
-	}
+    Component {
+        id: waiting
 
-	Component {
-		id: success
-		Success {}
-	}
+        Waiting {
+        }
 
-	Component {
-		id: failed
-		Failed {}
-	}
+    }
 
-	Component {
-		id: digitVerification
-		DigitVerification {}
-	}
+    Component {
+        id: success
 
-	Component {
-		id: emojiVerification
-		EmojiVerification {}
-	}
+        Success {
+        }
 
-	Item {
-	state: flow.state
+    }
+
+    Component {
+        id: failed
+
+        Failed {
+        }
+
+    }
+
+    Component {
+        id: digitVerification
+
+        DigitVerification {
+        }
+
+    }
+
+    Component {
+        id: emojiVerification
+
+        EmojiVerification {
+        }
+
+    }
+
+    Item {
+        state: flow.state
+        states: [
+            State {
+                name: "PromptStartVerification"
+
+                StateChangeScript {
+                    script: stack.replace(newVerificationRequest)
+                }
+
+            },
+            State {
+                name: "CompareEmoji"
+
+                StateChangeScript {
+                    script: stack.replace(emojiVerification)
+                }
+
+            },
+            State {
+                name: "CompareNumber"
+
+                StateChangeScript {
+                    script: stack.replace(digitVerification)
+                }
+
+            },
+            State {
+                name: "WaitingForKeys"
+
+                StateChangeScript {
+                    script: stack.replace(waiting)
+                }
+
+            },
+            State {
+                name: "WaitingForOtherToAccept"
+
+                StateChangeScript {
+                    script: stack.replace(waiting)
+                }
+
+            },
+            State {
+                name: "WaitingForMac"
+
+                StateChangeScript {
+                    script: stack.replace(waiting)
+                }
+
+            },
+            State {
+                name: "Success"
+
+                StateChangeScript {
+                    script: stack.replace(success)
+                }
+
+            },
+            State {
+                name: "Failed"
+
+                StateChangeScript {
+                    script: stack.replace(failed)
+                }
+
+            }
+        ]
+    }
 
-	states: [
-		State {
-			name: "PromptStartVerification"
-			StateChangeScript { script: stack.replace(newVerificationRequest) }
-		},
-		State {
-			name: "CompareEmoji"
-			StateChangeScript { script: stack.replace(emojiVerification) }
-		},
-		State {
-			name: "CompareNumber"
-			StateChangeScript { script: stack.replace(digitVerification) }
-		},
-		State {
-			name: "WaitingForKeys"
-			StateChangeScript { script: stack.replace(waiting) }
-		},
-		State {
-			name: "WaitingForOtherToAccept"
-			StateChangeScript { script: stack.replace(waiting) }
-		},
-		State {
-			name: "WaitingForMac"
-			StateChangeScript { script: stack.replace(waiting) }
-		},
-		State {
-			name: "Success"
-			StateChangeScript { script: stack.replace(success) }
-		},
-		State {
-			name: "Failed"
-			StateChangeScript { script: stack.replace(failed); }
-		}
-	]
-}
 }
diff --git a/resources/qml/device-verification/DigitVerification.qml b/resources/qml/device-verification/DigitVerification.qml
index ff878a50..11c32d26 100644
--- a/resources/qml/device-verification/DigitVerification.qml
+++ b/resources/qml/device-verification/DigitVerification.qml
@@ -1,60 +1,69 @@
 import QtQuick 2.3
 import QtQuick.Controls 2.10
 import QtQuick.Layouts 1.10
-
 import im.nheko 1.0
 
 Pane {
-	property string title: qsTr("Verification Code")
+    property string title: qsTr("Verification Code")
 
-	ColumnLayout {
-		spacing: 16
-		Label {
-			Layout.maximumWidth: 400
-			Layout.fillHeight: true
-			Layout.fillWidth: true
-			wrapMode: Text.Wrap
-			text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!")
-			color:colors.text
-			verticalAlignment: Text.AlignVCenter
-		}
-		RowLayout {
-			Layout.alignment: Qt.AlignHCenter
-			Label {
-				font.pixelSize: Qt.application.font.pixelSize * 2
-				text: flow.sasList[0]
-				color:colors.text
-			}
-			Label {
-				font.pixelSize: Qt.application.font.pixelSize * 2
-				text: flow.sasList[1]
-				color:colors.text
-			}
-			Label {
-				font.pixelSize: Qt.application.font.pixelSize * 2
-				text: flow.sasList[2]
-				color:colors.text
-			}
-		}
-		RowLayout {
-			Button {
-				Layout.alignment: Qt.AlignLeft
-				text: qsTr("They do not match!")
+    ColumnLayout {
+        spacing: 16
 
-				onClicked: {
-					flow.cancel();
-					dialog.close();
-				}
-			}
-			Item {
-				Layout.fillWidth: true
-			}
-			Button {
-				Layout.alignment: Qt.AlignRight
-				text: qsTr("They match!")
+        Label {
+            Layout.maximumWidth: 400
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            wrapMode: Text.Wrap
+            text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!")
+            color: colors.text
+            verticalAlignment: Text.AlignVCenter
+        }
+
+        RowLayout {
+            Layout.alignment: Qt.AlignHCenter
+
+            Label {
+                font.pixelSize: Qt.application.font.pixelSize * 2
+                text: flow.sasList[0]
+                color: colors.text
+            }
+
+            Label {
+                font.pixelSize: Qt.application.font.pixelSize * 2
+                text: flow.sasList[1]
+                color: colors.text
+            }
+
+            Label {
+                font.pixelSize: Qt.application.font.pixelSize * 2
+                text: flow.sasList[2]
+                color: colors.text
+            }
+
+        }
+
+        RowLayout {
+            Button {
+                Layout.alignment: Qt.AlignLeft
+                text: qsTr("They do not match!")
+                onClicked: {
+                    flow.cancel();
+                    dialog.close();
+                }
+            }
+
+            Item {
+                Layout.fillWidth: true
+            }
+
+            Button {
+                Layout.alignment: Qt.AlignRight
+                text: qsTr("They match!")
+                onClicked: flow.next()
+            }
+
+        }
+
+    }
 
-				onClicked: flow.next();
-			}
-		}
-	}
 }
diff --git a/resources/qml/device-verification/EmojiElement.qml b/resources/qml/device-verification/EmojiElement.qml
index 7e364594..73ad1c9b 100644
--- a/resources/qml/device-verification/EmojiElement.qml
+++ b/resources/qml/device-verification/EmojiElement.qml
@@ -3,24 +3,31 @@ import QtQuick.Controls 2.10
 import QtQuick.Layouts 1.10
 
 Rectangle {
-	color: "red"
-	implicitHeight: Qt.application.font.pixelSize * 4
-	implicitWidth: col.width
-	height: Qt.application.font.pixelSize * 4
-	width: col.width
-	ColumnLayout {
-		id: col
-		anchors.bottom: parent.bottom
-		property var emoji: emojis.mapping[Math.floor(Math.random()*64)]
-		Label {
-			height: font.pixelSize * 2
-			Layout.alignment: Qt.AlignHCenter
-			text: col.emoji.emoji
-			font.pixelSize: Qt.application.font.pixelSize * 2
-		}
-		Label {
-			Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
-			text: col.emoji.description
-		}
-	}
+    color: "red"
+    implicitHeight: Qt.application.font.pixelSize * 4
+    implicitWidth: col.width
+    height: Qt.application.font.pixelSize * 4
+    width: col.width
+
+    ColumnLayout {
+        id: col
+
+        property var emoji: emojis.mapping[Math.floor(Math.random() * 64)]
+
+        anchors.bottom: parent.bottom
+
+        Label {
+            height: font.pixelSize * 2
+            Layout.alignment: Qt.AlignHCenter
+            text: col.emoji.emoji
+            font.pixelSize: Qt.application.font.pixelSize * 2
+        }
+
+        Label {
+            Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
+            text: col.emoji.description
+        }
+
+    }
+
 }
diff --git a/resources/qml/device-verification/EmojiVerification.qml b/resources/qml/device-verification/EmojiVerification.qml
index ed7727aa..6ac340e4 100644
--- a/resources/qml/device-verification/EmojiVerification.qml
+++ b/resources/qml/device-verification/EmojiVerification.qml
@@ -1,140 +1,414 @@
 import QtQuick 2.3
 import QtQuick.Controls 2.10
 import QtQuick.Layouts 1.10
-
 import im.nheko 1.0
 
 Pane {
-	property string title: qsTr("Verification Code")
+    property string title: qsTr("Verification Code")
 
-	ColumnLayout {
-		spacing: 16
-		Label {
-			Layout.maximumWidth: 400
-			Layout.fillHeight: true
-			Layout.fillWidth: true
-			wrapMode: Text.Wrap
-			text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!")
-			color:colors.text
-			verticalAlignment: Text.AlignVCenter
-		}
-		RowLayout {
-			Layout.alignment: Qt.AlignHCenter
-			id: emojis
-			property var mapping: [
-				{"number": 0, "emoji": "🐶", "description": "Dog", "unicode": "U+1F436"},
-				{"number": 1, "emoji": "🐱", "description": "Cat", "unicode": "U+1F431"},
-				{"number": 2, "emoji": "🦁", "description": "Lion", "unicode": "U+1F981"},
-				{"number": 3, "emoji": "🐎", "description": "Horse", "unicode": "U+1F40E"},
-				{"number": 4, "emoji": "🦄", "description": "Unicorn", "unicode": "U+1F984"},
-				{"number": 5, "emoji": "🐷", "description": "Pig", "unicode": "U+1F437"},
-				{"number": 6, "emoji": "🐘", "description": "Elephant", "unicode": "U+1F418"},
-				{"number": 7, "emoji": "🐰", "description": "Rabbit", "unicode": "U+1F430"},
-				{"number": 8, "emoji": "🐼", "description": "Panda", "unicode": "U+1F43C"},
-				{"number": 9, "emoji": "🐓", "description": "Rooster", "unicode": "U+1F413"},
-				{"number": 10, "emoji": "🐧", "description": "Penguin", "unicode": "U+1F427"},
-				{"number": 11, "emoji": "🐢", "description": "Turtle", "unicode": "U+1F422"},
-				{"number": 12, "emoji": "🐟", "description": "Fish", "unicode": "U+1F41F"},
-				{"number": 13, "emoji": "🐙", "description": "Octopus", "unicode": "U+1F419"},
-				{"number": 14, "emoji": "🦋", "description": "Butterfly", "unicode": "U+1F98B"},
-				{"number": 15, "emoji": "🌷", "description": "Flower", "unicode": "U+1F337"},
-				{"number": 16, "emoji": "🌳", "description": "Tree", "unicode": "U+1F333"},
-				{"number": 17, "emoji": "🌵", "description": "Cactus", "unicode": "U+1F335"},
-				{"number": 18, "emoji": "🍄", "description": "Mushroom", "unicode": "U+1F344"},
-				{"number": 19, "emoji": "🌏", "description": "Globe", "unicode": "U+1F30F"},
-				{"number": 20, "emoji": "🌙", "description": "Moon", "unicode": "U+1F319"},
-				{"number": 21, "emoji": "☁️", "description": "Cloud", "unicode": "U+2601U+FE0F"},
-				{"number": 22, "emoji": "🔥", "description": "Fire", "unicode": "U+1F525"},
-				{"number": 23, "emoji": "🍌", "description": "Banana", "unicode": "U+1F34C"},
-				{"number": 24, "emoji": "🍎", "description": "Apple", "unicode": "U+1F34E"},
-				{"number": 25, "emoji": "🍓", "description": "Strawberry", "unicode": "U+1F353"},
-				{"number": 26, "emoji": "🌽", "description": "Corn", "unicode": "U+1F33D"},
-				{"number": 27, "emoji": "🍕", "description": "Pizza", "unicode": "U+1F355"},
-				{"number": 28, "emoji": "🎂", "description": "Cake", "unicode": "U+1F382"},
-				{"number": 29, "emoji": "❤️", "description": "Heart", "unicode": "U+2764U+FE0F"},
-				{"number": 30, "emoji": "😀", "description": "Smiley", "unicode": "U+1F600"},
-				{"number": 31, "emoji": "🤖", "description": "Robot", "unicode": "U+1F916"},
-				{"number": 32, "emoji": "🎩", "description": "Hat", "unicode": "U+1F3A9"},
-				{"number": 33, "emoji": "👓", "description": "Glasses", "unicode": "U+1F453"},
-				{"number": 34, "emoji": "🔧", "description": "Spanner", "unicode": "U+1F527"},
-				{"number": 35, "emoji": "🎅", "description": "Santa", "unicode": "U+1F385"},
-				{"number": 36, "emoji": "👍", "description": "Thumbs Up", "unicode": "U+1F44D"},
-				{"number": 37, "emoji": "☂️", "description": "Umbrella", "unicode": "U+2602U+FE0F"},
-				{"number": 38, "emoji": "⌛", "description": "Hourglass", "unicode": "U+231B"},
-				{"number": 39, "emoji": "⏰", "description": "Clock", "unicode": "U+23F0"},
-				{"number": 40, "emoji": "🎁", "description": "Gift", "unicode": "U+1F381"},
-				{"number": 41, "emoji": "💡", "description": "Light Bulb", "unicode": "U+1F4A1"},
-				{"number": 42, "emoji": "📕", "description": "Book", "unicode": "U+1F4D5"},
-				{"number": 43, "emoji": "✏️", "description": "Pencil", "unicode": "U+270FU+FE0F"},
-				{"number": 44, "emoji": "📎", "description": "Paperclip", "unicode": "U+1F4CE"},
-				{"number": 45, "emoji": "✂️", "description": "Scissors", "unicode": "U+2702U+FE0F"},
-				{"number": 46, "emoji": "🔒", "description": "Lock", "unicode": "U+1F512"},
-				{"number": 47, "emoji": "🔑", "description": "Key", "unicode": "U+1F511"},
-				{"number": 48, "emoji": "🔨", "description": "Hammer", "unicode": "U+1F528"},
-				{"number": 49, "emoji": "☎️", "description": "Telephone", "unicode": "U+260EU+FE0F"},
-				{"number": 50, "emoji": "🏁", "description": "Flag", "unicode": "U+1F3C1"},
-				{"number": 51, "emoji": "🚂", "description": "Train", "unicode": "U+1F682"},
-				{"number": 52, "emoji": "🚲", "description": "Bicycle", "unicode": "U+1F6B2"},
-				{"number": 53, "emoji": "✈️", "description": "Aeroplane", "unicode": "U+2708U+FE0F"},
-				{"number": 54, "emoji": "🚀", "description": "Rocket", "unicode": "U+1F680"},
-				{"number": 55, "emoji": "🏆", "description": "Trophy", "unicode": "U+1F3C6"},
-				{"number": 56, "emoji": "⚽", "description": "Ball", "unicode": "U+26BD"},
-				{"number": 57, "emoji": "🎸", "description": "Guitar", "unicode": "U+1F3B8"},
-				{"number": 58, "emoji": "🎺", "description": "Trumpet", "unicode": "U+1F3BA"},
-				{"number": 59, "emoji": "🔔", "description": "Bell", "unicode": "U+1F514"},
-				{"number": 60, "emoji": "⚓", "description": "Anchor", "unicode": "U+2693"},
-				{"number": 61, "emoji": "🎧", "description": "Headphones", "unicode": "U+1F3A7"},
-				{"number": 62, "emoji": "📁", "description": "Folder", "unicode": "U+1F4C1"},
-				{"number": 63, "emoji": "📌", "description": "Pin", "unicode": "U+1F4CC"}
-			]
-			Repeater {
-				id: repeater
-				model: 7
-				delegate: Rectangle {
-					color: "transparent"
-					implicitHeight: Qt.application.font.pixelSize * 8
-					implicitWidth: col.width
-					ColumnLayout {
-						id: col
-						Layout.fillWidth: true
-						anchors.bottom: parent.bottom
-						property var emoji: emojis.mapping[flow.sasList[index]]
-						Label {
-							//height: font.pixelSize * 2
-							Layout.alignment: Qt.AlignHCenter
-							text: col.emoji.emoji
-							font.pixelSize: Qt.application.font.pixelSize * 2
-							font.family: Settings.emojiFont
-							color:colors.text
-						}
-						Label {
-							Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
-							text: col.emoji.description
-							color:colors.text
-						}
-					}
-				}
-			}
-		}
-		RowLayout {
-			Button {
-				Layout.alignment: Qt.AlignLeft
-				text: qsTr("They do not match!")
+    ColumnLayout {
+        spacing: 16
 
-				onClicked: {  
-					flow.cancel();
-					dialog.close();
-				}
-			}
-			Item {
-				Layout.fillWidth: true
-			}
-			Button {
-				Layout.alignment: Qt.AlignRight
-				text: qsTr("They match!")
+        Label {
+            Layout.maximumWidth: 400
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            wrapMode: Text.Wrap
+            text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!")
+            color: colors.text
+            verticalAlignment: Text.AlignVCenter
+        }
+
+        RowLayout {
+            id: emojis
+
+            property var mapping: [{
+                "number": 0,
+                "emoji": "🐶",
+                "description": "Dog",
+                "unicode": "U+1F436"
+            }, {
+                "number": 1,
+                "emoji": "🐱",
+                "description": "Cat",
+                "unicode": "U+1F431"
+            }, {
+                "number": 2,
+                "emoji": "🦁",
+                "description": "Lion",
+                "unicode": "U+1F981"
+            }, {
+                "number": 3,
+                "emoji": "🐎",
+                "description": "Horse",
+                "unicode": "U+1F40E"
+            }, {
+                "number": 4,
+                "emoji": "🦄",
+                "description": "Unicorn",
+                "unicode": "U+1F984"
+            }, {
+                "number": 5,
+                "emoji": "🐷",
+                "description": "Pig",
+                "unicode": "U+1F437"
+            }, {
+                "number": 6,
+                "emoji": "🐘",
+                "description": "Elephant",
+                "unicode": "U+1F418"
+            }, {
+                "number": 7,
+                "emoji": "🐰",
+                "description": "Rabbit",
+                "unicode": "U+1F430"
+            }, {
+                "number": 8,
+                "emoji": "🐼",
+                "description": "Panda",
+                "unicode": "U+1F43C"
+            }, {
+                "number": 9,
+                "emoji": "🐓",
+                "description": "Rooster",
+                "unicode": "U+1F413"
+            }, {
+                "number": 10,
+                "emoji": "🐧",
+                "description": "Penguin",
+                "unicode": "U+1F427"
+            }, {
+                "number": 11,
+                "emoji": "🐢",
+                "description": "Turtle",
+                "unicode": "U+1F422"
+            }, {
+                "number": 12,
+                "emoji": "🐟",
+                "description": "Fish",
+                "unicode": "U+1F41F"
+            }, {
+                "number": 13,
+                "emoji": "🐙",
+                "description": "Octopus",
+                "unicode": "U+1F419"
+            }, {
+                "number": 14,
+                "emoji": "🦋",
+                "description": "Butterfly",
+                "unicode": "U+1F98B"
+            }, {
+                "number": 15,
+                "emoji": "🌷",
+                "description": "Flower",
+                "unicode": "U+1F337"
+            }, {
+                "number": 16,
+                "emoji": "🌳",
+                "description": "Tree",
+                "unicode": "U+1F333"
+            }, {
+                "number": 17,
+                "emoji": "🌵",
+                "description": "Cactus",
+                "unicode": "U+1F335"
+            }, {
+                "number": 18,
+                "emoji": "🍄",
+                "description": "Mushroom",
+                "unicode": "U+1F344"
+            }, {
+                "number": 19,
+                "emoji": "🌏",
+                "description": "Globe",
+                "unicode": "U+1F30F"
+            }, {
+                "number": 20,
+                "emoji": "🌙",
+                "description": "Moon",
+                "unicode": "U+1F319"
+            }, {
+                "number": 21,
+                "emoji": "☁️",
+                "description": "Cloud",
+                "unicode": "U+2601U+FE0F"
+            }, {
+                "number": 22,
+                "emoji": "🔥",
+                "description": "Fire",
+                "unicode": "U+1F525"
+            }, {
+                "number": 23,
+                "emoji": "🍌",
+                "description": "Banana",
+                "unicode": "U+1F34C"
+            }, {
+                "number": 24,
+                "emoji": "🍎",
+                "description": "Apple",
+                "unicode": "U+1F34E"
+            }, {
+                "number": 25,
+                "emoji": "🍓",
+                "description": "Strawberry",
+                "unicode": "U+1F353"
+            }, {
+                "number": 26,
+                "emoji": "🌽",
+                "description": "Corn",
+                "unicode": "U+1F33D"
+            }, {
+                "number": 27,
+                "emoji": "🍕",
+                "description": "Pizza",
+                "unicode": "U+1F355"
+            }, {
+                "number": 28,
+                "emoji": "🎂",
+                "description": "Cake",
+                "unicode": "U+1F382"
+            }, {
+                "number": 29,
+                "emoji": "❤️",
+                "description": "Heart",
+                "unicode": "U+2764U+FE0F"
+            }, {
+                "number": 30,
+                "emoji": "😀",
+                "description": "Smiley",
+                "unicode": "U+1F600"
+            }, {
+                "number": 31,
+                "emoji": "🤖",
+                "description": "Robot",
+                "unicode": "U+1F916"
+            }, {
+                "number": 32,
+                "emoji": "🎩",
+                "description": "Hat",
+                "unicode": "U+1F3A9"
+            }, {
+                "number": 33,
+                "emoji": "👓",
+                "description": "Glasses",
+                "unicode": "U+1F453"
+            }, {
+                "number": 34,
+                "emoji": "🔧",
+                "description": "Spanner",
+                "unicode": "U+1F527"
+            }, {
+                "number": 35,
+                "emoji": "🎅",
+                "description": "Santa",
+                "unicode": "U+1F385"
+            }, {
+                "number": 36,
+                "emoji": "👍",
+                "description": "Thumbs Up",
+                "unicode": "U+1F44D"
+            }, {
+                "number": 37,
+                "emoji": "☂️",
+                "description": "Umbrella",
+                "unicode": "U+2602U+FE0F"
+            }, {
+                "number": 38,
+                "emoji": "⌛",
+                "description": "Hourglass",
+                "unicode": "U+231B"
+            }, {
+                "number": 39,
+                "emoji": "⏰",
+                "description": "Clock",
+                "unicode": "U+23F0"
+            }, {
+                "number": 40,
+                "emoji": "🎁",
+                "description": "Gift",
+                "unicode": "U+1F381"
+            }, {
+                "number": 41,
+                "emoji": "💡",
+                "description": "Light Bulb",
+                "unicode": "U+1F4A1"
+            }, {
+                "number": 42,
+                "emoji": "📕",
+                "description": "Book",
+                "unicode": "U+1F4D5"
+            }, {
+                "number": 43,
+                "emoji": "✏️",
+                "description": "Pencil",
+                "unicode": "U+270FU+FE0F"
+            }, {
+                "number": 44,
+                "emoji": "📎",
+                "description": "Paperclip",
+                "unicode": "U+1F4CE"
+            }, {
+                "number": 45,
+                "emoji": "✂️",
+                "description": "Scissors",
+                "unicode": "U+2702U+FE0F"
+            }, {
+                "number": 46,
+                "emoji": "🔒",
+                "description": "Lock",
+                "unicode": "U+1F512"
+            }, {
+                "number": 47,
+                "emoji": "🔑",
+                "description": "Key",
+                "unicode": "U+1F511"
+            }, {
+                "number": 48,
+                "emoji": "🔨",
+                "description": "Hammer",
+                "unicode": "U+1F528"
+            }, {
+                "number": 49,
+                "emoji": "☎️",
+                "description": "Telephone",
+                "unicode": "U+260EU+FE0F"
+            }, {
+                "number": 50,
+                "emoji": "🏁",
+                "description": "Flag",
+                "unicode": "U+1F3C1"
+            }, {
+                "number": 51,
+                "emoji": "🚂",
+                "description": "Train",
+                "unicode": "U+1F682"
+            }, {
+                "number": 52,
+                "emoji": "🚲",
+                "description": "Bicycle",
+                "unicode": "U+1F6B2"
+            }, {
+                "number": 53,
+                "emoji": "✈️",
+                "description": "Aeroplane",
+                "unicode": "U+2708U+FE0F"
+            }, {
+                "number": 54,
+                "emoji": "🚀",
+                "description": "Rocket",
+                "unicode": "U+1F680"
+            }, {
+                "number": 55,
+                "emoji": "🏆",
+                "description": "Trophy",
+                "unicode": "U+1F3C6"
+            }, {
+                "number": 56,
+                "emoji": "⚽",
+                "description": "Ball",
+                "unicode": "U+26BD"
+            }, {
+                "number": 57,
+                "emoji": "🎸",
+                "description": "Guitar",
+                "unicode": "U+1F3B8"
+            }, {
+                "number": 58,
+                "emoji": "🎺",
+                "description": "Trumpet",
+                "unicode": "U+1F3BA"
+            }, {
+                "number": 59,
+                "emoji": "🔔",
+                "description": "Bell",
+                "unicode": "U+1F514"
+            }, {
+                "number": 60,
+                "emoji": "⚓",
+                "description": "Anchor",
+                "unicode": "U+2693"
+            }, {
+                "number": 61,
+                "emoji": "🎧",
+                "description": "Headphones",
+                "unicode": "U+1F3A7"
+            }, {
+                "number": 62,
+                "emoji": "📁",
+                "description": "Folder",
+                "unicode": "U+1F4C1"
+            }, {
+                "number": 63,
+                "emoji": "📌",
+                "description": "Pin",
+                "unicode": "U+1F4CC"
+            }]
+
+            Layout.alignment: Qt.AlignHCenter
+
+            Repeater {
+                id: repeater
+
+                model: 7
+
+                delegate: Rectangle {
+                    color: "transparent"
+                    implicitHeight: Qt.application.font.pixelSize * 8
+                    implicitWidth: col.width
+
+                    ColumnLayout {
+                        id: col
+
+                        property var emoji: emojis.mapping[flow.sasList[index]]
+
+                        Layout.fillWidth: true
+                        anchors.bottom: parent.bottom
+
+                        Label {
+                            //height: font.pixelSize * 2
+                            Layout.alignment: Qt.AlignHCenter
+                            text: col.emoji.emoji
+                            font.pixelSize: Qt.application.font.pixelSize * 2
+                            font.family: Settings.emojiFont
+                            color: colors.text
+                        }
+
+                        Label {
+                            Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
+                            text: col.emoji.description
+                            color: colors.text
+                        }
+
+                    }
+
+                }
+
+            }
+
+        }
+
+        RowLayout {
+            Button {
+                Layout.alignment: Qt.AlignLeft
+                text: qsTr("They do not match!")
+                onClicked: {
+                    flow.cancel();
+                    dialog.close();
+                }
+            }
+
+            Item {
+                Layout.fillWidth: true
+            }
+
+            Button {
+                Layout.alignment: Qt.AlignRight
+                text: qsTr("They match!")
+                onClicked: flow.next()
+            }
+
+        }
+
+    }
 
-				onClicked: flow.next()
-			}
-		}
-	}
 }
diff --git a/resources/qml/device-verification/Failed.qml b/resources/qml/device-verification/Failed.qml
index fcff7893..5f9a2859 100644
--- a/resources/qml/device-verification/Failed.qml
+++ b/resources/qml/device-verification/Failed.qml
@@ -1,44 +1,56 @@
 import QtQuick 2.3
 import QtQuick.Controls 2.10
 import QtQuick.Layouts 1.10
-
 import im.nheko 1.0
 
 Pane {
-	property string title: qsTr("Verification failed")
-	ColumnLayout {
-		spacing: 16
-		Text {
-			id: content
+    property string title: qsTr("Verification failed")
 
-			Layout.maximumWidth: 400
-			Layout.fillHeight: true
-			Layout.fillWidth: true
+    ColumnLayout {
+        spacing: 16
 
-			wrapMode: Text.Wrap
-			text: switch (flow.error) {
-				case DeviceVerificationFlow.UnknownMethod: return qsTr("Other client does not support our verification protocol.")
-				case DeviceVerificationFlow.MismatchedCommitment:
-				case DeviceVerificationFlow.MismatchedSAS:
-				case DeviceVerificationFlow.KeyMismatch: return qsTr("Key mismatch detected!")
-				case DeviceVerificationFlow.Timeout: return qsTr("Device verification timed out.")
-				case DeviceVerificationFlow.User: return qsTr("Other party canceled the verification.")
-				case DeviceVerificationFlow.OutOfOrder: return qsTr("Device verification timed out.")
-				default: return "Unknown verification error.";
-			}
-			color:colors.text
-			verticalAlignment: Text.AlignVCenter
-		}
-		RowLayout {
-			Item {
-				Layout.fillWidth: true
-			}
-			Button {
-				Layout.alignment: Qt.AlignRight
-				text: qsTr("Close")
+        Text {
+            id: content
+
+            Layout.maximumWidth: 400
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            wrapMode: Text.Wrap
+            text: {
+                switch (flow.error) {
+                case DeviceVerificationFlow.UnknownMethod:
+                    return qsTr("Other client does not support our verification protocol.");
+                case DeviceVerificationFlow.MismatchedCommitment:
+                case DeviceVerificationFlow.MismatchedSAS:
+                case DeviceVerificationFlow.KeyMismatch:
+                    return qsTr("Key mismatch detected!");
+                case DeviceVerificationFlow.Timeout:
+                    return qsTr("Device verification timed out.");
+                case DeviceVerificationFlow.User:
+                    return qsTr("Other party canceled the verification.");
+                case DeviceVerificationFlow.OutOfOrder:
+                    return qsTr("Device verification timed out.");
+                default:
+                    return "Unknown verification error.";
+                }
+            }
+            color: colors.text
+            verticalAlignment: Text.AlignVCenter
+        }
+
+        RowLayout {
+            Item {
+                Layout.fillWidth: true
+            }
+
+            Button {
+                Layout.alignment: Qt.AlignRight
+                text: qsTr("Close")
+                onClicked: dialog.close()
+            }
+
+        }
+
+    }
 
-				onClicked: dialog.close()
-			}
-		}
-	}
 }
diff --git a/resources/qml/device-verification/NewVerificationRequest.qml b/resources/qml/device-verification/NewVerificationRequest.qml
index bd25bb90..d411ed47 100644
--- a/resources/qml/device-verification/NewVerificationRequest.qml
+++ b/resources/qml/device-verification/NewVerificationRequest.qml
@@ -1,44 +1,46 @@
 import QtQuick 2.3
 import QtQuick.Controls 2.10
 import QtQuick.Layouts 1.10
-
 import im.nheko 1.0
 
 Pane {
-	property string title: flow.sender ? qsTr("Send Device Verification Request") : qsTr("Recieved Device Verification Request")
+    property string title: flow.sender ? qsTr("Send Device Verification Request") : qsTr("Recieved Device Verification Request")
 
-	ColumnLayout {
-		spacing: 16
-		Label {
-			Layout.maximumWidth: 400
-			Layout.fillHeight: true
-			Layout.fillWidth: true
-			wrapMode: Text.Wrap
-			text: flow.sender ?
-			    qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications, you can verify this device.")
-			    : qsTr("The device was requested to be verified")
-			color:colors.text
-			verticalAlignment: Text.AlignVCenter
-		}
-		RowLayout {
-			Button {
-				Layout.alignment: Qt.AlignLeft
-				text: flow.sender ? qsTr("Cancel") : qsTr("Deny")
+    ColumnLayout {
+        spacing: 16
 
-				onClicked: { 
-					flow.cancel();
-					dialog.close();
-				}
-			}
-			Item {
-				Layout.fillWidth: true
-			}
-			Button {
-				Layout.alignment: Qt.AlignRight
-				text: flow.sender ? qsTr("Start verification") : qsTr("Accept")
+        Label {
+            Layout.maximumWidth: 400
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            wrapMode: Text.Wrap
+            text: flow.sender ? qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications, you can verify this device.") : qsTr("The device was requested to be verified")
+            color: colors.text
+            verticalAlignment: Text.AlignVCenter
+        }
+
+        RowLayout {
+            Button {
+                Layout.alignment: Qt.AlignLeft
+                text: flow.sender ? qsTr("Cancel") : qsTr("Deny")
+                onClicked: {
+                    flow.cancel();
+                    dialog.close();
+                }
+            }
+
+            Item {
+                Layout.fillWidth: true
+            }
+
+            Button {
+                Layout.alignment: Qt.AlignRight
+                text: flow.sender ? qsTr("Start verification") : qsTr("Accept")
+                onClicked: flow.next()
+            }
+
+        }
+
+    }
 
-				onClicked: flow.next();
-			}
-		}
-	}
 }
diff --git a/resources/qml/device-verification/Success.qml b/resources/qml/device-verification/Success.qml
index b17b293c..175f7524 100644
--- a/resources/qml/device-verification/Success.qml
+++ b/resources/qml/device-verification/Success.qml
@@ -3,29 +3,36 @@ import QtQuick.Controls 2.10
 import QtQuick.Layouts 1.10
 
 Pane {
-	property string title: qsTr("Successful Verification")
-	ColumnLayout {
-		spacing: 16
-		Label {
-			Layout.maximumWidth: 400
-			Layout.fillHeight: true
-			Layout.fillWidth: true
-			wrapMode: Text.Wrap
-			id: content
-			text: qsTr("Verification successful! Both sides verified their devices!")
-			color:colors.text
-			verticalAlignment: Text.AlignVCenter
-		}
-		RowLayout {
-			Item {
-				Layout.fillWidth: true
-			}
-			Button {
-				Layout.alignment: Qt.AlignRight
-				text: qsTr("Close")
+    property string title: qsTr("Successful Verification")
+
+    ColumnLayout {
+        spacing: 16
+
+        Label {
+            id: content
+
+            Layout.maximumWidth: 400
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            wrapMode: Text.Wrap
+            text: qsTr("Verification successful! Both sides verified their devices!")
+            color: colors.text
+            verticalAlignment: Text.AlignVCenter
+        }
+
+        RowLayout {
+            Item {
+                Layout.fillWidth: true
+            }
+
+            Button {
+                Layout.alignment: Qt.AlignRight
+                text: qsTr("Close")
+                onClicked: dialog.close()
+            }
+
+        }
+
+    }
 
-				onClicked: dialog.close();
-			}
-		}
-	}
 }
diff --git a/resources/qml/device-verification/Waiting.qml b/resources/qml/device-verification/Waiting.qml
index 38abf767..0c4ae405 100644
--- a/resources/qml/device-verification/Waiting.qml
+++ b/resources/qml/device-verification/Waiting.qml
@@ -1,45 +1,56 @@
 import QtQuick 2.3
 import QtQuick.Controls 2.10
 import QtQuick.Layouts 1.10
-
 import im.nheko 1.0
 
 Pane {
-	property string title: qsTr("Waiting for other party")
-	ColumnLayout {
-		spacing: 16
-		Label {
-			Layout.maximumWidth: 400
-			Layout.fillHeight: true
-			Layout.fillWidth: true
-			wrapMode: Text.Wrap
-			id: content
-			text: switch (flow.state) {
-				case "WaitingForOtherToAccept": return qsTr("Waiting for other side to accept the verification request.")
-				case "WaitingForKeys": return qsTr("Waiting for other side to continue the verification request.")
-				case "WaitingForMac": return qsTr("Waiting for other side to complete the verification request.")
-			}
+    property string title: qsTr("Waiting for other party")
 
-			color: colors.text
-			verticalAlignment: Text.AlignVCenter
-		}
-		BusyIndicator {
-			Layout.alignment: Qt.AlignHCenter
-			palette: colors
-		}
-		RowLayout {
-			Button {
-				Layout.alignment: Qt.AlignLeft
-				text: qsTr("Cancel")
+    ColumnLayout {
+        spacing: 16
+
+        Label {
+            id: content
+
+            Layout.maximumWidth: 400
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            wrapMode: Text.Wrap
+            text: {
+                switch (flow.state) {
+                case "WaitingForOtherToAccept":
+                    return qsTr("Waiting for other side to accept the verification request.");
+                case "WaitingForKeys":
+                    return qsTr("Waiting for other side to continue the verification request.");
+                case "WaitingForMac":
+                    return qsTr("Waiting for other side to complete the verification request.");
+                }
+            }
+            color: colors.text
+            verticalAlignment: Text.AlignVCenter
+        }
+
+        BusyIndicator {
+            Layout.alignment: Qt.AlignHCenter
+            palette: colors
+        }
+
+        RowLayout {
+            Button {
+                Layout.alignment: Qt.AlignLeft
+                text: qsTr("Cancel")
+                onClicked: {
+                    flow.cancel();
+                    dialog.close();
+                }
+            }
+
+            Item {
+                Layout.fillWidth: true
+            }
+
+        }
+
+    }
 
-				onClicked: { 
-					flow.cancel();
-					dialog.close();
-				}
-			}
-			Item {
-				Layout.fillWidth: true
-			}
-		}
-	}
 }
diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml
index c5eee4e4..1fcfc0c5 100644
--- a/resources/qml/emoji/EmojiButton.qml
+++ b/resources/qml/emoji/EmojiButton.qml
@@ -1,17 +1,16 @@
+import "../"
 import QtQuick 2.10
 import QtQuick.Controls 2.1
 import im.nheko 1.0
 import im.nheko.EmojiModel 1.0
 
-import "../"
-
 ImageButton {
+    id: emojiButton
+
     property var colors: currentActivePalette
     property var emojiPicker
     property string event_id
 
     image: ":/icons/icons/ui/smile.png"
-    id: emojiButton
     onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, event_id)
-
 }
diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml
index cbb77beb..3a5ee57a 100644
--- a/resources/qml/emoji/EmojiPicker.qml
+++ b/resources/qml/emoji/EmojiPicker.qml
@@ -1,25 +1,13 @@
+import "../"
+import QtGraphicalEffects 1.0
 import QtQuick 2.9
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.3
-import QtGraphicalEffects 1.0
-
 import im.nheko 1.0
 import im.nheko.EmojiModel 1.0
 
-import "../"
-
 Popup {
-
-	function show(showAt, event_id) {
-		console.debug("Showing emojiPicker for " + event_id)
-		if (showAt){
-			parent = showAt
-			x = Math.round((showAt.width - width) / 2)
-			y = showAt.height
-		}
-		emojiPopup.event_id = event_id
-		open()
-	}
+    id: emojiPopup
 
     property string event_id
     property var colors
@@ -30,19 +18,28 @@ Popup {
     property real highlightSat: colors.highlight.hslSaturation
     property real highlightLight: colors.highlight.hslLightness
 
-    id: emojiPopup
+    function show(showAt, event_id) {
+        console.debug("Showing emojiPicker for " + event_id);
+        if (showAt) {
+            parent = showAt;
+            x = Math.round((showAt.width - width) / 2);
+            y = showAt.height;
+        }
+        emojiPopup.event_id = event_id;
+        open();
+    }
 
     margins: 0
     bottomPadding: 1
     leftPadding: 1
     rightPadding: 1
-
     modal: true
     focus: true
     closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
 
     ColumnLayout {
         id: columnView
+
         anchors.fill: parent
         spacing: 0
         Layout.bottomMargin: 0
@@ -58,23 +55,41 @@ Popup {
             Layout.fillWidth: true
             Layout.fillHeight: true
             Layout.leftMargin: 4
-
             cellWidth: 52
             cellHeight: 52
-
             boundsBehavior: Flickable.StopAtBounds
-
             clip: true
 
             // Individual emoji
             delegate: AbstractButton {
                 width: 48
                 height: 48
+                hoverEnabled: true
+                ToolTip.text: model.shortName
+                ToolTip.visible: hovered
+                // TODO: maybe add favorites at some point?
+                onClicked: {
+                    console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id);
+                    emojiPopup.close();
+                    TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode);
+                }
+
+                // give the emoji a little oomf
+                DropShadow {
+                    width: parent.width
+                    height: parent.height
+                    horizontalOffset: 3
+                    verticalOffset: 3
+                    radius: 8
+                    samples: 17
+                    color: "#80000000"
+                    source: parent.contentItem
+                }
+
                 contentItem: Text {
                     horizontalAlignment: Text.AlignHCenter
                     verticalAlignment: Text.AlignVCenter
                     font.family: Settings.emojiFont
-                    
                     font.pixelSize: 36
                     text: model.unicode
                 }
@@ -85,76 +100,66 @@ Popup {
                     radius: 5
                 }
 
-                hoverEnabled: true
-                ToolTip.text: model.shortName
-                ToolTip.visible: hovered
-
-                // give the emoji a little oomf
-                DropShadow {
-                    width: parent.width;
-                    height: parent.height;
-                    horizontalOffset: 3
-                    verticalOffset: 3
-                    radius: 8.0
-                    samples: 17
-                    color: "#80000000"
-                    source: parent.contentItem
-                }
-                // TODO: maybe add favorites at some point?
-                onClicked: {
-                    console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id)
-                    emojiPopup.close()
-                    TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode)
-                }
             }
 
             // Search field
             header: TextField {
                 id: emojiSearch
+
                 anchors.left: parent.left
                 anchors.right: parent.right
                 anchors.rightMargin: emojiScroll.width + 4
                 placeholderText: qsTr("Search")
                 selectByMouse: true
                 rightPadding: clearSearch.width
+                onTextChanged: searchTimer.restart()
+                onVisibleChanged: {
+                    if (visible)
+                        forceActiveFocus();
+
+                }
 
                 Timer {
                     id: searchTimer
+
                     interval: 350 // tweak as needed?
                     onTriggered: {
-                        emojiPopup.model.filter = emojiSearch.text
-                        emojiPopup.model.category = EmojiCategory.Search
+                        emojiPopup.model.filter = emojiSearch.text;
+                        emojiPopup.model.category = EmojiCategory.Search;
                     }
                 }
 
                 ToolButton {
                     id: clearSearch
+
+                    visible: emojiSearch.text !== ''
+                    icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText)
+                    focusPolicy: Qt.NoFocus
+                    onClicked: emojiSearch.clear()
+
                     anchors {
                         verticalCenter: parent.verticalCenter
                         right: parent.right
                     }
                     // clear the default hover effects.
-                    background: Item {}
-                    visible: emojiSearch.text !== ''
-                    icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText)
-                    focusPolicy: Qt.NoFocus
-                    onClicked: emojiSearch.clear()
+
+                    background: Item {
+                    }
+
                 }
 
-                onTextChanged: searchTimer.restart()
-                onVisibleChanged: if (visible) forceActiveFocus()
             }
 
-			ScrollBar.vertical: ScrollBar {
-				id: emojiScroll
-			}
+            ScrollBar.vertical: ScrollBar {
+                id: emojiScroll
+            }
+
         }
 
         // Separator
         Rectangle {
             Layout.fillWidth: true
             Layout.preferredHeight: 1
-
             color: emojiPopup.colors.dark
         }
 
@@ -164,23 +169,90 @@ Popup {
             Layout.preferredHeight: 42
             implicitHeight: 42
             Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
+
             // Display the normal categories
             Repeater {
+
                 model: ListModel {
                     // TODO: Would like to get 'simple' icons for the categories
-                    ListElement { image: ":/icons/icons/emoji-categories/people.png"; category: EmojiCategory.People }
-                    ListElement { image: ":/icons/icons/emoji-categories/nature.png"; category: EmojiCategory.Nature }
-                    ListElement { image: ":/icons/icons/emoji-categories/foods.png"; category: EmojiCategory.Food }
-                    ListElement { image: ":/icons/icons/emoji-categories/activity.png"; category: EmojiCategory.Activity }
-                    ListElement { image: ":/icons/icons/emoji-categories/travel.png"; category: EmojiCategory.Travel }
-                    ListElement { image: ":/icons/icons/emoji-categories/objects.png"; category: EmojiCategory.Objects }
-                    ListElement { image: ":/icons/icons/emoji-categories/symbols.png"; category: EmojiCategory.Symbols }
-                    ListElement { image: ":/icons/icons/emoji-categories/flags.png"; category: EmojiCategory.Flags }
+                    ListElement {
+                        image: ":/icons/icons/emoji-categories/people.png"
+                        category: EmojiCategory.People
+                    }
+
+                    ListElement {
+                        image: ":/icons/icons/emoji-categories/nature.png"
+                        category: EmojiCategory.Nature
+                    }
+
+                    ListElement {
+                        image: ":/icons/icons/emoji-categories/foods.png"
+                        category: EmojiCategory.Food
+                    }
+
+                    ListElement {
+                        image: ":/icons/icons/emoji-categories/activity.png"
+                        category: EmojiCategory.Activity
+                    }
+
+                    ListElement {
+                        image: ":/icons/icons/emoji-categories/travel.png"
+                        category: EmojiCategory.Travel
+                    }
+
+                    ListElement {
+                        image: ":/icons/icons/emoji-categories/objects.png"
+                        category: EmojiCategory.Objects
+                    }
+
+                    ListElement {
+                        image: ":/icons/icons/emoji-categories/symbols.png"
+                        category: EmojiCategory.Symbols
+                    }
+
+                    ListElement {
+                        image: ":/icons/icons/emoji-categories/flags.png"
+                        category: EmojiCategory.Flags
+                    }
+
                 }
 
                 delegate: AbstractButton {
                     Layout.preferredWidth: 36
                     Layout.preferredHeight: 36
+                    hoverEnabled: true
+                    ToolTip.text: {
+                        switch (model.category) {
+                        case EmojiCategory.People:
+                            return qsTr('People');
+                        case EmojiCategory.Nature:
+                            return qsTr('Nature');
+                        case EmojiCategory.Food:
+                            return qsTr('Food');
+                        case EmojiCategory.Activity:
+                            return qsTr('Activity');
+                        case EmojiCategory.Travel:
+                            return qsTr('Travel');
+                        case EmojiCategory.Objects:
+                            return qsTr('Objects');
+                        case EmojiCategory.Symbols:
+                            return qsTr('Symbols');
+                        case EmojiCategory.Flags:
+                            return qsTr('Flags');
+                        }
+                    }
+                    ToolTip.visible: hovered
+                    onClicked: {
+                        emojiPopup.model.category = model.category;
+                    }
+
+                    MouseArea {
+                        id: mouseArea
+
+                        anchors.fill: parent
+                        onPressed: mouse.accepted = false
+                        cursorShape: Qt.PointingHandCursor
+                    }
 
                     contentItem: Image {
                         horizontalAlignment: Image.AlignHCenter
@@ -191,49 +263,15 @@ Popup {
                         source: "image://colorimage/" + model.image + "?" + (hovered ? colors.highlight : colors.buttonText)
                     }
 
-                    MouseArea
-                    {
-                        id: mouseArea
-                        anchors.fill: parent
-                        onPressed:  mouse.accepted = false
-                        cursorShape: Qt.PointingHandCursor
-                    }
-
                     background: Rectangle {
                         anchors.fill: parent
-
-                        color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.20)  : 'transparent'
+                        color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : 'transparent'
                         radius: 5
                         border.color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent'
                     }
 
-                    hoverEnabled: true
-					ToolTip.text: {
-						switch (model.category) {
-						case EmojiCategory.People:
-							return qsTr('People');
-						case EmojiCategory.Nature:
-							return qsTr('Nature');
-						case EmojiCategory.Food:
-							return qsTr('Food');
-						case EmojiCategory.Activity:
-							return qsTr('Activity');
-						case EmojiCategory.Travel:
-							return qsTr('Travel');
-						case EmojiCategory.Objects:
-							return qsTr('Objects');
-						case EmojiCategory.Symbols:
-							return qsTr('Symbols');
-						case EmojiCategory.Flags:
-							return qsTr('Flags');
-						}
-					}
-                    ToolTip.visible: hovered
-
-                    onClicked: {
-                        emojiPopup.model.category = model.category
-                    }
                 }
+
             }
 
             // Separator
@@ -242,30 +280,37 @@ Popup {
                 Layout.preferredWidth: 1
                 implicitWidth: 1
                 height: parent.height
-
                 color: emojiPopup.colors.dark
             }
 
             // Search Button is special
             AbstractButton {
                 id: searchBtn
+
                 hoverEnabled: true
                 Layout.alignment: Qt.AlignRight
                 Layout.bottomMargin: 0
-
                 ToolTip.text: qsTr("Search")
                 ToolTip.visible: hovered
                 onClicked: {
                     // clear any filters
-                    emojiPopup.model.category = EmojiCategory.Search
-                    gridView.positionViewAtBeginning()
-                    emojiSearch.forceActiveFocus()
+                    emojiPopup.model.category = EmojiCategory.Search;
+                    gridView.positionViewAtBeginning();
+                    emojiSearch.forceActiveFocus();
                 }
                 Layout.preferredWidth: 36
                 Layout.preferredHeight: 36
                 implicitWidth: 36
                 implicitHeight: 36
 
+                MouseArea {
+                    id: mouseArea
+
+                    anchors.fill: parent
+                    onPressed: mouse.accepted = false
+                    cursorShape: Qt.PointingHandCursor
+                }
+
                 contentItem: Image {
                     anchors.right: parent.right
                     horizontalAlignment: Image.AlignHCenter
@@ -277,14 +322,10 @@ Popup {
                     source: "image://colorimage/:/icons/icons/ui/search.png?" + (parent.hovered ? colors.highlight : colors.buttonText)
                 }
 
-                MouseArea
-                {
-                    id: mouseArea
-                    anchors.fill: parent
-                    onPressed:  mouse.accepted = false
-                    cursorShape: Qt.PointingHandCursor
-                }
             }
+
         }
+
     }
+
 }