diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml new file mode 100644 index 00000000..8a63725e --- /dev/null +++ b/resources/qml/ActiveCallBar.qml @@ -0,0 +1,110 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 + +import im.nheko 1.0 + +Rectangle { + id: activeCallBar + visible: timelineManager.callState != WebRTCState.DISCONNECTED + color: "#2ECC71" + implicitHeight: rowLayout.height + 8 + + RowLayout { + id: rowLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 8 + + Avatar { + width: avatarSize + height: avatarSize + + url: timelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") + displayName: timelineManager.callPartyName + } + + Label { + font.pointSize: fontMetrics.font.pointSize * 1.1 + text: " " + timelineManager.callPartyName + " " + } + + Image { + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + source: "qrc:/icons/icons/ui/place-call.png" + } + + Label { + id: callStateLabel + font.pointSize: fontMetrics.font.pointSize * 1.1 + } + + Connections { + target: timelineManager + function onCallStateChanged(state) { + switch (state) { + case WebRTCState.INITIATING: + callStateLabel.text = qsTr("Initiating...") + break; + case WebRTCState.OFFERSENT: + callStateLabel.text = qsTr("Calling...") + break; + case WebRTCState.CONNECTING: + callStateLabel.text = qsTr("Connecting...") + break; + case WebRTCState.CONNECTED: + callStateLabel.text = "00:00" + var d = new Date() + callTimer.startTime = Math.floor(d.getTime() / 1000) + break; + case WebRTCState.DISCONNECTED: + callStateLabel.text = "" + } + } + } + + Timer { + id: callTimer + property int startTime + interval: 1000 + running: timelineManager.callState == WebRTCState.CONNECTED + repeat: true + onTriggered: { + var d = new Date() + let seconds = Math.floor(d.getTime() / 1000 - startTime) + let s = Math.floor(seconds % 60) + let m = Math.floor(seconds / 60) % 60 + let h = Math.floor(seconds / 3600) + callStateLabel.text = (h ? (pad(h) + ":") : "") + pad(m) + ":" + pad(s) + } + + function pad(n) { + return (n < 10) ? ("0" + n) : n + } + } + + Item { + Layout.fillWidth: true + } + + ImageButton { + width: 24 + height: 24 + src: timelineManager.isMicMuted ? + "qrc:/icons/icons/ui/microphone-unmute.png" : + "qrc:/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 + } + } +} diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index a3943806..c030c843 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -14,7 +14,7 @@ Rectangle { Label { anchors.fill: parent - text: timelineManager.escapeEmoji(String.fromCodePoint(displayName.codePointAt(0))) + text: timelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") textFormat: Text.RichText font.pixelSize: avatar.height/2 verticalAlignment: Text.AlignVCenter diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 4ea15f7b..07c5e1a4 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -498,144 +498,9 @@ Page { } } - Rectangle { - id: activeCallBar - visible: timelineManager.callState != WebRTCState.DISCONNECTED - + ActiveCallBar { Layout.fillWidth: true - implicitHeight: topLayout.height + 16 - color: "#2ECC71" z: 3 - - GridLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 8 - anchors.verticalCenter: parent.verticalCenter - - Avatar { - Layout.column: 1 - Layout.row: 0 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - - width: avatarSize - height: avatarSize - - url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" - displayName: chat.model ? chat.model.roomName : qsTr("No room selected") - } - - Label { - Layout.column: 2 - Layout.row: 0 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - - font.pointSize: fontMetrics.font.pointSize * 1.1 - text: chat.model ? " " + chat.model.roomName + " " : "" - } - - Image { - Layout.column: 3 - Layout.row: 0 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - Layout.preferredWidth: 23 - Layout.preferredHeight: 23 - source: "qrc:/icons/icons/ui/place-call.png" - } - - Connections { - target: timelineManager - function onCallStateChanged(state) { - switch (state) { - case WebRTCState.INITIATING: - callStateLabel.text = "Initiating call..." - break; - case WebRTCState.INITIATED: - callStateLabel.text = "Call initiated..." - break; - case WebRTCState.OFFERSENT: - callStateLabel.text = "Calling..." - break; - case WebRTCState.CONNECTING: - callStateLabel.text = "Connecting..." - break; - case WebRTCState.CONNECTED: - callStateLabel.text = "00:00" - var d = new Date() - callTimer.startTime = Math.floor(d.getTime() / 1000) - break; - } - } - } - - Label { - id: callStateLabel - Layout.column: 4 - Layout.row: 0 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - font.pointSize: fontMetrics.font.pointSize * 1.1 - } - - Timer { - id: callTimer - property int startTime - interval: 1000 - running: timelineManager.callState == WebRTCState.CONNECTED - repeat: true - onTriggered: { - var d = new Date() - let seconds = Math.floor(d.getTime() / 1000 - startTime) - let s = Math.floor(seconds % 60) - let m = Math.floor(seconds / 60) % 60 - let h = Math.floor(seconds / 3600) - callStateLabel.text = (h ? (pad(h) + ":") : "") + pad(m) + ":" + pad(s) - } - - function pad(n) { - return (n < 10) ? ("0" + n) : n - } - } - - Item { - Layout.column: 5 - Layout.fillWidth: true - } - - ImageButton { - Layout.column: 6 - Layout.row: 0 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignVCenter - - width: 22 - height: 22 - src: "qrc:/icons/icons/ui/microphone-mute.png" - - hoverEnabled: true - ToolTip.visible: hovered - ToolTip.text: qsTr("Mute Mic") - - onClicked: { - if (timelineManager.toggleMuteAudioSource()) { - src = "qrc:/icons/icons/ui/microphone-unmute.png" - ToolTip.text = qsTr("Unmute Mic") - } - else { - src = "qrc:/icons/icons/ui/microphone-mute.png" - ToolTip.text = qsTr("Mute Mic") - } - } - } - - Item { - Layout.column: 7 - implicitWidth: 16 - } - } } } } diff --git a/resources/res.qrc b/resources/res.qrc index b245f48f..63a40431 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -121,6 +121,7 @@ qtquickcontrols2.conf qml/TimelineView.qml + qml/ActiveCallBar.qml qml/Avatar.qml qml/ImageButton.qml qml/MatrixText.qml diff --git a/src/CallManager.cpp b/src/CallManager.cpp index 2c0f9d5a..b1d1a75a 100644 --- a/src/CallManager.cpp +++ b/src/CallManager.cpp @@ -152,6 +152,12 @@ CallManager::sendInvite(const QString &roomid) generateCallID(); nhlog::ui()->debug("WebRTC: call id: {} - creating invite", callid_); + 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; + callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); + emit newCallParty(); playRingtone("qrc:/media/media/ringback.ogg", true); if (!session_.createOffer()) { emit ChatPage::instance()->showNotification("Problem setting up call."); @@ -186,7 +192,7 @@ CallManager::hangUp(CallHangUp::Reason reason) } bool -CallManager::onActiveCall() +CallManager::onActiveCall() const { return session_.state() != webrtc::State::DISCONNECTED; } @@ -252,6 +258,9 @@ 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; + callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); + emit newCallParty(); auto dialog = new dialogs::AcceptCall(caller.user_id, caller.display_name, QString::fromStdString(roomInfo.name), @@ -364,6 +373,8 @@ void CallManager::clear() { roomid_.clear(); + callPartyName_.clear(); + callPartyAvatarUrl_.clear(); callid_.clear(); remoteICECandidates_.clear(); } diff --git a/src/CallManager.h b/src/CallManager.h index 1de8d2ae..640230a4 100644 --- a/src/CallManager.h +++ b/src/CallManager.h @@ -29,7 +29,9 @@ public: void sendInvite(const QString &roomid); void hangUp( mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User); - bool onActiveCall(); + bool onActiveCall() const; + QString callPartyName() const { return callPartyName_; } + QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; } void refreshTurnServer(); public slots: @@ -40,6 +42,7 @@ signals: 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 turnServerRetrieved(const mtx::responses::TurnServer &); private slots: @@ -48,6 +51,8 @@ private slots: private: WebRTCSession &session_; QString roomid_; + QString callPartyName_; + QString callPartyAvatarUrl_; std::string callid_; const uint32_t timeoutms_ = 120000; std::vector remoteICECandidates_; diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp index b4eaadab..14f17030 100644 --- a/src/WebRTCSession.cpp +++ b/src/WebRTCSession.cpp @@ -636,7 +636,20 @@ WebRTCSession::createPipeline(int opusPayloadType) } bool -WebRTCSession::toggleMuteAudioSource() +WebRTCSession::isMicMuted() const +{ + if (state_ < State::INITIATED) + return false; + + GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel"); + gboolean muted; + g_object_get(srclevel, "mute", &muted, nullptr); + gst_object_unref(srclevel); + return muted; +} + +bool +WebRTCSession::toggleMicMute() { if (state_ < State::INITIATED) return false; diff --git a/src/WebRTCSession.h b/src/WebRTCSession.h index 9593def9..83cabf5c 100644 --- a/src/WebRTCSession.h +++ b/src/WebRTCSession.h @@ -47,7 +47,8 @@ public: bool acceptAnswer(const std::string &sdp); void acceptICECandidates(const std::vector &); - bool toggleMuteAudioSource(); + bool isMicMuted() const; + bool toggleMicMute(); void end(); void setStunServer(const std::string &stunServer) { stunServer_ = stunServer; } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 09ab8cf8..2b453e56 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -7,7 +7,6 @@ #include #include "BlurhashProvider.h" -#include "CallManager.h" #include "ChatPage.h" #include "ColorImageProvider.h" #include "DelegateChooser.h" @@ -145,6 +144,8 @@ TimelineViewManager::TimelineViewManager(QSharedPointer userSettin &WebRTCSession::stateChanged, this, &TimelineViewManager::callStateChanged); + connect( + callManager_, &CallManager::newCallParty, this, &TimelineViewManager::callPartyChanged); } void @@ -216,6 +217,13 @@ TimelineViewManager::escapeEmoji(QString str) const return utils::replaceEmoji(str); } +void +TimelineViewManager::toggleMicMute() +{ + WebRTCSession::instance().toggleMicMute(); + emit micMuteChanged(); +} + void TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const { diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 243927ef..9ff9adac 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -10,6 +10,7 @@ #include #include "Cache.h" +#include "CallManager.h" #include "Logging.h" #include "TimelineModel.h" #include "Utils.h" @@ -19,7 +20,6 @@ class MxcImageProvider; class BlurhashProvider; -class CallManager; class ColorImageProvider; class UserSettings; class ChatPage; @@ -35,6 +35,9 @@ class TimelineViewManager : public QObject Q_PROPERTY( bool isNarrowView MEMBER isNarrowView_ READ isNarrowView NOTIFY narrowViewChanged) Q_PROPERTY(webrtc::State callState READ callState NOTIFY callStateChanged) + Q_PROPERTY(QString callPartyName READ callPartyName NOTIFY callPartyChanged) + Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY callPartyChanged) + Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged) public: TimelineViewManager(QSharedPointer userSettings, @@ -51,10 +54,10 @@ public: Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } bool isNarrowView() const { return isNarrowView_; } webrtc::State callState() const { return WebRTCSession::instance().state(); } - Q_INVOKABLE bool toggleMuteAudioSource() - { - return WebRTCSession::instance().toggleMuteAudioSource(); - } + 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 openImageOverlay(QString mxcUrl, QString eventId) const; Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString escapeEmoji(QString str) const; @@ -80,6 +83,8 @@ signals: void showRoomList(); void narrowViewChanged(); void callStateChanged(webrtc::State); + void callPartyChanged(); + void micMuteChanged(); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids);