1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-07 16:26:12 +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(WindowGeometry, QRectF(qQNaN(), qQNaN(), 0., 0.)) \
X(WindowState, QWindow::AutomaticVisibility) \
X(EnableExperimentalSwarm, false) \
X(LANG, "SYSTEM")
/*

View file

@ -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_);

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
width: ListView.view ? ListView.view.width : 0
height: mainColumnLayout.implicitHeight
rightPadding: hPadding
leftPadding: hPadding

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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_;

View file

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

View file

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

View file

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

View file

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

View file

@ -43,6 +43,7 @@ Rectangle {
errorRect.visible = CurrentConversation.errors.length > 0 && LRCInstance.debugMode()
}
}
color: JamiTheme.filterBadgeColor
RowLayout {
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 {
roleValue: Interaction.Type.CALL
GeneratedMessageDelegate {
CallMessageDelegate {
Component.onCompleted: {
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 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 {

View file

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

View file

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

View file

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

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();
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)

View file

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

View file

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

View file

@ -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_;

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: {

View file

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

View file

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