diff --git a/.ci/macos/Brewfile b/.ci/macos/Brewfile
index 7e9687c7..e7a62374 100644
--- a/.ci/macos/Brewfile
+++ b/.ci/macos/Brewfile
@@ -1,12 +1,12 @@
tap "nlohmann/json"
+brew "python3"
brew "pkg-config"
brew "clang-format"
brew "cmake"
brew "ninja"
brew "openssl"
brew "qt5"
-brew "python3"
brew "nlohmann_json"
brew "gstreamer"
brew "gst-plugins-base"
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8a6775db..f03dcbbe 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -52,7 +52,8 @@ build-macos:
tags: [macos]
before_script:
- brew update
- - brew bundle --file=./.ci/macos/Brewfile
+ - brew reinstall --force python3
+ - brew bundle --file=./.ci/macos/Brewfile --force --cleanup
- pip3 install dmgbuild
script:
- export PATH=/usr/local/opt/qt/bin/:${PATH}
@@ -95,8 +96,8 @@ build-flatpak-amd64:
- export VERSION=$(git describe)
- mkdir -p build-flatpak
- cd build-flatpak
- - flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME} --subject="Build of Nheko ${VERSION} `date`" app ../io.github.NhekoReborn.Nheko.json
- - flatpak build-bundle repo nheko-amd64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME}
+ - flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date`" app ../io.github.NhekoReborn.Nheko.json
+ - flatpak build-bundle repo nheko-amd64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME//\//_}
after_script:
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-amd64.flatpak
cache:
@@ -122,8 +123,8 @@ build-flatpak-arm64:
- export VERSION=$(git describe)
- mkdir -p build-flatpak
- cd build-flatpak
- - flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME} --subject="Build of Nheko ${VERSION} `date` for arm64" app ../io.github.NhekoReborn.Nheko.json
- - flatpak build-bundle repo nheko-arm64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME}
+ - flatpak-builder --user --disable-rofiles-fuse --ccache --repo=repo --default-branch=${CI_COMMIT_REF_NAME//\//_} --subject="Build of Nheko ${VERSION} `date` for arm64" app ../io.github.NhekoReborn.Nheko.json
+ - flatpak build-bundle repo nheko-arm64.flatpak io.github.NhekoReborn.Nheko ${CI_COMMIT_REF_NAME//\//_}
after_script:
- bash ./.ci/upload-nightly-gitlab.sh build-flatpak/nheko-arm64.flatpak
cache:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ead1c32b..ec9c8c1f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -245,7 +245,6 @@ configure_file(cmake/nheko.h config/nheko.h)
#
set(SRC_FILES
# Dialogs
- src/dialogs/AcceptCall.cpp
src/dialogs/CreateRoom.cpp
src/dialogs/FallbackAuth.cpp
src/dialogs/ImageOverlay.cpp
@@ -254,7 +253,6 @@ set(SRC_FILES
src/dialogs/LeaveRoom.cpp
src/dialogs/Logout.cpp
src/dialogs/MemberList.cpp
- src/dialogs/PlaceCall.cpp
src/dialogs/PreviewUploadOverlay.cpp
src/dialogs/ReCaptcha.cpp
src/dialogs/ReadReceipts.cpp
@@ -356,7 +354,7 @@ if(USE_BUNDLED_MTXCLIENT)
FetchContent_Declare(
MatrixClient
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
- GIT_TAG ce8bc9c3dd6bba432e716f55136133111b0186e7
+ GIT_TAG cad81d1677a4845366b93112f8f2e267ee8c9ae0
)
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@@ -445,7 +443,12 @@ else()
endif()
include(FindPkgConfig)
-pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.14 gstreamer-webrtc-1.0>=1.14)
+pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.16 gstreamer-webrtc-1.0>=1.16)
+if (TARGET PkgConfig::GSTREAMER)
+ add_feature_info(voip ON "GStreamer found. Call support is enabled automatically.")
+else()
+ add_feature_info(voip OFF "GStreamer could not be found on your system. As a consequence call support has been disabled. If you don't want that, make sure gstreamer-sdp-1.0>=1.16 gstreamer-webrtc-1.0>=1.16 can be found via pkgconfig.")
+endif()
# single instance functionality
set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication")
@@ -455,7 +458,6 @@ feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAG
qt5_wrap_cpp(MOC_HEADERS
# Dialogs
- src/dialogs/AcceptCall.h
src/dialogs/CreateRoom.h
src/dialogs/FallbackAuth.h
src/dialogs/ImageOverlay.h
@@ -464,7 +466,6 @@ qt5_wrap_cpp(MOC_HEADERS
src/dialogs/LeaveRoom.h
src/dialogs/Logout.h
src/dialogs/MemberList.h
- src/dialogs/PlaceCall.h
src/dialogs/PreviewUploadOverlay.h
src/dialogs/RawMessage.h
src/dialogs/ReCaptcha.h
diff --git a/README.md b/README.md
index f24b8d13..a4b1354d 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ Most of the features you would expect from a chat application are missing right
but we are getting close to a more feature complete client.
Specifically there is support for:
- E2E encryption.
-- VoIP calls (voice & video)
+- VoIP calls (voice & video).
- User registration.
- Creating, joining & leaving rooms.
- Sending & receiving invites.
@@ -210,6 +210,14 @@ sudo apt install cmake gcc make automake liblmdb-dev \
qt5keychain-dev
```
+##### Fedora
+
+```bash
+sudo dnf install qt5-qtbase-devel qt5-linguist qt5-qtsvg-devel qt5-qtmultimedia-devel \
+ qt5-qtquickcontrols2-devel qtkeychain-qt5-devel spdlog-devel openssl-devel \
+ libolm-devel cmark-devel lmdb-devel lmdbxx-devel tweeny-devel
+```
+
##### Guix
```bash
diff --git a/io.github.NhekoReborn.Nheko.json b/io.github.NhekoReborn.Nheko.json
index 34b0d7e7..7aba130b 100644
--- a/io.github.NhekoReborn.Nheko.json
+++ b/io.github.NhekoReborn.Nheko.json
@@ -97,7 +97,8 @@
{
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
- "-DBUILD_TEST_APPLICATION=OFF"
+ "-DBUILD_TEST_APPLICATION=OFF",
+ "-DQTKEYCHAIN_STATIC=ON"
],
"buildsystem": "cmake-ninja",
"name": "QtKeychain",
@@ -124,16 +125,6 @@
}
]
},
- {
- "name": "sodium",
- "sources": [
- {
- "sha256": "6f504490b342a4f8a4c4a02fc9b866cbef8622d5df4e5452b46be121e46636c1",
- "type": "archive",
- "url": "https://github.com/jedisct1/libsodium/releases/download/1.0.18-RELEASE/libsodium-1.0.18.tar.gz"
- }
- ]
- },
{
"build-commands": [
"./bootstrap.sh --with-libraries=thread,system,iostreams --prefix=/app",
@@ -161,7 +152,7 @@
"name": "mtxclient",
"sources": [
{
- "commit": "ce8bc9c3dd6bba432e716f55136133111b0186e7",
+ "commit": "cad81d1677a4845366b93112f8f2e267ee8c9ae0",
"type": "git",
"url": "https://github.com/Nheko-Reborn/mtxclient.git"
}
diff --git a/resources/nheko.desktop b/resources/nheko.desktop
index 16e04926..4404e460 100644
--- a/resources/nheko.desktop
+++ b/resources/nheko.desktop
@@ -8,3 +8,4 @@ Type=Application
Categories=Network;InstantMessaging;Qt;
StartupWMClass=nheko
Terminal=false
+MimeType=x-scheme-handler/matrix;
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
similarity index 68%
rename from resources/qml/ActiveCallBar.qml
rename to 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
similarity index 100%
rename from resources/qml/VideoCall.qml
rename to resources/qml/voip/VideoCall.qml
diff --git a/resources/res.qrc b/resources/res.qrc
index 9b05575e..0603d19c 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -123,7 +123,6 @@
qtquickcontrols2.conf
qml/TimelineView.qml
- qml/ActiveCallBar.qml
qml/Avatar.qml
qml/Completer.qml
qml/EncryptionIndicator.qml
@@ -139,7 +138,6 @@
qml/TimelineRow.qml
qml/TopBar.qml
qml/TypingIndicator.qml
- qml/VideoCall.qml
qml/emoji/EmojiButton.qml
qml/emoji/EmojiPicker.qml
qml/UserProfile.qml
@@ -159,7 +157,16 @@
qml/device-verification/NewVerificationRequest.qml
qml/device-verification/Failed.qml
qml/device-verification/Success.qml
+<<<<<<< HEAD
qml/ui/Ripple.qml
+=======
+ qml/voip/ActiveCallBar.qml
+ qml/voip/CallDevices.qml
+ qml/voip/CallInviteBar.qml
+ qml/voip/DeviceError.qml
+ qml/voip/PlaceCall.qml
+ qml/voip/VideoCall.qml
+>>>>>>> b8b642219db37b1bf46ae47c01a446a7a4e24317
media/ring.ogg
diff --git a/src/Cache.cpp b/src/Cache.cpp
index dac0b23a..17b55144 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -124,17 +124,15 @@ Cache::isHiddenEvent(lmdb::txn &txn,
EventType::Reaction, EventType::CallCandidates, EventType::Unsupported};
if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, ""))
- hiddenEvents = std::move(
- std::get<
- mtx::events::Event>(
- *temp)
- .content);
+ hiddenEvents =
+ std::move(std::get>(*temp)
+ .content);
if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, room_id))
- hiddenEvents = std::move(
- std::get<
- mtx::events::Event>(
- *temp)
- .content);
+ hiddenEvents =
+ std::move(std::get>(*temp)
+ .content);
return std::visit(
[hiddenEvents](const auto &ev) {
@@ -1197,7 +1195,7 @@ void
Cache::saveState(const mtx::responses::Sync &res)
{
using namespace mtx::events;
- auto user_id = this->localUserId_.toStdString();
+ auto local_user_id = this->localUserId_.toStdString();
auto currentBatchToken = nextBatchToken();
@@ -1252,13 +1250,19 @@ Cache::saveState(const mtx::responses::Sync &res)
evt);
// for tag events
- if (std::holds_alternative>(evt)) {
- auto tags_evt = std::get>(evt);
- has_new_tags = true;
+ if (std::holds_alternative>(
+ evt)) {
+ auto tags_evt =
+ std::get>(evt);
+ has_new_tags = true;
for (const auto &tag : tags_evt.content.tags) {
updatedInfo.tags.push_back(tag.first);
}
}
+ if (auto fr = std::get_if>(&evt)) {
+ nhlog::db()->debug("Fully read: {}", fr->content.event_id);
+ }
}
if (!has_new_tags) {
// retrieve the old tags, they haven't changed
@@ -1282,7 +1286,20 @@ Cache::saveState(const mtx::responses::Sync &res)
lmdb::dbi_put(
txn, roomsDb_, lmdb::val(room.first), lmdb::val(json(updatedInfo).dump()));
- updateReadReceipt(txn, room.first, room.second.ephemeral.receipts);
+ for (const auto &e : room.second.ephemeral.events) {
+ if (auto receiptsEv = std::get_if<
+ mtx::events::EphemeralEvent>(&e)) {
+ Receipts receipts;
+
+ for (const auto &[event_id, userReceipts] :
+ receiptsEv->content.receipts) {
+ for (const auto &[user_id, receipt] : userReceipts.users) {
+ receipts[event_id][user_id] = receipt.ts;
+ }
+ }
+ updateReadReceipt(txn, room.first, receipts);
+ }
+ }
// Clean up non-valid invites.
removeInvite(txn, room.first);
@@ -1302,19 +1319,27 @@ Cache::saveState(const mtx::responses::Sync &res)
std::map readStatus;
for (const auto &room : res.rooms.join) {
- if (!room.second.ephemeral.receipts.empty()) {
- std::vector receipts;
- for (const auto &receipt : room.second.ephemeral.receipts) {
- for (const auto &receiptUsersTs : receipt.second) {
- if (receiptUsersTs.first != user_id) {
- receipts.push_back(
- QString::fromStdString(receipt.first));
- break;
+ for (const auto &e : room.second.ephemeral.events) {
+ if (auto receiptsEv = std::get_if<
+ mtx::events::EphemeralEvent>(&e)) {
+ std::vector receipts;
+
+ for (const auto &[event_id, userReceipts] :
+ receiptsEv->content.receipts) {
+ for (const auto &[user_id, receipt] : userReceipts.users) {
+ (void)receipt;
+
+ if (user_id != local_user_id) {
+ receipts.push_back(
+ QString::fromStdString(event_id));
+ break;
+ }
}
}
+ if (!receipts.empty())
+ emit newReadReceipts(QString::fromStdString(room.first),
+ receipts);
}
- if (!receipts.empty())
- emit newReadReceipts(QString::fromStdString(room.first), receipts);
}
readStatus.emplace(QString::fromStdString(room.first),
calculateRoomReadStatus(room.first));
@@ -1440,7 +1465,7 @@ Cache::roomsWithTagUpdates(const mtx::responses::Sync &res)
for (const auto &room : res.rooms.join) {
bool hasUpdates = false;
for (const auto &evt : room.second.account_data.events) {
- if (std::holds_alternative>(evt)) {
+ if (std::holds_alternative>(evt)) {
hasUpdates = true;
}
}
@@ -2196,6 +2221,34 @@ Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb)
return QString("1");
}
+std::optional
+Cache::getRoomAliases(const std::string &roomid)
+{
+ using namespace mtx::events;
+ using namespace mtx::events::state;
+
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+ auto statesdb = getStatesDb(txn, roomid);
+
+ lmdb::val event;
+ bool res = lmdb::dbi_get(
+ txn, statesdb, lmdb::val(to_string(mtx::events::EventType::RoomCanonicalAlias)), event);
+
+ if (res) {
+ try {
+ StateEvent msg =
+ json::parse(std::string_view(event.data(), event.size()));
+
+ return msg.content;
+ } catch (const json::exception &e) {
+ nhlog::db()->warn("failed to parse m.room.canonical_alias event: {}",
+ e.what());
+ }
+ }
+
+ return std::nullopt;
+}
+
QString
Cache::getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
{
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 059c1461..e2ce1668 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -81,6 +81,7 @@ public:
std::vector joinedRooms();
QMap roomInfo(bool withInvites = true);
+ std::optional getRoomAliases(const std::string &roomid);
std::map invites();
//! Calculate & return the name of the room.
diff --git a/src/CallManager.cpp b/src/CallManager.cpp
index 89cfeaf9..f725d49f 100644
--- a/src/CallManager.cpp
+++ b/src/CallManager.cpp
@@ -10,11 +10,9 @@
#include "CallManager.h"
#include "ChatPage.h"
#include "Logging.h"
-#include "MainWindow.h"
#include "MatrixClient.h"
+#include "UserSettingsPage.h"
#include "Utils.h"
-#include "WebRTCSession.h"
-#include "dialogs/AcceptCall.h"
#include "mtx/responses/turn_server.hpp"
@@ -112,6 +110,23 @@ CallManager::CallManager(QObject *parent)
default:
break;
}
+ emit newCallState();
+ });
+
+ connect(&session_, &WebRTCSession::devicesChanged, this, [this]() {
+ if (ChatPage::instance()->userSettings()->microphone().isEmpty()) {
+ auto mics = session_.getDeviceNames(false, std::string());
+ if (!mics.empty())
+ ChatPage::instance()->userSettings()->setMicrophone(
+ QString::fromStdString(mics.front()));
+ }
+ if (ChatPage::instance()->userSettings()->camera().isEmpty()) {
+ auto cameras = session_.getDeviceNames(true, std::string());
+ if (!cameras.empty())
+ ChatPage::instance()->userSettings()->setCamera(
+ QString::fromStdString(cameras.front()));
+ }
+ emit devicesChanged();
});
connect(&player_,
@@ -144,7 +159,7 @@ CallManager::CallManager(QObject *parent)
void
CallManager::sendInvite(const QString &roomid, bool isVideo)
{
- if (onActiveCall())
+ if (isOnCall())
return;
auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
@@ -160,7 +175,8 @@ CallManager::sendInvite(const QString &roomid, bool isVideo)
return;
}
- roomid_ = roomid;
+ isVideo_ = isVideo;
+ roomid_ = roomid;
session_.setTurnServers(turnURIs_);
generateCallID();
nhlog::ui()->debug(
@@ -168,16 +184,14 @@ CallManager::sendInvite(const QString &roomid, bool isVideo)
std::vector members(cache::getMembers(roomid.toStdString()));
const RoomMember &callee =
members.front().user_id == utils::localUser() ? members.back() : members.front();
- callPartyName_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name;
+ callParty_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
- emit newCallParty();
+ emit newInviteState();
playRingtone(QUrl("qrc:/media/media/ringback.ogg"), true);
if (!session_.createOffer(isVideo)) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
endCall();
}
- if (isVideo)
- emit newVideoCallState();
}
namespace {
@@ -206,12 +220,6 @@ CallManager::hangUp(CallHangUp::Reason reason)
}
}
-bool
-CallManager::onActiveCall() const
-{
- return session_.state() != webrtc::State::DISCONNECTED;
-}
-
void
CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
{
@@ -257,7 +265,7 @@ CallManager::handleEvent(const RoomEvent &callInviteEvent)
return;
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
- if (onActiveCall() || roomInfo.member_count != 2) {
+ if (isOnCall() || roomInfo.member_count != 2) {
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
CallHangUp{callInviteEvent.content.call_id,
0,
@@ -277,48 +285,41 @@ CallManager::handleEvent(const RoomEvent &callInviteEvent)
std::vector members(cache::getMembers(callInviteEvent.room_id));
const RoomMember &caller =
members.front().user_id == utils::localUser() ? members.back() : members.front();
- callPartyName_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
+ callParty_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
- emit newCallParty();
- auto dialog = new dialogs::AcceptCall(caller.user_id,
- caller.display_name,
- QString::fromStdString(roomInfo.name),
- QString::fromStdString(roomInfo.avatar_url),
- isVideo,
- MainWindow::instance());
- connect(dialog, &dialogs::AcceptCall::accept, this, [this, callInviteEvent, isVideo]() {
- MainWindow::instance()->hideOverlay();
- answerInvite(callInviteEvent.content, isVideo);
- });
- connect(dialog, &dialogs::AcceptCall::reject, this, [this]() {
- MainWindow::instance()->hideOverlay();
- hangUp();
- });
- MainWindow::instance()->showSolidOverlayModal(dialog);
+
+ haveCallInvite_ = true;
+ isVideo_ = isVideo;
+ inviteSDP_ = callInviteEvent.content.sdp;
+ session_.refreshDevices();
+ emit newInviteState();
}
void
-CallManager::answerInvite(const CallInvite &invite, bool isVideo)
+CallManager::acceptInvite()
{
+ if (!haveCallInvite_)
+ return;
+
stopRingtone();
std::string errorMessage;
if (!session_.havePlugins(false, &errorMessage) ||
- (isVideo && !session_.havePlugins(true, &errorMessage))) {
+ (isVideo_ && !session_.havePlugins(true, &errorMessage))) {
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
hangUp();
return;
}
session_.setTurnServers(turnURIs_);
- if (!session_.acceptOffer(invite.sdp)) {
+ if (!session_.acceptOffer(inviteSDP_)) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
hangUp();
return;
}
session_.acceptICECandidates(remoteICECandidates_);
remoteICECandidates_.clear();
- if (isVideo)
- emit newVideoCallState();
+ haveCallInvite_ = false;
+ emit newInviteState();
}
void
@@ -332,7 +333,7 @@ CallManager::handleEvent(const RoomEvent &callCandidatesEvent)
callCandidatesEvent.sender);
if (callid_ == callCandidatesEvent.content.call_id) {
- if (onActiveCall())
+ if (isOnCall())
session_.acceptICECandidates(callCandidatesEvent.content.candidates);
else {
// CallInvite has been received and we're awaiting localUser to accept or
@@ -350,15 +351,19 @@ CallManager::handleEvent(const RoomEvent &callAnswerEvent)
callAnswerEvent.content.call_id,
callAnswerEvent.sender);
- if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() &&
+ if (callAnswerEvent.sender == utils::localUser().toStdString() &&
callid_ == callAnswerEvent.content.call_id) {
- emit ChatPage::instance()->showNotification("Call answered on another device.");
- stopRingtone();
- MainWindow::instance()->hideOverlay();
+ if (!isOnCall()) {
+ emit ChatPage::instance()->showNotification(
+ "Call answered on another device.");
+ stopRingtone();
+ haveCallInvite_ = false;
+ emit newInviteState();
+ }
return;
}
- if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) {
+ if (isOnCall() && callid_ == callAnswerEvent.content.call_id) {
stopRingtone();
if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
emit ChatPage::instance()->showNotification("Problem setting up call.");
@@ -375,10 +380,42 @@ CallManager::handleEvent(const RoomEvent &callHangUpEvent)
callHangUpReasonString(callHangUpEvent.content.reason),
callHangUpEvent.sender);
- if (callid_ == callHangUpEvent.content.call_id) {
- MainWindow::instance()->hideOverlay();
+ if (callid_ == callHangUpEvent.content.call_id)
endCall();
- }
+}
+
+void
+CallManager::toggleMicMute()
+{
+ session_.toggleMicMute();
+ emit micMuteChanged();
+}
+
+bool
+CallManager::callsSupported() const
+{
+#ifdef GSTREAMER_AVAILABLE
+ return true;
+#else
+ return false;
+#endif
+}
+
+QStringList
+CallManager::devices(bool isVideo) const
+{
+ QStringList ret;
+ const QString &defaultDevice = isVideo ? ChatPage::instance()->userSettings()->camera()
+ : ChatPage::instance()->userSettings()->microphone();
+ std::vector devices =
+ session_.getDeviceNames(isVideo, defaultDevice.toStdString());
+ ret.reserve(devices.size());
+ std::transform(devices.cbegin(),
+ devices.cend(),
+ std::back_inserter(ret),
+ [](const auto &d) { return QString::fromStdString(d); });
+
+ return ret;
}
void
@@ -393,9 +430,13 @@ void
CallManager::clear()
{
roomid_.clear();
- callPartyName_.clear();
+ callParty_.clear();
callPartyAvatarUrl_.clear();
callid_.clear();
+ isVideo_ = false;
+ haveCallInvite_ = false;
+ emit newInviteState();
+ inviteSDP_.clear();
remoteICECandidates_.clear();
}
@@ -403,11 +444,8 @@ void
CallManager::endCall()
{
stopRingtone();
- clear();
- bool isVideo = session_.isVideo();
session_.end();
- if (isVideo)
- emit newVideoCallState();
+ clear();
}
void
diff --git a/src/CallManager.h b/src/CallManager.h
index 8004e838..7d388efd 100644
--- a/src/CallManager.h
+++ b/src/CallManager.h
@@ -8,6 +8,7 @@
#include
#include
+#include "WebRTCSession.h"
#include "mtx/events/collections.hpp"
#include "mtx/events/voip.hpp"
@@ -15,34 +16,59 @@ namespace mtx::responses {
struct TurnServer;
}
+class QStringList;
class QUrl;
-class WebRTCSession;
class CallManager : public QObject
{
Q_OBJECT
+ Q_PROPERTY(bool haveCallInvite READ haveCallInvite NOTIFY newInviteState)
+ Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState)
+ Q_PROPERTY(bool isVideo READ isVideo NOTIFY newInviteState)
+ Q_PROPERTY(bool haveLocalVideo READ haveLocalVideo NOTIFY newCallState)
+ Q_PROPERTY(webrtc::State callState READ callState NOTIFY newCallState)
+ Q_PROPERTY(QString callParty READ callParty NOTIFY newInviteState)
+ Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY newInviteState)
+ Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged)
+ Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT)
+ Q_PROPERTY(QStringList mics READ mics NOTIFY devicesChanged)
+ Q_PROPERTY(QStringList cameras READ cameras NOTIFY devicesChanged)
public:
CallManager(QObject *);
- void sendInvite(const QString &roomid, bool isVideo);
- void hangUp(
- mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
- bool onActiveCall() const;
- QString callPartyName() const { return callPartyName_; }
+ bool haveCallInvite() const { return haveCallInvite_; }
+ bool isOnCall() const { return session_.state() != webrtc::State::DISCONNECTED; }
+ bool isVideo() const { return isVideo_; }
+ bool haveLocalVideo() const { return session_.haveLocalVideo(); }
+ webrtc::State callState() const { return session_.state(); }
+ QString callParty() const { return callParty_; }
QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; }
+ bool isMicMuted() const { return session_.isMicMuted(); }
+ bool callsSupported() const;
+ QStringList mics() const { return devices(false); }
+ QStringList cameras() const { return devices(true); }
void refreshTurnServer();
public slots:
+ void sendInvite(const QString &roomid, bool isVideo);
void syncEvent(const mtx::events::collections::TimelineEvents &event);
+ void refreshDevices() { session_.refreshDevices(); }
+ void toggleMicMute();
+ void toggleCameraView() { session_.toggleCameraView(); }
+ void acceptInvite();
+ void hangUp(
+ mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
signals:
void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
- void newCallParty();
- void newVideoCallState();
+ void newInviteState();
+ void newCallState();
+ void micMuteChanged();
+ void devicesChanged();
void turnServerRetrieved(const mtx::responses::TurnServer &);
private slots:
@@ -51,10 +77,13 @@ private slots:
private:
WebRTCSession &session_;
QString roomid_;
- QString callPartyName_;
+ QString callParty_;
QString callPartyAvatarUrl_;
std::string callid_;
const uint32_t timeoutms_ = 120000;
+ bool isVideo_ = false;
+ bool haveCallInvite_ = false;
+ std::string inviteSDP_;
std::vector remoteICECandidates_;
std::vector turnURIs_;
QTimer turnServerTimer_;
@@ -68,6 +97,7 @@ private:
void handleEvent(const mtx::events::RoomEvent &);
void answerInvite(const mtx::events::msg::CallInvite &, bool isVideo);
void generateCallID();
+ QStringList devices(bool isVideo) const;
void clear();
void endCall();
void playRingtone(const QUrl &ringtone, bool repeat);
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 37248022..33c993ae 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -47,7 +47,6 @@
#include "notifications/Manager.h"
-#include "dialogs/PlaceCall.h"
#include "dialogs/ReadReceipts.h"
#include "popups/UserMentions.h"
#include "timeline/TimelineViewManager.h"
@@ -282,6 +281,14 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
room_list_->highlightSelectedRoom(roomid);
activateWindow();
});
+ connect(¬ificationsManager,
+ &NotificationsManager::sendNotificationReply,
+ this,
+ [this](const QString &roomid, const QString &eventid, const QString &body) {
+ view_manager_->queueReply(roomid, eventid, body);
+ room_list_->highlightSelectedRoom(roomid);
+ activateWindow();
+ });
setGroupViewState(userSettings_->groupView());
@@ -911,6 +918,8 @@ ChatPage::joinRoom(const QString &room)
} catch (const lmdb::error &e) {
emit showNotification(tr("Failed to remove invite: %1").arg(e.what()));
}
+
+ room_list_->highlightSelectedRoom(QString::fromStdString(room_id));
});
}
@@ -1261,3 +1270,141 @@ ChatPage::decryptDownloadedSecrets(mtx::secret_storage::AesHmacSha2KeyDescriptio
cache::storeSecret(secretName, decrypted);
}
}
+
+void
+ChatPage::startChat(QString userid)
+{
+ auto joined_rooms = cache::joinedRooms();
+ auto room_infos = cache::getRoomInfo(joined_rooms);
+
+ for (std::string room_id : joined_rooms) {
+ if (room_infos[QString::fromStdString(room_id)].member_count == 2) {
+ auto room_members = cache::roomMembers(room_id);
+ if (std::find(room_members.begin(),
+ room_members.end(),
+ (userid).toStdString()) != room_members.end()) {
+ room_list_->highlightSelectedRoom(QString::fromStdString(room_id));
+ return;
+ }
+ }
+ }
+
+ mtx::requests::CreateRoom req;
+ req.preset = mtx::requests::Preset::PrivateChat;
+ req.visibility = mtx::requests::Visibility::Private;
+ if (utils::localUser() != userid)
+ req.invite = {userid.toStdString()};
+ emit ChatPage::instance()->createRoom(req);
+}
+
+static QString
+mxidFromSegments(QStringRef sigil, QStringRef mxid)
+{
+ if (mxid.isEmpty())
+ return "";
+
+ auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
+
+ if (sigil == "user") {
+ return "@" + mxid_;
+ } else if (sigil == "roomid") {
+ return "!" + mxid_;
+ } else if (sigil == "room") {
+ return "#" + mxid_;
+ } else if (sigil == "group") {
+ return "+" + mxid_;
+ } else {
+ return "";
+ }
+}
+
+void
+ChatPage::handleMatrixUri(const QByteArray &uri)
+{
+ nhlog::ui()->info("Received uri! {}", uri.toStdString());
+ QUrl uri_{QString::fromUtf8(uri)};
+
+ if (uri_.scheme() != "matrix")
+ return;
+
+ auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded);
+ if (tempPath.startsWith('/'))
+ tempPath.remove(0, 1);
+ auto segments = tempPath.splitRef('/');
+
+ if (segments.size() != 2 && segments.size() != 4)
+ return;
+
+ auto sigil1 = segments[0];
+ auto mxid1 = mxidFromSegments(sigil1, segments[1]);
+ if (mxid1.isEmpty())
+ return;
+
+ QString mxid2;
+ if (segments.size() == 4 && segments[2] == "event") {
+ if (segments[3].isEmpty())
+ return;
+ else
+ mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8());
+ }
+
+ std::vector vias;
+ QString action;
+
+ for (QString item : uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&')) {
+ nhlog::ui()->info("item: {}", item.toStdString());
+
+ if (item.startsWith("action=")) {
+ action = item.remove("action=");
+ } else if (item.startsWith("via=")) {
+ vias.push_back(
+ QUrl::fromPercentEncoding(item.remove("via=").toUtf8()).toStdString());
+ }
+ }
+
+ if (sigil1 == "user") {
+ if (action.isEmpty()) {
+ view_manager_->activeTimeline()->openUserProfile(mxid1);
+ } else if (action == "chat") {
+ this->startChat(mxid1);
+ }
+ } else if (sigil1 == "roomid") {
+ auto joined_rooms = cache::joinedRooms();
+ auto targetRoomId = mxid1.toStdString();
+
+ for (auto roomid : joined_rooms) {
+ if (roomid == targetRoomId) {
+ room_list_->highlightSelectedRoom(mxid1);
+ break;
+ }
+ }
+
+ if (action == "join") {
+ joinRoom(mxid1);
+ }
+ } else if (sigil1 == "room") {
+ auto joined_rooms = cache::joinedRooms();
+ auto targetRoomAlias = mxid1.toStdString();
+
+ for (auto roomid : joined_rooms) {
+ auto aliases = cache::client()->getRoomAliases(roomid);
+ if (aliases) {
+ if (aliases->alias == targetRoomAlias) {
+ room_list_->highlightSelectedRoom(
+ QString::fromStdString(roomid));
+ break;
+ }
+ }
+ }
+
+ if (action == "join") {
+ joinRoom(mxid1);
+ }
+ }
+}
+
+void
+ChatPage::handleMatrixUri(const QUrl &uri)
+{
+ handleMatrixUri(uri.toString(QUrl::ComponentFormattingOption::FullyEncoded).toUtf8());
+}
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 45a4ff63..004bb3e8 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -110,6 +110,10 @@ public:
mtx::presence::PresenceState currentPresence() const;
public slots:
+ void handleMatrixUri(const QByteArray &uri);
+ void handleMatrixUri(const QUrl &uri);
+
+ void startChat(QString userid);
void leaveRoom(const QString &room_id);
void createRoom(const mtx::requests::CreateRoom &req);
void joinRoom(const QString &room);
diff --git a/src/LoginPage.cpp b/src/LoginPage.cpp
index 05741cca..dba5ba51 100644
--- a/src/LoginPage.cpp
+++ b/src/LoginPage.cpp
@@ -315,10 +315,14 @@ LoginPage::checkHomeserverVersion()
if (err || flows.flows.empty())
emit versionOkCb(LoginMethod::Password);
- if (flows.flows[0].type == mtx::user_interactive::auth_types::sso)
- emit versionOkCb(LoginMethod::SSO);
- else
- emit versionOkCb(LoginMethod::Password);
+ LoginMethod loginMethod_ = LoginMethod::Password;
+ for (const auto &flow : flows.flows) {
+ if (flow.type == mtx::user_interactive::auth_types::sso) {
+ loginMethod_ = LoginMethod::SSO;
+ break;
+ }
+ }
+ emit versionOk(loginMethod_);
});
});
}
diff --git a/src/Olm.cpp b/src/Olm.cpp
index 07fc49f6..fe789560 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -579,13 +579,12 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
mtx::common::RelatesTo r_relation;
// relations shouldn't be encrypted...
- if (body["content"].contains("m.relates_to") &&
- body["content"]["m.relates_to"].contains("m.in_reply_to")) {
- relation = body["content"]["m.relates_to"];
- body["content"].erase("m.relates_to");
- } else if (body["content"]["m.relates_to"].contains("event_id")) {
- r_relation = body["content"]["m.relates_to"];
- body["content"].erase("m.relates_to");
+ if (body["content"].contains("m.relates_to")) {
+ if (body["content"]["m.relates_to"].contains("m.in_reply_to")) {
+ relation = body["content"]["m.relates_to"];
+ } else if (body["content"]["m.relates_to"].contains("event_id")) {
+ r_relation = body["content"]["m.relates_to"];
+ }
}
auto payload = olm::client()->encrypt_group_message(session.get(), body.dump());
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 4ca3be49..f133c87d 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -54,7 +54,7 @@ QSharedPointer UserSettings::instance_;
UserSettings::UserSettings()
{
- connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, [this]() {
+ connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, []() {
instance_.clear();
});
}
@@ -464,7 +464,7 @@ UserSettings::applyTheme()
stylefile.setFileName(":/styles/styles/nheko.qss");
QPalette lightActive(
/*windowText*/ QColor("#333"),
- /*button*/ QColor("#333"),
+ /*button*/ QColor("white"),
/*light*/ QColor(0xef, 0xef, 0xef),
/*dark*/ QColor(110, 110, 110),
/*mid*/ QColor(220, 220, 220),
@@ -477,7 +477,7 @@ UserSettings::applyTheme()
lightActive.setColor(QPalette::ToolTipBase, lightActive.base().color());
lightActive.setColor(QPalette::ToolTipText, lightActive.text().color());
lightActive.setColor(QPalette::Link, QColor("#0077b5"));
- lightActive.setColor(QPalette::ButtonText, QColor("#495057"));
+ lightActive.setColor(QPalette::ButtonText, QColor("#333"));
QApplication::setPalette(lightActive);
} else if (this->theme() == "dark") {
stylefile.setFileName(":/styles/styles/nheko-dark.qss");
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index af73202e..6744d101 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -23,6 +23,8 @@
#include
#include
+#include
+
class Toggle;
class QLabel;
class QFormLayout;
diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp
index 0770a439..094a2906 100644
--- a/src/WebRTCSession.cpp
+++ b/src/WebRTCSession.cpp
@@ -242,12 +242,14 @@ newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data)
GstDevice *device;
gst_message_parse_device_added(msg, &device);
addDevice(device);
+ emit WebRTCSession::instance().devicesChanged();
break;
}
case GST_MESSAGE_DEVICE_REMOVED: {
GstDevice *device;
gst_message_parse_device_removed(msg, &device);
removeDevice(device, false);
+ emit WebRTCSession::instance().devicesChanged();
break;
}
case GST_MESSAGE_DEVICE_CHANGED: {
@@ -553,7 +555,10 @@ getResolution(GstPad *pad)
void
addCameraView(GstElement *pipe, const std::pair &videoCallSize)
{
- GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee");
+ GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe), "videosrctee");
+ if (!tee)
+ return;
+
GstElement *queue = gst_element_factory_make("queue", nullptr);
GstElement *videorate = gst_element_factory_make("videorate", nullptr);
gst_bin_add_many(GST_BIN(pipe), queue, videorate, nullptr);
@@ -1150,6 +1155,19 @@ WebRTCSession::addVideoPipeline(int vp8PayloadType)
return true;
}
+bool
+WebRTCSession::haveLocalVideo() const
+{
+ if (isVideo_ && state_ >= State::INITIATED) {
+ GstElement *tee = gst_bin_get_by_name(GST_BIN(pipe_), "videosrctee");
+ if (tee) {
+ gst_object_unref(tee);
+ return true;
+ }
+ }
+ return false;
+}
+
bool
WebRTCSession::isMicMuted() const
{
@@ -1274,6 +1292,7 @@ WebRTCSession::refreshDevices()
addDevice(GST_DEVICE_CAST(l->data));
g_list_free(devices);
}
+ emit devicesChanged();
#endif
}
@@ -1324,6 +1343,12 @@ WebRTCSession::havePlugins(bool, std::string *)
return false;
}
+bool
+WebRTCSession::haveLocalVideo() const
+{
+ return false;
+}
+
bool
WebRTCSession::createOffer(bool)
{
diff --git a/src/WebRTCSession.h b/src/WebRTCSession.h
index 57002f8f..2f0fb70e 100644
--- a/src/WebRTCSession.h
+++ b/src/WebRTCSession.h
@@ -43,6 +43,7 @@ public:
bool havePlugins(bool isVideo, std::string *errorMessage = nullptr);
webrtc::State state() const { return state_; }
bool isVideo() const { return isVideo_; }
+ bool haveLocalVideo() const;
bool isOffering() const { return isOffering_; }
bool isRemoteVideoRecvOnly() const { return isRemoteVideoRecvOnly_; }
@@ -75,6 +76,7 @@ signals:
const std::vector &);
void newICECandidate(const mtx::events::msg::CallCandidates::Candidate &);
void stateChanged(webrtc::State);
+ void devicesChanged();
private slots:
void setState(webrtc::State state) { state_ = state; }
diff --git a/src/dialogs/AcceptCall.cpp b/src/dialogs/AcceptCall.cpp
deleted file mode 100644
index 3d25ad82..00000000
--- a/src/dialogs/AcceptCall.cpp
+++ /dev/null
@@ -1,152 +0,0 @@
-#include
-#include
-#include
-#include
-#include
-
-#include "ChatPage.h"
-#include "Config.h"
-#include "UserSettingsPage.h"
-#include "Utils.h"
-#include "WebRTCSession.h"
-#include "dialogs/AcceptCall.h"
-#include "ui/Avatar.h"
-
-namespace dialogs {
-
-AcceptCall::AcceptCall(const QString &caller,
- const QString &displayName,
- const QString &roomName,
- const QString &avatarUrl,
- bool isVideo,
- QWidget *parent)
- : QWidget(parent)
-{
- std::string errorMessage;
- WebRTCSession *session = &WebRTCSession::instance();
- if (!session->havePlugins(false, &errorMessage)) {
- emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
- emit close();
- return;
- }
- if (isVideo && !session->havePlugins(true, &errorMessage)) {
- emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
- emit close();
- return;
- }
-
- session->refreshDevices();
- microphones_ = session->getDeviceNames(
- false, ChatPage::instance()->userSettings()->microphone().toStdString());
- if (microphones_.empty()) {
- emit ChatPage::instance()->showNotification(
- tr("Incoming call: No microphone found."));
- emit close();
- return;
- }
- if (isVideo)
- cameras_ = session->getDeviceNames(
- true, ChatPage::instance()->userSettings()->camera().toStdString());
-
- setAutoFillBackground(true);
- setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
- setWindowModality(Qt::WindowModal);
- setAttribute(Qt::WA_DeleteOnClose, true);
-
- setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH);
- setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-
- auto layout = new QVBoxLayout(this);
- layout->setSpacing(conf::modals::WIDGET_SPACING);
- layout->setMargin(conf::modals::WIDGET_MARGIN);
-
- QFont f;
- f.setPointSizeF(f.pointSizeF());
-
- QFont labelFont;
- labelFont.setWeight(QFont::Medium);
-
- QLabel *displayNameLabel = nullptr;
- if (!displayName.isEmpty() && displayName != caller) {
- displayNameLabel = new QLabel(displayName, this);
- labelFont.setPointSizeF(f.pointSizeF() * 2);
- displayNameLabel->setFont(labelFont);
- displayNameLabel->setAlignment(Qt::AlignCenter);
- }
-
- QLabel *callerLabel = new QLabel(caller, this);
- labelFont.setPointSizeF(f.pointSizeF() * 1.2);
- callerLabel->setFont(labelFont);
- callerLabel->setAlignment(Qt::AlignCenter);
-
- auto avatar = new Avatar(this, QFontMetrics(f).height() * 6);
- if (!avatarUrl.isEmpty())
- avatar->setImage(avatarUrl);
- else
- avatar->setLetter(utils::firstChar(roomName));
-
- const int iconSize = 22;
- QLabel *callTypeIndicator = new QLabel(this);
- callTypeIndicator->setPixmap(
- QIcon(isVideo ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png")
- .pixmap(QSize(iconSize * 2, iconSize * 2)));
-
- QLabel *callTypeLabel = new QLabel(isVideo ? tr("Video Call") : tr("Voice Call"), this);
- labelFont.setPointSizeF(f.pointSizeF() * 1.1);
- callTypeLabel->setFont(labelFont);
- callTypeLabel->setAlignment(Qt::AlignCenter);
-
- auto buttonLayout = new QHBoxLayout;
- buttonLayout->setSpacing(18);
- acceptBtn_ = new QPushButton(tr("Accept"), this);
- acceptBtn_->setDefault(true);
- acceptBtn_->setIcon(
- QIcon(isVideo ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png"));
- acceptBtn_->setIconSize(QSize(iconSize, iconSize));
-
- rejectBtn_ = new QPushButton(tr("Reject"), this);
- rejectBtn_->setIcon(QIcon(":/icons/icons/ui/end-call.png"));
- rejectBtn_->setIconSize(QSize(iconSize, iconSize));
- buttonLayout->addWidget(acceptBtn_);
- buttonLayout->addWidget(rejectBtn_);
-
- microphoneCombo_ = new QComboBox(this);
- for (const auto &m : microphones_)
- microphoneCombo_->addItem(QIcon(":/icons/icons/ui/microphone-unmute.png"),
- QString::fromStdString(m));
-
- if (!cameras_.empty()) {
- cameraCombo_ = new QComboBox(this);
- for (const auto &c : cameras_)
- cameraCombo_->addItem(QIcon(":/icons/icons/ui/video-call.png"),
- QString::fromStdString(c));
- }
-
- if (displayNameLabel)
- layout->addWidget(displayNameLabel, 0, Qt::AlignCenter);
- layout->addWidget(callerLabel, 0, Qt::AlignCenter);
- layout->addWidget(avatar, 0, Qt::AlignCenter);
- layout->addWidget(callTypeIndicator, 0, Qt::AlignCenter);
- layout->addWidget(callTypeLabel, 0, Qt::AlignCenter);
- layout->addLayout(buttonLayout);
- layout->addWidget(microphoneCombo_);
- if (cameraCombo_)
- layout->addWidget(cameraCombo_);
-
- connect(acceptBtn_, &QPushButton::clicked, this, [this]() {
- ChatPage::instance()->userSettings()->setMicrophone(
- QString::fromStdString(microphones_[microphoneCombo_->currentIndex()]));
- if (cameraCombo_) {
- ChatPage::instance()->userSettings()->setCamera(
- QString::fromStdString(cameras_[cameraCombo_->currentIndex()]));
- }
- emit accept();
- emit close();
- });
- connect(rejectBtn_, &QPushButton::clicked, this, [this]() {
- emit reject();
- emit close();
- });
-}
-
-}
diff --git a/src/dialogs/AcceptCall.h b/src/dialogs/AcceptCall.h
deleted file mode 100644
index 76ca7ae1..00000000
--- a/src/dialogs/AcceptCall.h
+++ /dev/null
@@ -1,39 +0,0 @@
-#pragma once
-
-#include
-#include
-
-#include
-
-class QComboBox;
-class QPushButton;
-class QString;
-
-namespace dialogs {
-
-class AcceptCall : public QWidget
-{
- Q_OBJECT
-
-public:
- AcceptCall(const QString &caller,
- const QString &displayName,
- const QString &roomName,
- const QString &avatarUrl,
- bool isVideo,
- QWidget *parent = nullptr);
-
-signals:
- void accept();
- void reject();
-
-private:
- QPushButton *acceptBtn_ = nullptr;
- QPushButton *rejectBtn_ = nullptr;
- QComboBox *microphoneCombo_ = nullptr;
- QComboBox *cameraCombo_ = nullptr;
- std::vector microphones_;
- std::vector cameras_;
-};
-
-}
diff --git a/src/dialogs/PlaceCall.cpp b/src/dialogs/PlaceCall.cpp
deleted file mode 100644
index 85a398a2..00000000
--- a/src/dialogs/PlaceCall.cpp
+++ /dev/null
@@ -1,131 +0,0 @@
-#include
-#include
-#include
-#include
-#include
-
-#include "ChatPage.h"
-#include "Config.h"
-#include "UserSettingsPage.h"
-#include "Utils.h"
-#include "WebRTCSession.h"
-#include "dialogs/PlaceCall.h"
-#include "ui/Avatar.h"
-
-namespace dialogs {
-
-PlaceCall::PlaceCall(const QString &callee,
- const QString &displayName,
- const QString &roomName,
- const QString &avatarUrl,
- QSharedPointer settings,
- QWidget *parent)
- : QWidget(parent)
-{
- std::string errorMessage;
- WebRTCSession *session = &WebRTCSession::instance();
- if (!session->havePlugins(false, &errorMessage)) {
- emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
- emit close();
- return;
- }
- session->refreshDevices();
- microphones_ = session->getDeviceNames(false, settings->microphone().toStdString());
- if (microphones_.empty()) {
- emit ChatPage::instance()->showNotification(tr("No microphone found."));
- emit close();
- return;
- }
- cameras_ = session->getDeviceNames(true, settings->camera().toStdString());
-
- setAutoFillBackground(true);
- setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
- setWindowModality(Qt::WindowModal);
- setAttribute(Qt::WA_DeleteOnClose, true);
-
- auto layout = new QVBoxLayout(this);
- layout->setSpacing(conf::modals::WIDGET_SPACING);
- layout->setMargin(conf::modals::WIDGET_MARGIN);
-
- auto buttonLayout = new QHBoxLayout;
- buttonLayout->setSpacing(15);
- buttonLayout->setMargin(0);
-
- QFont f;
- f.setPointSizeF(f.pointSizeF());
- auto avatar = new Avatar(this, QFontMetrics(f).height() * 3);
- if (!avatarUrl.isEmpty())
- avatar->setImage(avatarUrl);
- else
- avatar->setLetter(utils::firstChar(roomName));
-
- voiceBtn_ = new QPushButton(tr("Voice"), this);
- voiceBtn_->setIcon(QIcon(":/icons/icons/ui/place-call.png"));
- voiceBtn_->setIconSize(QSize(iconSize_, iconSize_));
- voiceBtn_->setDefault(true);
-
- if (!cameras_.empty()) {
- videoBtn_ = new QPushButton(tr("Video"), this);
- videoBtn_->setIcon(QIcon(":/icons/icons/ui/video-call.png"));
- videoBtn_->setIconSize(QSize(iconSize_, iconSize_));
- }
- cancelBtn_ = new QPushButton(tr("Cancel"), this);
-
- buttonLayout->addWidget(avatar);
- buttonLayout->addStretch();
- buttonLayout->addWidget(voiceBtn_);
- if (videoBtn_)
- buttonLayout->addWidget(videoBtn_);
- buttonLayout->addWidget(cancelBtn_);
-
- QString name = displayName.isEmpty() ? callee : displayName;
- QLabel *label = new QLabel(tr("Place a call to ") + name + "?", this);
-
- microphoneCombo_ = new QComboBox(this);
- for (const auto &m : microphones_)
- microphoneCombo_->addItem(QIcon(":/icons/icons/ui/microphone-unmute.png"),
- QString::fromStdString(m));
-
- if (videoBtn_) {
- cameraCombo_ = new QComboBox(this);
- for (const auto &c : cameras_)
- cameraCombo_->addItem(QIcon(":/icons/icons/ui/video-call.png"),
- QString::fromStdString(c));
- }
-
- layout->addWidget(label);
- layout->addLayout(buttonLayout);
- layout->addStretch();
- layout->addWidget(microphoneCombo_);
- if (videoBtn_)
- layout->addWidget(cameraCombo_);
-
- connect(voiceBtn_, &QPushButton::clicked, this, [this, settings]() {
- settings->setMicrophone(
- QString::fromStdString(microphones_[microphoneCombo_->currentIndex()]));
- emit voice();
- emit close();
- });
- if (videoBtn_)
- connect(videoBtn_, &QPushButton::clicked, this, [this, settings, session]() {
- std::string error;
- if (!session->havePlugins(true, &error)) {
- emit ChatPage::instance()->showNotification(
- QString::fromStdString(error));
- emit close();
- return;
- }
- settings->setMicrophone(
- QString::fromStdString(microphones_[microphoneCombo_->currentIndex()]));
- settings->setCamera(
- QString::fromStdString(cameras_[cameraCombo_->currentIndex()]));
- emit video();
- emit close();
- });
- connect(cancelBtn_, &QPushButton::clicked, this, [this]() {
- emit cancel();
- emit close();
- });
-}
-
-}
diff --git a/src/dialogs/PlaceCall.h b/src/dialogs/PlaceCall.h
deleted file mode 100644
index e042258f..00000000
--- a/src/dialogs/PlaceCall.h
+++ /dev/null
@@ -1,44 +0,0 @@
-#pragma once
-
-#include
-#include
-
-#include
-#include
-
-class QComboBox;
-class QPushButton;
-class QString;
-class UserSettings;
-
-namespace dialogs {
-
-class PlaceCall : public QWidget
-{
- Q_OBJECT
-
-public:
- PlaceCall(const QString &callee,
- const QString &displayName,
- const QString &roomName,
- const QString &avatarUrl,
- QSharedPointer settings,
- QWidget *parent = nullptr);
-
-signals:
- void voice();
- void video();
- void cancel();
-
-private:
- const int iconSize_ = 18;
- QPushButton *voiceBtn_ = nullptr;
- QPushButton *videoBtn_ = nullptr;
- QPushButton *cancelBtn_ = nullptr;
- QComboBox *microphoneCombo_ = nullptr;
- QComboBox *cameraCombo_ = nullptr;
- std::vector microphones_;
- std::vector cameras_;
-};
-
-}
diff --git a/src/main.cpp b/src/main.cpp
index a60c66c4..7a417ae2 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -19,6 +19,7 @@
#include
#include
+#include
#include
#include
#include
@@ -33,6 +34,7 @@
#include
#include
+#include "ChatPage.h"
#include "Config.h"
#include "Logging.h"
#include "MainWindow.h"
@@ -128,34 +130,43 @@ main(int argc, char *argv[])
// This is some hacky programming, but it's necessary (AFAIK?) to get the unique config name
// parsed before the SingleApplication userdata is set.
QString userdata{""};
+ QString matrixUri;
for (int i = 0; i < argc; ++i) {
- if (QString{argv[i]}.startsWith("--profile=")) {
- QString q{argv[i]};
- q.remove("--profile=");
- userdata = q;
- } else if (QString{argv[i]}.startsWith("--p=")) {
- QString q{argv[i]};
- q.remove("-p=");
- userdata = q;
- } else if (QString{argv[i]} == "--profile" || QString{argv[i]} == "-p") {
+ QString arg{argv[i]};
+ if (arg.startsWith("--profile=")) {
+ arg.remove("--profile=");
+ userdata = arg;
+ } else if (arg.startsWith("--p=")) {
+ arg.remove("-p=");
+ userdata = arg;
+ } else if (arg == "--profile" || arg == "-p") {
if (i < argc - 1) // if i is less than argc - 1, we still have a parameter
// left to process as the name
{
++i; // the next arg is the name, so increment
userdata = QString{argv[i]};
}
+ } else if (arg.startsWith("matrix:")) {
+ matrixUri = arg;
}
}
SingleApplication app(argc,
argv,
- false,
+ true,
SingleApplication::Mode::User |
SingleApplication::Mode::ExcludeAppPath |
- SingleApplication::Mode::ExcludeAppVersion,
+ SingleApplication::Mode::ExcludeAppVersion |
+ SingleApplication::Mode::SecondaryNotification,
100,
userdata);
+ if (app.isSecondary()) {
+ // open uri in main instance
+ app.sendMessage(matrixUri.toUtf8());
+ return 0;
+ }
+
QCommandLineParser parser;
parser.addHelpOption();
parser.addVersionOption();
@@ -245,6 +256,25 @@ main(int argc, char *argv[])
w.activateWindow();
});
+ QObject::connect(
+ &app,
+ &SingleApplication::receivedMessage,
+ ChatPage::instance(),
+ [&](quint32, QByteArray message) { ChatPage::instance()->handleMatrixUri(message); });
+
+ QMetaObject::Connection uriConnection;
+ if (app.isPrimary() && !matrixUri.isEmpty()) {
+ uriConnection = QObject::connect(ChatPage::instance(),
+ &ChatPage::contentLoaded,
+ ChatPage::instance(),
+ [&uriConnection, matrixUri]() {
+ ChatPage::instance()->handleMatrixUri(
+ matrixUri.toUtf8());
+ QObject::disconnect(uriConnection);
+ });
+ }
+ QDesktopServices::setUrlHandler("matrix", ChatPage::instance(), "handleMatrixUri");
+
#if defined(Q_OS_MAC)
// Temporary solution for the emoji picker until
// nheko has a proper menu bar with more functionality.
diff --git a/src/notifications/Manager.h b/src/notifications/Manager.h
index e6be5953..b5347bd6 100644
--- a/src/notifications/Manager.h
+++ b/src/notifications/Manager.h
@@ -36,6 +36,7 @@ public:
signals:
void notificationClicked(const QString roomId, const QString eventId);
+ void sendNotificationReply(const QString roomId, const QString eventId, const QString body);
public slots:
void removeNotification(const QString &roomId, const QString &eventId);
@@ -58,6 +59,7 @@ private:
private slots:
void actionInvoked(uint id, QString action);
void notificationClosed(uint id, uint reason);
+ void notificationReplied(uint id, QString reply);
};
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
diff --git a/src/notifications/ManagerLinux.cpp b/src/notifications/ManagerLinux.cpp
index b9eca1a8..b5e9a6a4 100644
--- a/src/notifications/ManagerLinux.cpp
+++ b/src/notifications/ManagerLinux.cpp
@@ -28,6 +28,12 @@ NotificationsManager::NotificationsManager(QObject *parent)
"NotificationClosed",
this,
SLOT(notificationClosed(uint, uint)));
+ QDBusConnection::sessionBus().connect("org.freedesktop.Notifications",
+ "/org/freedesktop/Notifications",
+ "org.freedesktop.Notifications",
+ "NotificationReplied",
+ this,
+ SLOT(notificationReplied(uint, QString)));
}
void
@@ -56,14 +62,19 @@ NotificationsManager::showNotification(const QString summary,
hints["image-data"] = image;
hints["sound-name"] = "message-new-instant";
QList argumentList;
- argumentList << "nheko"; // app_name
- argumentList << (uint)0; // replace_id
- argumentList << ""; // app_icon
- argumentList << summary; // summary
- argumentList << text; // body
- argumentList << (QStringList("default") << "reply"); // actions
- argumentList << hints; // hints
- argumentList << (int)-1; // timeout in ms
+ argumentList << "nheko"; // app_name
+ argumentList << (uint)0; // replace_id
+ argumentList << ""; // app_icon
+ argumentList << summary; // summary
+ argumentList << text; // body
+ // The list of actions has always the action name and then a localized version of that
+ // action. Currently we just use an empty string for that.
+ // TODO(Nico): Look into what to actually put there.
+ argumentList << (QStringList("default") << ""
+ << "inline-reply"
+ << ""); // actions
+ argumentList << hints; // hints
+ argumentList << (int)-1; // timeout in ms
static QDBusInterface notifyApp("org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
@@ -121,9 +132,20 @@ NotificationsManager::removeNotification(const QString &roomId, const QString &e
void
NotificationsManager::actionInvoked(uint id, QString action)
{
- if (action == "default" && notificationIds.contains(id)) {
+ if (notificationIds.contains(id)) {
roomEventId idEntry = notificationIds[id];
- emit notificationClicked(idEntry.roomId, idEntry.eventId);
+ if (action == "default") {
+ emit notificationClicked(idEntry.roomId, idEntry.eventId);
+ }
+ }
+}
+
+void
+NotificationsManager::notificationReplied(uint id, QString reply)
+{
+ if (notificationIds.contains(id)) {
+ roomEventId idEntry = notificationIds[id];
+ emit sendNotificationReply(idEntry.roomId, idEntry.eventId, reply);
}
}
diff --git a/src/notifications/ManagerMac.mm b/src/notifications/ManagerMac.mm
index f035e5f2..c09e894c 100644
--- a/src/notifications/ManagerMac.mm
+++ b/src/notifications/ManagerMac.mm
@@ -39,6 +39,11 @@ NotificationsManager::postNotification(
//unused
void
NotificationsManager::actionInvoked(uint, QString)
+{
+ }
+
+void
+NotificationsManager::notificationReplied(uint, QString)
{
}
diff --git a/src/notifications/ManagerWin.cpp b/src/notifications/ManagerWin.cpp
index 5a9cb83e..cc61c645 100644
--- a/src/notifications/ManagerWin.cpp
+++ b/src/notifications/ManagerWin.cpp
@@ -61,6 +61,7 @@ NotificationsManager::postNotification(const QString &room_id,
}
void NotificationsManager::actionInvoked(uint, QString) {}
+void NotificationsManager::notificationReplied(uint, QString) {}
void NotificationsManager::notificationClosed(uint, uint) {}
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 5cbc33e0..3cddd613 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -13,7 +13,6 @@
#include
#include "Cache.h"
-#include "CallManager.h"
#include "ChatPage.h"
#include "CompletionProxyModel.h"
#include "Logging.h"
@@ -25,7 +24,6 @@
#include "UserSettingsPage.h"
#include "UsersModel.h"
#include "Utils.h"
-#include "dialogs/PlaceCall.h"
#include "dialogs/PreviewUploadOverlay.h"
#include "emoji/EmojiModel.h"
@@ -593,48 +591,6 @@ InputBar::showPreview(const QMimeData &source, QString path, const QStringList &
});
}
-void
-InputBar::callButton()
-{
- auto callManager_ = ChatPage::instance()->callManager();
- if (callManager_->onActiveCall()) {
- callManager_->hangUp();
- } else {
- auto current_room_ = room->roomId();
- if (auto roomInfo = cache::singleRoomInfo(current_room_.toStdString());
- roomInfo.member_count != 2) {
- ChatPage::instance()->showNotification("Calls are limited to 1:1 rooms.");
- } else {
- std::vector members(
- cache::getMembers(current_room_.toStdString()));
- const RoomMember &callee = members.front().user_id == utils::localUser()
- ? members.back()
- : members.front();
- auto dialog =
- new dialogs::PlaceCall(callee.user_id,
- callee.display_name,
- QString::fromStdString(roomInfo.name),
- QString::fromStdString(roomInfo.avatar_url),
- ChatPage::instance()->userSettings(),
- MainWindow::instance());
- connect(dialog,
- &dialogs::PlaceCall::voice,
- callManager_,
- [callManager_, current_room_]() {
- callManager_->sendInvite(current_room_, false);
- });
- connect(dialog,
- &dialogs::PlaceCall::video,
- callManager_,
- [callManager_, current_room_]() {
- callManager_->sendInvite(current_room_, true);
- });
- utils::centerWidget(dialog, MainWindow::instance());
- dialog->show();
- }
- }
-}
-
void
InputBar::startTyping()
{
diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h
index 27aa4bc3..c729a6fc 100644
--- a/src/timeline/InputBar.h
+++ b/src/timeline/InputBar.h
@@ -41,7 +41,7 @@ public slots:
void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text);
void openFileSelection();
bool uploading() const { return uploading_; }
- void callButton();
+ void message(QString body);
QObject *completerFor(QString completerName);
@@ -54,7 +54,6 @@ signals:
void uploadingChanged(bool value);
private:
- void message(QString body);
void emote(QString body);
void command(QString name, QString args);
void image(const QString &filename,
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index adef886d..852f584d 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -613,8 +613,15 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
std::visit(
[this](auto &event) {
event.room_id = room_id_.toStdString();
- if (event.sender != http::client()->user_id().to_string())
+ if constexpr (std::is_same_v,
+ RoomEvent> ||
+ std::is_same_v,
+ RoomEvent>)
emit newCallEvent(event);
+ else {
+ if (event.sender != http::client()->user_id().to_string())
+ emit newCallEvent(event);
+ }
},
e);
else if (std::holds_alternative>(e))
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 03eb53fc..97af0065 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -136,6 +136,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
"im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
return ChatPage::instance()->userSettings().data();
});
+ qmlRegisterSingletonType(
+ "im.nheko", 1, 0, "CallManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
+ return ChatPage::instance()->callManager();
+ });
qRegisterMetaType();
qRegisterMetaType>();
@@ -237,36 +241,6 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
isInitialSync_ = true;
emit initialSyncChanged(true);
});
- connect(&WebRTCSession::instance(),
- &WebRTCSession::stateChanged,
- this,
- &TimelineViewManager::callStateChanged);
- connect(
- callManager_, &CallManager::newCallParty, this, &TimelineViewManager::callPartyChanged);
- connect(callManager_,
- &CallManager::newVideoCallState,
- this,
- &TimelineViewManager::videoCallChanged);
-
- connect(&WebRTCSession::instance(),
- &WebRTCSession::stateChanged,
- this,
- &TimelineViewManager::onCallChanged);
-}
-
-bool
-TimelineViewManager::isOnCall() const
-{
- return callManager_->onActiveCall();
-}
-bool
-TimelineViewManager::callsSupported() const
-{
-#ifdef GSTREAMER_AVAILABLE
- return true;
-#else
- return false;
-#endif
}
void
@@ -297,13 +271,20 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
&CallManager::syncEvent);
if (ChatPage::instance()->userSettings()->typingNotifications()) {
- std::vector typing;
- typing.reserve(room.ephemeral.typing.size());
- for (const auto &user : room.ephemeral.typing) {
- if (user != http::client()->user_id().to_string())
- typing.push_back(QString::fromStdString(user));
+ for (const auto &ev : room.ephemeral.events) {
+ if (auto t = std::get_if<
+ mtx::events::EphemeralEvent>(
+ &ev)) {
+ std::vector typing;
+ typing.reserve(t->content.user_ids.size());
+ for (const auto &user : t->content.user_ids) {
+ if (user != http::client()->user_id().to_string())
+ typing.push_back(
+ QString::fromStdString(user));
+ }
+ room_model->updateTypingUsers(typing);
+ }
}
- room_model->updateTypingUsers(typing);
}
}
@@ -347,19 +328,6 @@ TimelineViewManager::escapeEmoji(QString str) const
return utils::replaceEmoji(str);
}
-void
-TimelineViewManager::toggleMicMute()
-{
- WebRTCSession::instance().toggleMicMute();
- emit micMuteChanged();
-}
-
-void
-TimelineViewManager::toggleCameraView()
-{
- WebRTCSession::instance().toggleCameraView();
-}
-
void
TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const
{
@@ -501,6 +469,18 @@ TimelineViewManager::initWithMessages(const std::vector &roomIds)
addRoom(roomId);
}
+void
+TimelineViewManager::queueReply(const QString &roomid,
+ const QString &repliedToEvent,
+ const QString &replyBody)
+{
+ auto room = models.find(roomid);
+ if (room != models.end()) {
+ room.value()->setReply(repliedToEvent);
+ room.value()->input()->message(replyBody);
+ }
+}
+
void
TimelineViewManager::queueReactionMessage(const QString &reactedEvent, const QString &reactionKey)
{
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index f346acf8..23a960b8 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -36,13 +36,6 @@ class TimelineViewManager : public QObject
bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
Q_PROPERTY(
bool isNarrowView MEMBER isNarrowView_ READ isNarrowView NOTIFY narrowViewChanged)
- Q_PROPERTY(webrtc::State callState READ callState NOTIFY callStateChanged)
- Q_PROPERTY(bool onVideoCall READ onVideoCall NOTIFY videoCallChanged)
- Q_PROPERTY(QString callPartyName READ callPartyName NOTIFY callPartyChanged)
- Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY callPartyChanged)
- Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged)
- Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY onCallChanged)
- Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT)
public:
TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr);
@@ -61,14 +54,6 @@ public:
Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
bool isNarrowView() const { return isNarrowView_; }
- webrtc::State callState() const { return WebRTCSession::instance().state(); }
- bool onVideoCall() const { return WebRTCSession::instance().isVideo(); }
- Q_INVOKABLE void setVideoCallItem();
- QString callPartyName() const { return callManager_->callPartyName(); }
- QString callPartyAvatarUrl() const { return callManager_->callPartyAvatarUrl(); }
- bool isMicMuted() const { return WebRTCSession::instance().isMicMuted(); }
- Q_INVOKABLE void toggleMicMute();
- Q_INVOKABLE void toggleCameraView();
Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const;
Q_INVOKABLE QColor userColor(QString id, QColor background);
Q_INVOKABLE QString escapeEmoji(QString str) const;
@@ -98,11 +83,6 @@ signals:
void inviteUsers(QStringList users);
void showRoomList();
void narrowViewChanged();
- void callStateChanged(webrtc::State);
- void videoCallChanged();
- void callPartyChanged();
- void micMuteChanged();
- void onCallChanged();
public slots:
void updateReadReceipts(const QString &room_id, const std::vector &event_ids);
@@ -120,6 +100,9 @@ public slots:
}
void updateColorPalette();
+ void queueReply(const QString &roomid,
+ const QString &repliedToEvent,
+ const QString &replyBody);
void queueReactionMessage(const QString &reactedEvent, const QString &reactionKey);
void queueCallMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
void queueCallMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
@@ -127,8 +110,7 @@ public slots:
void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
void updateEncryptedDescriptions();
- bool isOnCall() const;
- bool callsSupported() const;
+ void setVideoCallItem();
void enableBackButton()
{
diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp
index 974aa5cc..6ef82123 100644
--- a/src/ui/UserProfile.cpp
+++ b/src/ui/UserProfile.cpp
@@ -202,12 +202,7 @@ UserProfile::kickUser()
void
UserProfile::startChat()
{
- mtx::requests::CreateRoom req;
- req.preset = mtx::requests::Preset::PrivateChat;
- req.visibility = mtx::requests::Visibility::Private;
- if (utils::localUser() != this->userid_)
- req.invite = {this->userid_.toStdString()};
- emit ChatPage::instance()->createRoom(req);
+ ChatPage::instance()->startChat(this->userid_);
}
void