1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-08 00:35:50 +02:00

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
This commit is contained in:
Sébastien Blin 2022-06-09 11:23:14 -04:00
parent 9b2dbb64ea
commit 0996b167d9
43 changed files with 1276 additions and 90 deletions

2
daemon

@ -1 +1 @@
Subproject commit 54ffd0f4380bdbdc6a2fcb80847b2f5aefad4958 Subproject commit 08ef8dd80d571816259b195b0956a472a40b58ad

View file

@ -54,6 +54,7 @@ extern const QString defaultDownloadPath;
X(NeverShowMeAgain, false) \ X(NeverShowMeAgain, false) \
X(WindowGeometry, QRectF(qQNaN(), qQNaN(), 0., 0.)) \ X(WindowGeometry, QRectF(qQNaN(), qQNaN(), 0., 0.)) \
X(WindowState, QWindow::AutomaticVisibility) \ X(WindowState, QWindow::AutomaticVisibility) \
X(EnableExperimentalSwarm, false) \
X(LANG, "SYSTEM") X(LANG, "SYSTEM")
/* /*

View file

@ -216,6 +216,7 @@ CallAdapter::onParticipantUpdated(const QString& callId, int index)
return; return;
} }
auto infos = getConferencesInfos(); auto infos = getConferencesInfos();
if (index < infos.size())
participantsModel_->updateParticipant(index, infos[index]); participantsModel_->updateParticipant(index, infos[index]);
} catch (...) { } catch (...) {
} }
@ -256,7 +257,8 @@ CallAdapter::onCallStatusChanged(const QString& callId, int code)
const auto& currentConvInfo = lrcInstance_->getConversationFromConvUid(currentConvId); const auto& currentConvInfo = lrcInstance_->getConversationFromConvUid(currentConvId);
// was it a conference and now is a dialog? // 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(), auto it = std::find_if(currentConfSubcalls_.cbegin(),
currentConfSubcalls_.cend(), currentConfSubcalls_.cend(),
[&callId](const QString& cid) { return cid != callId; }); [&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; accountId_ = accountId.isEmpty() ? accountId_ : accountId;
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid); const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid);
if (convInfo.uid.isEmpty()) { if (convInfo.uid.isEmpty())
return; return;
}
auto call = lrcInstance_->getCallInfoForConversation(convInfo, forceCallOnly); auto call = lrcInstance_->getCallInfoForConversation(convInfo, forceCallOnly);
if (!call) { if (!call)
return; return;
}
if (convInfo.uid == lrcInstance_->get_selectedConvUid()) { if (convInfo.uid == lrcInstance_->get_selectedConvUid()) {
auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_); auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_);

View file

@ -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
}

View file

@ -56,6 +56,7 @@ Control {
readonly property real hPadding: JamiTheme.sbsMessageBasePreferredPadding readonly property real hPadding: JamiTheme.sbsMessageBasePreferredPadding
width: ListView.view ? ListView.view.width : 0 width: ListView.view ? ListView.view.width : 0
height: mainColumnLayout.implicitHeight height: mainColumnLayout.implicitHeight
rightPadding: hPadding rightPadding: hPadding
leftPadding: hPadding leftPadding: hPadding

View file

@ -70,7 +70,6 @@ SBSMessageBase {
Math.min(implicitWidth, innerContent.width - senderMargin) Math.min(implicitWidth, innerContent.width - senderMargin)
} }
height: implicitHeight
wrapMode: Label.WrapAtWordBoundaryOrAnywhere wrapMode: Label.WrapAtWordBoundaryOrAnywhere
selectByMouse: true selectByMouse: true
font.pixelSize: isEmojiOnly? JamiTheme.chatviewEmojiSize : JamiTheme.chatviewFontSize font.pixelSize: isEmojiOnly? JamiTheme.chatviewEmojiSize : JamiTheme.chatviewFontSize

View file

@ -487,6 +487,9 @@ Item {
property string troubleshootButton: qsTr("Open logs") property string troubleshootButton: qsTr("Open logs")
property string troubleshootText: qsTr("Get 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 // Recording Settings
property string tipRecordFolder: qsTr("Select a record directory") property string tipRecordFolder: qsTr("Select a record directory")
property string quality: qsTr("Quality") property string quality: qsTr("Quality")
@ -727,6 +730,15 @@ Item {
property string writeTo: qsTr("Write to %1") property string writeTo: qsTr("Write to %1")
property string edit: qsTr("Edit") property string edit: qsTr("Edit")
property string edited: qsTr("Edited") 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 // Invitation View
property string invitationViewSentRequest: qsTr("%1 has sent you a request for a conversation.") 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 muteConversation: qsTr("Mute conversation")
property string ignoreNotificationsTooltip: qsTr("Ignore all notifications from this conversation") property string ignoreNotificationsTooltip: qsTr("Ignore all notifications from this conversation")
property string chooseAColor: qsTr("Choose a color") property string chooseAColor: qsTr("Choose a color")
property string defaultCallHost: qsTr("Default host (calls)")
property string leaveTheSwarm: qsTr("Leave the swarm") property string leaveTheSwarm: qsTr("Leave the swarm")
property string leave: qsTr("Leave") property string leave: qsTr("Leave")
property string typeOfSwarm: qsTr("Type of swarm") property string typeOfSwarm: qsTr("Type of swarm")
property string none: qsTr("None")
// NewSwarmPage // NewSwarmPage
property string youCanAdd8: qsTr("You can add 8 people in the swarm") property string youCanAdd8: qsTr("You can add 8 people in the swarm")

View file

@ -94,6 +94,9 @@ ConversationListModelBase::dataForItem(item_t item, int role) const
return lrcInstance_->getContentDraft(item.uid, item.accountId); return lrcInstance_->getContentDraft(item.uid, item.accountId);
return {}; return {};
} }
case Role::ActiveCallsCount: {
return item.activeCalls.size();
}
case Role::IsRequest: case Role::IsRequest:
return QVariant(item.isRequest); return QVariant(item.isRequest);
case Role::Title: case Role::Title:

View file

@ -44,6 +44,7 @@
X(CallState) \ X(CallState) \
X(SectionName) \ X(SectionName) \
X(AccountId) \ X(AccountId) \
X(ActiveCallsCount) \
X(Draft) \ X(Draft) \
X(IsRequest) \ X(IsRequest) \
X(Mode) \ X(Mode) \

View file

@ -59,7 +59,7 @@ ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
} else { } else {
// selected // selected
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId); const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
if (convInfo.uid.isEmpty()) if (convInfo.uid.isEmpty() || convInfo.accountId != lrcInstance_->get_currentAccountId())
return; return;
auto& accInfo = lrcInstance_->getAccountInfo(convInfo.accountId); auto& accInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
@ -528,6 +528,16 @@ ConversationsAdapter::popFrontError(const QString& convId)
convModel->popFrontError(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 void
ConversationsAdapter::updateConversationDescription(const QString& convId, ConversationsAdapter::updateConversationDescription(const QString& convId,
const QString& newDescription) const QString& newDescription)

View file

@ -59,6 +59,10 @@ public:
Q_INVOKABLE void restartConversation(const QString& convId); Q_INVOKABLE void restartConversation(const QString& convId);
Q_INVOKABLE void updateConversationTitle(const QString& convId, const QString& newTitle); Q_INVOKABLE void updateConversationTitle(const QString& convId, const QString& newTitle);
Q_INVOKABLE void popFrontError(const QString& convId); 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, Q_INVOKABLE void updateConversationDescription(const QString& convId,
const QString& newDescription); const QString& newDescription);

View file

@ -111,6 +111,7 @@ CurrentAccount::updateData()
set_enabled(accInfo.enabled); set_enabled(accInfo.enabled);
set_managerUri(accConfig.managerUri); set_managerUri(accConfig.managerUri);
set_keepAliveEnabled(accConfig.keepAliveEnabled, true); set_keepAliveEnabled(accConfig.keepAliveEnabled, true);
set_deviceId(accConfig.deviceId);
set_peerDiscovery(accConfig.peerDiscovery, true); set_peerDiscovery(accConfig.peerDiscovery, true);
set_sendReadReceipt(accConfig.sendReadReceipt, true); set_sendReadReceipt(accConfig.sendReadReceipt, true);
set_isRendezVous(accConfig.isRendezVous, true); set_isRendezVous(accConfig.isRendezVous, true);

View file

@ -101,6 +101,7 @@ class CurrentAccount final : public QObject
QML_RO_PROPERTY(QString, id) QML_RO_PROPERTY(QString, id)
QML_RO_PROPERTY(QString, uri) QML_RO_PROPERTY(QString, uri)
QML_RO_PROPERTY(QString, deviceId)
QML_RO_PROPERTY(QString, registeredName) QML_RO_PROPERTY(QString, registeredName)
QML_RO_PROPERTY(QString, alias) QML_RO_PROPERTY(QString, alias)
QML_RO_PROPERTY(QString, bestId) QML_RO_PROPERTY(QString, bestId)

View file

@ -53,8 +53,6 @@ CurrentConversation::updateData()
const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
if (auto optConv = accInfo.conversationModel->getConversationForUid(convId)) { if (auto optConv = accInfo.conversationModel->getConversationForUid(convId)) {
auto& convInfo = optConv->get(); auto& convInfo = optConv->get();
set_title(accInfo.conversationModel->title(convId));
set_description(accInfo.conversationModel->description(convId));
set_uris(convInfo.participantsUris()); set_uris(convInfo.participantsUris());
set_isSwarm(convInfo.isSwarm()); set_isSwarm(convInfo.isSwarm());
set_isLegacy(convInfo.isLegacy()); set_isLegacy(convInfo.isLegacy());
@ -104,6 +102,9 @@ CurrentConversation::updateData()
} else if (convInfo.mode == conversation::Mode::PUBLIC) { } else if (convInfo.mode == conversation::Mode::PUBLIC) {
set_modeString(tr("Public group")); set_modeString(tr("Public group"));
} }
onProfileUpdated(convId);
updateActiveCalls(accountId, convId);
} }
} catch (...) { } catch (...) {
qWarning() << "Can't update current conversation data for" << convId; qWarning() << "Can't update current conversation data for" << convId;
@ -111,33 +112,58 @@ CurrentConversation::updateData()
updateErrors(convId); updateErrors(convId);
} }
void
CurrentConversation::onNeedsHost(const QString& convId)
{
if (id_ != convId)
return;
Q_EMIT needsHost();
}
void void
CurrentConversation::setPreference(const QString& key, const QString& value) 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(); auto accountId = lrcInstance_->get_currentAccountId();
const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
auto convId = lrcInstance_->get_selectedConvUid(); 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 QString
CurrentConversation::getPreference(const QString& key) const CurrentConversation::getPreference(const QString& key) const
{
return getPreferences()[key];
}
MapStringString
CurrentConversation::getPreferences() const
{ {
auto accountId = lrcInstance_->get_currentAccountId(); auto accountId = lrcInstance_->get_currentAccountId();
const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId); const auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
auto convId = lrcInstance_->get_selectedConvUid(); auto convId = lrcInstance_->get_selectedConvUid();
if (auto optConv = accInfo.conversationModel->getConversationForUid(convId)) { if (auto optConv = accInfo.conversationModel->getConversationForUid(convId)) {
auto& convInfo = optConv->get(); auto& convInfo = optConv->get();
return convInfo.preferences[key]; auto preferences = accInfo.conversationModel->getConversationPreferences(convId);
return preferences;
} }
return {}; 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 void
CurrentConversation::onConversationUpdated(const QString& convId) CurrentConversation::onConversationUpdated(const QString& convId)
{ {
@ -153,8 +179,27 @@ CurrentConversation::onProfileUpdated(const QString& convId)
// filter for our currently set id // filter for our currently set id
if (id_ != convId) if (id_ != convId)
return; return;
set_title(lrcInstance_->getCurrentConversationModel()->title(convId)); const auto& convModel = lrcInstance_->getCurrentConversationModel();
set_description(lrcInstance_->getCurrentConversationModel()->description(convId)); 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 void
@ -200,11 +245,21 @@ CurrentConversation::connectModel()
this, this,
&CurrentConversation::updateErrors, &CurrentConversation::updateErrors,
Qt::UniqueConnection); Qt::UniqueConnection);
connect(lrcInstance_->getCurrentConversationModel(),
&ConversationModel::activeCallsChanged,
this,
&CurrentConversation::updateActiveCalls,
Qt::UniqueConnection);
connect(lrcInstance_->getCurrentConversationModel(), connect(lrcInstance_->getCurrentConversationModel(),
&ConversationModel::conversationPreferencesUpdated, &ConversationModel::conversationPreferencesUpdated,
this, this,
&CurrentConversation::updateConversationPreferences, &CurrentConversation::updateConversationPreferences,
Qt::UniqueConnection); Qt::UniqueConnection);
connect(lrcInstance_->getCurrentConversationModel(),
&ConversationModel::needsHost,
this,
&CurrentConversation::onNeedsHost,
Qt::UniqueConnection);
} }
void 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 void
CurrentConversation::scrollToMsg(const QString& msg) CurrentConversation::scrollToMsg(const QString& msg)
{ {

View file

@ -43,12 +43,15 @@ class CurrentConversation final : public QObject
QML_PROPERTY(bool, ignoreNotifications) QML_PROPERTY(bool, ignoreNotifications)
QML_PROPERTY(QString, callId) QML_PROPERTY(QString, callId)
QML_PROPERTY(QString, color) QML_PROPERTY(QString, color)
QML_PROPERTY(QString, rdvAccount)
QML_PROPERTY(QString, rdvDevice)
QML_PROPERTY(call::Status, callState) QML_PROPERTY(call::Status, callState)
QML_PROPERTY(bool, inCall) QML_PROPERTY(bool, inCall)
QML_PROPERTY(bool, isTemporary) QML_PROPERTY(bool, isTemporary)
QML_PROPERTY(bool, isContact) QML_PROPERTY(bool, isContact)
QML_PROPERTY(bool, allMessagesLoaded) QML_PROPERTY(bool, allMessagesLoaded)
QML_PROPERTY(QString, modeString) QML_PROPERTY(QString, modeString)
QML_PROPERTY(QVariantList, activeCalls)
QML_PROPERTY(QStringList, errors) QML_PROPERTY(QStringList, errors)
QML_PROPERTY(QStringList, backendErrors) QML_PROPERTY(QStringList, backendErrors)
@ -63,6 +66,8 @@ public:
Q_INVOKABLE void showSwarmDetails() const; Q_INVOKABLE void showSwarmDetails() const;
Q_INVOKABLE void setPreference(const QString& key, const QString& value); Q_INVOKABLE void setPreference(const QString& key, const QString& value);
Q_INVOKABLE QString getPreference(const QString& key) const; 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: Q_SIGNALS:
void scrollTo(const QString& msgId); void scrollTo(const QString& msgId);
@ -70,10 +75,15 @@ Q_SIGNALS:
private Q_SLOTS: private Q_SLOTS:
void updateData(); void updateData();
void onNeedsHost(const QString& convId);
void onConversationUpdated(const QString& convId); void onConversationUpdated(const QString& convId);
void onProfileUpdated(const QString& convId); void onProfileUpdated(const QString& convId);
void updateErrors(const QString& convId); void updateErrors(const QString& convId);
void updateConversationPreferences(const QString& convId); void updateConversationPreferences(const QString& convId);
void updateActiveCalls(const QString&, const QString& convId);
Q_SIGNALS:
void needsHost();
private: private:
LRCInstance* lrcInstance_; LRCInstance* lrcInstance_;

View file

@ -190,9 +190,8 @@ LRCInstance::getCallInfoForConversation(const conversation::Info& convInfo, bool
auto callId = forceCallOnly auto callId = forceCallOnly
? convInfo.callId ? convInfo.callId
: (convInfo.confId.isEmpty() ? convInfo.callId : convInfo.confId); : (convInfo.confId.isEmpty() ? convInfo.callId : convInfo.confId);
if (!accInfo.callModel->hasCall(callId)) { if (!accInfo.callModel->hasCall(callId))
return nullptr; return nullptr;
}
return &accInfo.callModel->getCall(callId); return &accInfo.callModel->getCall(callId);
} catch (...) { } catch (...) {
return nullptr; return nullptr;
@ -372,6 +371,16 @@ LRCInstance::selectConversation(const QString& convId, const QString& accountId)
set_selectedConvUid(convId); 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 void
LRCInstance::deselectConversation() LRCInstance::deselectConversation()
{ {

View file

@ -108,6 +108,9 @@ public:
Q_INVOKABLE void setContentDraft(const QString& convUid, Q_INVOKABLE void setContentDraft(const QString& convUid,
const QString& accountId, const QString& accountId,
const QString& content); const QString& content);
Q_INVOKABLE int indexOfActiveCall(const QString& confId,
const QString& uri,
const QString& deviceId);
int getCurrentAccountIndex(); int getCurrentAccountIndex();
void setCurrAccDisplayName(const QString& displayName); void setCurrAccDisplayName(const QString& displayName);

View file

@ -47,6 +47,10 @@ Rectangle {
color: JamiTheme.chatviewBgColor color: JamiTheme.chatviewBgColor
HostPopup {
id: hostPopup
}
ColumnLayout { ColumnLayout {
anchors.fill: root anchors.fill: root
@ -88,6 +92,10 @@ Rectangle {
addMemberPanel.visible = !addMemberPanel.visible addMemberPanel.visible = !addMemberPanel.visible
} }
} }
function onNeedsHost() {
hostPopup.open()
}
} }
onAddToConversationClicked: { 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 { ConversationErrorsRow {
id: errorRect id: errorRect
color: JamiTheme.filterBadgeColor Layout.fillWidth: true
Layout.preferredHeight: JamiTheme.chatViewHeaderPreferredHeight
visible: false
}
NotificationArea {
id: notificationArea
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: JamiTheme.chatViewHeaderPreferredHeight Layout.preferredHeight: JamiTheme.chatViewHeaderPreferredHeight
visible: false visible: false

View file

@ -20,9 +20,10 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import net.jami.Models 1.1
import net.jami.Constants 1.1
import net.jami.Adapters 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" import "../../commoncomponents"
@ -155,7 +156,7 @@ Rectangle {
PushButton { PushButton {
id: startAAudioCallButton id: startAAudioCallButton
visible: interactionButtonsVisibility && !addMemberVisibility visible: interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm))
source: JamiResources.place_audiocall_24dp_svg source: JamiResources.place_audiocall_24dp_svg
toolTipText: JamiStrings.placeAudioCall toolTipText: JamiStrings.placeAudioCall
@ -169,7 +170,7 @@ Rectangle {
PushButton { PushButton {
id: startAVideoCallButton id: startAVideoCallButton
visible: CurrentAccount.videoEnabled_Video && interactionButtonsVisibility && !addMemberVisibility visible: CurrentAccount.videoEnabled_Video && interactionButtonsVisibility && (!addMemberVisibility || UtilsAdapter.getAppValue(Settings.EnableExperimentalSwarm))
source: JamiResources.videocam_24dp_svg source: JamiResources.videocam_24dp_svg
toolTipText: JamiStrings.placeVideoCall toolTipText: JamiStrings.placeVideoCall

View file

@ -43,6 +43,7 @@ Rectangle {
errorRect.visible = CurrentConversation.errors.length > 0 && LRCInstance.debugMode() errorRect.visible = CurrentConversation.errors.length > 0 && LRCInstance.debugMode()
} }
} }
color: JamiTheme.filterBadgeColor
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent

View file

@ -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()
}
}
}
}
}
}

View file

@ -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()
}
}
}
}
}

View file

@ -241,7 +241,7 @@ JamiListView {
DelegateChoice { DelegateChoice {
roleValue: Interaction.Type.CALL roleValue: Interaction.Type.CALL
GeneratedMessageDelegate { CallMessageDelegate {
Component.onCompleted: { Component.onCompleted: {
computeChatview(this, index) computeChatview(this, index)
} }

View file

@ -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
}
}
}

View file

@ -22,9 +22,10 @@ import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Qt5Compat.GraphicalEffects import Qt5Compat.GraphicalEffects
import net.jami.Models 1.1
import net.jami.Adapters 1.1 import net.jami.Adapters 1.1
import net.jami.Constants 1.1 import net.jami.Constants 1.1
import net.jami.Enums 1.1
import net.jami.Models 1.1
import "../../commoncomponents" import "../../commoncomponents"
@ -148,11 +149,6 @@ ItemDelegate {
font.hintingPreference: Font.PreferNoHinting font.hintingPreference: Font.PreferNoHinting
maximumLineCount: 1 maximumLineCount: 1
color: JamiTheme.textColor 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 { Text {
@ -175,6 +171,13 @@ ItemDelegate {
color: JamiTheme.primaryForegroundColor 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 { ColumnLayout {
Layout.fillHeight: true Layout.fillHeight: true
spacing: 2 spacing: 2
@ -232,6 +235,8 @@ ItemDelegate {
if (!interactive) if (!interactive)
return; return;
ListView.view.model.select(index) 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) if (LRCInstance.currentAccountType === Profile.Type.SIP || !CurrentAccount.videoEnabled_Video)
CallAdapter.placeAudioOnlyCall() CallAdapter.placeAudioOnlyCall()
else { else {

View file

@ -35,6 +35,11 @@ Rectangle {
color: CurrentConversation.color color: CurrentConversation.color
property var isAdmin: UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, CurrentAccount.uri) === Member.Role.ADMIN property var isAdmin: UtilsAdapter.getParticipantRole(CurrentAccount.id, CurrentConversation.id, CurrentAccount.uri) === Member.Role.ADMIN
DevicesListPopup {
id: devicesListPopup
}
ColumnLayout { ColumnLayout {
id: swarmProfileDetails id: swarmProfileDetails
Layout.fillHeight: true 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 { RowLayout {
Layout.leftMargin: JamiTheme.preferredMarginSize Layout.leftMargin: JamiTheme.preferredMarginSize
Layout.preferredHeight: JamiTheme.settingsFontSize + 2 * JamiTheme.preferredMarginSize + 4 Layout.preferredHeight: JamiTheme.settingsFontSize + 2 * JamiTheme.preferredMarginSize + 4

View file

@ -200,6 +200,19 @@ MessagesAdapter::retryInteraction(const QString& interactionId)
->retryInteraction(lrcInstance_->get_selectedConvUid(), 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 void
MessagesAdapter::copyToDownloads(const QString& interactionId, const QString& displayName) MessagesAdapter::copyToDownloads(const QString& interactionId, const QString& displayName)
{ {

View file

@ -79,6 +79,10 @@ protected:
Q_INVOKABLE void openDirectory(const QString& arg); Q_INVOKABLE void openDirectory(const QString& arg);
Q_INVOKABLE void retryInteraction(const QString& interactionId); Q_INVOKABLE void retryInteraction(const QString& interactionId);
Q_INVOKABLE void deleteInteraction(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 copyToDownloads(const QString& interactionId, const QString& displayName);
Q_INVOKABLE void userIsComposing(bool isComposing); Q_INVOKABLE void userIsComposing(bool isComposing);
Q_INVOKABLE QVariantMap isLocalImage(const QString& mimeName); Q_INVOKABLE QVariantMap isLocalImage(const QString& mimeName);

View file

@ -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)
}
}
} }

View file

@ -371,6 +371,8 @@ UtilsAdapter::setAppValue(const Settings::Key key, const QVariant& value)
settingsManager_->loadTranslations(); settingsManager_->loadTranslations();
else if (key == Settings::Key::BaseZoom) else if (key == Settings::Key::BaseZoom)
Q_EMIT changeFontSize(); Q_EMIT changeFontSize();
else if (key == Settings::Key::EnableExperimentalSwarm)
Q_EMIT showExperimentalCallSwarm();
else if (key == Settings::Key::ShowChatviewHorizontally) else if (key == Settings::Key::ShowChatviewHorizontally)
Q_EMIT chatviewPositionChanged(); Q_EMIT chatviewPositionChanged();
else if (key == Settings::Key::AppTheme) else if (key == Settings::Key::AppTheme)

View file

@ -126,6 +126,7 @@ Q_SIGNALS:
void changeFontSize(); void changeFontSize();
void chatviewPositionChanged(); void chatviewPositionChanged();
void appThemeChanged(); void appThemeChanged();
void showExperimentalCallSwarm();
private: private:
QClipboard* clipboard_; QClipboard* clipboard_;

View file

@ -71,6 +71,9 @@ struct Info
QString uid = ""; QString uid = "";
QString accountId; QString accountId;
QVector<member::Member> participants; QVector<member::Member> participants;
VectorMapStringString activeCalls;
VectorMapStringString ignoredActiveCalls;
QString callId; QString callId;
QString confId; QString confId;
std::unique_ptr<MessageListModel> interactions; std::unique_ptr<MessageListModel> interactions;
@ -84,6 +87,18 @@ struct Info
MapStringString infos {}; MapStringString infos {};
MapStringString preferences {}; 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 QString getCallId() const
{ {
return confId.isEmpty() ? callId : confId; return confId.isEmpty() ? callId : confId;

View file

@ -210,6 +210,11 @@ public:
* @param uid of the conversation * @param uid of the conversation
*/ */
void placeAudioOnlyCall(const QString& uid); 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 * Send a message to the conversation
* @param uid of the conversation * @param uid of the conversation
@ -378,6 +383,17 @@ public:
* @param conversationId * @param conversationId
*/ */
void popFrontError(const QString& 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. * @return if conversations requests exists.
@ -432,15 +448,6 @@ Q_SIGNALS:
void newInteraction(const QString& uid, void newInteraction(const QString& uid,
QString& interactionId, QString& interactionId,
const interaction::Info& interactionInfo) const; 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 * Emitted when an interaction got removed from the conversation
* @param convUid conversation which owns the interaction * @param convUid conversation which owns the interaction
@ -546,6 +553,11 @@ Q_SIGNALS:
*/ */
void newMessagesAvailable(const QString& accountId, const QString& conversationId) const; 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 * Emitted when creation of conversation started, finished with success or finisfed with error
* @param accountId account id * @param accountId account id
@ -598,6 +610,11 @@ Q_SIGNALS:
void messagesFoundProcessed(const QString& accountId, void messagesFoundProcessed(const QString& accountId,
const VectorMapStringString& messageIds, const VectorMapStringString& messageIds,
const QVector<interaction::Info>& messageInformations) const; 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: private:
std::unique_ptr<ConversationModelPimpl> pimpl_; std::unique_ptr<ConversationModelPimpl> pimpl_;

View file

@ -272,6 +272,7 @@ struct Info
QString authorUri; QString authorUri;
QString body; QString body;
QString parentId = ""; QString parentId = "";
QString confId;
std::time_t timestamp = 0; std::time_t timestamp = 0;
std::time_t duration = 0; std::time_t duration = 0;
Type type = Type::INVALID; Type type = Type::INVALID;
@ -318,6 +319,8 @@ struct Info
body = QObject::tr("Swarm created"); body = QObject::tr("Swarm created");
} else if (type == Type::CALL) { } else if (type == Type::CALL) {
duration = message["duration"].toInt() / 1000; duration = message["duration"].toInt() / 1000;
if (message.contains("confId"))
confId = message["confId"];
} }
commit = message; commit = message;
} }

View file

@ -167,7 +167,7 @@ getFormattedCallDuration(const std::time_t duration)
} }
QString QString
getCallInteractionString(const QString& authorUri, const std::time_t& duration) getCallInteractionStringNonSwarm(const QString& authorUri, const std::time_t& duration)
{ {
if (duration < 0) { if (duration < 0) {
if (authorUri.isEmpty()) { 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 QString
getContactInteractionString(const QString& authorUri, const api::interaction::Status& status) getContactInteractionString(const QString& authorUri, const api::interaction::Status& status)
{ {
@ -510,7 +521,7 @@ getHistory(Database& db, api::conversation::Info& conversation)
: std::stoi(durationString.toStdString()); : std::stoi(durationString.toStdString());
auto status = api::interaction::to_status(payloads[i + 5]); auto status = api::interaction::to_status(payloads[i + 5]);
if (type == api::interaction::Type::CALL) { 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) { } else if (type == api::interaction::Type::CONTACT) {
body = getContactInteractionString(payloads[i + 1], status); body = getContactInteractionString(payloads[i + 1], status);
} }

View file

@ -56,11 +56,11 @@ QString prepareUri(const QString& uri, api::profile::Type type);
/** /**
* Get a formatted string for a call interaction's body * Get a formatted string for a call interaction's body
* @param author_uri * @param info
* @param duration of the call
* @return the formatted and translated call message string * @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 * Get a formatted string for a contact interaction's body

View file

@ -128,6 +128,12 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent)
&CallbacksHandler::slotAccountMessageStatusChanged, &CallbacksHandler::slotAccountMessageStatusChanged,
Qt::QueuedConnection); Qt::QueuedConnection);
connect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::needsHost,
this,
&CallbacksHandler::slotNeedsHost,
Qt::QueuedConnection);
connect(&ConfigurationManager::instance(), connect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::accountDetailsChanged, &ConfigurationManagerInterface::accountDetailsChanged,
this, this,
@ -349,6 +355,11 @@ CallbacksHandler::CallbacksHandler(const Lrc& parent)
this, this,
&CallbacksHandler::slotOnConversationError, &CallbacksHandler::slotOnConversationError,
Qt::QueuedConnection); Qt::QueuedConnection);
connect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::activeCallsChanged,
this,
&CallbacksHandler::slotActiveCallsChanged,
Qt::QueuedConnection);
connect(&ConfigurationManager::instance(), connect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::conversationPreferencesUpdated, &ConfigurationManagerInterface::conversationPreferencesUpdated,
this, this,
@ -576,6 +587,7 @@ CallbacksHandler::slotConferenceChanged(const QString& accountId,
const QString& callId, const QString& callId,
const QString& state) const QString& state)
{ {
Q_EMIT conferenceChanged(accountId, callId, state);
slotCallStateChanged(accountId, callId, state, 0); slotCallStateChanged(accountId, callId, state, 0);
} }
@ -595,6 +607,12 @@ CallbacksHandler::slotAccountMessageStatusChanged(const QString& accountId,
Q_EMIT accountMessageStatusChanged(accountId, conversationId, peer, messageId, status); Q_EMIT accountMessageStatusChanged(accountId, conversationId, peer, messageId, status);
} }
void
CallbacksHandler::slotNeedsHost(const QString& accountId, const QString& conversationId)
{
Q_EMIT needsHost(accountId, conversationId);
}
void void
CallbacksHandler::slotDataTransferEvent(const QString& accountId, CallbacksHandler::slotDataTransferEvent(const QString& accountId,
const QString& conversationId, const QString& conversationId,
@ -823,6 +841,14 @@ CallbacksHandler::slotOnConversationError(const QString& accountId,
Q_EMIT conversationError(accountId, conversationId, code, what); 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 void
CallbacksHandler::slotConversationPreferencesUpdated(const QString& accountId, CallbacksHandler::slotConversationPreferencesUpdated(const QString& accountId,
const QString& conversationId, const QString& conversationId,

View file

@ -195,12 +195,19 @@ Q_SIGNALS:
* @param callId of the conference * @param callId of the conference
*/ */
void conferenceCreated(const QString& accountId, const QString& callId); 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 * Connect this signal to know when a conference is removed
* @param accountId * @param accountId
* @param callId of the conference * @param callId of the conference
*/ */
void conferenceRemoved(const QString& accountId, const QString& callId); 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 * Connect this signal to know when a message sent get a new status
* @param accountId, account linked * @param accountId, account linked
@ -374,6 +381,9 @@ Q_SIGNALS:
const QString& conversationId, const QString& conversationId,
int code, int code,
const QString& what); const QString& what);
void activeCallsChanged(const QString& accountId,
const QString& conversationId,
const VectorMapStringString& activeCalls);
void conversationPreferencesUpdated(const QString& accountId, void conversationPreferencesUpdated(const QString& accountId,
const QString& conversationId, const QString& conversationId,
const MapStringString& preferences); const MapStringString& preferences);
@ -549,6 +559,12 @@ private Q_SLOTS:
const QString& peer, const QString& peer,
const QString& messageId, const QString& messageId,
int status); 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, void slotDataTransferEvent(const QString& accountId,
const QString& conversationId, const QString& conversationId,
@ -699,6 +715,9 @@ private Q_SLOTS:
const QString& conversationId, const QString& conversationId,
int code, int code,
const QString& what); const QString& what);
void slotActiveCallsChanged(const QString& accountId,
const QString& conversationId,
const VectorMapStringString& activeCalls);
private: private:
const api::Lrc& parent; const api::Lrc& parent;

View file

@ -239,6 +239,9 @@ public Q_SLOTS:
* @param callId * @param callId
*/ */
void slotConferenceCreated(const QString& accountId, const QString& 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 * Listen from CallbacksHandler when a voice mail notice is incoming
* @param accountId * @param accountId
@ -409,6 +412,23 @@ CallModel::getAdvancedInformation() const
return pimpl_->callAdvancedInformation(); 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 void
CallModel::muteMedia(const QString& callId, const QString& label, bool mute) CallModel::muteMedia(const QString& callId, const QString& label, bool mute)
{ {
@ -940,6 +960,10 @@ CallModelPimpl::CallModelPimpl(const CallModel& linked,
&CallbacksHandler::conferenceCreated, &CallbacksHandler::conferenceCreated,
this, this,
&CallModelPimpl::slotConferenceCreated); &CallModelPimpl::slotConferenceCreated);
connect(&callbacksHandler,
&CallbacksHandler::conferenceChanged,
this,
&CallModelPimpl::slotConferenceChanged);
connect(&callbacksHandler, connect(&callbacksHandler,
&CallbacksHandler::voiceMailNotify, &CallbacksHandler::voiceMailNotify,
this, this,
@ -1566,10 +1590,14 @@ CallModelPimpl::slotOnConferenceInfosUpdated(const QString& confId,
QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId); QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId);
Q_FOREACH (const auto& call, callList) { Q_FOREACH (const auto& call, callList) {
Q_EMIT linked.callAddedToConference(call, confId); Q_EMIT linked.callAddedToConference(call, confId);
if (calls.find(call) == calls.end()) {
qWarning() << "Call not found";
} else {
calls[call]->videoMuted = it->second->videoMuted; calls[call]->videoMuted = it->second->videoMuted;
calls[call]->audioMuted = it->second->audioMuted; calls[call]->audioMuted = it->second->audioMuted;
Q_EMIT linked.callInfosChanged(linked.owner.id, call); Q_EMIT linked.callInfosChanged(linked.owner.id, call);
} }
}
Q_EMIT linked.callInfosChanged(linked.owner.id, confId); Q_EMIT linked.callInfosChanged(linked.owner.id, confId);
Q_EMIT linked.onParticipantsChanged(confId); Q_EMIT linked.onParticipantsChanged(confId);
} }
@ -1585,14 +1613,7 @@ CallModelPimpl::slotConferenceCreated(const QString& accountId, const QString& c
{ {
if (accountId != linked.owner.id) if (accountId != linked.owner.id)
return; return;
// Detect if conference is created for this account
QStringList callList = CallManager::instance().getParticipantList(linked.owner.id, confId); 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>(); auto callInfo = std::make_shared<call::Info>();
callInfo->id = confId; 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 void
CallModelPimpl::sendProfile(const QString& callId) CallModelPimpl::sendProfile(const QString& callId)
{ {

View file

@ -193,10 +193,7 @@ public:
/** /**
* Handle data transfer progression * Handle data transfer progression
*/ */
void updateTransferProgress(QTimer* timer, void updateTransferProgress(QTimer* timer, int conversationIdx, const QString& interactionId);
const QString& conversation,
int conversationIdx,
const QString& interactionId);
bool usefulDataFromDataTransfer(const QString& fileId, bool usefulDataFromDataTransfer(const QString& fileId,
const datatransfer::Info& info, const datatransfer::Info& info,
@ -385,6 +382,9 @@ public Q_SLOTS:
const QString& conversationId, const QString& conversationId,
int code, int code,
const QString& what); const QString& what);
void slotActiveCallsChanged(const QString& accountId,
const QString& conversationId,
const VectorMapStringString& activeCalls);
void slotConversationReady(const QString& accountId, const QString& conversationId); void slotConversationReady(const QString& accountId, const QString& conversationId);
void slotConversationRemoved(const QString& accountId, const QString& conversationId); void slotConversationRemoved(const QString& accountId, const QString& conversationId);
void slotConversationPreferencesUpdated(const QString& accountId, void slotConversationPreferencesUpdated(const QString& accountId,
@ -853,6 +853,30 @@ ConversationModel::deleteObsoleteHistory(int days)
storage::deleteObsoleteHistory(pimpl_->db, date); 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 void
ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly) 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"; << "ConversationModel::placeCall can't call a conversation without participant";
return; 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); auto& peers = peersForConversation(conversation);
// there is no calls in group with more than 2 participants // there is no calls in group with more than 2 participants
if (peers.size() != 1) { if (peers.size() != 1) {
@ -1028,6 +1065,25 @@ ConversationModel::popFrontError(const QString& conversationId)
Q_EMIT onConversationErrorsUpdated(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 void
ConversationModel::setConversationPreferences(const QString& conversationId, ConversationModel::setConversationPreferences(const QString& conversationId,
const MapStringString prefs) const MapStringString prefs)
@ -1835,6 +1891,9 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
&ConfigurationManagerInterface::composingStatusChanged, &ConfigurationManagerInterface::composingStatusChanged,
this, this,
&ConversationModelPimpl::slotComposingStatusChanged); &ConversationModelPimpl::slotComposingStatusChanged);
connect(&callbacksHandler, &CallbacksHandler::needsHost, this, [&](auto, auto convId) {
emit linked.needsHost(convId);
});
// data transfer // data transfer
connect(&*linked.owner.contactModel, connect(&*linked.owner.contactModel,
@ -1918,6 +1977,10 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
&CallbacksHandler::conversationPreferencesUpdated, &CallbacksHandler::conversationPreferencesUpdated,
this, this,
&ConversationModelPimpl::slotConversationPreferencesUpdated); &ConversationModelPimpl::slotConversationPreferencesUpdated);
connect(&callbacksHandler,
&CallbacksHandler::activeCallsChanged,
this,
&ConversationModelPimpl::slotActiveCallsChanged);
} }
ConversationModelPimpl::~ConversationModelPimpl() ConversationModelPimpl::~ConversationModelPimpl()
@ -2066,6 +2129,10 @@ ConversationModelPimpl::~ConversationModelPimpl()
&CallbacksHandler::conversationError, &CallbacksHandler::conversationError,
this, this,
&ConversationModelPimpl::slotOnConversationError); &ConversationModelPimpl::slotOnConversationError);
disconnect(&callbacksHandler,
&CallbacksHandler::activeCallsChanged,
this,
&ConversationModelPimpl::slotActiveCallsChanged);
disconnect(&callbacksHandler, disconnect(&callbacksHandler,
&CallbacksHandler::conversationPreferencesUpdated, &CallbacksHandler::conversationPreferencesUpdated,
this, this,
@ -2383,7 +2450,7 @@ ConversationModelPimpl::slotConversationLoaded(uint32_t requestId,
linked.owner.dataTransferModel->registerTransferId(fileId, msgId); linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
downloadFile = (bytesProgress == 0); downloadFile = (bytesProgress == 0);
} else if (msg.type == interaction::Type::CALL) { } 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) { } else if (msg.type == interaction::Type::CONTACT) {
auto bestName = msg.authorUri == linked.owner.profileInfo.uri auto bestName = msg.authorUri == linked.owner.profileInfo.uri
? linked.owner.accountModel->bestNameForAccount(linked.owner.id) ? linked.owner.accountModel->bestNameForAccount(linked.owner.id)
@ -2498,6 +2565,8 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId,
api::datatransfer::Info info; api::datatransfer::Info info;
QString fileId; QString fileId;
auto updateUnread = false;
if (msg.type == interaction::Type::DATA_TRANSFER) { if (msg.type == interaction::Type::DATA_TRANSFER) {
// save data transfer interaction to db and assosiate daemon id with interaction id, // save data transfer interaction to db and assosiate daemon id with interaction id,
// conversation id and db id // conversation id and db id
@ -2520,8 +2589,14 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId,
: bytesProgress == totalSize ? interaction::Status::TRANSFER_FINISHED : bytesProgress == totalSize ? interaction::Status::TRANSFER_FINISHED
: interaction::Status::TRANSFER_ONGOING; : interaction::Status::TRANSFER_ONGOING;
linked.owner.dataTransferModel->registerTransferId(fileId, msgId); linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
if (msg.authorUri != linked.owner.profileInfo.uri) {
updateUnread = true;
}
} else if (msg.type == interaction::Type::CALL) { } 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) { } else if (msg.type == interaction::Type::CONTACT) {
auto bestName = msg.authorUri == linked.owner.profileInfo.uri auto bestName = msg.authorUri == linked.owner.profileInfo.uri
? linked.owner.accountModel->bestNameForAccount(linked.owner.id) ? linked.owner.accountModel->bestNameForAccount(linked.owner.id)
@ -2529,16 +2604,24 @@ ConversationModelPimpl::slotMessageReceived(const QString& accountId,
msg.body = interaction::getContactInteractionString(bestName, msg.body = interaction::getContactInteractionString(bestName,
interaction::to_action( interaction::to_action(
message["action"])); message["action"]));
} else if (msg.type == interaction::Type::TEXT if (msg.authorUri != linked.owner.profileInfo.uri) {
&& msg.authorUri != linked.owner.profileInfo.uri) { updateUnread = true;
conversation.unreadMessages++; }
} else if (msg.type == interaction::Type::TEXT) {
if (msg.authorUri != linked.owner.profileInfo.uri) {
updateUnread = true;
}
} else if (msg.type == interaction::Type::EDITED) { } else if (msg.type == interaction::Type::EDITED) {
conversation.interactions->addEdition(msgId, msg, true); conversation.interactions->addEdition(msgId, msg, true);
} }
if (!insertSwarmInteraction(msgId, msg, conversation, false)) { if (!insertSwarmInteraction(msgId, msg, conversation, false)) {
// message already exists // message already exists
return; return;
} }
if (updateUnread) {
conversation.unreadMessages++;
}
if (msg.type == interaction::Type::MERGE) { if (msg.type == interaction::Type::MERGE) {
invalidateModel(); invalidateModel();
return; 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 void
ConversationModelPimpl::slotIncomingContactRequest(const QString& contactUri) ConversationModelPimpl::slotIncomingContactRequest(const QString& contactUri)
{ {
@ -3074,6 +3175,9 @@ ConversationModelPimpl::addSwarmConversation(const QString& convId)
conversation.infos = details; conversation.infos = details;
conversation.uid = convId; conversation.uid = convId;
conversation.accountId = linked.owner.id; conversation.accountId = linked.owner.id;
VectorMapStringString activeCalls = ConfigurationManager::instance()
.getActiveCalls(linked.owner.id, convId);
conversation.activeCalls = activeCalls;
QString lastRead; QString lastRead;
VectorString membersLeft; VectorString membersLeft;
for (auto& member : members) { for (auto& member : members) {
@ -3387,12 +3491,13 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
bool incoming, bool incoming,
const std::time_t& duration) const std::time_t& duration)
{ {
// do not save call interaction for swarm conversation // Get conversation
try { auto conv_it = std::find_if(conversations.begin(),
auto& conv = getConversationForPeerUri(from).get(); conversations.end(),
if (conv.isSwarm()) [&callId](const conversation::Info& conversation) {
return; return conversation.callId == callId;
} catch (const std::exception&) { });
if (conv_it == conversations.end()) {
// If we have no conversation with peer. // If we have no conversation with peer.
try { try {
auto contact = linked.owner.contactModel->getContact(from); auto contact = linked.owner.contactModel->getContact(from);
@ -3401,18 +3506,18 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
storage::beginConversationWithPeer(db, contact.profileInfo.uri); storage::beginConversationWithPeer(db, contact.profileInfo.uri);
} }
} catch (const std::exception&) { } 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()) {
return; return;
} }
try {
auto& conv = getConversationForPeerUri(from).get();
conv.callId = callId;
} catch (...) {
return;
}
}
// do not save call interaction for swarm conversation
if (conv_it->isSwarm())
return;
auto uid = conv_it->uid; auto uid = conv_it->uid;
auto uriString = incoming ? storage::prepareUri(from, linked.owner.profileInfo.type) : ""; auto uriString = incoming ? storage::prepareUri(from, linked.owner.profileInfo.type) : "";
auto msg = interaction::Info {uriString, auto msg = interaction::Info {uriString,
@ -3425,7 +3530,7 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
// update the db // update the db
auto msgId = storage::addOrUpdateMessage(db, conv_it->uid, msg, callId); auto msgId = storage::addOrUpdateMessage(db, conv_it->uid, msg, callId);
// now set the formatted call message string in memory only // now set the formatted call message string in memory only
msg.body = storage::getCallInteractionString(uriString, duration); msg.body = storage::getCallInteractionString(msg);
bool newInteraction = false; bool newInteraction = false;
{ {
std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]); 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); .getConferenceDetails(linked.owner.id, confId);
if (confDetails["STATE"] == "ACTIVE_ATTACHED") if (confDetails["STATE"] == "ACTIVE_ATTACHED")
Q_EMIT linked.selectConversation(conversation.uid); Q_EMIT linked.selectConversation(conversation.uid);
return;
} }
} }
} }
@ -3693,7 +3799,7 @@ ConversationModelPimpl::slotUpdateInteractionStatus(const QString& accountId,
if (peerId != linked.owner.profileInfo.uri) if (peerId != linked.owner.profileInfo.uri)
conversation.interactions->setRead(peerId, messageId); conversation.interactions->setRead(peerId, messageId);
else { 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. // so, compute the number of unread messages.
conversation.unreadMessages = ConfigurationManager::instance() conversation.unreadMessages = ConfigurationManager::instance()
.countInteractions(linked.owner.id, .countInteractions(linked.owner.id,
@ -4150,7 +4256,7 @@ ConversationModelPimpl::slotTransferStatusOngoing(const QString& fileId, datatra
auto conversationIdx = indexOf(conversationId); auto conversationIdx = indexOf(conversationId);
auto* timer = new QTimer(); auto* timer = new QTimer();
connect(timer, &QTimer::timeout, [=] { connect(timer, &QTimer::timeout, [=] {
updateTransferProgress(timer, conversationId, conversationIdx, interactionId); updateTransferProgress(timer, conversationIdx, interactionId);
}); });
timer->start(1000); timer->start(1000);
} }
@ -4281,7 +4387,6 @@ ConversationModelPimpl::updateTransferStatus(const QString& fileId,
void void
ConversationModelPimpl::updateTransferProgress(QTimer* timer, ConversationModelPimpl::updateTransferProgress(QTimer* timer,
const QString&,
int conversationIdx, int conversationIdx,
const QString& interactionId) const QString& interactionId)
{ {

View file

@ -68,6 +68,20 @@ MessageListModel::find(const QString& msgId)
return interactions_.end(); 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 iterator
MessageListModel::erase(const iterator& it) MessageListModel::erase(const iterator& it)
{ {
@ -403,6 +417,13 @@ MessageListModel::dataForItem(item_t item, int, int role) const
case Role::Timestamp: case Role::Timestamp:
return QVariant::fromValue(item.second.timestamp); return QVariant::fromValue(item.second.timestamp);
case Role::Duration: 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); return QVariant::fromValue(item.second.duration);
case Role::Type: case Role::Type:
return QVariant(static_cast<int>(item.second.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); return QVariant(item.second.linkified);
case Role::ActionUri: case Role::ActionUri:
return QVariant(item.second.commit["uri"]); 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: case Role::ContactAction:
return QVariant(item.second.commit["action"]); return QVariant(item.second.commit["action"]);
case Role::PreviousBodies: { case Role::PreviousBodies: {

View file

@ -43,6 +43,8 @@ struct Info;
X(IsRead) \ X(IsRead) \
X(ContactAction) \ X(ContactAction) \
X(ActionUri) \ X(ActionUri) \
X(ConfId) \
X(DeviceId) \
X(LinkPreviewInfo) \ X(LinkPreviewInfo) \
X(Linkified) \ X(Linkified) \
X(PreviousBodies) \ X(PreviousBodies) \
@ -84,7 +86,9 @@ public:
interaction::Info message, interaction::Info message,
bool beginning = false); bool beginning = false);
iterator find(const QString& msgId); iterator find(const QString& msgId);
iterator findActiveCall(const MapStringString& commit);
iterator erase(const iterator& it); iterator erase(const iterator& it);
constIterator find(const QString& msgId) const; constIterator find(const QString& msgId) const;
QPair<iterator, bool> insert(std::pair<QString, interaction::Info> message, QPair<iterator, bool> insert(std::pair<QString, interaction::Info> message,
bool beginning = false); bool beginning = false);

View file

@ -92,8 +92,6 @@ public:
Q_EMIT this->volatileAccountDetailsChanged(QString(accountID.c_str()), Q_EMIT this->volatileAccountDetailsChanged(QString(accountID.c_str()),
convertMap(details)); convertMap(details));
}), }),
exportable_callback<ConfigurationSignal::Error>(
[this](int code) { Q_EMIT this->errorAlert(code); }),
exportable_callback<ConfigurationSignal::CertificateExpired>( exportable_callback<ConfigurationSignal::CertificateExpired>(
[this](const std::string& certId) { [this](const std::string& certId) {
Q_EMIT this->certificateExpired(QString(certId.c_str())); Q_EMIT this->certificateExpired(QString(certId.c_str()));
@ -129,6 +127,11 @@ public:
QString(message_id.c_str()), QString(message_id.c_str()),
state); 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>( exportable_callback<ConfigurationSignal::IncomingTrustRequest>(
[this](const std::string& accountId, [this](const std::string& accountId,
const std::string& conversationId, const std::string& conversationId,
@ -356,6 +359,14 @@ public:
QString(conversationId.c_str()), QString(conversationId.c_str()),
code, code,
QString(what.c_str())); 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 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; return temp;
} }
@ -1195,7 +1215,6 @@ Q_SIGNALS: // SIGNALS
unsigned detail_code, unsigned detail_code,
const QString& detail_str); const QString& detail_str);
void stunStatusSuccess(const QString& message); void stunStatusSuccess(const QString& message);
void errorAlert(int code);
void volatileAccountDetailsChanged(const QString& accountID, MapStringString details); void volatileAccountDetailsChanged(const QString& accountID, MapStringString details);
void certificatePinned(const QString& certId); void certificatePinned(const QString& certId);
void certificatePathPinned(const QString& path, const QStringList& certIds); void certificatePathPinned(const QString& path, const QStringList& certIds);
@ -1222,6 +1241,7 @@ Q_SIGNALS: // SIGNALS
const QString& peer, const QString& peer,
const QString& messageId, const QString& messageId,
int status); int status);
void needsHost(const QString& accountId, const QString& conversationId);
void nameRegistrationEnded(const QString& accountId, int status, const QString& name); void nameRegistrationEnded(const QString& accountId, int status, const QString& name);
void registeredNameFound(const QString& accountId, void registeredNameFound(const QString& accountId,
int status, int status,
@ -1278,6 +1298,9 @@ Q_SIGNALS: // SIGNALS
const QString& conversationId, const QString& conversationId,
int code, int code,
const QString& what); const QString& what);
void activeCallsChanged(const QString& accountId,
const QString& conversationId,
const VectorMapStringString& activeCalls);
void conversationPreferencesUpdated(const QString& accountId, void conversationPreferencesUpdated(const QString& accountId,
const QString& conversationId, const QString& conversationId,
const MapStringString& message); const MapStringString& message);