diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index e8ebd5fc..0090ea95 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -1,3 +1,4 @@
+import "./voip"
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
@@ -10,6 +11,14 @@ Rectangle {
Layout.preferredHeight: textInput.height
Layout.minimumHeight: 40
+ Component {
+ id: placeCallDialog
+
+ PlaceCall {
+ }
+
+ }
+
RowLayout {
id: inputBar
@@ -17,18 +26,31 @@ Rectangle {
spacing: 16
ImageButton {
- visible: TimelineManager.callsSupported
+ visible: CallManager.callsSupported
+ opacity: CallManager.haveCallInvite ? 0.3 : 1
Layout.alignment: Qt.AlignBottom
hoverEnabled: true
width: 22
height: 22
- image: TimelineManager.isOnCall ? ":/icons/icons/ui/end-call.png" : ":/icons/icons/ui/place-call.png"
+ image: CallManager.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")
+ ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call")
Layout.topMargin: 8
Layout.bottomMargin: 8
Layout.leftMargin: 16
- onClicked: TimelineManager.timeline.input.callButton()
+ onClicked: {
+ if (TimelineManager.timeline) {
+ if (CallManager.haveCallInvite) {
+ return ;
+ } else if (CallManager.isOnCall) {
+ CallManager.hangUp();
+ } else {
+ CallManager.refreshDevices();
+ var dialog = placeCallDialog.createObject(timelineRoot);
+ dialog.open();
+ }
+ }
+ }
}
ImageButton {
@@ -39,7 +61,7 @@ Rectangle {
image: ":/icons/icons/ui/paper-clip-outline.png"
Layout.topMargin: 8
Layout.bottomMargin: 8
- Layout.leftMargin: TimelineManager.callsSupported ? 0 : 16
+ Layout.leftMargin: CallManager.callsSupported ? 0 : 16
onClicked: TimelineManager.timeline.input.openFileSelection()
ToolTip.visible: hovered
ToolTip.text: qsTr("Send a file")
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 679c1f50..aa222ac5 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -140,6 +140,15 @@ ListView {
}
+ Label {
+ color: colors.buttonText
+ text: TimelineManager.userStatus(modelData.userId)
+ textFormat: Text.PlainText
+ elide: Text.ElideRight
+ width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - avatarSize
+ font.italic: true
+ }
+
}
}
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 6e9cd665..e596d8e2 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -1,6 +1,7 @@
import "./delegates"
import "./device-verification"
import "./emoji"
+import "./voip"
import QtGraphicalEffects 1.0
import QtQuick 2.9
import QtQuick.Controls 2.3
@@ -210,7 +211,7 @@ Page {
}
Loader {
- source: TimelineManager.onVideoCall ? "VideoCall.qml" : ""
+ source: CallManager.isOnCall && CallManager.isVideo ? "voip/VideoCall.qml" : ""
onLoaded: TimelineManager.setVideoCallItem()
}
@@ -223,6 +224,13 @@ Page {
}
+ CallInviteBar {
+ id: callInviteBar
+
+ Layout.fillWidth: true
+ z: 3
+ }
+
ActiveCallBar {
Layout.fillWidth: true
z: 3
diff --git a/resources/qml/ui/Ripple.qml b/resources/qml/ui/Ripple.qml
index 9b404a68..93380f77 100644
--- a/resources/qml/ui/Ripple.qml
+++ b/resources/qml/ui/Ripple.qml
@@ -116,10 +116,10 @@ Item {
]
Connections {
- function onPressed(mouse) {
- // Button
- // Default to center
+ // Button
+ // Default to center
+ function onPressed(mouse) {
// MouseArea
if (mouse) {
ripple.centerX = mouse.x;
diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml
index 3059e213..85da4e3c 100644
--- a/resources/qml/ActiveCallBar.qml
+++ b/resources/qml/voip/ActiveCallBar.qml
@@ -1,19 +1,18 @@
+import "../"
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"
+ visible: CallManager.isOnCall
+ color: callInviteBar.color
implicitHeight: visible ? rowLayout.height + 8 : 0
MouseArea {
anchors.fill: parent
onClicked: {
- if (TimelineManager.onVideoCall)
+ if (CallManager.isVideo)
stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
}
@@ -30,63 +29,66 @@ Rectangle {
Avatar {
width: avatarSize
height: avatarSize
- url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
- displayName: TimelineManager.callPartyName
+ url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
+ displayName: CallManager.callParty
}
Label {
+ Layout.leftMargin: 8
font.pointSize: fontMetrics.font.pointSize * 1.1
- text: " " + TimelineManager.callPartyName + " "
+ text: CallManager.callParty
+ color: "#000000"
}
Image {
+ Layout.leftMargin: 4
Layout.preferredWidth: 24
Layout.preferredHeight: 24
- source: TimelineManager.onVideoCall ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
+ source: CallManager.isVideo ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
}
Label {
id: callStateLabel
font.pointSize: fontMetrics.font.pointSize * 1.1
+ color: "#000000"
}
Item {
- state: TimelineManager.callState
states: [
State {
name: "OFFERSENT"
- when: state == WebRTCState.OFFERSENT
+ when: CallManager.callState == WebRTCState.OFFERSENT
PropertyChanges {
target: callStateLabel
- text: "Calling..."
+ text: qsTr("Calling...")
}
},
State {
name: "CONNECTING"
- when: state == WebRTCState.CONNECTING
+ when: CallManager.callState == WebRTCState.CONNECTING
PropertyChanges {
target: callStateLabel
- text: "Connecting..."
+ text: qsTr("Connecting...")
}
},
State {
name: "ANSWERSENT"
- when: state == WebRTCState.ANSWERSENT
+ when: CallManager.callState == WebRTCState.ANSWERSENT
PropertyChanges {
target: callStateLabel
- text: "Connecting..."
+ text: qsTr("Connecting...")
}
},
State {
name: "CONNECTED"
- when: state == WebRTCState.CONNECTED
+ when: CallManager.callState == WebRTCState.CONNECTED
PropertyChanges {
target: callStateLabel
@@ -100,13 +102,13 @@ Rectangle {
PropertyChanges {
target: stackLayout
- currentIndex: TimelineManager.onVideoCall ? 1 : 0
+ currentIndex: CallManager.isVideo ? 1 : 0
}
},
State {
name: "DISCONNECTED"
- when: state == WebRTCState.DISCONNECTED
+ when: CallManager.callState == WebRTCState.DISCONNECTED
PropertyChanges {
target: callStateLabel
@@ -132,7 +134,7 @@ Rectangle {
}
interval: 1000
- running: TimelineManager.callState == WebRTCState.CONNECTED
+ running: CallManager.callState == WebRTCState.CONNECTED
repeat: true
onTriggered: {
var d = new Date();
@@ -149,34 +151,28 @@ Rectangle {
}
ImageButton {
- visible: TimelineManager.onVideoCall
+ visible: CallManager.haveLocalVideo
width: 24
height: 24
buttonTextColor: "#000000"
image: ":/icons/icons/ui/toggle-camera-view.png"
hoverEnabled: true
ToolTip.visible: hovered
- ToolTip.text: "Toggle camera view"
- onClicked: TimelineManager.toggleCameraView()
- }
-
- Item {
- implicitWidth: 8
+ ToolTip.text: qsTr("Toggle camera view")
+ onClicked: CallManager.toggleCameraView()
}
ImageButton {
+ Layout.leftMargin: 8
+ Layout.rightMargin: 16
width: 24
height: 24
buttonTextColor: "#000000"
- image: TimelineManager.isMicMuted ? ":/icons/icons/ui/microphone-unmute.png" : ":/icons/icons/ui/microphone-mute.png"
+ image: CallManager.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
+ ToolTip.text: CallManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic")
+ onClicked: CallManager.toggleMicMute()
}
}
diff --git a/resources/qml/voip/CallDevices.qml b/resources/qml/voip/CallDevices.qml
new file mode 100644
index 00000000..8b30c540
--- /dev/null
+++ b/resources/qml/voip/CallDevices.qml
@@ -0,0 +1,78 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+Popup {
+ modal: true
+ anchors.centerIn: parent
+ palette: colors
+
+ ColumnLayout {
+ spacing: 16
+
+ ColumnLayout {
+ spacing: 8
+ Layout.topMargin: 8
+ Layout.leftMargin: 8
+ Layout.rightMargin: 8
+
+ RowLayout {
+ Image {
+ Layout.preferredWidth: 22
+ Layout.preferredHeight: 22
+ source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + colors.windowText
+ }
+
+ ComboBox {
+ id: micCombo
+
+ Layout.fillWidth: true
+ model: CallManager.mics
+ }
+
+ }
+
+ RowLayout {
+ visible: CallManager.isVideo && CallManager.cameras.length > 0
+
+ Image {
+ Layout.preferredWidth: 22
+ Layout.preferredHeight: 22
+ source: "image://colorimage/:/icons/icons/ui/video-call.png?" + colors.windowText
+ }
+
+ ComboBox {
+ id: cameraCombo
+
+ Layout.fillWidth: true
+ model: CallManager.cameras
+ }
+
+ }
+
+ }
+
+ DialogButtonBox {
+ Layout.leftMargin: 128
+ standardButtons: DialogButtonBox.Ok | DialogButtonBox.Cancel
+ onAccepted: {
+ Settings.microphone = micCombo.currentText;
+ if (cameraCombo.visible)
+ Settings.camera = cameraCombo.currentText;
+
+ close();
+ }
+ onRejected: {
+ close();
+ }
+ }
+
+ }
+
+ background: Rectangle {
+ color: colors.window
+ border.color: colors.windowText
+ }
+
+}
diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml
new file mode 100644
index 00000000..e349332f
--- /dev/null
+++ b/resources/qml/voip/CallInviteBar.qml
@@ -0,0 +1,128 @@
+import "../"
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+Rectangle {
+ visible: CallManager.haveCallInvite
+ color: "#2ECC71"
+ implicitHeight: visible ? rowLayout.height + 8 : 0
+
+ Component {
+ id: devicesDialog
+
+ CallDevices {
+ }
+
+ }
+
+ Component {
+ id: deviceError
+
+ DeviceError {
+ }
+
+ }
+
+ RowLayout {
+ id: rowLayout
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: 8
+
+ Avatar {
+ width: avatarSize
+ height: avatarSize
+ url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
+ displayName: CallManager.callParty
+ }
+
+ Label {
+ Layout.leftMargin: 8
+ font.pointSize: fontMetrics.font.pointSize * 1.1
+ text: CallManager.callParty
+ color: "#000000"
+ }
+
+ Image {
+ Layout.leftMargin: 4
+ Layout.preferredWidth: 24
+ Layout.preferredHeight: 24
+ source: CallManager.isVideo ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
+ }
+
+ Label {
+ font.pointSize: fontMetrics.font.pointSize * 1.1
+ text: CallManager.isVideo ? qsTr("Video Call") : qsTr("Voice Call")
+ color: "#000000"
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ ImageButton {
+ Layout.rightMargin: 16
+ width: 20
+ height: 20
+ buttonTextColor: "#000000"
+ image: ":/icons/icons/ui/settings.png"
+ hoverEnabled: true
+ ToolTip.visible: hovered
+ ToolTip.text: qsTr("Devices")
+ onClicked: {
+ CallManager.refreshDevices();
+ var dialog = devicesDialog.createObject(timelineRoot);
+ dialog.open();
+ }
+ }
+
+ Button {
+ Layout.rightMargin: 4
+ icon.source: CallManager.isVideo ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
+ text: qsTr(" Accept ")
+ palette: colors
+ onClicked: {
+ if (CallManager.mics.length == 0) {
+ var dialog = deviceError.createObject(timelineRoot, {
+ "errorString": qsTr("No microphone found."),
+ "image": ":/icons/icons/ui/place-call.png"
+ });
+ dialog.open();
+ return ;
+ } else if (!CallManager.mics.includes(Settings.microphone)) {
+ var dialog = deviceError.createObject(timelineRoot, {
+ "errorString": qsTr("Unknown microphone: ") + Settings.microphone,
+ "image": ":/icons/icons/ui/place-call.png"
+ });
+ dialog.open();
+ return ;
+ }
+ if (CallManager.isVideo && CallManager.cameras.length > 0 && !CallManager.cameras.includes(Settings.camera)) {
+ var dialog = deviceError.createObject(timelineRoot, {
+ "errorString": qsTr("Unknown camera: ") + Settings.camera,
+ "image": ":/icons/icons/ui/video-call.png"
+ });
+ dialog.open();
+ return ;
+ }
+ CallManager.acceptInvite();
+ }
+ }
+
+ Button {
+ Layout.rightMargin: 16
+ icon.source: "qrc:/icons/icons/ui/end-call.png"
+ text: qsTr(" Decline ")
+ palette: colors
+ onClicked: {
+ CallManager.hangUp();
+ }
+ }
+
+ }
+
+}
diff --git a/resources/qml/voip/DeviceError.qml b/resources/qml/voip/DeviceError.qml
new file mode 100644
index 00000000..81872ef7
--- /dev/null
+++ b/resources/qml/voip/DeviceError.qml
@@ -0,0 +1,32 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+Popup {
+ property string errorString
+ property var image
+
+ modal: true
+ anchors.centerIn: parent
+
+ RowLayout {
+ Image {
+ Layout.preferredWidth: 16
+ Layout.preferredHeight: 16
+ source: "image://colorimage/" + image + "?" + colors.windowText
+ }
+
+ Label {
+ text: errorString
+ color: colors.windowText
+ }
+
+ }
+
+ background: Rectangle {
+ color: colors.window
+ border.color: colors.windowText
+ }
+
+}
diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
new file mode 100644
index 00000000..65f2f350
--- /dev/null
+++ b/resources/qml/voip/PlaceCall.qml
@@ -0,0 +1,154 @@
+import "../"
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.2
+import im.nheko 1.0
+
+Popup {
+ modal: true
+ anchors.centerIn: parent
+ palette: colors
+
+ Component {
+ id: deviceError
+
+ DeviceError {
+ }
+
+ }
+
+ ColumnLayout {
+ id: columnLayout
+
+ spacing: 16
+
+ RowLayout {
+ Layout.topMargin: 8
+ Layout.leftMargin: 8
+
+ Label {
+ text: qsTr("Place a call to ") + TimelineManager.timeline.roomName + "?"
+ color: colors.windowText
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ }
+
+ RowLayout {
+ id: buttonLayout
+
+ function validateMic() {
+ if (CallManager.mics.length == 0) {
+ var dialog = deviceError.createObject(timelineRoot, {
+ "errorString": qsTr("No microphone found."),
+ "image": ":/icons/icons/ui/place-call.png"
+ });
+ dialog.open();
+ return false;
+ }
+ return true;
+ }
+
+ Layout.leftMargin: 8
+ Layout.rightMargin: 8
+
+ Avatar {
+ Layout.rightMargin: cameraCombo.visible ? 16 : 64
+ width: avatarSize
+ height: avatarSize
+ url: TimelineManager.timeline.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
+ displayName: TimelineManager.timeline.roomName
+ }
+
+ Button {
+ text: qsTr(" Voice ")
+ icon.source: "qrc:/icons/icons/ui/place-call.png"
+ onClicked: {
+ if (buttonLayout.validateMic()) {
+ Settings.microphone = micCombo.currentText;
+ CallManager.sendInvite(TimelineManager.timeline.roomId(), false);
+ close();
+ }
+ }
+ }
+
+ Button {
+ visible: CallManager.cameras.length > 0
+ text: qsTr(" Video ")
+ icon.source: "qrc:/icons/icons/ui/video-call.png"
+ onClicked: {
+ if (buttonLayout.validateMic()) {
+ Settings.microphone = micCombo.currentText;
+ Settings.camera = cameraCombo.currentText;
+ CallManager.sendInvite(TimelineManager.timeline.roomId(), true);
+ close();
+ }
+ }
+ }
+
+ Button {
+ text: qsTr("Cancel")
+ onClicked: {
+ close();
+ }
+ }
+
+ }
+
+ ColumnLayout {
+ spacing: 8
+
+ RowLayout {
+ Layout.leftMargin: 8
+ Layout.rightMargin: 8
+ Layout.bottomMargin: cameraCombo.visible ? 0 : 8
+
+ Image {
+ Layout.preferredWidth: 22
+ Layout.preferredHeight: 22
+ source: "image://colorimage/:/icons/icons/ui/microphone-unmute.png?" + colors.windowText
+ }
+
+ ComboBox {
+ id: micCombo
+
+ Layout.fillWidth: true
+ model: CallManager.mics
+ }
+
+ }
+
+ RowLayout {
+ visible: CallManager.cameras.length > 0
+ Layout.leftMargin: 8
+ Layout.rightMargin: 8
+ Layout.bottomMargin: 8
+
+ Image {
+ Layout.preferredWidth: 22
+ Layout.preferredHeight: 22
+ source: "image://colorimage/:/icons/icons/ui/video-call.png?" + colors.windowText
+ }
+
+ ComboBox {
+ id: cameraCombo
+
+ Layout.fillWidth: true
+ model: CallManager.cameras
+ }
+
+ }
+
+ }
+
+ }
+
+ background: Rectangle {
+ color: colors.window
+ border.color: colors.windowText
+ }
+
+}
diff --git a/resources/qml/VideoCall.qml b/resources/qml/voip/VideoCall.qml
index 14408b6e..14408b6e 100644
--- a/resources/qml/VideoCall.qml
+++ b/resources/qml/voip/VideoCall.qml
|