summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorRohit Sutradhar <rohitsutradhar311@gmail.com>2022-10-14 19:19:05 +0530
committerGitHub <noreply@github.com>2022-10-14 13:49:05 +0000
commitac48c332867e773e0e0eb9ad0139b7b625e26851 (patch)
tree9f4799c650889bb770f72e127441426c9ae42a07 /src
parentAdd toggle to disable decrypting notifications (diff)
downloadnheko-ac48c332867e773e0e0eb9ad0139b7b625e26851.tar.xz
VoIP v1 implementation (#1161)
* Initial commit for VoIP v1 implementation

* Added draft of event handlers for voip methods

* Added event handlers for VoIP events, added rejectCall, added version tracking for call version for V0 and V1 compatibility

* Added call events to the general message pipeline. Modified Call Reject mechanism

* Added message delegates for new events. Modified hidden events. Updated handle events.

* Updated implementation to keep track of calls on other devices

* Fixed linting

* Fixed code warnings

* Fixed minor bugs

* fixed ci

* Added acceptNegotiation method definition when missing gstreamer

* Fixed warnings

* Fixed linting
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp17
-rw-r--r--src/ChatPage.cpp3
-rw-r--r--src/PowerlevelsEditModels.cpp2
-rw-r--r--src/Utils.cpp3
-rw-r--r--src/Utils.h7
-rw-r--r--src/timeline/TimelineModel.cpp65
-rw-r--r--src/timeline/TimelineModel.h6
-rw-r--r--src/timeline/TimelineViewManager.cpp22
-rw-r--r--src/timeline/TimelineViewManager.h3
-rw-r--r--src/ui/HiddenEvents.cpp11
-rw-r--r--src/voip/CallManager.cpp402
-rw-r--r--src/voip/CallManager.h37
-rw-r--r--src/voip/WebRTCSession.cpp15
-rw-r--r--src/voip/WebRTCSession.h1
14 files changed, 521 insertions, 73 deletions
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 863f0683..a83b73f7 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -184,8 +184,18 @@ Cache::isHiddenEvent(lmdb::txn &txn,
     hiddenEvents.hidden_event_types = std::vector{
       EventType::Reaction,
       EventType::CallCandidates,
+      EventType::CallNegotiate,
       EventType::Unsupported,
     };
+    // check if selected answer is from to local user
+    /*
+     * localUser accepts/rejects the call and it is selected by caller - No message
+     * Another User accepts/rejects the call and it is selected by caller - "Call answered/rejected
+     * elsewhere"
+     */
+    bool callLocalUser_ = true;
+    if (callLocalUser_)
+        hiddenEvents.hidden_event_types->push_back(EventType::CallSelectAnswer);
 
     if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, "")) {
         auto h = std::get<
@@ -1661,11 +1671,18 @@ isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallAnswer> &)
 {
     return true;
 }
+
 auto
 isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallHangUp> &)
 {
     return true;
 }
+
+// auto
+// isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &)
+// {
+//     return true;
+// }
 }
 
 void
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 3736ec6b..756ef425 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -363,6 +363,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QObject *parent)
     connectCallMessage<mtx::events::voip::CallCandidates>();
     connectCallMessage<mtx::events::voip::CallAnswer>();
     connectCallMessage<mtx::events::voip::CallHangUp>();
+    connectCallMessage<mtx::events::voip::CallSelectAnswer>();
+    connectCallMessage<mtx::events::voip::CallReject>();
+    connectCallMessage<mtx::events::voip::CallNegotiate>();
 }
 
 void
diff --git a/src/PowerlevelsEditModels.cpp b/src/PowerlevelsEditModels.cpp
index 8cc2dcc0..2c2d4a7f 100644
--- a/src/PowerlevelsEditModels.cpp
+++ b/src/PowerlevelsEditModels.cpp
@@ -222,6 +222,8 @@ PowerlevelsTypeListModel::data(const QModelIndex &index, int role) const
             return tr("Answer a call");
         else if (type.type == "m.call.hangup")
             return tr("Hang up a call");
+        else if (type.type == "m.call.reject")
+            return tr("Reject a call");
         else if (type.type == "im.ponies.room_emotes")
             return tr("Change the room emotes");
         return QString::fromStdString(type.type);
diff --git a/src/Utils.cpp b/src/Utils.cpp
index cedf537a..b92c6cce 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -219,6 +219,7 @@ utils::getMessageDescription(const TimelineEvent &event,
     using CallInvite = mtx::events::RoomEvent<mtx::events::voip::CallInvite>;
     using CallAnswer = mtx::events::RoomEvent<mtx::events::voip::CallAnswer>;
     using CallHangUp = mtx::events::RoomEvent<mtx::events::voip::CallHangUp>;
+    using CallReject = mtx::events::RoomEvent<mtx::events::voip::CallReject>;
     using Encrypted  = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
 
     if (std::holds_alternative<Audio>(event)) {
@@ -241,6 +242,8 @@ utils::getMessageDescription(const TimelineEvent &event,
         return createDescriptionInfo<CallAnswer>(event, localUser, displayName);
     } else if (std::holds_alternative<CallHangUp>(event)) {
         return createDescriptionInfo<CallHangUp>(event, localUser, displayName);
+    } else if (std::holds_alternative<CallReject>(event)) {
+        return createDescriptionInfo<CallReject>(event, localUser, displayName);
     } else if (std::holds_alternative<mtx::events::Sticker>(event)) {
         return createDescriptionInfo<mtx::events::Sticker>(event, localUser, displayName);
     } else if (auto msg = std::get_if<Encrypted>(&event); msg != nullptr) {
diff --git a/src/Utils.h b/src/Utils.h
index 8f8b9cad..e4c3ccb3 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -110,6 +110,7 @@ messageDescription(const QString &username = QString(),
     using CallInvite = mtx::events::RoomEvent<mtx::events::voip::CallInvite>;
     using CallAnswer = mtx::events::RoomEvent<mtx::events::voip::CallAnswer>;
     using CallHangUp = mtx::events::RoomEvent<mtx::events::voip::CallHangUp>;
+    using CallReject = mtx::events::RoomEvent<mtx::events::voip::CallReject>;
     using Encrypted  = mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>;
 
     if (std::is_same<T, Audio>::value) {
@@ -185,6 +186,12 @@ messageDescription(const QString &username = QString(),
         else
             return QCoreApplication::translate("message-description sent:", "%1 ended a call")
               .arg(username);
+    } else if (std::is_same<T, CallReject>::value) {
+        if (isLocal)
+            return QCoreApplication::translate("message-description sent:", "You rejected a call");
+        else
+            return QCoreApplication::translate("message-description sent:", "%1 rejected a call")
+              .arg(username);
     } else {
         return QCoreApplication::translate("utils", "Unknown Message Type");
     }
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 0e726bde..6aa81d8b 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -138,6 +138,20 @@ struct RoomEventType
     {
         return qml_mtx_events::EventType::CallCandidates;
     }
+    qml_mtx_events::EventType
+    operator()(const mtx::events::Event<mtx::events::voip::CallSelectAnswer> &)
+    {
+        return qml_mtx_events::EventType::CallSelectAnswer;
+    }
+    qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::voip::CallReject> &)
+    {
+        return qml_mtx_events::EventType::CallReject;
+    }
+    qml_mtx_events::EventType
+    operator()(const mtx::events::Event<mtx::events::voip::CallNegotiate> &)
+    {
+        return qml_mtx_events::EventType::CallNegotiate;
+    }
     // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return
     // ::EventType::LocationMessage; }
 };
@@ -258,6 +272,15 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
     /// m.call.candidates
     case qml_mtx_events::CallCandidates:
         return mtx::events::EventType::CallCandidates;
+    /// m.call.select_answer
+    case qml_mtx_events::CallSelectAnswer:
+        return mtx::events::EventType::CallSelectAnswer;
+    /// m.call.reject
+    case qml_mtx_events::CallReject:
+        return mtx::events::EventType::CallReject;
+    /// m.call.negotiate
+    case qml_mtx_events::CallNegotiate:
+        return mtx::events::EventType::CallNegotiate;
     /// m.room.canonical_alias
     case qml_mtx_events::CanonicalAlias:
         return mtx::events::EventType::RoomCanonicalAlias;
@@ -922,16 +945,22 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
         }
 
         if (std::holds_alternative<RoomEvent<voip::CallCandidates>>(e) ||
+            std::holds_alternative<RoomEvent<voip::CallNegotiate>>(e) ||
             std::holds_alternative<RoomEvent<voip::CallInvite>>(e) ||
             std::holds_alternative<RoomEvent<voip::CallAnswer>>(e) ||
+            std::holds_alternative<RoomEvent<voip::CallSelectAnswer>>(e) ||
+            std::holds_alternative<RoomEvent<voip::CallReject>>(e) ||
             std::holds_alternative<RoomEvent<voip::CallHangUp>>(e))
             std::visit(
               [this](auto &event) {
                   event.room_id = room_id_.toStdString();
-                  if constexpr (std::is_same_v<std::decay_t<decltype(event)>,
-                                               RoomEvent<voip::CallAnswer>> ||
-                                std::is_same_v<std::decay_t<decltype(event)>,
-                                               RoomEvent<voip::CallHangUp>>)
+                  if constexpr (
+                    std::is_same_v<std::decay_t<decltype(event)>, RoomEvent<voip::CallAnswer>> ||
+                    std::is_same_v<std::decay_t<decltype(event)>, RoomEvent<voip::CallInvite>> ||
+                    std::is_same_v<std::decay_t<decltype(event)>,
+                                   RoomEvent<voip::CallSelectAnswer>> ||
+                    std::is_same_v<std::decay_t<decltype(event)>, RoomEvent<voip::CallReject>> ||
+                    std::is_same_v<std::decay_t<decltype(event)>, RoomEvent<voip::CallHangUp>>)
                       emit newCallEvent(event);
                   else {
                       if (event.sender != http::client()->user_id().to_string())
@@ -1007,6 +1036,17 @@ isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallHangUp> &)
     return true;
 }
 
+auto
+isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &)
+{
+    return true;
+}
+auto
+isMessage(const mtx::events::RoomEvent<mtx::events::voip::CallSelectAnswer> &)
+{
+    return true;
+}
+
 // Workaround. We also want to see a room at the top, if we just joined it
 auto
 isYourJoin(const mtx::events::StateEvent<mtx::events::state::Member> &e)
@@ -1503,6 +1543,23 @@ struct SendMessageVisitor
         sendRoomEvent<mtx::events::voip::CallHangUp, mtx::events::EventType::CallHangUp>(event);
     }
 
+    void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallSelectAnswer> &event)
+    {
+        sendRoomEvent<mtx::events::voip::CallSelectAnswer,
+                      mtx::events::EventType::CallSelectAnswer>(event);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &event)
+    {
+        sendRoomEvent<mtx::events::voip::CallReject, mtx::events::EventType::CallReject>(event);
+    }
+
+    void operator()(const mtx::events::RoomEvent<mtx::events::voip::CallNegotiate> &event)
+    {
+        sendRoomEvent<mtx::events::voip::CallNegotiate, mtx::events::EventType::CallNegotiate>(
+          event);
+    }
+
     void operator()(const mtx::events::RoomEvent<mtx::events::msg::KeyVerificationRequest> &msg)
     {
         sendRoomEvent<mtx::events::msg::KeyVerificationRequest,
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index a4904f4f..2a04c9c9 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -58,6 +58,12 @@ enum EventType
     CallHangUp,
     /// m.call.candidates
     CallCandidates,
+    /// m.call.select_answer
+    CallSelectAnswer,
+    /// m.call.reject
+    CallReject,
+    /// m.call.negotiate
+    CallNegotiate,
     /// m.room.canonical_alias
     CanonicalAlias,
     /// m.room.create
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index a75a79d1..9c46d201 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -378,6 +378,28 @@ TimelineViewManager::queueCallMessage(const QString &roomid,
 }
 
 void
+TimelineViewManager::queueCallMessage(const QString &roomid,
+                                      const mtx::events::voip::CallSelectAnswer &callSelectAnswer)
+{
+    if (auto room = rooms_->getRoomById(roomid))
+        room->sendMessageEvent(callSelectAnswer, mtx::events::EventType::CallSelectAnswer);
+}
+void
+TimelineViewManager::queueCallMessage(const QString &roomid,
+                                      const mtx::events::voip::CallReject &callReject)
+{
+    if (auto room = rooms_->getRoomById(roomid))
+        room->sendMessageEvent(callReject, mtx::events::EventType::CallReject);
+}
+void
+TimelineViewManager::queueCallMessage(const QString &roomid,
+                                      const mtx::events::voip::CallNegotiate &callNegotiate)
+{
+    if (auto room = rooms_->getRoomById(roomid))
+        room->sendMessageEvent(callNegotiate, mtx::events::EventType::CallNegotiate);
+}
+
+void
 TimelineViewManager::focusMessageInput()
 {
     emit focusInput();
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index c0895b2c..c305fe66 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -109,6 +109,9 @@ public slots:
     void queueCallMessage(const QString &roomid, const mtx::events::voip::CallCandidates &);
     void queueCallMessage(const QString &roomid, const mtx::events::voip::CallAnswer &);
     void queueCallMessage(const QString &roomid, const mtx::events::voip::CallHangUp &);
+    void queueCallMessage(const QString &roomid, const mtx::events::voip::CallSelectAnswer &);
+    void queueCallMessage(const QString &roomid, const mtx::events::voip::CallReject &);
+    void queueCallMessage(const QString &roomid, const mtx::events::voip::CallNegotiate &);
 
     void setVideoCallItem();
 
diff --git a/src/ui/HiddenEvents.cpp b/src/ui/HiddenEvents.cpp
index 1ebcda3e..4686dfef 100644
--- a/src/ui/HiddenEvents.cpp
+++ b/src/ui/HiddenEvents.cpp
@@ -17,9 +17,20 @@ HiddenEvents::load()
     hiddenEvents.hidden_event_types = std::vector{
       EventType::Reaction,
       EventType::CallCandidates,
+      EventType::CallNegotiate,
       EventType::Unsupported,
     };
 
+    // check if selected answer is from to local user
+    /*
+     * localUser accepts/rejects the call and it is selected by caller - No message
+     * Another User accepts/rejects the call and it is selected by caller - "Call answered/rejected
+     * elsewhere"
+     */
+    bool callLocalUser_ = true;
+    if (callLocalUser_)
+        hiddenEvents.hidden_event_types->push_back(EventType::CallSelectAnswer);
+
     if (auto temp =
           cache::client()->getAccountData(mtx::events::EventType::NhekoHiddenEvents, "")) {
         auto h = std::get<
diff --git a/src/voip/CallManager.cpp b/src/voip/CallManager.cpp
index 14f3adf8..5e711499 100644
--- a/src/voip/CallManager.cpp
+++ b/src/voip/CallManager.cpp
@@ -24,6 +24,10 @@
 
 #include "mtx/responses/turn_server.hpp"
 
+/*
+ * Select Answer when one instance of the client supports v0
+ */
+
 #ifdef XCB_AVAILABLE
 #include <xcb/xcb.h>
 #include <xcb/xcb_ewmh.h>
@@ -67,10 +71,15 @@ CallManager::CallManager(QObject *parent)
       this,
       [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
           nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_);
-          emit newMessage(
-            roomid_,
-            CallInvite{callid_, partyid_, SDO{sdp, SDO::Type::Offer}, "0", timeoutms_, invitee_});
-          emit newMessage(roomid_, CallCandidates{callid_, partyid_, candidates, "0"});
+          emit newMessage(roomid_,
+                          CallInvite{callid_,
+                                     partyid_,
+                                     SDO{sdp, SDO::Type::Offer},
+                                     callPartyVersion_,
+                                     timeoutms_,
+                                     invitee_});
+          emit newMessage(roomid_,
+                          CallCandidates{callid_, partyid_, candidates, callPartyVersion_});
           std::string callid(callid_);
           QTimer::singleShot(timeoutms_, this, [this, callid]() {
               if (session_.state() == webrtc::State::OFFERSENT && callid == callid_) {
@@ -87,8 +96,10 @@ CallManager::CallManager(QObject *parent)
       this,
       [this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
           nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_);
-          emit newMessage(roomid_, CallAnswer{callid_, partyid_, "0", SDO{sdp, SDO::Type::Answer}});
-          emit newMessage(roomid_, CallCandidates{callid_, partyid_, candidates, "0"});
+          emit newMessage(
+            roomid_, CallAnswer{callid_, partyid_, callPartyVersion_, SDO{sdp, SDO::Type::Answer}});
+          emit newMessage(roomid_,
+                          CallCandidates{callid_, partyid_, candidates, callPartyVersion_});
       });
 
     connect(&session_,
@@ -96,7 +107,8 @@ CallManager::CallManager(QObject *parent)
             this,
             [this](const CallCandidates::Candidate &candidate) {
                 nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_);
-                emit newMessage(roomid_, CallCandidates{callid_, partyid_, {candidate}, "0"});
+                emit newMessage(roomid_,
+                                CallCandidates{callid_, partyid_, {candidate}, callPartyVersion_});
             });
 
     connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer);
@@ -170,23 +182,14 @@ CallManager::CallManager(QObject *parent)
 void
 CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int windowIndex)
 {
-    if (isOnCall())
+    if (isOnCall() || isOnCallOnOtherDevice()) {
+        if (isOnCallOnOtherDevice_ != "")
+            emit ChatPage::instance()->showNotification(
+              QStringLiteral("User is already in a call"));
         return;
-    if (callType == CallType::SCREEN) {
-        if (!screenShareSupported())
-            return;
-        if (windows_.empty() || windowIndex >= windows_.size()) {
-            nhlog::ui()->error("WebRTC: window index out of range");
-            return;
-        }
     }
 
     auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
-    if (roomInfo.member_count != 2) {
-        emit ChatPage::instance()->showNotification(
-          QStringLiteral("Calls are limited to 1:1 rooms."));
-        return;
-    }
 
     std::string errorMessage;
     if (!session_.havePlugins(false, &errorMessage) ||
@@ -198,17 +201,60 @@ CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int w
 
     callType_ = callType;
     roomid_   = roomid;
-    session_.setTurnServers(turnURIs_);
     generateCallID();
+    std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
+    const RoomMember *callee;
+    if (roomInfo.member_count == 1)
+        callee = &members.front();
+    else if (roomInfo.member_count == 2)
+        callee = members.front().user_id == utils::localUser() ? &members.back() : &members.front();
+    else {
+        emit ChatPage::instance()->showNotification(
+          QStringLiteral("Calls are limited to rooms with less than two members"));
+        return;
+    }
+
+    if (callType == CallType::SCREEN) {
+        if (!screenShareSupported())
+            return;
+        if (windows_.empty() || windowIndex >= windows_.size()) {
+            nhlog::ui()->error("WebRTC: window index out of range");
+            return;
+        }
+    }
+
+    if (haveCallInvite_) {
+        nhlog::ui()->debug(
+          "WebRTC: Discarding outbound call for inbound call. localUser is polite party");
+        if (callParty_ == callee->user_id) {
+            if (callType == callType_)
+                acceptInvite();
+            else {
+                emit ChatPage::instance()->showNotification(
+                  QStringLiteral("Can't place call. Call types do not match"));
+                emit newMessage(
+                  roomid_,
+                  CallHangUp{callid_, partyid_, callPartyVersion_, CallHangUp::Reason::UserBusy});
+            }
+        } else {
+            emit ChatPage::instance()->showNotification(
+              QStringLiteral("Already on a call with a different user"));
+            emit newMessage(
+              roomid_,
+              CallHangUp{callid_, partyid_, callPartyVersion_, CallHangUp::Reason::UserBusy});
+        }
+        return;
+    }
+
+    session_.setTurnServers(turnURIs_);
     std::string strCallType =
       callType_ == CallType::VOICE ? "voice" : (callType_ == CallType::VIDEO ? "video" : "screen");
+
     nhlog::ui()->debug("WebRTC: call id: {} - creating {} invite", callid_, strCallType);
-    std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
-    const RoomMember &callee =
-      members.front().user_id == utils::localUser() ? members.back() : members.front();
-    callParty_            = callee.user_id;
-    callPartyDisplayName_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name;
+    callParty_            = callee->user_id;
+    callPartyDisplayName_ = callee->display_name.isEmpty() ? callee->user_id : callee->display_name;
     callPartyAvatarUrl_   = QString::fromStdString(roomInfo.avatar_url);
+    invitee_              = callParty_.toStdString();
     emit newInviteState();
     playRingtone(QUrl(QStringLiteral("qrc:/media/media/ringback.ogg")), true);
     if (!session_.createOffer(callType,
@@ -249,7 +295,7 @@ CallManager::hangUp(CallHangUp::Reason reason)
     if (!callid_.empty()) {
         nhlog::ui()->debug(
           "WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason));
-        emit newMessage(roomid_, CallHangUp{callid_, partyid_, "0", reason});
+        emit newMessage(roomid_, CallHangUp{callid_, partyid_, callPartyVersion_, reason});
         endCall();
     }
 }
@@ -259,7 +305,9 @@ CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
 {
 #ifdef GSTREAMER_AVAILABLE
     if (handleEvent<CallInvite>(event) || handleEvent<CallCandidates>(event) ||
-        handleEvent<CallAnswer>(event) || handleEvent<CallHangUp>(event))
+        handleEvent<CallNegotiate>(event) || handleEvent<CallSelectAnswer>(event) ||
+        handleEvent<CallAnswer>(event) || handleEvent<CallReject>(event) ||
+        handleEvent<CallHangUp>(event))
         return;
 #else
     (void)event;
@@ -289,41 +337,121 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
                                [](unsigned char c1, unsigned char c2) {
                                    return std::tolower(c1) == std::tolower(c2);
                                }) != sdp.cend();
-
-    nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}",
+    nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from ({},{}) ",
                        callInviteEvent.content.call_id,
                        (isVideo ? "video" : "voice"),
-                       callInviteEvent.sender);
+                       callInviteEvent.sender,
+                       callInviteEvent.content.party_id);
 
     if (callInviteEvent.content.call_id.empty())
         return;
 
-    auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
-    if (isOnCall() || roomInfo.member_count != 2) {
-        emit newMessage(
-          QString::fromStdString(callInviteEvent.room_id),
-          CallHangUp{
-            callInviteEvent.content.call_id, partyid_, "0", CallHangUp::Reason::InviteTimeOut});
-        return;
+    if (callInviteEvent.sender == utils::localUser().toStdString()) {
+        if (callInviteEvent.content.party_id == partyid_)
+            return;
+        else {
+            if (callInviteEvent.content.invitee != utils::localUser().toStdString()) {
+                isOnCallOnOtherDevice_ = callInviteEvent.content.call_id;
+                emit newCallDeviceState();
+                nhlog::ui()->debug("WebRTC: User is on call on other device.");
+                return;
+            }
+        }
     }
 
+    auto roomInfo     = cache::singleRoomInfo(callInviteEvent.room_id);
+    callPartyVersion_ = callInviteEvent.content.version;
+
     const QString &ringtone = UserSettings::instance()->ringtone();
-    if (ringtone != QLatin1String("Mute"))
+    bool sharesRoom         = true;
+
+    std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
+    const RoomMember &caller =
+      *std::find_if(members.begin(), members.end(), [&](RoomMember member) {
+          return member.user_id.toStdString() == callInviteEvent.sender;
+      });
+    if (isOnCall() || isOnCallOnOtherDevice()) {
+        if (isOnCallOnOtherDevice_ != "")
+            return;
+        if (callParty_.toStdString() == callInviteEvent.sender) {
+            if (session_.state() == webrtc::State::OFFERSENT) {
+                if (callid_ > callInviteEvent.content.call_id) {
+                    endCall();
+                    callParty_ = caller.user_id;
+                    callPartyDisplayName_ =
+                      caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
+                    callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
+
+                    roomid_ = QString::fromStdString(callInviteEvent.room_id);
+                    callid_ = callInviteEvent.content.call_id;
+                    remoteICECandidates_.clear();
+                    haveCallInvite_ = true;
+                    callType_       = isVideo ? CallType::VIDEO : CallType::VOICE;
+                    inviteSDP_      = callInviteEvent.content.offer.sdp;
+                    emit newInviteState();
+                    acceptInvite();
+                }
+                return;
+            } else if (session_.state() < webrtc::State::OFFERSENT)
+                endCall();
+            else
+                return;
+        } else
+            return;
+    }
+
+    if (callPartyVersion_ == "0") {
+        if (roomInfo.member_count != 2) {
+            emit newMessage(QString::fromStdString(callInviteEvent.room_id),
+                            CallHangUp{callInviteEvent.content.call_id,
+                                       partyid_,
+                                       callPartyVersion_,
+                                       CallHangUp::Reason::InviteTimeOut});
+            return;
+        }
+    } else {
+        if (caller.user_id == utils::localUser() &&
+            callInviteEvent.content.party_id == partyid_) // remote echo
+            return;
+
+        if (roomInfo.member_count == 2 || // general call in room with two members
+            (roomInfo.member_count == 1 &&
+             partyid_ !=
+               callInviteEvent.content.party_id) ||  // self call, ring if not the same party_id
+            callInviteEvent.content.invitee == "" || // empty, meant for everyone
+            callInviteEvent.content.invitee ==
+              utils::localUser().toStdString()) // meant specifically for local user
+        {
+            if (roomInfo.member_count > 2) {
+                // check if shares room
+                sharesRoom = checkSharesRoom(QString::fromStdString(callInviteEvent.room_id),
+                                             callInviteEvent.content.invitee);
+            }
+        } else {
+            emit newMessage(QString::fromStdString(callInviteEvent.room_id),
+                            CallHangUp{callInviteEvent.content.call_id,
+                                       partyid_,
+                                       callPartyVersion_,
+                                       CallHangUp::Reason::InviteTimeOut});
+            return;
+        }
+    }
+
+    // ring if not mute or does not have direct message room
+    if (ringtone != QLatin1String("Mute") && sharesRoom)
         playRingtone(ringtone == QLatin1String("Default")
                        ? QUrl(QStringLiteral("qrc:/media/media/ring.ogg"))
                        : QUrl::fromLocalFile(ringtone),
                      true);
-    roomid_ = QString::fromStdString(callInviteEvent.room_id);
-    callid_ = callInviteEvent.content.call_id;
-    remoteICECandidates_.clear();
 
-    std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
-    const RoomMember &caller =
-      members.front().user_id == utils::localUser() ? members.back() : members.front();
     callParty_            = caller.user_id;
     callPartyDisplayName_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name;
     callPartyAvatarUrl_   = QString::fromStdString(roomInfo.avatar_url);
 
+    roomid_ = QString::fromStdString(callInviteEvent.room_id);
+    callid_ = callInviteEvent.content.call_id;
+    remoteICECandidates_.clear();
+
     haveCallInvite_ = true;
     callType_       = isVideo ? CallType::VIDEO : CallType::VOICE;
     inviteSDP_      = callInviteEvent.content.offer.sdp;
@@ -333,6 +461,8 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
 void
 CallManager::acceptInvite()
 {
+    // if call was accepted/rejected elsewhere and m.call.select_answer is received
+    // before acceptInvite
     if (!haveCallInvite_)
         return;
 
@@ -341,7 +471,7 @@ CallManager::acceptInvite()
     if (!session_.havePlugins(false, &errorMessage) ||
         (callType_ == CallType::VIDEO && !session_.havePlugins(true, &errorMessage))) {
         emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
-        hangUp();
+        hangUp(CallHangUp::Reason::UserMediaFailed);
         return;
     }
 
@@ -358,14 +488,30 @@ CallManager::acceptInvite()
 }
 
 void
+CallManager::rejectInvite()
+{
+    if (callPartyVersion_ == "0") {
+        hangUp();
+        // send m.call.reject after sending hangup as mentioned in MSC2746
+        emit newMessage(roomid_, CallReject{callid_, partyid_, callPartyVersion_});
+    }
+    if (!callid_.empty()) {
+        nhlog::ui()->debug("WebRTC: call id: {} - rejecting call", callid_);
+        emit newMessage(roomid_, CallReject{callid_, partyid_, callPartyVersion_});
+        endCall(false);
+    }
+}
+
+void
 CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
 {
-    if (callCandidatesEvent.sender == utils::localUser().toStdString())
+    if (callCandidatesEvent.sender == utils::localUser().toStdString() &&
+        callCandidatesEvent.content.party_id == partyid_)
         return;
-
-    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}",
+    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from ({}, {})",
                        callCandidatesEvent.content.call_id,
-                       callCandidatesEvent.sender);
+                       callCandidatesEvent.sender,
+                       callCandidatesEvent.content.party_id);
 
     if (callid_ == callCandidatesEvent.content.call_id) {
         if (isOnCall())
@@ -382,20 +528,31 @@ CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
 void
 CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
 {
-    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}",
+    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from ({}, {})",
                        callAnswerEvent.content.call_id,
-                       callAnswerEvent.sender);
+                       callAnswerEvent.sender,
+                       callAnswerEvent.content.party_id);
+    if (answerSelected_)
+        return;
 
     if (callAnswerEvent.sender == utils::localUser().toStdString() &&
         callid_ == callAnswerEvent.content.call_id) {
+        if (partyid_ == callAnswerEvent.content.party_id)
+            return;
+
         if (!isOnCall()) {
             emit ChatPage::instance()->showNotification(
               QStringLiteral("Call answered on another device."));
             stopRingtone();
             haveCallInvite_ = false;
+            if (callPartyVersion_ != "1") {
+                isOnCallOnOtherDevice_ = callid_;
+                emit newCallDeviceState();
+            }
             emit newInviteState();
         }
-        return;
+        if (callParty_ != utils::localUser())
+            return;
     }
 
     if (isOnCall() && callid_ == callAnswerEvent.content.call_id) {
@@ -405,18 +562,139 @@ CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
             hangUp();
         }
     }
+    emit newMessage(
+      roomid_,
+      CallSelectAnswer{callid_, partyid_, callPartyVersion_, callAnswerEvent.content.party_id});
+    selectedpartyid_ = callAnswerEvent.content.party_id;
+    answerSelected_  = true;
 }
 
 void
 CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
 {
-    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}",
+    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from ({}, {})",
                        callHangUpEvent.content.call_id,
                        callHangUpReasonString(callHangUpEvent.content.reason),
-                       callHangUpEvent.sender);
+                       callHangUpEvent.sender,
+                       callHangUpEvent.content.party_id);
+
+    if (callid_ == callHangUpEvent.content.call_id ||
+        isOnCallOnOtherDevice_ == callHangUpEvent.content.call_id)
+        endCall();
+}
+
+void
+CallManager::handleEvent(const RoomEvent<CallSelectAnswer> &callSelectAnswerEvent)
+{
+    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallSelectAnswer from ({}, {})",
+                       callSelectAnswerEvent.content.call_id,
+                       callSelectAnswerEvent.sender,
+                       callSelectAnswerEvent.content.party_id);
+    if (callSelectAnswerEvent.sender == utils::localUser().toStdString()) {
+        if (callSelectAnswerEvent.content.party_id != partyid_) {
+            if (std::find(rejectCallPartyIDs_.begin(),
+                          rejectCallPartyIDs_.begin(),
+                          callSelectAnswerEvent.content.selected_party_id) !=
+                rejectCallPartyIDs_.end())
+                endCall();
+            else {
+                if (callSelectAnswerEvent.content.selected_party_id == partyid_)
+                    return;
+                nhlog::ui()->debug("WebRTC: call id: {} - user is on call with this user!",
+                                   callSelectAnswerEvent.content.call_id);
+                isOnCallOnOtherDevice_ = callSelectAnswerEvent.content.call_id;
+                emit newCallDeviceState();
+            }
+        }
+        return;
+    } else if (callid_ == callSelectAnswerEvent.content.call_id) {
+        if (callSelectAnswerEvent.content.selected_party_id != partyid_) {
+            bool endAllCalls = false;
+            if (std::find(rejectCallPartyIDs_.begin(),
+                          rejectCallPartyIDs_.begin(),
+                          callSelectAnswerEvent.content.selected_party_id) !=
+                rejectCallPartyIDs_.end())
+                endAllCalls = true;
+            else {
+                isOnCallOnOtherDevice_ = callid_;
+                emit newCallDeviceState();
+            }
+            endCall(endAllCalls);
+        } else if (session_.state() == webrtc::State::DISCONNECTED)
+            endCall();
+    }
+}
 
-    if (callid_ == callHangUpEvent.content.call_id)
+void
+CallManager::handleEvent(const RoomEvent<CallReject> &callRejectEvent)
+{
+    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallReject from ({}, {})",
+                       callRejectEvent.content.call_id,
+                       callRejectEvent.sender,
+                       callRejectEvent.content.party_id);
+    if (answerSelected_)
+        return;
+
+    rejectCallPartyIDs_.push_back(callRejectEvent.content.party_id);
+    // check remote echo
+    if (callRejectEvent.sender == utils::localUser().toStdString()) {
+        if (callRejectEvent.content.party_id != partyid_ && callParty_ != utils::localUser())
+            emit ChatPage::instance()->showNotification(
+              QStringLiteral("Call rejected on another device."));
         endCall();
+        return;
+    }
+
+    if (callRejectEvent.content.call_id == callid_) {
+        if (session_.state() == webrtc::State::OFFERSENT) {
+            // only accept reject if webrtc is in OFFERSENT state, else call has been accepted
+            emit newMessage(
+              roomid_,
+              CallSelectAnswer{
+                callid_, partyid_, callPartyVersion_, callRejectEvent.content.party_id});
+            endCall();
+        }
+    }
+}
+
+void
+CallManager::handleEvent(const RoomEvent<CallNegotiate> &callNegotiateEvent)
+{
+    nhlog::ui()->debug("WebRTC: call id: {} - incoming CallNegotiate from ({}, {})",
+                       callNegotiateEvent.content.call_id,
+                       callNegotiateEvent.sender,
+                       callNegotiateEvent.content.party_id);
+
+    std::string negotiationSDP_ = callNegotiateEvent.content.description.sdp;
+    if (!session_.acceptNegotiation(negotiationSDP_)) {
+        emit ChatPage::instance()->showNotification(QStringLiteral("Problem accepting new SDP"));
+        hangUp();
+        return;
+    }
+    session_.acceptICECandidates(remoteICECandidates_);
+    remoteICECandidates_.clear();
+}
+
+bool
+CallManager::checkSharesRoom(QString roomid, std::string invitee) const
+{
+    /*
+        IMPLEMENTATION REQUIRED
+        Check if room is shared to determine whether to ring or not.
+        Called from handle callInvite event
+    */
+    if (roomid.toStdString() != "") {
+        if (invitee == "") {
+            // check all members
+            return true;
+        } else {
+            return true;
+            // check if invitee shares a direct room with local user
+        }
+        return true;
+    }
+
+    return true;
 }
 
 void
@@ -467,7 +745,7 @@ CallManager::generateCallID()
 }
 
 void
-CallManager::clear()
+CallManager::clear(bool endAllCalls)
 {
     roomid_.clear();
     callParty_.clear();
@@ -476,17 +754,23 @@ CallManager::clear()
     callid_.clear();
     callType_       = CallType::VOICE;
     haveCallInvite_ = false;
+    answerSelected_ = false;
+    if (endAllCalls) {
+        isOnCallOnOtherDevice_ = "";
+        rejectCallPartyIDs_.clear();
+        emit newCallDeviceState();
+    }
     emit newInviteState();
     inviteSDP_.clear();
     remoteICECandidates_.clear();
 }
 
 void
-CallManager::endCall()
+CallManager::endCall(bool endAllCalls)
 {
     stopRingtone();
     session_.end();
-    clear();
+    clear(endAllCalls);
 }
 
 void
diff --git a/src/voip/CallManager.h b/src/voip/CallManager.h
index 8f1615f8..3011444f 100644
--- a/src/voip/CallManager.h
+++ b/src/voip/CallManager.h
@@ -18,6 +18,7 @@
 #include "WebRTCSession.h"
 #include "mtx/events/collections.hpp"
 #include "mtx/events/voip.hpp"
+#include <mtxclient/utils.hpp>
 
 namespace mtx::responses {
 struct TurnServer;
@@ -30,6 +31,7 @@ class CallManager final : public QObject
     Q_OBJECT
     Q_PROPERTY(bool haveCallInvite READ haveCallInvite NOTIFY newInviteState)
     Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState)
+    Q_PROPERTY(bool isOnCallOnOtherDevice READ isOnCallOnOtherDevice NOTIFY newCallDeviceState)
     Q_PROPERTY(webrtc::CallType callType READ callType NOTIFY newInviteState)
     Q_PROPERTY(webrtc::State callState READ callState NOTIFY newCallState)
     Q_PROPERTY(QString callParty READ callParty NOTIFY newInviteState)
@@ -46,7 +48,9 @@ public:
     CallManager(QObject *);
 
     bool haveCallInvite() const { return haveCallInvite_; }
-    bool isOnCall() const { return session_.state() != webrtc::State::DISCONNECTED; }
+    bool isOnCall() const { return (session_.state() != webrtc::State::DISCONNECTED); }
+    bool isOnCallOnOtherDevice() const { return (isOnCallOnOtherDevice_ != ""); }
+    bool checkSharesRoom(QString roomid_, std::string invitee) const;
     webrtc::CallType callType() const { return callType_; }
     webrtc::State callState() const { return session_.state(); }
     QString callParty() const { return callParty_; }
@@ -67,8 +71,9 @@ public slots:
     void toggleMicMute();
     void toggleLocalPiP() { session_.toggleLocalPiP(); }
     void acceptInvite();
-    void
-      hangUp(mtx::events::voip::CallHangUp::Reason = mtx::events::voip::CallHangUp::Reason::User);
+    void hangUp(
+      mtx::events::voip::CallHangUp::Reason = mtx::events::voip::CallHangUp::Reason::UserHangUp);
+    void rejectInvite();
     QStringList windowList();
     void previewWindow(unsigned int windowIndex) const;
 
@@ -77,8 +82,12 @@ signals:
     void newMessage(const QString &roomid, const mtx::events::voip::CallCandidates &);
     void newMessage(const QString &roomid, const mtx::events::voip::CallAnswer &);
     void newMessage(const QString &roomid, const mtx::events::voip::CallHangUp &);
+    void newMessage(const QString &roomid, const mtx::events::voip::CallSelectAnswer &);
+    void newMessage(const QString &roomid, const mtx::events::voip::CallReject &);
+    void newMessage(const QString &roomid, const mtx::events::voip::CallNegotiate &);
     void newInviteState();
     void newCallState();
+    void newCallDeviceState();
     void micMuteChanged();
     void devicesChanged();
     void turnServerRetrieved(const mtx::responses::TurnServer &);
@@ -92,18 +101,23 @@ private:
     QString callParty_;
     QString callPartyDisplayName_;
     QString callPartyAvatarUrl_;
+    std::string callPartyVersion_ = "1";
     std::string callid_;
-    std::string partyid_       = "";
-    std::string invitee_       = "";
-    const uint32_t timeoutms_  = 120000;
-    webrtc::CallType callType_ = webrtc::CallType::VOICE;
-    bool haveCallInvite_       = false;
+    std::string partyid_               = mtx::client::utils::random_token(8, false);
+    std::string selectedpartyid_       = "";
+    std::string invitee_               = "";
+    const uint32_t timeoutms_          = 120000;
+    webrtc::CallType callType_         = webrtc::CallType::VOICE;
+    bool haveCallInvite_               = false;
+    bool answerSelected_               = false;
+    std::string isOnCallOnOtherDevice_ = "";
     std::string inviteSDP_;
     std::vector<mtx::events::voip::CallCandidates::Candidate> remoteICECandidates_;
     std::vector<std::string> turnURIs_;
     QTimer turnServerTimer_;
     QMediaPlayer player_;
     std::vector<std::pair<QString, uint32_t>> windows_;
+    std::vector<std::string> rejectCallPartyIDs_;
 
     template<typename T>
     bool handleEvent(const mtx::events::collections::TimelineEvents &event);
@@ -111,11 +125,14 @@ private:
     void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallCandidates> &);
     void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallAnswer> &);
     void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallHangUp> &);
+    void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallSelectAnswer> &);
+    void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallReject> &);
+    void handleEvent(const mtx::events::RoomEvent<mtx::events::voip::CallNegotiate> &);
     void answerInvite(const mtx::events::voip::CallInvite &, bool isVideo);
     void generateCallID();
     QStringList devices(bool isVideo) const;
-    void clear();
-    void endCall();
+    void clear(bool endAllCalls = true);
+    void endCall(bool endAllCalls = true);
     void playRingtone(const QUrl &ringtone, bool repeat);
     void stopRingtone();
 };
diff --git a/src/voip/WebRTCSession.cpp b/src/voip/WebRTCSession.cpp
index 49a5cca0..b3e6bf26 100644
--- a/src/voip/WebRTCSession.cpp
+++ b/src/voip/WebRTCSession.cpp
@@ -700,6 +700,15 @@ WebRTCSession::acceptOffer(const std::string &sdp)
 }
 
 bool
+WebRTCSession::acceptNegotiation(const std::string &sdp)
+{
+    nhlog::ui()->debug("WebRTC: received negotiation offer:\n{}", sdp);
+    if (state_ == State::DISCONNECTED)
+        return false;
+    return false;
+}
+
+bool
 WebRTCSession::acceptAnswer(const std::string &sdp)
 {
     nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp);
@@ -1139,6 +1148,12 @@ WebRTCSession::createOffer(webrtc::CallType, uint32_t)
 // clang-format on
 
 bool
+WebRTCSession::acceptNegotiation(const std::string &)
+{
+    return false;
+}
+
+bool
 WebRTCSession::acceptOffer(const std::string &)
 {
     return false;
diff --git a/src/voip/WebRTCSession.h b/src/voip/WebRTCSession.h
index a0ee9720..081611e5 100644
--- a/src/voip/WebRTCSession.h
+++ b/src/voip/WebRTCSession.h
@@ -64,6 +64,7 @@ public:
     bool createOffer(webrtc::CallType, uint32_t shareWindowId);
     bool acceptOffer(const std::string &sdp);
     bool acceptAnswer(const std::string &sdp);
+    bool acceptNegotiation(const std::string &sdp);
     void acceptICECandidates(const std::vector<mtx::events::voip::CallCandidates::Candidate> &);
 
     bool isMicMuted() const;