conference: fix host-side conference transition to 1:1 call

Detailed reasoning available on GitLab issue page

* Implements helper method getFallbackConversationForConference that returns a suitable conversation if a switch should be done
* Moves slotConferenceRemoved from ConversationModel to CallModel
* slotConferenceRemoved now properly removes conference data and handles conversation switch
* Bumps daemon to commit fc3e8806946180ef40edcc2dfd64a5e3bb01878f

GitLab: #1569
Change-Id: Ief97833613c3f289a2aeda747cf85a0d39712245
This commit is contained in:
ebjarstal
2025-11-26 13:11:40 -05:00
committed by Adrien Béraud
parent 3e796c60f2
commit ed4960eb6a
2 changed files with 136 additions and 27 deletions

View File

@@ -47,6 +47,8 @@
#include <QString>
#include <QUrl>
#include <QSize>
#include <qlogging.h>
#include <qtcoreexports.h>
// std
#include <chrono>
@@ -141,6 +143,15 @@ public:
QVariantList callAdvancedInformation();
MapStringString advancedInformationForCallId(QString callId);
/**
* Get a suitable fallback conversation for when the client should select a new one.
* For example if the peer whose conversation was used to host the conference leaves the conference,
* we need to pick another conversation to display the conference call.
* @param confId the conference id
* @return the conversation to use as fallback, nullopt if client does not need to change conversation or if none found
*/
OptRef<conversation::Info> getFallbackConversationForConference(const QString& confId);
QStringList getCallIds();
CallModel::CallInfoMap calls;
@@ -226,6 +237,7 @@ public Q_SLOTS:
*/
void slotConferenceCreated(const QString& accountId, const QString& conversationId, const QString& callId);
void slotConferenceChanged(const QString& accountId, const QString& callId, const QString& state);
void slotConferenceRemoved(const QString& accountId, const QString& confId);
/**
* Listen from CallbacksHandler when a voice mail notice is incoming
* @param accountId
@@ -1007,6 +1019,7 @@ CallModelPimpl::CallModelPimpl(const CallModel& linked,
connect(&callbacksHandler, &CallbacksHandler::incomingVCardChunk, this, &CallModelPimpl::slotincomingVCardChunk);
connect(&callbacksHandler, &CallbacksHandler::conferenceCreated, this, &CallModelPimpl::slotConferenceCreated);
connect(&callbacksHandler, &CallbacksHandler::conferenceChanged, this, &CallModelPimpl::slotConferenceChanged);
connect(&callbacksHandler, &CallbacksHandler::conferenceRemoved, this, &CallModelPimpl::slotConferenceRemoved);
connect(&callbacksHandler, &CallbacksHandler::voiceMailNotify, this, &CallModelPimpl::slotVoiceMailNotify);
connect(&CallManager::instance(),
&CallManagerInterface::onConferenceInfosUpdated,
@@ -1056,6 +1069,54 @@ CallModelPimpl::advancedInformationForCallId(QString callId)
return infoMap;
}
OptRef<conversation::Info>
CallModelPimpl::getFallbackConversationForConference(const QString& confId)
{
if (confId.isEmpty()) {
qWarning() << "Empty conference ID provided.";
return std::nullopt;
}
auto currentConversation = linked.owner.conversationModel->getConversationForCallId(confId);
if (!currentConversation) {
qWarning() << "No conversation found for conference ID:" << confId;
return std::nullopt;
}
const auto participantsIt = participantsModel.find(confId);
if (participantsIt == participantsModel.end()) {
qWarning() << "No participants model found for conference ID:" << confId;
return std::nullopt;
}
const auto participants = participantsIt->second->getParticipants();
if (participants.size() <= 1) {
qWarning() << "Not enough participants to determine a fallback conversation for conference ID:" << confId;
return std::nullopt;
}
// Check if any participant matches the current conversation
// If yes, then no need to switch conversation, so return nullopt
for (const auto& participant : participants) {
const auto conv = linked.owner.conversationModel->getConversationForPeerUri(participant.uri);
if (conv && currentConversation->get().uid == conv->get().uid) {
qWarning() << "Current conversation matches a participant. No fallback needed.";
return std::nullopt;
}
}
OptRef<conversation::Info> fallbackConversation = std::nullopt;
for (const auto& participant : participants) {
if (participant.uri != linked.owner.profileInfo.uri) {
fallbackConversation = linked.owner.conversationModel->getConversationForPeerUri(participant.uri);
qWarning() << "Fallback conversation found with URI:" << participant.uri;
break;
}
}
return fallbackConversation;
}
QStringList
CallModelPimpl::getCallIds()
{
@@ -1691,6 +1752,81 @@ CallModelPimpl::slotConferenceChanged(const QString& accountId, const QString& c
Q_EMIT linked.currentCallChanged(currentCall_);
}
void
CallModelPimpl::slotConferenceRemoved(const QString& accountId, const QString& confId)
{
if (accountId != linked.owner.id || !linked.owner.conversationModel) {
return;
}
auto confIt = calls.find(confId);
if (confIt == calls.end() || !confIt->second) {
return;
}
qWarning() << "[conf:" << confId
<< "] Attempting to transition to 1:1 call. Checking if conversation switch is needed.";
QString remainingPeerUri;
QString remainingCallId;
auto participantsIt = participantsModel.find(confId);
if (participantsIt != participantsModel.end()) {
const auto participants = participantsIt->second->getParticipants();
// Assuming the participants model is up to date, it should contain all remaining participants in the conference
// Which should be 2: the host and the remaining remote peer
// Therefore, we look for the participant which is not ourselves
qWarning() << "[conf:" << confId << "] Found" << participants.size() << "participants in conference";
for (const auto& participant : participants) {
if (participant.uri != linked.owner.profileInfo.uri) {
remainingPeerUri = participant.uri;
try {
remainingCallId = linked.getCallFromURI(remainingPeerUri, true).id;
} catch (const std::exception& e) {
qWarning() << "[conf:" << confId << "] Could not find call for peerUri:" << remainingPeerUri << ":"
<< e.what();
}
break;
}
}
} else {
qWarning() << "[conf:" << confId << "] Participants model not found for conference";
}
auto currentConversation = linked.owner.conversationModel->getConversationForCallId(confId);
auto fallbackConversation = getFallbackConversationForConference(confId);
// If we have a fallback conversation, that means the client should switch to it
if (fallbackConversation) {
if (currentConversation) {
currentConversation->get().confId.clear();
}
auto& targetConv = fallbackConversation->get();
targetConv.confId.clear();
if (!remainingCallId.isEmpty()) {
targetConv.callId = remainingCallId;
}
qWarning() << "[conf:" << confId << "] Switching to fallback conversation:" << targetConv.uid;
linked.owner.conversationModel->selectConversation(targetConv.uid);
} else if (currentConversation) {
auto& targetConv = currentConversation->get();
targetConv.confId.clear();
if (!remainingCallId.isEmpty()) {
targetConv.callId = remainingCallId;
}
qWarning() << "[conf:" << confId << "] Staying in current conversation:" << targetConv.uid;
} else {
qWarning() << "[conf:" << confId << "] No conversation available to update after conference removal";
}
// Now remove the conference from our models and list of calls then update currentCall_
participantsModel.erase(confId);
calls.erase(confIt);
currentCall_ = remainingCallId;
Q_EMIT linked.currentCallChanged(currentCall_);
qDebug() << "[conf:" << confId << "] Conference removed, transitioned to callId:" << currentCall_;
}
void
CallModelPimpl::onRemoteRecordingChanged(const QString& callId, const QString& peerUri, bool state)
{

View File

@@ -309,12 +309,6 @@ public Q_SLOTS:
* @param confId
*/
void slotCallAddedToConference(const QString& callId, const QString& conversationId, const QString& confId);
/**
* Listen from CallbacksHandler when a conference is deleted.
* @param accountId
* @param confId
*/
void slotConferenceRemoved(const QString& accountId, const QString& confId);
/**
* Listen for when a contact is composing
* @param accountId
@@ -1648,10 +1642,6 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
&lrc::api::CallModel::callAddedToConference,
this,
&ConversationModelPimpl::slotCallAddedToConference);
connect(&callbacksHandler,
&CallbacksHandler::conferenceRemoved,
this,
&ConversationModelPimpl::slotConferenceRemoved);
connect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::composingStatusChanged,
this,
@@ -1793,10 +1783,6 @@ ConversationModelPimpl::~ConversationModelPimpl()
&lrc::api::CallModel::callAddedToConference,
this,
&ConversationModelPimpl::slotCallAddedToConference);
disconnect(&callbacksHandler,
&CallbacksHandler::conferenceRemoved,
this,
&ConversationModelPimpl::slotConferenceRemoved);
disconnect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::composingStatusChanged,
this,
@@ -3311,19 +3297,6 @@ ConversationModelPimpl::updateInteractionStatus(const QString& accountId,
}
}
void
ConversationModelPimpl::slotConferenceRemoved(const QString& accountId, const QString& confId)
{
if (accountId != linked.owner.id)
return;
// Get conversation
for (auto& i : conversations) {
if (i.confId == confId) {
i.confId = "";
}
}
}
void
ConversationModelPimpl::slotComposingStatusChanged(const QString& accountId,
const QString& convId,