diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml
index f68c531e..9bbac632 100644
--- a/resources/qml/ActiveCallBar.qml
+++ b/resources/qml/ActiveCallBar.qml
@@ -12,8 +12,11 @@ Rectangle {
MouseArea {
anchors.fill: parent
- onClicked: if (TimelineManager.onVideoCall)
- stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
+ onClicked: {
+ if (TimelineManager.onVideoCall)
+ stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
+
+ }
}
RowLayout {
@@ -39,8 +42,7 @@ Rectangle {
Image {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
- source: TimelineManager.onVideoCall ?
- "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
+ source: TimelineManager.onVideoCall ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
}
Label {
diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
new file mode 100644
index 00000000..9703da64
--- /dev/null
+++ b/resources/qml/Completer.qml
@@ -0,0 +1,178 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+Popup {
+ id: popup
+
+ property int currentIndex: -1
+ property string completerName
+ property var completer
+ property bool bottomToTop: true
+
+ signal completionClicked(string completion)
+
+ function up() {
+ if (bottomToTop)
+ down_();
+ else
+ up_();
+ }
+
+ function down() {
+ if (bottomToTop)
+ up_();
+ else
+ down_();
+ }
+
+ function up_() {
+ currentIndex = currentIndex - 1;
+ if (currentIndex == -2)
+ currentIndex = listView.count - 1;
+
+ }
+
+ function down_() {
+ currentIndex = currentIndex + 1;
+ if (currentIndex >= listView.count)
+ currentIndex = -1;
+
+ }
+
+ function currentCompletion() {
+ if (currentIndex > -1 && currentIndex < listView.count)
+ return completer.completionAt(currentIndex);
+ else
+ return null;
+ }
+
+ onCompleterNameChanged: {
+ if (completerName) {
+ completer = TimelineManager.timeline.input.completerFor(completerName);
+ completer.setSearchString("");
+ } else {
+ completer = undefined;
+ }
+ }
+ padding: 0
+ onAboutToShow: currentIndex = -1
+ height: listView.contentHeight
+
+ Connections {
+ onTimelineChanged: completer = null
+ target: TimelineManager
+ }
+
+ ListView {
+ id: listView
+
+ anchors.fill: parent
+ implicitWidth: contentItem.childrenRect.width
+ model: completer
+ verticalLayoutDirection: popup.bottomToTop ? ListView.BottomToTop : ListView.TopToBottom
+
+ delegate: Rectangle {
+ color: model.index == popup.currentIndex ? colors.highlight : colors.base
+ height: chooser.childrenRect.height + 4
+ implicitWidth: chooser.childrenRect.width + 4
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ onEntered: popup.currentIndex = model.index
+ onClicked: popup.completionClicked(completer.completionAt(model.index))
+ }
+
+ DelegateChooser {
+ id: chooser
+
+ roleValue: popup.completerName
+ anchors.centerIn: parent
+
+ DelegateChoice {
+ roleValue: "user"
+
+ RowLayout {
+ id: del
+
+ anchors.centerIn: parent
+
+ Avatar {
+ height: 24
+ width: 24
+ displayName: model.displayName
+ url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+ }
+
+ Label {
+ text: model.displayName
+ color: model.index == popup.currentIndex ? colors.highlightedText : colors.text
+ }
+
+ Label {
+ text: "(" + model.userid + ")"
+ color: model.index == popup.currentIndex ? colors.highlightedText : colors.buttonText
+ }
+
+ }
+
+ }
+
+ DelegateChoice {
+ roleValue: "emoji"
+
+ RowLayout {
+ id: del
+
+ anchors.centerIn: parent
+
+ Label {
+ text: model.unicode
+ color: model.index == popup.currentIndex ? colors.highlightedText : colors.text
+ font: Settings.emojiFont
+ }
+
+ Label {
+ text: model.shortName
+ color: model.index == popup.currentIndex ? colors.highlightedText : colors.text
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ enter: Transition {
+ NumberAnimation {
+ property: "opacity"
+ from: 0
+ to: 1
+ duration: 100
+ }
+
+ }
+
+ exit: Transition {
+ NumberAnimation {
+ property: "opacity"
+ from: 1
+ to: 0
+ duration: 100
+ }
+
+ }
+
+ background: Rectangle {
+ color: colors.base
+ implicitHeight: popup.contentHeight
+ implicitWidth: popup.contentWidth
+ }
+
+}
diff --git a/resources/qml/ImageButton.qml b/resources/qml/ImageButton.qml
index 49ddf671..4ebda680 100644
--- a/resources/qml/ImageButton.qml
+++ b/resources/qml/ImageButton.qml
@@ -8,6 +8,7 @@ AbstractButton {
property color highlightColor: colors.highlight
property color buttonTextColor: colors.buttonText
+ focusPolicy: Qt.NoFocus
width: 16
height: 16
diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml
index a5781c73..bb3b4296 100644
--- a/resources/qml/MatrixText.qml
+++ b/resources/qml/MatrixText.qml
@@ -5,6 +5,7 @@ import im.nheko 1.0
TextEdit {
textFormat: TextEdit.RichText
readOnly: true
+ focus: false
wrapMode: Text.Wrap
selectByMouse: !Settings.mobileMode
color: colors.text
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 71da9cae..ae95cbb6 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -2,6 +2,7 @@ import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
+import im.nheko 1.0
Rectangle {
color: colors.window
@@ -16,14 +17,18 @@ Rectangle {
spacing: 16
ImageButton {
+ visible: TimelineManager.callsSupported
Layout.alignment: Qt.AlignBottom
hoverEnabled: true
width: 22
height: 22
- image: ":/icons/icons/ui/place-call.png"
+ image: TimelineManager.isOnCall ? ":/icons/icons/ui/end-call.png" : ":/icons/icons/ui/place-call.png"
+ ToolTip.visible: hovered
+ ToolTip.text: TimelineManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call")
Layout.topMargin: 8
Layout.bottomMargin: 8
Layout.leftMargin: 16
+ onClicked: TimelineManager.timeline.input.callButton()
}
ImageButton {
@@ -34,6 +39,23 @@ Rectangle {
image: ":/icons/icons/ui/paper-clip-outline.png"
Layout.topMargin: 8
Layout.bottomMargin: 8
+ Layout.leftMargin: TimelineManager.callsSupported ? 0 : 16
+ onClicked: TimelineManager.timeline.input.openFileSelection()
+ ToolTip.visible: hovered
+ ToolTip.text: qsTr("Send a file")
+
+ Rectangle {
+ anchors.fill: parent
+ color: colors.window
+ visible: TimelineManager.timeline.input.uploading
+
+ NhekoBusyIndicator {
+ anchors.fill: parent
+ running: parent.visible
+ }
+
+ }
+
}
ScrollView {
@@ -44,16 +66,145 @@ Rectangle {
Layout.fillWidth: true
TextArea {
+ id: textArea
+
+ property int completerTriggeredAt: -1
+
+ function insertCompletion(completion) {
+ textArea.remove(completerTriggeredAt, cursorPosition);
+ textArea.insert(cursorPosition, completion);
+ }
+
+ function openCompleter(pos, type) {
+ completerTriggeredAt = pos;
+ popup.completerName = type;
+ popup.open();
+ popup.completer.setSearchString(textArea.getText(completerTriggeredAt, cursorPosition));
+ }
+
placeholderText: qsTr("Write a message...")
placeholderTextColor: colors.buttonText
color: colors.text
wrapMode: TextEdit.Wrap
+ focus: true
+ onTextChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
+ onCursorPositionChanged: {
+ TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text);
+ if (cursorPosition <= completerTriggeredAt) {
+ completerTriggeredAt = -1;
+ popup.close();
+ }
+ if (popup.opened)
+ popup.completer.setSearchString(textArea.getText(completerTriggeredAt, cursorPosition));
+
+ }
+ onSelectionStartChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
+ onSelectionEndChanged: TimelineManager.timeline.input.updateState(selectionStart, selectionEnd, cursorPosition, text)
+ // Ensure that we get escape key press events first.
+ Keys.onShortcutOverride: event.accepted = (completerTriggeredAt != -1 && (event.key === Qt.Key_Escape || event.key === Qt.Key_Tab || event.key === Qt.Key_Enter))
+ Keys.onPressed: {
+ if (event.matches(StandardKey.Paste)) {
+ TimelineManager.timeline.input.paste(false);
+ event.accepted = true;
+ } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_U) {
+ textArea.clear();
+ } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_P) {
+ textArea.text = TimelineManager.timeline.input.previousText();
+ } else if (event.modifiers == Qt.ControlModifier && event.key == Qt.Key_N) {
+ textArea.text = TimelineManager.timeline.input.nextText();
+ } else if (event.key == Qt.Key_At) {
+ textArea.openCompleter(cursorPosition, "user");
+ popup.open();
+ } else if (event.key == Qt.Key_Colon) {
+ textArea.openCompleter(cursorPosition, "emoji");
+ popup.open();
+ } else if (event.key == Qt.Key_Escape && popup.opened) {
+ completerTriggeredAt = -1;
+ popup.completerName = "";
+ event.accepted = true;
+ popup.close();
+ } else if (event.matches(StandardKey.InsertParagraphSeparator)) {
+ if (popup.opened) {
+ var currentCompletion = popup.currentCompletion();
+ popup.completerName = "";
+ popup.close();
+ if (currentCompletion) {
+ textArea.insertCompletion(currentCompletion);
+ event.accepted = true;
+ return ;
+ }
+ }
+ TimelineManager.timeline.input.send();
+ textArea.clear();
+ event.accepted = true;
+ } else if (event.key == Qt.Key_Tab) {
+ event.accepted = true;
+ if (popup.opened) {
+ popup.up();
+ } else {
+ var pos = cursorPosition - 1;
+ while (pos > -1) {
+ var t = textArea.getText(pos, pos + 1);
+ console.log('"' + t + '"');
+ if (t == '@' || t == ' ' || t == '\t') {
+ textArea.openCompleter(pos, "user");
+ return ;
+ } else if (t == ':') {
+ textArea.openCompleter(pos, "emoji");
+ return ;
+ }
+ pos = pos - 1;
+ }
+ // At start of input
+ textArea.openCompleter(0, "user");
+ }
+ } else if (event.key == Qt.Key_Up && popup.opened) {
+ event.accepted = true;
+ popup.up();
+ } else if (event.key == Qt.Key_Down && popup.opened) {
+ event.accepted = true;
+ popup.down();
+ }
+ }
+
+ Connections {
+ onTimelineChanged: {
+ textArea.clear();
+ textArea.append(TimelineManager.timeline.input.text());
+ textArea.completerTriggeredAt = -1;
+ popup.completerName = "";
+ }
+ target: TimelineManager
+ }
+
+ Connections {
+ onCompletionClicked: textArea.insertCompletion(completion)
+ target: popup
+ }
+
+ Completer {
+ id: popup
+
+ x: textArea.positionToRectangle(textArea.completerTriggeredAt).x
+ y: textArea.positionToRectangle(textArea.completerTriggeredAt).y - height
+ }
+
+ Connections {
+ onInsertText: textArea.insert(textArea.cursorPosition, text)
+ target: TimelineManager.timeline.input
+ }
MouseArea {
// workaround for wrong cursor shape on some platforms
anchors.fill: parent
- acceptedButtons: Qt.NoButton
+ acceptedButtons: Qt.MiddleButton
cursorShape: Qt.IBeamCursor
+ onClicked: TimelineManager.timeline.input.paste(true)
+ }
+
+ NhekoDropArea {
+ anchors.fill: parent
+ roomid: TimelineManager.timeline.roomId()
}
background: Rectangle {
@@ -65,6 +216,8 @@ Rectangle {
}
ImageButton {
+ id: emojiButton
+
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
hoverEnabled: true
width: 22
@@ -72,6 +225,11 @@ Rectangle {
image: ":/icons/icons/ui/smile.png"
Layout.topMargin: 8
Layout.bottomMargin: 8
+ ToolTip.visible: hovered
+ ToolTip.text: qsTr("Emoji")
+ onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(emojiButton, function(emoji) {
+ textArea.insert(textArea.cursorPosition, emoji);
+ })
}
ImageButton {
@@ -83,6 +241,12 @@ Rectangle {
Layout.topMargin: 8
Layout.bottomMargin: 8
Layout.rightMargin: 16
+ ToolTip.visible: hovered
+ ToolTip.text: qsTr("Send")
+ onClicked: {
+ TimelineManager.timeline.input.send();
+ textArea.clear();
+ }
}
}
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index bc0acfa4..679c1f50 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -28,6 +28,7 @@ ListView {
ScrollHelper {
flickable: parent
anchors.fill: parent
+ enabled: !Settings.mobileMode
}
Shortcut {
@@ -181,7 +182,6 @@ ListView {
Connections {
target: chat
-
onMovementEnded: {
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
chat.model.currentIndex = index;
diff --git a/resources/qml/NhekoBusyIndicator.qml b/resources/qml/NhekoBusyIndicator.qml
new file mode 100644
index 00000000..89a40dd5
--- /dev/null
+++ b/resources/qml/NhekoBusyIndicator.qml
@@ -0,0 +1,64 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+
+BusyIndicator {
+ id: control
+
+ contentItem: Item {
+ implicitWidth: 64
+ implicitHeight: 64
+
+ Item {
+ id: item
+
+ height: Math.min(parent.height, parent.width)
+ width: height
+ opacity: control.running ? 1 : 0
+
+ RotationAnimator {
+ target: item
+ running: control.visible && control.running
+ from: 0
+ to: 360
+ loops: Animation.Infinite
+ duration: 2000
+ }
+
+ Repeater {
+ id: repeater
+
+ model: 6
+
+ Rectangle {
+ implicitWidth: radius * 2
+ implicitHeight: radius * 2
+ radius: item.height / 6
+ color: colors.text
+ opacity: (index + 2) / (repeater.count + 2)
+ transform: [
+ Translate {
+ y: -Math.min(item.width, item.height) * 0.5 + item.height / 6
+ },
+ Rotation {
+ angle: index / repeater.count * 360
+ origin.x: item.height / 2
+ origin.y: item.height / 2
+ }
+ ]
+ }
+
+ }
+
+ Behavior on opacity {
+ OpacityAnimator {
+ duration: 250
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 95143b18..4234bb6d 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -72,7 +72,9 @@ Page {
MenuItem {
text: qsTr("React")
- onClicked: emojiPopup.show(messageContextMenu.parent, messageContextMenu.eventId)
+ onClicked: emojiPopup.show(messageContextMenu.parent, function(emoji) {
+ TimelineManager.queueReactionMessage(messageContextMenu.eventId, emoji);
+ })
}
MenuItem {
@@ -95,6 +97,7 @@ Page {
}
MenuItem {
+ // TODO(Nico): Fix this still being iterated over, when using keyboard to select options
visible: messageContextMenu.isEncrypted
height: visible ? implicitHeight : 0
text: qsTr("View decrypted raw message")
@@ -129,7 +132,6 @@ Page {
Connections {
target: TimelineManager
-
onNewDeviceVerificationRequest: {
var dialog = deviceVerificationDialog.createObject(timelineRoot, {
"flow": flow
@@ -140,7 +142,6 @@ Page {
Connections {
target: TimelineManager.timeline
-
onOpenProfile: {
var userProfile = userProfileComponent.createObject(timelineRoot, {
"profile": profile
@@ -192,13 +193,15 @@ Page {
StackLayout {
id: stackLayout
+
currentIndex: 0
Connections {
- target: TimelineManager
function onActiveTimelineChanged() {
stackLayout.currentIndex = 0;
}
+
+ target: TimelineManager
}
MessageView {
@@ -210,6 +213,7 @@ Page {
source: TimelineManager.onVideoCall ? "VideoCall.qml" : ""
onLoaded: TimelineManager.setVideoCallItem()
}
+
}
TypingIndicator {
@@ -234,8 +238,8 @@ Page {
ReplyPopup {
}
- //MessageInput {
- //}
+ MessageInput {
+ }
}
diff --git a/resources/qml/VideoCall.qml b/resources/qml/VideoCall.qml
index 69fc1a2b..14408b6e 100644
--- a/resources/qml/VideoCall.qml
+++ b/resources/qml/VideoCall.qml
@@ -1,7 +1,6 @@
import QtQuick 2.9
-
import org.freedesktop.gstreamer.GLVideoItem 1.0
GstGLVideoItem {
- objectName: "videoCallItem"
+ objectName: "videoCallItem"
}
diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml
index 1fcfc0c5..928d6226 100644
--- a/resources/qml/emoji/EmojiButton.qml
+++ b/resources/qml/emoji/EmojiButton.qml
@@ -12,5 +12,7 @@ ImageButton {
property string event_id
image: ":/icons/icons/ui/smile.png"
- onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, event_id)
+ onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
+ TimelineManager.queueReactionMessage(event_id, emoji);
+ })
}
diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml
index afe16350..e0e1ceaf 100644
--- a/resources/qml/emoji/EmojiPicker.qml
+++ b/resources/qml/emoji/EmojiPicker.qml
@@ -9,7 +9,7 @@ import im.nheko.EmojiModel 1.0
Popup {
id: emojiPopup
- property string event_id
+ property var callback
property var colors
property alias model: gridView.model
property var textArea
@@ -18,14 +18,14 @@ Popup {
property real highlightSat: colors.highlight.hslSaturation
property real highlightLight: colors.highlight.hslLightness
- function show(showAt, event_id) {
- console.debug("Showing emojiPicker for " + event_id);
+ function show(showAt, callback) {
+ console.debug("Showing emojiPicker");
if (showAt) {
parent = showAt;
x = Math.round((showAt.width - width) / 2);
y = showAt.height;
}
- emojiPopup.event_id = event_id;
+ emojiPopup.callback = callback;
open();
}
@@ -70,9 +70,9 @@ Popup {
ToolTip.visible: hovered
// TODO: maybe add favorites at some point?
onClicked: {
- console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id);
+ console.debug("Picked " + model.unicode);
emojiPopup.close();
- TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode);
+ callback(model.unicode);
}
// give the emoji a little oomf
|