From 0996b167d9efee6b3fa202a948a9984a299aa15f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Thu, 9 Jun 2022 11:23:14 -0400 Subject: [PATCH] swarm: add call buttons and interactions for multi-swarm + Add call buttons to start a new call + React to events from the swarm + call interactions (Join call/Call ended, etc) + active calls area + Add call management logic in LRC + Feature is enabled via the experimental checkbox https://git.jami.net/savoirfairelinux/jami-daemon/-/issues/312 Change-Id: I83fd20b5e772097c0792bdc66feec69b0cb0009a --- daemon | 2 +- src/app/appsettingsmanager.h | 1 + src/app/calladapter.cpp | 12 +- .../commoncomponents/CallMessageDelegate.qml | 118 ++++++++++ src/app/commoncomponents/SBSMessageBase.qml | 1 + .../commoncomponents/TextMessageDelegate.qml | 1 - src/app/constant/JamiStrings.qml | 14 ++ src/app/conversationlistmodelbase.cpp | 3 + src/app/conversationlistmodelbase.h | 1 + src/app/conversationsadapter.cpp | 12 +- src/app/conversationsadapter.h | 4 + src/app/currentaccount.cpp | 1 + src/app/currentaccount.h | 1 + src/app/currentconversation.cpp | 113 ++++++++- src/app/currentconversation.h | 10 + src/app/lrcinstance.cpp | 13 +- src/app/lrcinstance.h | 3 + src/app/mainview/components/ChatView.qml | 39 +++- .../mainview/components/ChatViewHeader.qml | 9 +- .../components/ConversationErrorsRow.qml | 1 + .../mainview/components/DevicesListPopup.qml | 219 ++++++++++++++++++ src/app/mainview/components/HostPopup.qml | 111 +++++++++ .../mainview/components/MessageListView.qml | 2 +- .../mainview/components/NotificationArea.qml | 117 ++++++++++ .../components/SmartListItemDelegate.qml | 17 +- .../mainview/components/SwarmDetailsPanel.qml | 113 +++++++++ src/app/messagesadapter.cpp | 13 ++ src/app/messagesadapter.h | 4 + .../components/TroubleshootSettings.qml | 13 ++ src/app/utilsadapter.cpp | 2 + src/app/utilsadapter.h | 1 + src/libclient/api/conversation.h | 15 ++ src/libclient/api/conversationmodel.h | 35 ++- src/libclient/api/interaction.h | 3 + src/libclient/authority/storagehelper.cpp | 15 +- src/libclient/authority/storagehelper.h | 6 +- src/libclient/callbackshandler.cpp | 26 +++ src/libclient/callbackshandler.h | 19 ++ src/libclient/callmodel.cpp | 55 ++++- src/libclient/conversationmodel.cpp | 161 ++++++++++--- src/libclient/messagelistmodel.cpp | 25 ++ src/libclient/messagelistmodel.h | 4 + .../qtwrapper/configurationmanager_wrap.h | 31 ++- 43 files changed, 1276 insertions(+), 90 deletions(-) create mode 100644 src/app/commoncomponents/CallMessageDelegate.qml create mode 100644 src/app/mainview/components/DevicesListPopup.qml create mode 100644 src/app/mainview/components/HostPopup.qml create mode 100644 src/app/mainview/components/NotificationArea.qml diff --git a/daemon b/daemon index 54ffd0f43..08ef8dd80 160000 --- a/daemon +++ b/daemon @@ -1 +1 @@ -Subproject commit 54ffd0f4380bdbdc6a2fcb80847b2f5aefad4958 +Subproject commit 08ef8dd80d571816259b195b0956a472a40b58ad diff --git a/src/app/appsettingsmanager.h b/src/app/appsettingsmanager.h index 6acca9096..272d972ca 100644 --- a/src/app/appsettingsmanager.h +++ b/src/app/appsettingsmanager.h @@ -54,6 +54,7 @@ extern const QString defaultDownloadPath; X(NeverShowMeAgain, false) \ X(WindowGeometry, QRectF(qQNaN(), qQNaN(), 0., 0.)) \ X(WindowState, QWindow::AutomaticVisibility) \ + X(EnableExperimentalSwarm, false) \ X(LANG, "SYSTEM") /* diff --git a/src/app/calladapter.cpp b/src/app/calladapter.cpp index 25c473c58..be21ec2d8 100644 --- a/src/app/calladapter.cpp +++ b/src/app/calladapter.cpp @@ -216,7 +216,8 @@ CallAdapter::onParticipantUpdated(const QString& callId, int index) return; } auto infos = getConferencesInfos(); - participantsModel_->updateParticipant(index, infos[index]); + if (index < infos.size()) + participantsModel_->updateParticipant(index, infos[index]); } catch (...) { } } @@ -256,7 +257,8 @@ CallAdapter::onCallStatusChanged(const QString& callId, int code) const auto& currentConvInfo = lrcInstance_->getConversationFromConvUid(currentConvId); // was it a conference and now is a dialog? - if (currentConvInfo.confId.isEmpty() && currentConfSubcalls_.size() == 2) { + if (currentConvInfo.isCoreDialog() && currentConvInfo.confId.isEmpty() + && currentConfSubcalls_.size() == 2) { auto it = std::find_if(currentConfSubcalls_.cbegin(), currentConfSubcalls_.cend(), [&callId](const QString& cid) { return cid != callId; }); @@ -495,14 +497,12 @@ CallAdapter::updateCall(const QString& convUid, const QString& accountId, bool f accountId_ = accountId.isEmpty() ? accountId_ : accountId; const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid); - if (convInfo.uid.isEmpty()) { + if (convInfo.uid.isEmpty()) return; - } auto call = lrcInstance_->getCallInfoForConversation(convInfo, forceCallOnly); - if (!call) { + if (!call) return; - } if (convInfo.uid == lrcInstance_->get_selectedConvUid()) { auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_); diff --git a/src/app/commoncomponents/CallMessageDelegate.qml b/src/app/commoncomponents/CallMessageDelegate.qml new file mode 100644 index 000000000..52ece160f --- /dev/null +++ b/src/app/commoncomponents/CallMessageDelegate.qml @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +SBSMessageBase { + id: root + + component JoinCallButton: PushButton { + visible: root.isActive + toolTipText: JamiStrings.joinCall + preferredSize: 40 + imageColor: callLabel.color + normalColor: "transparent" + hoveredColor: Qt.rgba(255, 255, 255, 0.2) + border.width: 1 + border.color: callLabel.color + } + + property bool isRemoteImage + + isOutgoing: Author === "" + author: Author + readers: Readers + formattedTime: MessagesAdapter.getFormattedTime(Timestamp) + + + Connections { + target: CurrentConversation + enabled: root.isActive + + function onActiveCallsChanged() { + root.isActive = LRCInstance.indexOfActiveCall(ConfId, ActionUri, DeviceId) !== -1 + } + } + + property bool isActive: LRCInstance.indexOfActiveCall(ConfId, ActionUri, DeviceId) !== -1 + visible: isActive || ConfId === "" || Duration > 0 + + bubble.color: { + if (ConfId === "" && Duration === 0) { + // If missed, we can add a darker pattern + return isOutgoing ? + Qt.darker(JamiTheme.messageOutBgColor, 1.5) : + Qt.darker(JamiTheme.messageInBgColor, 1.5) + } + return isOutgoing ? + JamiTheme.messageOutBgColor : + CurrentConversation.isCoreDialog ? JamiTheme.messageInBgColor : Qt.lighter(CurrentConversation.color, 1.5) + } + + innerContent.children: [ + RowLayout { + id: msg + anchors.right: isOutgoing ? parent.right : undefined + spacing: 10 + visible: root.visible + + Label { + id: callLabel + padding: 10 + Layout.margins: 8 + Layout.fillWidth: true + + text:{ + if (root.isActive) + return JamiStrings.joinCall + return Body + } + horizontalAlignment: Qt.AlignHCenter + font.pointSize: JamiTheme.contactEventPointSize + font.bold: true + color: UtilsAdapter.luma(bubble.color) ? + JamiTheme.chatviewTextColorLight : + JamiTheme.chatviewTextColorDark + } + + JoinCallButton { + id: joinCallInAudio + + source: JamiResources.place_audiocall_24dp_svg + onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, ConfId, true) + } + + JoinCallButton { + id: joinCallInVideo + + source: JamiResources.videocam_24dp_svg + onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, ConfId) + Layout.rightMargin: parent.spacing + } + } + ] + + opacity: 0 + Behavior on opacity { NumberAnimation { duration: 100 } } + Component.onCompleted: opacity = 1 +} \ No newline at end of file diff --git a/src/app/commoncomponents/SBSMessageBase.qml b/src/app/commoncomponents/SBSMessageBase.qml index 13ccfd497..50847d957 100644 --- a/src/app/commoncomponents/SBSMessageBase.qml +++ b/src/app/commoncomponents/SBSMessageBase.qml @@ -56,6 +56,7 @@ Control { readonly property real hPadding: JamiTheme.sbsMessageBasePreferredPadding width: ListView.view ? ListView.view.width : 0 height: mainColumnLayout.implicitHeight + rightPadding: hPadding leftPadding: hPadding diff --git a/src/app/commoncomponents/TextMessageDelegate.qml b/src/app/commoncomponents/TextMessageDelegate.qml index 93fcc59ad..7b1a9118c 100644 --- a/src/app/commoncomponents/TextMessageDelegate.qml +++ b/src/app/commoncomponents/TextMessageDelegate.qml @@ -70,7 +70,6 @@ SBSMessageBase { Math.min(implicitWidth, innerContent.width - senderMargin) } - height: implicitHeight wrapMode: Label.WrapAtWordBoundaryOrAnywhere selectByMouse: true font.pixelSize: isEmojiOnly? JamiTheme.chatviewEmojiSize : JamiTheme.chatviewFontSize diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index 070de9197..6a469b55b 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -487,6 +487,9 @@ Item { property string troubleshootButton: qsTr("Open logs") property string troubleshootText: qsTr("Get logs") + property string experimentalCallSwarm: qsTr("(Experimental) Enable call support for swarm") + property string experimentalCallSwarmTooltip: qsTr("This feature will enable call buttons in swarms with multiple participants.") + // Recording Settings property string tipRecordFolder: qsTr("Select a record directory") property string quality: qsTr("Quality") @@ -727,6 +730,15 @@ Item { property string writeTo: qsTr("Write to %1") property string edit: qsTr("Edit") property string edited: qsTr("Edited") + property string joinCall: qsTr("Join call") + property string wantToJoin: qsTr("A call is in progress. Do you want to join the call?") + property string needsHost: qsTr("Current host for this swarm seems unreachable. Do you want to host the call?") + property string chooseHoster: qsTr("Choose a dedicated device for hosting future calls in this swarm. If not set, the device starting a call will host it.") + property string chooseThisDevice: qsTr("Choose this device") + property string removeCurrentDevice: qsTr("Remove current device") + property string becomeHostOneCall: qsTr("Host only this call") + property string hostThisCall: qsTr("Host this call") + property string becomeDefaultHost: qsTr("Make me the default host for future calls") // Invitation View property string invitationViewSentRequest: qsTr("%1 has sent you a request for a conversation.") @@ -745,9 +757,11 @@ Item { property string muteConversation: qsTr("Mute conversation") property string ignoreNotificationsTooltip: qsTr("Ignore all notifications from this conversation") property string chooseAColor: qsTr("Choose a color") + property string defaultCallHost: qsTr("Default host (calls)") property string leaveTheSwarm: qsTr("Leave the swarm") property string leave: qsTr("Leave") property string typeOfSwarm: qsTr("Type of swarm") + property string none: qsTr("None") // NewSwarmPage property string youCanAdd8: qsTr("You can add 8 people in the swarm") diff --git a/src/app/conversationlistmodelbase.cpp b/src/app/conversationlistmodelbase.cpp index 827c84bd9..b722e1fd4 100644 --- a/src/app/conversationlistmodelbase.cpp +++ b/src/app/conversationlistmodelbase.cpp @@ -94,6 +94,9 @@ ConversationListModelBase::dataForItem(item_t item, int role) const return lrcInstance_->getContentDraft(item.uid, item.accountId); return {}; } + case Role::ActiveCallsCount: { + return item.activeCalls.size(); + } case Role::IsRequest: return QVariant(item.isRequest); case Role::Title: diff --git a/src/app/conversationlistmodelbase.h b/src/app/conversationlistmodelbase.h index f22e1c923..a47b3dc05 100644 --- a/src/app/conversationlistmodelbase.h +++ b/src/app/conversationlistmodelbase.h @@ -44,6 +44,7 @@ X(CallState) \ X(SectionName) \ X(AccountId) \ + X(ActiveCallsCount) \ X(Draft) \ X(IsRequest) \ X(Mode) \ diff --git a/src/app/conversationsadapter.cpp b/src/app/conversationsadapter.cpp index 6f973726f..9b47615ba 100644 --- a/src/app/conversationsadapter.cpp +++ b/src/app/conversationsadapter.cpp @@ -59,7 +59,7 @@ ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray, } else { // selected const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId); - if (convInfo.uid.isEmpty()) + if (convInfo.uid.isEmpty() || convInfo.accountId != lrcInstance_->get_currentAccountId()) return; auto& accInfo = lrcInstance_->getAccountInfo(convInfo.accountId); @@ -528,6 +528,16 @@ ConversationsAdapter::popFrontError(const QString& convId) convModel->popFrontError(convId); } +void +ConversationsAdapter::ignoreActiveCall(const QString& convId, + const QString& id, + const QString& uri, + const QString& device) +{ + auto convModel = lrcInstance_->getCurrentConversationModel(); + convModel->ignoreActiveCall(convId, id, uri, device); +} + void ConversationsAdapter::updateConversationDescription(const QString& convId, const QString& newDescription) diff --git a/src/app/conversationsadapter.h b/src/app/conversationsadapter.h index 0685f1032..bf908cf39 100644 --- a/src/app/conversationsadapter.h +++ b/src/app/conversationsadapter.h @@ -59,6 +59,10 @@ public: Q_INVOKABLE void restartConversation(const QString& convId); Q_INVOKABLE void updateConversationTitle(const QString& convId, const QString& newTitle); Q_INVOKABLE void popFrontError(const QString& convId); + Q_INVOKABLE void ignoreActiveCall(const QString& convId, + const QString& id, + const QString& uri, + const QString& device); Q_INVOKABLE void updateConversationDescription(const QString& convId, const QString& newDescription); diff --git a/src/app/currentaccount.cpp b/src/app/currentaccount.cpp index 440744ef2..319678dcf 100644 --- a/src/app/currentaccount.cpp +++ b/src/app/currentaccount.cpp @@ -111,6 +111,7 @@ CurrentAccount::updateData() set_enabled(accInfo.enabled); set_managerUri(accConfig.managerUri); set_keepAliveEnabled(accConfig.keepAliveEnabled, true); + set_deviceId(accConfig.deviceId); set_peerDiscovery(accConfig.peerDiscovery, true); set_sendReadReceipt(accConfig.sendReadReceipt, true); set_isRendezVous(accConfig.isRendezVous, true); diff --git a/src/app/currentaccount.h b/src/app/currentaccount.h index 4f99b1045..d359658fe 100644 --- a/src/app/currentaccount.h +++ b/src/app/currentaccount.h @@ -101,6 +101,7 @@ class CurrentAccount final : public QObject QML_RO_PROPERTY(QString, id) QML_RO_PROPERTY(QString, uri) + QML_RO_PROPERTY(QString, deviceId) QML_RO_PROPERTY(QString, registeredName) QML_RO_PROPERTY(QString, alias) QML_RO_PROPERTY(QString, bestId) diff --git a/src/app/currentconversation.cpp b/src/app/currentconversation.cpp index e2cbc264d..787fa2429 100644 --- a/src/app/currentconversation.cpp +++ b/src/app/currentconversation.cpp @@ -53,8 +53,6 @@ CurrentConversation::updateData() const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); if (auto optConv = accInfo.conversationModel->getConversationForUid(convId)) { auto& convInfo = optConv->get(); - set_title(accInfo.conversationModel->title(convId)); - set_description(accInfo.conversationModel->description(convId)); set_uris(convInfo.participantsUris()); set_isSwarm(convInfo.isSwarm()); set_isLegacy(convInfo.isLegacy()); @@ -104,6 +102,9 @@ CurrentConversation::updateData() } else if (convInfo.mode == conversation::Mode::PUBLIC) { set_modeString(tr("Public group")); } + + onProfileUpdated(convId); + updateActiveCalls(accountId, convId); } } catch (...) { qWarning() << "Can't update current conversation data for" << convId; @@ -111,33 +112,58 @@ CurrentConversation::updateData() updateErrors(convId); } +void +CurrentConversation::onNeedsHost(const QString& convId) +{ + if (id_ != convId) + return; + Q_EMIT needsHost(); +} + void CurrentConversation::setPreference(const QString& key, const QString& value) { + if (key == "color") + set_color(value); + auto preferences = getPreferences(); + preferences[key] = value; auto accountId = lrcInstance_->get_currentAccountId(); const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); auto convId = lrcInstance_->get_selectedConvUid(); - if (auto optConv = accInfo.conversationModel->getConversationForUid(convId)) { - auto& convInfo = optConv->get(); - auto preferences = convInfo.preferences; - preferences[key] = value; - accInfo.conversationModel->setConversationPreferences(convId, preferences); - } + accInfo.conversationModel->setConversationPreferences(convId, preferences); } QString CurrentConversation::getPreference(const QString& key) const +{ + return getPreferences()[key]; +} + +MapStringString +CurrentConversation::getPreferences() const { auto accountId = lrcInstance_->get_currentAccountId(); const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); auto convId = lrcInstance_->get_selectedConvUid(); if (auto optConv = accInfo.conversationModel->getConversationForUid(convId)) { auto& convInfo = optConv->get(); - return convInfo.preferences[key]; + auto preferences = accInfo.conversationModel->getConversationPreferences(convId); + return preferences; } return {}; } +void +CurrentConversation::setInfo(const QString& key, const QString& value) +{ + MapStringString infos; + infos[key] = value; + auto accountId = lrcInstance_->get_currentAccountId(); + const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); + auto convId = lrcInstance_->get_selectedConvUid(); + accInfo.conversationModel->updateConversationInfos(convId, infos); +} + void CurrentConversation::onConversationUpdated(const QString& convId) { @@ -153,8 +179,27 @@ CurrentConversation::onProfileUpdated(const QString& convId) // filter for our currently set id if (id_ != convId) return; - set_title(lrcInstance_->getCurrentConversationModel()->title(convId)); - set_description(lrcInstance_->getCurrentConversationModel()->description(convId)); + const auto& convModel = lrcInstance_->getCurrentConversationModel(); + set_title(convModel->title(convId)); + set_description(convModel->description(convId)); + + try { + if (auto optConv = convModel->getConversationForUid(convId)) { + auto& convInfo = optConv->get(); + // Now, update call informations (rdvAccount/device) + if (convInfo.infos.contains("rdvAccount")) { + set_rdvAccount(convInfo.infos["rdvAccount"]); + } else { + set_rdvAccount(""); + } + if (convInfo.infos.contains("rdvDevice")) { + set_rdvDevice(convInfo.infos["rdvDevice"]); + } else { + set_rdvDevice(""); + } + } + } catch (...) { + } } void @@ -200,11 +245,21 @@ CurrentConversation::connectModel() this, &CurrentConversation::updateErrors, Qt::UniqueConnection); + connect(lrcInstance_->getCurrentConversationModel(), + &ConversationModel::activeCallsChanged, + this, + &CurrentConversation::updateActiveCalls, + Qt::UniqueConnection); connect(lrcInstance_->getCurrentConversationModel(), &ConversationModel::conversationPreferencesUpdated, this, &CurrentConversation::updateConversationPreferences, Qt::UniqueConnection); + connect(lrcInstance_->getCurrentConversationModel(), + &ConversationModel::needsHost, + this, + &CurrentConversation::onNeedsHost, + Qt::UniqueConnection); } void @@ -246,6 +301,42 @@ CurrentConversation::updateErrors(const QString& convId) } } +void +CurrentConversation::updateActiveCalls(const QString&, const QString& convId) +{ + if (convId != id_) + return; + const auto& convModel = lrcInstance_->getCurrentConversationModel(); + if (auto optConv = convModel->getConversationForUid(convId)) { + auto& convInfo = optConv->get(); + QVariantList callList; + for (int i = 0; i < convInfo.activeCalls.size(); i++) { + // Check if ignored. + auto ignored = false; + for (int ignoredIdx = 0; ignoredIdx < convInfo.ignoredActiveCalls.size(); ignoredIdx++) { + auto& ignoreCall = convInfo.ignoredActiveCalls[ignoredIdx]; + if (ignoreCall["id"] == convInfo.activeCalls[i]["id"] + && ignoreCall["uri"] == convInfo.activeCalls[i]["uri"] + && ignoreCall["device"] == convInfo.activeCalls[i]["device"]) { + ignored = true; + break; + } + } + if (ignored) { + continue; + } + + // Else, add to model + QVariantMap mapCall; + Q_FOREACH (QString key, convInfo.activeCalls[i].keys()) { + mapCall[key] = convInfo.activeCalls[i][key]; + } + callList.append(mapCall); + } + set_activeCalls(callList); + } +} + void CurrentConversation::scrollToMsg(const QString& msg) { diff --git a/src/app/currentconversation.h b/src/app/currentconversation.h index 25f705305..62c7944f2 100644 --- a/src/app/currentconversation.h +++ b/src/app/currentconversation.h @@ -43,12 +43,15 @@ class CurrentConversation final : public QObject QML_PROPERTY(bool, ignoreNotifications) QML_PROPERTY(QString, callId) QML_PROPERTY(QString, color) + QML_PROPERTY(QString, rdvAccount) + QML_PROPERTY(QString, rdvDevice) QML_PROPERTY(call::Status, callState) QML_PROPERTY(bool, inCall) QML_PROPERTY(bool, isTemporary) QML_PROPERTY(bool, isContact) QML_PROPERTY(bool, allMessagesLoaded) QML_PROPERTY(QString, modeString) + QML_PROPERTY(QVariantList, activeCalls) QML_PROPERTY(QStringList, errors) QML_PROPERTY(QStringList, backendErrors) @@ -63,6 +66,8 @@ public: Q_INVOKABLE void showSwarmDetails() const; Q_INVOKABLE void setPreference(const QString& key, const QString& value); Q_INVOKABLE QString getPreference(const QString& key) const; + Q_INVOKABLE MapStringString getPreferences() const; + Q_INVOKABLE void setInfo(const QString& key, const QString& value); Q_SIGNALS: void scrollTo(const QString& msgId); @@ -70,10 +75,15 @@ Q_SIGNALS: private Q_SLOTS: void updateData(); + void onNeedsHost(const QString& convId); void onConversationUpdated(const QString& convId); void onProfileUpdated(const QString& convId); void updateErrors(const QString& convId); void updateConversationPreferences(const QString& convId); + void updateActiveCalls(const QString&, const QString& convId); + +Q_SIGNALS: + void needsHost(); private: LRCInstance* lrcInstance_; diff --git a/src/app/lrcinstance.cpp b/src/app/lrcinstance.cpp index 21b2254cf..c5d3ee731 100644 --- a/src/app/lrcinstance.cpp +++ b/src/app/lrcinstance.cpp @@ -190,9 +190,8 @@ LRCInstance::getCallInfoForConversation(const conversation::Info& convInfo, bool auto callId = forceCallOnly ? convInfo.callId : (convInfo.confId.isEmpty() ? convInfo.callId : convInfo.confId); - if (!accInfo.callModel->hasCall(callId)) { + if (!accInfo.callModel->hasCall(callId)) return nullptr; - } return &accInfo.callModel->getCall(callId); } catch (...) { return nullptr; @@ -372,6 +371,16 @@ LRCInstance::selectConversation(const QString& convId, const QString& accountId) set_selectedConvUid(convId); } +int +LRCInstance::indexOfActiveCall(const QString& confId, const QString& uri, const QString& deviceId) +{ + if (auto optConv = getCurrentConversationModel()->getConversationForUid(selectedConvUid_)) { + auto& convInfo = optConv->get(); + return convInfo.indexOfActiveCall({{"confId", confId}, {"uri", uri}, {"device", deviceId}}); + } + return -1; +} + void LRCInstance::deselectConversation() { diff --git a/src/app/lrcinstance.h b/src/app/lrcinstance.h index d8e7a495f..ae8633ace 100644 --- a/src/app/lrcinstance.h +++ b/src/app/lrcinstance.h @@ -108,6 +108,9 @@ public: Q_INVOKABLE void setContentDraft(const QString& convUid, const QString& accountId, const QString& content); + Q_INVOKABLE int indexOfActiveCall(const QString& confId, + const QString& uri, + const QString& deviceId); int getCurrentAccountIndex(); void setCurrAccDisplayName(const QString& displayName); diff --git a/src/app/mainview/components/ChatView.qml b/src/app/mainview/components/ChatView.qml index ee90d9251..d8d946fe9 100644 --- a/src/app/mainview/components/ChatView.qml +++ b/src/app/mainview/components/ChatView.qml @@ -47,6 +47,10 @@ Rectangle { color: JamiTheme.chatviewBgColor + HostPopup { + id: hostPopup + } + ColumnLayout { anchors.fill: root @@ -88,6 +92,10 @@ Rectangle { addMemberPanel.visible = !addMemberPanel.visible } } + + function onNeedsHost() { + hostPopup.open() + } } onAddToConversationClicked: { @@ -105,9 +113,38 @@ Rectangle { } } + + Connections { + target: CurrentConversation + enabled: true + + function onActiveCallsChanged() { + if (CurrentConversation.activeCalls.length > 0) { + notificationArea.id = CurrentConversation.activeCalls[0]["id"] + notificationArea.uri = CurrentConversation.activeCalls[0]["uri"] + notificationArea.device = CurrentConversation.activeCalls[0]["device"] + } + notificationArea.visible = CurrentConversation.activeCalls.length > 0 + } + + function onErrorsChanged() { + if (CurrentConversation.errors.length > 0) { + errorRect.errorLabel.text = CurrentConversation.errors[0] + errorRect.backendErrorToolTip.text = JamiStrings.backendError.arg(CurrentConversation.backendErrors[0]) + } + errorRect.visible = CurrentConversation.errors.length > 0 // If too much noise: && LRCInstance.debugMode() + } + } + ConversationErrorsRow { id: errorRect - color: JamiTheme.filterBadgeColor + Layout.fillWidth: true + Layout.preferredHeight: JamiTheme.chatViewHeaderPreferredHeight + visible: false + } + + NotificationArea { + id: notificationArea Layout.fillWidth: true Layout.preferredHeight: JamiTheme.chatViewHeaderPreferredHeight visible: false diff --git a/src/app/mainview/components/ChatViewHeader.qml b/src/app/mainview/components/ChatViewHeader.qml index 60cb8a109..51e44d6eb 100644 --- a/src/app/mainview/components/ChatViewHeader.qml +++ b/src/app/mainview/components/ChatViewHeader.qml @@ -20,9 +20,10 @@ import QtQuick import QtQuick.Layouts -import net.jami.Models 1.1 -import net.jami.Constants 1.1 import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 +import net.jami.Enums 1.1 +import net.jami.Models 1.1 import "../../commoncomponents" @@ -155,7 +156,7 @@ Rectangle { PushButton { id: startAAudioCallButton - visible: interactionButtonsVisibility && !addMemberVisibility + visible: interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm)) source: JamiResources.place_audiocall_24dp_svg toolTipText: JamiStrings.placeAudioCall @@ -169,7 +170,7 @@ Rectangle { PushButton { id: startAVideoCallButton - visible: CurrentAccount.videoEnabled_Video && interactionButtonsVisibility && !addMemberVisibility + visible: CurrentAccount.videoEnabled_Video && interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm)) source: JamiResources.videocam_24dp_svg toolTipText: JamiStrings.placeVideoCall diff --git a/src/app/mainview/components/ConversationErrorsRow.qml b/src/app/mainview/components/ConversationErrorsRow.qml index 9b08fad6a..8fdc2cc58 100644 --- a/src/app/mainview/components/ConversationErrorsRow.qml +++ b/src/app/mainview/components/ConversationErrorsRow.qml @@ -43,6 +43,7 @@ Rectangle { errorRect.visible = CurrentConversation.errors.length > 0 && LRCInstance.debugMode() } } + color: JamiTheme.filterBadgeColor RowLayout { anchors.fill: parent diff --git a/src/app/mainview/components/DevicesListPopup.qml b/src/app/mainview/components/DevicesListPopup.qml new file mode 100644 index 000000000..3e3f97e64 --- /dev/null +++ b/src/app/mainview/components/DevicesListPopup.qml @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2020-2022 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects + +import SortFilterProxyModel 0.2 + +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +import "../../commoncomponents" + +BaseModalDialog { + id: root + + width: 488 + height: 320 + + popupContent: Rectangle { + id: rect + + color: JamiTheme.transparentColor + width: root.width + + + PushButton { + id: btnCancel + imageColor: "grey" + normalColor: "transparent" + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 10 + anchors.rightMargin: 10 + source: JamiResources.round_close_24dp_svg + onClicked: { close(); } + } + + ColumnLayout { + id: mainLayout + anchors.fill: parent + anchors.margins: JamiTheme.preferredMarginSize + spacing: JamiTheme.preferredMarginSize + + Label { + id: informativeLabel + + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + Layout.topMargin: 26 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: JamiStrings.chooseHoster + color: JamiTheme.primaryForegroundColor + } + + JamiListView { + id: devicesListView + + Layout.fillWidth: true + Layout.preferredHeight: 160 + + model: SortFilterProxyModel { + sourceModel: DeviceItemListModel + sorters: [ + RoleSorter { roleName: "IsCurrent"; sortOrder: Qt.DescendingOrder }, + StringSorter { + roleName: "DeviceName" + caseSensitivity: Qt.CaseInsensitive + } + ] + } + + delegate: ItemDelegate { + id: item + + property string deviceName : DeviceName + property string deviceId : DeviceID + property bool isCurrent : DeviceName + + implicitWidth: devicesListView.width + width: devicesListView.width + height: 70 + + highlighted: ListView.isCurrentItem + + MouseArea { + anchors.fill: parent + onClicked: { + devicesListView.currentIndex = index + } + } + + background: Rectangle { + color: highlighted? JamiTheme.selectedColor : JamiTheme.editBackgroundColor + } + + RowLayout { + anchors.fill: item + + Image { + id: deviceImage + + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + Layout.leftMargin: JamiTheme.preferredMarginSize + + layer { + enabled: true + effect: ColorOverlay { + color: JamiTheme.textColor + } + } + source: JamiResources.baseline_desktop_windows_24dp_svg + } + + ColumnLayout { + id: deviceInfoColumnLayout + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: JamiTheme.preferredMarginSize + + Text { + id: labelDeviceName + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: true + + elide: Text.ElideRight + color: JamiTheme.textColor + text: deviceName + } + + Text { + id: labelDeviceId + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: true + + elide: Text.ElideRight + color: JamiTheme.textColor + text: deviceId === "" ? qsTr("Device Id") : deviceId + } + } + } + + CustomBorder { + commonBorder: false + lBorderwidth: 0 + rBorderwidth: 0 + tBorderwidth: 0 + bBorderwidth: 2 + borderColor: JamiTheme.selectedColor + } + } + } + + RowLayout { + spacing: JamiTheme.preferredMarginSize + Layout.preferredWidth: parent.width + + MaterialButton { + id: chooseBtn + + Layout.alignment: Qt.AlignCenter + enabled: devicesListView.currentItem + + text: JamiStrings.chooseThisDevice + toolTipText: JamiStrings.chooseThisDevice + + onClicked: { + CurrentConversation.setInfo("rdvAccount", CurrentAccount.uri) + CurrentConversation.setInfo("rdvDevice", devicesListView.currentItem.deviceId) + close() + } + } + + MaterialButton { + id: rmDeviceBtn + + Layout.alignment: Qt.AlignCenter + enabled: devicesListView.currentItem + + text: JamiStrings.removeCurrentDevice + toolTipText: JamiStrings.removeCurrentDevice + + onClicked: { + CurrentConversation.setInfo("rdvAccount", "") + CurrentConversation.setInfo("rdvDevice", "") + close() + } + } + } + + + + } + } +} \ No newline at end of file diff --git a/src/app/mainview/components/HostPopup.qml b/src/app/mainview/components/HostPopup.qml new file mode 100644 index 000000000..9f2a64bad --- /dev/null +++ b/src/app/mainview/components/HostPopup.qml @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects + +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +import "../../commoncomponents" + +BaseModalDialog { + id: root + + width: 488 + height: 256 + + property bool isAdmin: { + var role = UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, CurrentAccount.uri) + return role === Member.Role.ADMIN + } + + + popupContent: Rectangle { + id: rect + + color: JamiTheme.transparentColor + width: root.width + + + PushButton { + id: btnCancel + imageColor: "grey" + normalColor: "transparent" + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: 10 + anchors.rightMargin: 10 + source: JamiResources.round_close_24dp_svg + onClicked: { close();} + } + + ColumnLayout { + id: mainLayout + anchors.fill: parent + anchors.margins: JamiTheme.preferredMarginSize + + Label { + id: informativeLabel + + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + Layout.topMargin: 26 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: JamiStrings.needsHost + color: JamiTheme.primaryForegroundColor + } + + MaterialButton { + id: becomeHostBtn + + Layout.alignment: Qt.AlignCenter + + Layout.topMargin: 26 + text: isAdmin? JamiStrings.becomeHostOneCall : JamiStrings.hostThisCall + + onClicked: { + MessagesAdapter.joinCall(CurrentAccount.uri, CurrentAccount.deviceId, "0") + close() + } + } + + MaterialButton { + id: becomeDefaultHostBtn + + Layout.alignment: Qt.AlignCenter + + text: JamiStrings.becomeDefaultHost + toolTipText: JamiStrings.becomeDefaultHost + + visible: isAdmin + + onClicked: { + CurrentConversation.setInfo("rdvAccount", CurrentAccount.uri) + CurrentConversation.setInfo("rdvDevice", devicesListView.currentItem.deviceId) + MessagesAdapter.joinCall(CurrentAccount.uri, CurrentAccount.deviceId, "0") + close() + } + } + } + } +} \ No newline at end of file diff --git a/src/app/mainview/components/MessageListView.qml b/src/app/mainview/components/MessageListView.qml index b30564884..bd2e971b9 100644 --- a/src/app/mainview/components/MessageListView.qml +++ b/src/app/mainview/components/MessageListView.qml @@ -241,7 +241,7 @@ JamiListView { DelegateChoice { roleValue: Interaction.Type.CALL - GeneratedMessageDelegate { + CallMessageDelegate { Component.onCompleted: { computeChatview(this, index) } diff --git a/src/app/mainview/components/NotificationArea.qml b/src/app/mainview/components/NotificationArea.qml new file mode 100644 index 000000000..be025ba00 --- /dev/null +++ b/src/app/mainview/components/NotificationArea.qml @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects + +import net.jami.Models 1.1 +import net.jami.Adapters 1.1 +import net.jami.Constants 1.1 + +import "../../commoncomponents" + +Rectangle { + id: root + + opacity: visible + color: CurrentConversation.color + + property string id: "" + property string uri: "" + property string device: "" + + property string textColor: UtilsAdapter.luma(root.color) ? + JamiTheme.chatviewTextColorLight : + JamiTheme.chatviewTextColorDark + RowLayout { + anchors.fill: parent + anchors.margins: JamiTheme.preferredMarginSize + spacing: 0 + + Text { + id: errorLabel + Layout.alignment: Qt.AlignVCenter + Layout.margins: 0 + text: JamiStrings.wantToJoin + color: root.textColor + font.pixelSize: JamiTheme.headerFontSize + elide: Text.ElideRight + } + + RowLayout { + id: controls + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + + PushButton { + id: joinCallInAudio + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Layout.rightMargin: JamiTheme.preferredMarginSize + + source: JamiResources.place_audiocall_24dp_svg + toolTipText: JamiStrings.joinCall + + imageColor: root.textColor + normalColor: "transparent" + hoveredColor: Qt.rgba(255, 255, 255, 0.2) + border.width: 1 + border.color: root.textColor + + onClicked: MessagesAdapter.joinCall(uri, device, id, true) + } + + + PushButton { + id: joinCallInVideo + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Layout.rightMargin: JamiTheme.preferredMarginSize + + source: JamiResources.videocam_24dp_svg + toolTipText: JamiStrings.joinCall + + imageColor: root.textColor + normalColor: "transparent" + hoveredColor: Qt.rgba(255, 255, 255, 0.2) + border.width: 1 + border.color: root.textColor + visible: CurrentAccount.videoEnabled_Video + + onClicked: MessagesAdapter.joinCall(uri, device, id) + } + + PushButton { + id: btnClose + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + + imageColor: root.textColor + normalColor: JamiTheme.transparentColor + + source: JamiResources.round_close_24dp_svg + + onClicked: ConversationsAdapter.ignoreActiveCall(CurrentConversation.id, id, uri, device) + } + } + } + + Behavior on opacity { + NumberAnimation { + from: 0 + duration: JamiTheme.shortFadeDuration + } + } +} \ No newline at end of file diff --git a/src/app/mainview/components/SmartListItemDelegate.qml b/src/app/mainview/components/SmartListItemDelegate.qml index 563610587..6b6f9e32c 100644 --- a/src/app/mainview/components/SmartListItemDelegate.qml +++ b/src/app/mainview/components/SmartListItemDelegate.qml @@ -22,9 +22,10 @@ import QtQuick.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects -import net.jami.Models 1.1 import net.jami.Adapters 1.1 import net.jami.Constants 1.1 +import net.jami.Enums 1.1 +import net.jami.Models 1.1 import "../../commoncomponents" @@ -148,11 +149,6 @@ ItemDelegate { font.hintingPreference: Font.PreferNoHinting maximumLineCount: 1 color: JamiTheme.textColor - // deal with poor rendering of the pencil emoji on Windows - font.family: Qt.platform.os === "windows" && Draft ? - "Segoe UI Emoji" : - Qt.application.font.family - lineHeight: font.family === "Segoe UI Emoji" ? 1.25 : 1 } } Text { @@ -175,6 +171,13 @@ ItemDelegate { color: JamiTheme.primaryForegroundColor } + // Show that a call is ongoing for groups indicator + ResponsiveImage { + visible: ActiveCallsCount && !root.highlighted + source: JamiResources.videocam_24dp_svg + color: JamiTheme.primaryForegroundColor + } + ColumnLayout { Layout.fillHeight: true spacing: 2 @@ -232,6 +235,8 @@ ItemDelegate { if (!interactive) return; ListView.view.model.select(index) + if (CurrentConversation.isSwarm && !CurrentConversation.isCoreDialog && !UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm)) + return; // For now disable calls for swarm with multiple participants if (LRCInstance.currentAccountType === Profile.Type.SIP || !CurrentAccount.videoEnabled_Video) CallAdapter.placeAudioOnlyCall() else { diff --git a/src/app/mainview/components/SwarmDetailsPanel.qml b/src/app/mainview/components/SwarmDetailsPanel.qml index 7625a76e9..aa1af54e7 100644 --- a/src/app/mainview/components/SwarmDetailsPanel.qml +++ b/src/app/mainview/components/SwarmDetailsPanel.qml @@ -35,6 +35,11 @@ Rectangle { color: CurrentConversation.color property var isAdmin: UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, CurrentAccount.uri) === Member.Role.ADMIN + + DevicesListPopup { + id: devicesListPopup + } + ColumnLayout { id: swarmProfileDetails Layout.fillHeight: true @@ -343,6 +348,114 @@ Rectangle { } } + SwarmDetailsItem { + id: settingsSwarmItem + Layout.fillWidth: true + Layout.preferredHeight: JamiTheme.settingsFontSize + 2 * JamiTheme.preferredMarginSize + 4 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: JamiTheme.preferredMarginSize + + Text { + id: settingsSwarmText + Layout.fillWidth: true + Layout.preferredHeight: 30 + Layout.rightMargin: JamiTheme.preferredMarginSize + Layout.maximumWidth: settingsSwarmItem.width / 2 + + text: JamiStrings.defaultCallHost + font.pointSize: JamiTheme.settingsFontSize + font.kerning: true + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + + color: JamiTheme.textColor + } + + + RowLayout { + id: swarmRdvPref + spacing: 10 + Layout.alignment: Qt.AlignRight + Layout.maximumWidth: settingsSwarmItem.width / 2 + + Connections { + target: CurrentConversation + + function onRdvAccountChanged() { + // This avoid incorrect avatar by always modifying the mode before the imageId + avatar.mode = CurrentConversation.rdvAccount === CurrentAccount.uri ? Avatar.Mode.Account : Avatar.Mode.Contact + avatar.imageId = CurrentConversation.rdvAccount === CurrentAccount.uri ? CurrentAccount.id : CurrentConversation.rdvAccount + } + } + + Avatar { + id: avatar + width: JamiTheme.contactMessageAvatarSize + height: JamiTheme.contactMessageAvatarSize + Layout.leftMargin: JamiTheme.preferredMarginSize + Layout.topMargin: JamiTheme.preferredMarginSize / 2 + visible: CurrentConversation.rdvAccount !== "" + + imageId: "" + showPresenceIndicator: false + mode: Avatar.Mode.Account + } + + ColumnLayout { + spacing: 0 + Layout.alignment: Qt.AlignVCenter + + ElidedTextLabel { + id: bestName + + eText: { + if (CurrentConversation.rdvAccount === "") + return JamiStrings.none + else if (CurrentConversation.rdvAccount === CurrentAccount.uri) + return CurrentAccount.bestName + else + return UtilsAdapter.getBestNameForUri(CurrentAccount.id, CurrentConversation.rdvAccount) + } + maxWidth: JamiTheme.preferredFieldWidth + + font.pointSize: JamiTheme.participantFontSize + color: JamiTheme.primaryForegroundColor + font.kerning: true + + verticalAlignment: Text.AlignVCenter + } + + ElidedTextLabel { + id: deviceId + + eText: CurrentConversation.rdvDevice === "" ? JamiStrings.none : CurrentConversation.rdvDevice + visible: CurrentConversation.rdvDevice !== "" + maxWidth: JamiTheme.preferredFieldWidth + + font.pointSize: JamiTheme.participantFontSize + color: JamiTheme.textColorHovered + font.kerning: true + + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + } + } + } + } + + TapHandler { + target: parent + + enabled: parent.visible && root.isAdmin + onTapped: function onTapped(eventPoint) { + devicesListPopup.open() + } + } + } + RowLayout { Layout.leftMargin: JamiTheme.preferredMarginSize Layout.preferredHeight: JamiTheme.settingsFontSize + 2 * JamiTheme.preferredMarginSize + 4 diff --git a/src/app/messagesadapter.cpp b/src/app/messagesadapter.cpp index 5f8c0ae99..620d5ea5d 100644 --- a/src/app/messagesadapter.cpp +++ b/src/app/messagesadapter.cpp @@ -200,6 +200,19 @@ MessagesAdapter::retryInteraction(const QString& interactionId) ->retryInteraction(lrcInstance_->get_selectedConvUid(), interactionId); } +void +MessagesAdapter::joinCall(const QString& uri, + const QString& deviceId, + const QString& confId, + bool isAudioOnly) +{ + lrcInstance_->getCurrentConversationModel()->joinCall(lrcInstance_->get_selectedConvUid(), + uri, + deviceId, + confId, + isAudioOnly); +} + void MessagesAdapter::copyToDownloads(const QString& interactionId, const QString& displayName) { diff --git a/src/app/messagesadapter.h b/src/app/messagesadapter.h index 446f8b61e..c8c212879 100644 --- a/src/app/messagesadapter.h +++ b/src/app/messagesadapter.h @@ -79,6 +79,10 @@ protected: Q_INVOKABLE void openDirectory(const QString& arg); Q_INVOKABLE void retryInteraction(const QString& interactionId); Q_INVOKABLE void deleteInteraction(const QString& interactionId); + Q_INVOKABLE void joinCall(const QString& uri, + const QString& deviceId, + const QString& confId, + bool isAudioOnly = false); Q_INVOKABLE void copyToDownloads(const QString& interactionId, const QString& displayName); Q_INVOKABLE void userIsComposing(bool isComposing); Q_INVOKABLE QVariantMap isLocalImage(const QString& mimeName); diff --git a/src/app/settingsview/components/TroubleshootSettings.qml b/src/app/settingsview/components/TroubleshootSettings.qml index d7303ebae..4b9e7937a 100644 --- a/src/app/settingsview/components/TroubleshootSettings.qml +++ b/src/app/settingsview/components/TroubleshootSettings.qml @@ -85,4 +85,17 @@ ColumnLayout { } } } + + ToggleSwitch { + id: checkboxCallSwarm + Layout.fillWidth: true + Layout.leftMargin: JamiTheme.preferredMarginSize + checked: UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm) + labelText: JamiStrings.experimentalCallSwarm + fontPointSize: JamiTheme.settingsFontSize + tooltipText: JamiStrings.experimentalCallSwarmTooltip + onSwitchToggled: { + UtilsAdapter.setAppValue(Settings.Key.EnableExperimentalSwarm, checked) + } + } } diff --git a/src/app/utilsadapter.cpp b/src/app/utilsadapter.cpp index cc44971d9..bb9d4937c 100644 --- a/src/app/utilsadapter.cpp +++ b/src/app/utilsadapter.cpp @@ -371,6 +371,8 @@ UtilsAdapter::setAppValue(const Settings::Key key, const QVariant& value) settingsManager_->loadTranslations(); else if (key == Settings::Key::BaseZoom) Q_EMIT changeFontSize(); + else if (key == Settings::Key::EnableExperimentalSwarm) + Q_EMIT showExperimentalCallSwarm(); else if (key == Settings::Key::ShowChatviewHorizontally) Q_EMIT chatviewPositionChanged(); else if (key == Settings::Key::AppTheme) diff --git a/src/app/utilsadapter.h b/src/app/utilsadapter.h index 7a239c20b..dd2b2ac92 100644 --- a/src/app/utilsadapter.h +++ b/src/app/utilsadapter.h @@ -126,6 +126,7 @@ Q_SIGNALS: void changeFontSize(); void chatviewPositionChanged(); void appThemeChanged(); + void showExperimentalCallSwarm(); private: QClipboard* clipboard_; diff --git a/src/libclient/api/conversation.h b/src/libclient/api/conversation.h index 5cb87ecc0..2671b7a1c 100644 --- a/src/libclient/api/conversation.h +++ b/src/libclient/api/conversation.h @@ -71,6 +71,9 @@ struct Info QString uid = ""; QString accountId; QVector<member::Member> participants; + VectorMapStringString activeCalls; + VectorMapStringString ignoredActiveCalls; + QString callId; QString confId; std::unique_ptr<MessageListModel> interactions; @@ -84,6 +87,18 @@ struct Info MapStringString infos {}; MapStringString preferences {}; + int indexOfActiveCall(const MapStringString& commit) + { + for (auto idx = 0; idx != activeCalls.size(); ++idx) { + const auto& call = activeCalls[idx]; + if (call["id"] == commit["confId"] && call["uri"] == commit["uri"] + && call["device"] == commit["device"]) { + return idx; + } + } + return -1; + } + QString getCallId() const { return confId.isEmpty() ? callId : confId; diff --git a/src/libclient/api/conversationmodel.h b/src/libclient/api/conversationmodel.h index 13e158431..8106ba645 100644 --- a/src/libclient/api/conversationmodel.h +++ b/src/libclient/api/conversationmodel.h @@ -210,6 +210,11 @@ public: * @param uid of the conversation */ void placeAudioOnlyCall(const QString& uid); + void joinCall(const QString& uid, + const QString& confId, + const QString& uri, + const QString& deviceId, + bool isAudioOnly); /** * Send a message to the conversation * @param uid of the conversation @@ -378,6 +383,17 @@ public: * @param conversationId */ void popFrontError(const QString& conversationId); + /** + * Ignore an active call + * @param convId + * @param id + * @param uri + * @param device + */ + void ignoreActiveCall(const QString& convId, + const QString& id, + const QString& uri, + const QString& device); /** * @return if conversations requests exists. @@ -432,15 +448,6 @@ Q_SIGNALS: void newInteraction(const QString& uid, QString& interactionId, const interaction::Info& interactionInfo) const; - /** - * Emitted when an interaction got a new status - * @param convUid conversation which owns the interaction - * @param interactionId - * @param msg - */ - void interactionStatusUpdated(const QString& convUid, - const QString& interactionId, - const api::interaction::Info& msg) const; /** * Emitted when an interaction got removed from the conversation * @param convUid conversation which owns the interaction @@ -546,6 +553,11 @@ Q_SIGNALS: */ void newMessagesAvailable(const QString& accountId, const QString& conversationId) const; + /** + * Emitted whenever conversation's calls changed + */ + void activeCallsChanged(const QString& accountId, const QString& conversationId) const; + /** * Emitted when creation of conversation started, finished with success or finisfed with error * @param accountId account id @@ -598,6 +610,11 @@ Q_SIGNALS: void messagesFoundProcessed(const QString& accountId, const VectorMapStringString& messageIds, const QVector<interaction::Info>& messageInformations) const; + /** + * Emitted once a conversation needs somebody to host the call + * @param callId + */ + void needsHost(const QString& conversationId) const; private: std::unique_ptr<ConversationModelPimpl> pimpl_; diff --git a/src/libclient/api/interaction.h b/src/libclient/api/interaction.h index 5993fc79e..54b22fb6e 100644 --- a/src/libclient/api/interaction.h +++ b/src/libclient/api/interaction.h @@ -272,6 +272,7 @@ struct Info QString authorUri; QString body; QString parentId = ""; + QString confId; std::time_t timestamp = 0; std::time_t duration = 0; Type type = Type::INVALID; @@ -318,6 +319,8 @@ struct Info body = QObject::tr("Swarm created"); } else if (type == Type::CALL) { duration = message["duration"].toInt() / 1000; + if (message.contains("confId")) + confId = message["confId"]; } commit = message; } diff --git a/src/libclient/authority/storagehelper.cpp b/src/libclient/authority/storagehelper.cpp index 1425a49da..c17364c8b 100644 --- a/src/libclient/authority/storagehelper.cpp +++ b/src/libclient/authority/storagehelper.cpp @@ -167,7 +167,7 @@ getFormattedCallDuration(const std::time_t duration) } QString -getCallInteractionString(const QString& authorUri, const std::time_t& duration) +getCallInteractionStringNonSwarm(const QString& authorUri, const std::time_t& duration) { if (duration < 0) { if (authorUri.isEmpty()) { @@ -190,6 +190,17 @@ getCallInteractionString(const QString& authorUri, const std::time_t& duration) } } +QString +getCallInteractionString(const api::interaction::Info& info) +{ + if (!info.confId.isEmpty()) { + if (info.duration <= 0) { + return QObject::tr("Join call"); + } + } + return getCallInteractionStringNonSwarm(info.authorUri, info.duration); +} + QString getContactInteractionString(const QString& authorUri, const api::interaction::Status& status) { @@ -510,7 +521,7 @@ getHistory(Database& db, api::conversation::Info& conversation) : std::stoi(durationString.toStdString()); auto status = api::interaction::to_status(payloads[i + 5]); if (type == api::interaction::Type::CALL) { - body = getCallInteractionString(payloads[i + 1], duration); + body = getCallInteractionStringNonSwarm(payloads[i + 1], duration); } else if (type == api::interaction::Type::CONTACT) { body = getContactInteractionString(payloads[i + 1], status); } diff --git a/src/libclient/authority/storagehelper.h b/src/libclient/authority/storagehelper.h index 2dc68a315..4721f17c9 100644 --- a/src/libclient/authority/storagehelper.h +++ b/src/libclient/authority/storagehelper.h @@ -56,11 +56,11 @@ QString prepareUri(const QString& uri, api::profile::Type type); /** * Get a formatted string for a call interaction's body - * @param author_uri - * @param duration of the call + * @param info * @return the formatted and translated call message string */ -QString getCallInteractionString(const QString& authorUri, const std::time_t& duration); +QString getCallInteractionString(const api::interaction::Info& info); +QString getCallInteractionStringNonSwarm(const QString& authorUri, const std::time_t& duration); /** * Get a formatted string for a contact interaction's body diff --git a/src/libclient/callbackshandler.cpp b/src/libclient/callbackshandler.cpp index ab3949b35..76e07a542 100644 --- a/src/libclient/callbackshandler.cpp +++ b/src/libclient/callbackshandler.cpp @@ -128,6 +128,12 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent) &CallbacksHandler::slotAccountMessageStatusChanged, Qt::QueuedConnection); + connect(&ConfigurationManager::instance(), + &ConfigurationManagerInterface::needsHost, + this, + &CallbacksHandler::slotNeedsHost, + Qt::QueuedConnection); + connect(&ConfigurationManager::instance(), &ConfigurationManagerInterface::accountDetailsChanged, this, @@ -349,6 +355,11 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent) this, &CallbacksHandler::slotOnConversationError, Qt::QueuedConnection); + connect(&ConfigurationManager::instance(), + &ConfigurationManagerInterface::activeCallsChanged, + this, + &CallbacksHandler::slotActiveCallsChanged, + Qt::QueuedConnection); connect(&ConfigurationManager::instance(), &ConfigurationManagerInterface::conversationPreferencesUpdated, this, @@ -576,6 +587,7 @@ CallbacksHandler::slotConferenceChanged(const QString& accountId, const QString& callId, const QString& state) { + Q_EMIT conferenceChanged(accountId, callId, state); slotCallStateChanged(accountId, callId, state, 0); } @@ -595,6 +607,12 @@ CallbacksHandler::slotAccountMessageStatusChanged(const QString& accountId, Q_EMIT accountMessageStatusChanged(accountId, conversationId, peer, messageId, status); } +void +CallbacksHandler::slotNeedsHost(const QString& accountId, const QString& conversationId) +{ + Q_EMIT needsHost(accountId, conversationId); +} + void CallbacksHandler::slotDataTransferEvent(const QString& accountId, const QString& conversationId, @@ -823,6 +841,14 @@ CallbacksHandler::slotOnConversationError(const QString& accountId, Q_EMIT conversationError(accountId, conversationId, code, what); } +void +CallbacksHandler::slotActiveCallsChanged(const QString& accountId, + const QString& conversationId, + const VectorMapStringString& activeCalls) +{ + Q_EMIT activeCallsChanged(accountId, conversationId, activeCalls); +} + void CallbacksHandler::slotConversationPreferencesUpdated(const QString& accountId, const QString& conversationId, diff --git a/src/libclient/callbackshandler.h b/src/libclient/callbackshandler.h index 4f3ad2711..0466f7ec9 100644 --- a/src/libclient/callbackshandler.h +++ b/src/libclient/callbackshandler.h @@ -195,12 +195,19 @@ Q_SIGNALS: * @param callId of the conference */ void conferenceCreated(const QString& accountId, const QString& callId); + void conferenceChanged(const QString& accountId, const QString& confId, const QString& state); /** * Connect this signal to know when a conference is removed * @param accountId * @param callId of the conference */ void conferenceRemoved(const QString& accountId, const QString& callId); + /** + * Connect this signal to know if a conversation needs an host. + * @param accountId, account linked + * @param conversationId id of the conversation + */ + void needsHost(const QString& accountId, const QString& conversationId); /** * Connect this signal to know when a message sent get a new status * @param accountId, account linked @@ -374,6 +381,9 @@ Q_SIGNALS: const QString& conversationId, int code, const QString& what); + void activeCallsChanged(const QString& accountId, + const QString& conversationId, + const VectorMapStringString& activeCalls); void conversationPreferencesUpdated(const QString& accountId, const QString& conversationId, const MapStringString& preferences); @@ -549,6 +559,12 @@ private Q_SLOTS: const QString& peer, const QString& messageId, int status); + /** + * Emit needsHost + * @param accountId, account linked + * @param conversationId id of the conversation + */ + void slotNeedsHost(const QString& accountId, const QString& conversationId); void slotDataTransferEvent(const QString& accountId, const QString& conversationId, @@ -699,6 +715,9 @@ private Q_SLOTS: const QString& conversationId, int code, const QString& what); + void slotActiveCallsChanged(const QString& accountId, + const QString& conversationId, + const VectorMapStringString& activeCalls); private: const api::Lrc& parent; diff --git a/src/libclient/callmodel.cpp b/src/libclient/callmodel.cpp index e5fe8c8a2..354d67233 100644 --- a/src/libclient/callmodel.cpp +++ b/src/libclient/callmodel.cpp @@ -239,6 +239,9 @@ public Q_SLOTS: * @param callId */ void slotConferenceCreated(const QString& accountId, const QString& callId); + void slotConferenceChanged(const QString& accountId, + const QString& callId, + const QString& state); /** * Listen from CallbacksHandler when a voice mail notice is incoming * @param accountId @@ -409,6 +412,23 @@ CallModel::getAdvancedInformation() const return pimpl_->callAdvancedInformation(); } +void +CallModel::emplaceConversationConference(const QString& confId) +{ + if (hasCall(confId)) + return; + + auto callInfo = std::make_shared<call::Info>(); + callInfo->id = confId; + callInfo->isOutgoing = false; + callInfo->status = call::Status::SEARCHING; + callInfo->type = call::Type::CONFERENCE; + callInfo->isAudioOnly = false; + callInfo->videoMuted = false; + callInfo->mediaList = {}; + pimpl_->calls.emplace(confId, std::move(callInfo)); +} + void CallModel::muteMedia(const QString& callId, const QString& label, bool mute) { @@ -940,6 +960,10 @@ CallModelPimpl::CallModelPimpl(const CallModel& linked, &CallbacksHandler::conferenceCreated, this, &CallModelPimpl::slotConferenceCreated); + connect(&callbacksHandler, + &CallbacksHandler::conferenceChanged, + this, + &CallModelPimpl::slotConferenceChanged); connect(&callbacksHandler, &CallbacksHandler::voiceMailNotify, this, @@ -1566,9 +1590,13 @@ CallModelPimpl::slotOnConferenceInfosUpdated(const QString& confId, QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId); Q_FOREACH (const auto& call, callList) { Q_EMIT linked.callAddedToConference(call, confId); - calls[call]->videoMuted = it->second->videoMuted; - calls[call]->audioMuted = it->second->audioMuted; - Q_EMIT linked.callInfosChanged(linked.owner.id, call); + if (calls.find(call) == calls.end()) { + qWarning() << "Call not found"; + } else { + calls[call]->videoMuted = it->second->videoMuted; + calls[call]->audioMuted = it->second->audioMuted; + Q_EMIT linked.callInfosChanged(linked.owner.id, call); + } } Q_EMIT linked.callInfosChanged(linked.owner.id, confId); Q_EMIT linked.onParticipantsChanged(confId); @@ -1585,14 +1613,7 @@ CallModelPimpl::slotConferenceCreated(const QString& accountId, const QString& c { if (accountId != linked.owner.id) return; - // Detect if conference is created for this account QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId); - auto hasConference = false; - Q_FOREACH (const auto& call, callList) { - hasConference |= linked.hasCall(call); - } - if (!hasConference) - return; auto callInfo = std::make_shared<call::Info>(); callInfo->id = confId; @@ -1625,6 +1646,20 @@ CallModelPimpl::slotConferenceCreated(const QString& accountId, const QString& c } } +void +CallModelPimpl::slotConferenceChanged(const QString& accountId, + const QString& confId, + const QString& state) +{ + if (accountId != linked.owner.id) + return; + // Detect if conference is created for this account + QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId); + Q_FOREACH (const auto& call, callList) { + Q_EMIT linked.callAddedToConference(call, confId); + } +} + void CallModelPimpl::sendProfile(const QString& callId) { diff --git a/src/libclient/conversationmodel.cpp b/src/libclient/conversationmodel.cpp index 2e1d7b471..23b9bc558 100644 --- a/src/libclient/conversationmodel.cpp +++ b/src/libclient/conversationmodel.cpp @@ -193,10 +193,7 @@ public: /** * Handle data transfer progression */ - void updateTransferProgress(QTimer* timer, - const QString& conversation, - int conversationIdx, - const QString& interactionId); + void updateTransferProgress(QTimer* timer, int conversationIdx, const QString& interactionId); bool usefulDataFromDataTransfer(const QString& fileId, const datatransfer::Info& info, @@ -385,6 +382,9 @@ public Q_SLOTS: const QString& conversationId, int code, const QString& what); + void slotActiveCallsChanged(const QString& accountId, + const QString& conversationId, + const VectorMapStringString& activeCalls); void slotConversationReady(const QString& accountId, const QString& conversationId); void slotConversationRemoved(const QString& accountId, const QString& conversationId); void slotConversationPreferencesUpdated(const QString& accountId, @@ -853,6 +853,30 @@ ConversationModel::deleteObsoleteHistory(int days) storage::deleteObsoleteHistory(pimpl_->db, date); } +void +ConversationModel::joinCall(const QString& uid, + const QString& uri, + const QString& deviceId, + const QString& confId, + bool isAudioOnly) +{ + try { + auto& conversation = pimpl_->getConversationForUid(uid, true).get(); + if (!conversation.callId.isEmpty()) { + qWarning() << "Already in a call for swarm:" + uid; + return; + } + conversation.callId = owner.callModel->createCall("rdv:" + uid + "/" + uri + "/" + deviceId + + "/" + confId, + isAudioOnly); + // Update interaction status + pimpl_->invalidateModel(); + emit selectConversation(uid); + emit conversationUpdated(uid); + } catch (...) { + } +} + void ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly) { @@ -864,6 +888,19 @@ ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly) << "ConversationModel::placeCall can't call a conversation without participant"; return; } + + if (!conversation.isCoreDialog() && conversation.isSwarm()) { + qDebug() << "Start call for swarm:" + uid; + conversation.callId = linked.owner.callModel->createCall("swarm:" + uid, isAudioOnly); + + // Update interaction status + invalidateModel(); + emit linked.selectConversation(conversation.uid); + emit linked.conversationUpdated(conversation.uid); + Q_EMIT linked.dataChanged(indexOf(conversation.uid)); + return; + } + auto& peers = peersForConversation(conversation); // there is no calls in group with more than 2 participants if (peers.size() != 1) { @@ -1028,6 +1065,25 @@ ConversationModel::popFrontError(const QString& conversationId) Q_EMIT onConversationErrorsUpdated(conversationId); } +void +ConversationModel::ignoreActiveCall(const QString& conversationId, + const QString& id, + const QString& uri, + const QString& device) +{ + auto conversationOpt = getConversationForUid(conversationId); + if (!conversationOpt.has_value()) + return; + + auto& conversation = conversationOpt->get(); + MapStringString mapCall; + mapCall["id"] = id; + mapCall["uri"] = uri; + mapCall["device"] = device; + conversation.ignoredActiveCalls.push_back(mapCall); + Q_EMIT activeCallsChanged(owner.id, conversationId); +} + void ConversationModel::setConversationPreferences(const QString& conversationId, const MapStringString prefs) @@ -1835,6 +1891,9 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked, &ConfigurationManagerInterface::composingStatusChanged, this, &ConversationModelPimpl::slotComposingStatusChanged); + connect(&callbacksHandler, &CallbacksHandler::needsHost, this, [&](auto, auto convId) { + emit linked.needsHost(convId); + }); // data transfer connect(&*linked.owner.contactModel, @@ -1918,6 +1977,10 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked, &CallbacksHandler::conversationPreferencesUpdated, this, &ConversationModelPimpl::slotConversationPreferencesUpdated); + connect(&callbacksHandler, + &CallbacksHandler::activeCallsChanged, + this, + &ConversationModelPimpl::slotActiveCallsChanged); } ConversationModelPimpl::~ConversationModelPimpl() @@ -2066,6 +2129,10 @@ ConversationModelPimpl::~ConversationModelPimpl() &CallbacksHandler::conversationError, this, &ConversationModelPimpl::slotOnConversationError); + disconnect(&callbacksHandler, + &CallbacksHandler::activeCallsChanged, + this, + &ConversationModelPimpl::slotActiveCallsChanged); disconnect(&callbacksHandler, &CallbacksHandler::conversationPreferencesUpdated, this, @@ -2383,7 +2450,7 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId, linked.owner.dataTransferModel->registerTransferId(fileId, msgId); downloadFile = (bytesProgress == 0); } else if (msg.type == interaction::Type::CALL) { - msg.body = storage::getCallInteractionString(msg.authorUri, msg.duration); + msg.body = storage::getCallInteractionString(msg); } else if (msg.type == interaction::Type::CONTACT) { auto bestName = msg.authorUri == linked.owner.profileInfo.uri ? linked.owner.accountModel->bestNameForAccount(linked.owner.id) @@ -2498,6 +2565,8 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId, api::datatransfer::Info info; QString fileId; + auto updateUnread = false; + if (msg.type == interaction::Type::DATA_TRANSFER) { // save data transfer interaction to db and assosiate daemon id with interaction id, // conversation id and db id @@ -2520,8 +2589,14 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId, : bytesProgress == totalSize ? interaction::Status::TRANSFER_FINISHED : interaction::Status::TRANSFER_ONGOING; linked.owner.dataTransferModel->registerTransferId(fileId, msgId); + if (msg.authorUri != linked.owner.profileInfo.uri) { + updateUnread = true; + } } else if (msg.type == interaction::Type::CALL) { - msg.body = storage::getCallInteractionString(msg.authorUri, msg.duration); + // If we're a call in a swarm + if (msg.authorUri != linked.owner.profileInfo.uri) + updateUnread = true; + msg.body = storage::getCallInteractionString(msg); } else if (msg.type == interaction::Type::CONTACT) { auto bestName = msg.authorUri == linked.owner.profileInfo.uri ? linked.owner.accountModel->bestNameForAccount(linked.owner.id) @@ -2529,16 +2604,24 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId, msg.body = interaction::getContactInteractionString(bestName, interaction::to_action( message["action"])); - } else if (msg.type == interaction::Type::TEXT - && msg.authorUri != linked.owner.profileInfo.uri) { - conversation.unreadMessages++; + if (msg.authorUri != linked.owner.profileInfo.uri) { + updateUnread = true; + } + } else if (msg.type == interaction::Type::TEXT) { + if (msg.authorUri != linked.owner.profileInfo.uri) { + updateUnread = true; + } } else if (msg.type == interaction::Type::EDITED) { conversation.interactions->addEdition(msgId, msg, true); } + if (!insertSwarmInteraction(msgId, msg, conversation, false)) { // message already exists return; } + if (updateUnread) { + conversation.unreadMessages++; + } if (msg.type == interaction::Type::MERGE) { invalidateModel(); return; @@ -2811,6 +2894,24 @@ ConversationModelPimpl::slotOnConversationError(const QString& accountId, } } +void +ConversationModelPimpl::slotActiveCallsChanged(const QString& accountId, + const QString& conversationId, + const VectorMapStringString& activeCalls) +{ + if (accountId != linked.owner.id || indexOf(conversationId) < 0) { + return; + } + try { + auto& conversation = getConversationForUid(conversationId).get(); + conversation.activeCalls = activeCalls; + if (activeCalls.empty()) + conversation.ignoredActiveCalls.clear(); + Q_EMIT linked.activeCallsChanged(accountId, conversationId); + } catch (...) { + } +} + void ConversationModelPimpl::slotIncomingContactRequest(const QString& contactUri) { @@ -3074,6 +3175,9 @@ ConversationModelPimpl::addSwarmConversation(const QString& convId) conversation.infos = details; conversation.uid = convId; conversation.accountId = linked.owner.id; + VectorMapStringString activeCalls = ConfigurationManager::instance() + .getActiveCalls(linked.owner.id, convId); + conversation.activeCalls = activeCalls; QString lastRead; VectorString membersLeft; for (auto& member : members) { @@ -3387,12 +3491,13 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId, bool incoming, const std::time_t& duration) { - // do not save call interaction for swarm conversation - try { - auto& conv = getConversationForPeerUri(from).get(); - if (conv.isSwarm()) - return; - } catch (const std::exception&) { + // Get conversation + auto conv_it = std::find_if(conversations.begin(), + conversations.end(), + [&callId](const conversation::Info& conversation) { + return conversation.callId == callId; + }); + if (conv_it == conversations.end()) { // If we have no conversation with peer. try { auto contact = linked.owner.contactModel->getContact(from); @@ -3401,18 +3506,18 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId, storage::beginConversationWithPeer(db, contact.profileInfo.uri); } } catch (const std::exception&) { + return; + } + try { + auto& conv = getConversationForPeerUri(from).get(); + conv.callId = callId; + } catch (...) { + return; } } - - // Get conversation - auto conv_it = std::find_if(conversations.begin(), - conversations.end(), - [&callId](const conversation::Info& conversation) { - return conversation.callId == callId; - }); - if (conv_it == conversations.end()) { + // do not save call interaction for swarm conversation + if (conv_it->isSwarm()) return; - } auto uid = conv_it->uid; auto uriString = incoming ? storage::prepareUri(from, linked.owner.profileInfo.type) : ""; auto msg = interaction::Info {uriString, @@ -3425,7 +3530,7 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId, // update the db auto msgId = storage::addOrUpdateMessage(db, conv_it->uid, msg, callId); // now set the formatted call message string in memory only - msg.body = storage::getCallInteractionString(uriString, duration); + msg.body = storage::getCallInteractionString(msg); bool newInteraction = false; { std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]); @@ -3562,6 +3667,7 @@ ConversationModelPimpl::slotCallAddedToConference(const QString& callId, const Q .getConferenceDetails(linked.owner.id, confId); if (confDetails["STATE"] == "ACTIVE_ATTACHED") Q_EMIT linked.selectConversation(conversation.uid); + return; } } } @@ -3693,7 +3799,7 @@ ConversationModelPimpl::slotUpdateInteractionStatus(const QString& accountId, if (peerId != linked.owner.profileInfo.uri) conversation.interactions->setRead(peerId, messageId); else { - // Here, this means that the daemon synched the displayed message + // Here, this means that the daemon synced the displayed message // so, compute the number of unread messages. conversation.unreadMessages = ConfigurationManager::instance() .countInteractions(linked.owner.id, @@ -4150,7 +4256,7 @@ ConversationModelPimpl::slotTransferStatusOngoing(const QString& fileId, datatra auto conversationIdx = indexOf(conversationId); auto* timer = new QTimer(); connect(timer, &QTimer::timeout, [=] { - updateTransferProgress(timer, conversationId, conversationIdx, interactionId); + updateTransferProgress(timer, conversationIdx, interactionId); }); timer->start(1000); } @@ -4281,7 +4387,6 @@ ConversationModelPimpl::updateTransferStatus(const QString& fileId, void ConversationModelPimpl::updateTransferProgress(QTimer* timer, - const QString&, int conversationIdx, const QString& interactionId) { diff --git a/src/libclient/messagelistmodel.cpp b/src/libclient/messagelistmodel.cpp index e96a258b6..e7013b998 100644 --- a/src/libclient/messagelistmodel.cpp +++ b/src/libclient/messagelistmodel.cpp @@ -68,6 +68,20 @@ MessageListModel::find(const QString& msgId) return interactions_.end(); } +iterator +MessageListModel::findActiveCall(const MapStringString& commit) +{ + iterator it; + for (it = interactions_.begin(); it != interactions_.end(); ++it) { + const auto& itCommit = it->second.commit; + if (itCommit["confId"] == commit["confId"] && itCommit["uri"] == commit["uri"] + && itCommit["device"] == commit["device"]) { + return it; + } + } + return interactions_.end(); +} + iterator MessageListModel::erase(const iterator& it) { @@ -403,6 +417,13 @@ MessageListModel::dataForItem(item_t item, int, int role) const case Role::Timestamp: return QVariant::fromValue(item.second.timestamp); case Role::Duration: + if (!item.second.commit.empty()) { + // For swarm, check the commit value + if (item.second.commit.find("duration") == item.second.commit.end()) + return QVariant::fromValue(0); + else + return QVariant::fromValue(item.second.commit["duration"].toInt() / 1000); + } return QVariant::fromValue(item.second.duration); case Role::Type: return QVariant(static_cast<int>(item.second.type)); @@ -416,6 +437,10 @@ MessageListModel::dataForItem(item_t item, int, int role) const return QVariant(item.second.linkified); case Role::ActionUri: return QVariant(item.second.commit["uri"]); + case Role::ConfId: + return QVariant(item.second.commit["confId"]); + case Role::DeviceId: + return QVariant(item.second.commit["device"]); case Role::ContactAction: return QVariant(item.second.commit["action"]); case Role::PreviousBodies: { diff --git a/src/libclient/messagelistmodel.h b/src/libclient/messagelistmodel.h index fbbf3936f..f5b662bf4 100644 --- a/src/libclient/messagelistmodel.h +++ b/src/libclient/messagelistmodel.h @@ -43,6 +43,8 @@ struct Info; X(IsRead) \ X(ContactAction) \ X(ActionUri) \ + X(ConfId) \ + X(DeviceId) \ X(LinkPreviewInfo) \ X(Linkified) \ X(PreviousBodies) \ @@ -84,7 +86,9 @@ public: interaction::Info message, bool beginning = false); iterator find(const QString& msgId); + iterator findActiveCall(const MapStringString& commit); iterator erase(const iterator& it); + constIterator find(const QString& msgId) const; QPair<iterator, bool> insert(std::pair<QString, interaction::Info> message, bool beginning = false); diff --git a/src/libclient/qtwrapper/configurationmanager_wrap.h b/src/libclient/qtwrapper/configurationmanager_wrap.h index b3690d695..e66bc93d6 100644 --- a/src/libclient/qtwrapper/configurationmanager_wrap.h +++ b/src/libclient/qtwrapper/configurationmanager_wrap.h @@ -92,8 +92,6 @@ public: Q_EMIT this->volatileAccountDetailsChanged(QString(accountID.c_str()), convertMap(details)); }), - exportable_callback<ConfigurationSignal::Error>( - [this](int code) { Q_EMIT this->errorAlert(code); }), exportable_callback<ConfigurationSignal::CertificateExpired>( [this](const std::string& certId) { Q_EMIT this->certificateExpired(QString(certId.c_str())); @@ -129,6 +127,11 @@ public: QString(message_id.c_str()), state); }), + exportable_callback<libjami::ConfigurationSignal::NeedsHost>( + [this](const std::string& account_id, const std::string& conversation_id) { + Q_EMIT this->needsHost(QString(account_id.c_str()), + QString(conversation_id.c_str())); + }), exportable_callback<ConfigurationSignal::IncomingTrustRequest>( [this](const std::string& accountId, const std::string& conversationId, @@ -356,6 +359,14 @@ public: QString(conversationId.c_str()), code, QString(what.c_str())); + }), + exportable_callback<ConfigurationSignal::ActiveCallsChanged>( + [this](const std::string& accountId, + const std::string& conversationId, + const std::vector<std::map<std::string, std::string>>& activeCalls) { + Q_EMIT activeCallsChanged(QString(accountId.c_str()), + QString(conversationId.c_str()), + convertVecMap(activeCalls)); })}; } @@ -432,7 +443,16 @@ public Q_SLOTS: // METHODS QStringList getAccountList() { - QStringList temp = convertStringList(libjami::getAccountList()); + return convertStringList(libjami::getAccountList()); + } + + VectorMapStringString getActiveCalls(const QString& accountId, const QString& convId) + { + VectorMapStringString temp; + for (const auto& x : + libjami::getActiveCalls(accountId.toStdString(), convId.toStdString())) { + temp.push_back(convertMap(x)); + } return temp; } @@ -1195,7 +1215,6 @@ Q_SIGNALS: // SIGNALS unsigned detail_code, const QString& detail_str); void stunStatusSuccess(const QString& message); - void errorAlert(int code); void volatileAccountDetailsChanged(const QString& accountID, MapStringString details); void certificatePinned(const QString& certId); void certificatePathPinned(const QString& path, const QStringList& certIds); @@ -1222,6 +1241,7 @@ Q_SIGNALS: // SIGNALS const QString& peer, const QString& messageId, int status); + void needsHost(const QString& accountId, const QString& conversationId); void nameRegistrationEnded(const QString& accountId, int status, const QString& name); void registeredNameFound(const QString& accountId, int status, @@ -1278,6 +1298,9 @@ Q_SIGNALS: // SIGNALS const QString& conversationId, int code, const QString& what); + void activeCallsChanged(const QString& accountId, + const QString& conversationId, + const VectorMapStringString& activeCalls); void conversationPreferencesUpdated(const QString& accountId, const QString& conversationId, const MapStringString& message); -- GitLab