1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-12 18:55:39 +02:00
jami-client-qt/src/app/conversationsadapter.cpp
Sébastien Blin ce3afea995 chatview: display signaled back-end errors
A signal exists for showing errors to the user to make failing cases
more explicit. With this patch, errors detected are displayed to
the end user so that they know that an error occured and what
kind of error occured.

Change-Id: Ib2d4d4fdb171235e0598de0f1c190b8fd0fcc336
2022-08-15 15:33:32 -04:00

614 lines
No EOL
22 KiB
C++

/*
* Copyright (C) 2020-2022 Savoir-faire Linux Inc.
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@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 <http://www.gnu.org/licenses/>.
*/
#include "conversationsadapter.h"
#include "utils.h"
#include "qtutils.h"
#include "systemtray.h"
#include "qmlregister.h"
#include <QApplication>
#include <QJsonObject>
using namespace lrc::api;
ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
LRCInstance* instance,
QObject* parent)
: QmlAdapterBase(instance, parent)
, systemTray_(systemTray)
, convSrcModel_(new ConversationListModel(lrcInstance_))
, convModel_(new ConversationListProxyModel(convSrcModel_.get()))
, searchSrcModel_(new SearchResultsListModel(lrcInstance_))
, searchModel_(new SelectableListProxyModel(searchSrcModel_.get()))
{
QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, convModel_.get(), "ConversationListModel");
QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, searchModel_.get(), "SearchResultsListModel");
new SelectableListProxyGroupModel({convModel_.data(), searchModel_.data()}, this);
// this will trigger when the invite filter tab is selected
connect(this, &ConversationsAdapter::filterRequestsChanged, [this]() {
convModel_->setFilterRequests(filterRequests_);
});
connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() {
auto convId = lrcInstance_->get_selectedConvUid();
if (convId.isEmpty()) {
// deselected
convModel_->deselect();
searchModel_->deselect();
Q_EMIT navigateToWelcomePageRequested();
} else {
// selected
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
if (convInfo.uid.isEmpty())
return;
auto& accInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
accInfo.conversationModel->selectConversation(convInfo.uid);
accInfo.conversationModel->clearUnreadInteractions(convInfo.uid);
// this may be a request, so adjust that filter also
set_filterRequests(convInfo.isRequest);
// reposition index in case of programmatic selection
// currently, this may only occur for the conversation list
// and not the search list
convModel_->selectSourceRow(lrcInstance_->indexOf(convId));
}
});
connect(lrcInstance_, &LRCInstance::draftSaved, [this](const QString& convId) {
auto row = lrcInstance_->indexOf(convId);
const auto index = convSrcModel_->index(row, 0);
Q_EMIT convSrcModel_->dataChanged(index, index);
});
#ifdef Q_OS_LINUX
// notification responses
connect(systemTray_,
&SystemTray::openConversationActivated,
[this](const QString& accountId, const QString& convUid) {
Q_EMIT lrcInstance_->notificationClicked();
lrcInstance_->selectConversation(convUid, accountId);
});
connect(systemTray_,
&SystemTray::acceptPendingActivated,
[this](const QString& accountId, const QString& convUid) {
auto& accInfo = lrcInstance_->getAccountInfo(accountId);
accInfo.conversationModel->acceptConversationRequest(convUid);
});
connect(systemTray_,
&SystemTray::refusePendingActivated,
[this](const QString& accountId, const QString& convUid) {
auto& accInfo = lrcInstance_->getAccountInfo(accountId);
accInfo.conversationModel->removeConversation(convUid);
});
#endif
}
void
ConversationsAdapter::safeInit()
{
// TODO: remove these safeInits, they are possibly called
// multiple times during qml component inits
connect(&lrcInstance_->behaviorController(),
&BehaviorController::newUnreadInteraction,
this,
&ConversationsAdapter::onNewUnreadInteraction,
Qt::UniqueConnection);
connect(&lrcInstance_->behaviorController(),
&BehaviorController::newReadInteraction,
this,
&ConversationsAdapter::onNewReadInteraction,
Qt::UniqueConnection);
connect(&lrcInstance_->behaviorController(),
&BehaviorController::newTrustRequest,
this,
&ConversationsAdapter::onNewTrustRequest,
Qt::UniqueConnection);
connect(&lrcInstance_->behaviorController(),
&BehaviorController::trustRequestTreated,
this,
&ConversationsAdapter::onTrustRequestTreated,
Qt::UniqueConnection);
connect(lrcInstance_,
&LRCInstance::currentAccountIdChanged,
this,
&ConversationsAdapter::onCurrentAccountIdChanged,
Qt::UniqueConnection);
connectConversationModel();
}
void
ConversationsAdapter::onCurrentAccountIdChanged()
{
lrcInstance_->deselectConversation();
connectConversationModel();
// Always turn the requests filter off when switching account.
// Conversation selection will manage the filter state in the
// case of programmatic selection(incoming call, notification
// activation, etc.).
set_filterRequests(false);
}
void
ConversationsAdapter::onNewUnreadInteraction(const QString& accountId,
const QString& convUid,
const QString& interactionId,
const interaction::Info& interaction)
{
if (!interaction.authorUri.isEmpty()
&& (!QApplication::focusWindow() || accountId != lrcInstance_->get_currentAccountId()
|| convUid != lrcInstance_->get_selectedConvUid())) {
auto& accountInfo = lrcInstance_->getAccountInfo(accountId);
auto from = accountInfo.contactModel->bestNameForContact(interaction.authorUri);
#ifdef Q_OS_LINUX
auto contactPhoto = Utils::contactPhoto(lrcInstance_,
interaction.authorUri,
QSize(50, 50),
accountId);
auto notifId = QString("%1;%2;%3").arg(accountId).arg(convUid).arg(interactionId);
systemTray_->showNotification(notifId,
tr("New message"),
from + ": " + interaction.body,
NotificationType::CHAT,
Utils::QImageToByteArray(contactPhoto));
#else
Q_UNUSED(interactionId)
auto onClicked = [this, accountId, convUid, uri = interaction.authorUri] {
Q_EMIT lrcInstance_->notificationClicked();
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId);
if (convInfo.uid.isEmpty())
return;
lrcInstance_->selectConversation(convInfo.uid, accountId);
};
systemTray_->showNotification(interaction.body, from, onClicked);
#endif
updateConversationFilterData();
}
}
void
ConversationsAdapter::onNewReadInteraction(const QString& accountId,
const QString& convUid,
const QString& interactionId)
{
#ifdef Q_OS_LINUX
// hide notification
auto notifId = QString("%1;%2;%3").arg(accountId).arg(convUid).arg(interactionId);
systemTray_->hideNotification(notifId);
#else
Q_UNUSED(accountId)
Q_UNUSED(convUid)
Q_UNUSED(interactionId)
#endif
}
void
ConversationsAdapter::onNewTrustRequest(const QString& accountId,
const QString& convId,
const QString& peerUri)
{
#ifdef Q_OS_LINUX
if (!QApplication::focusWindow() || accountId != lrcInstance_->get_currentAccountId()) {
auto conv = convId;
if (conv.isEmpty()) {
auto& convInfo = lrcInstance_->getConversationFromPeerUri(peerUri);
if (convInfo.uid.isEmpty())
return;
}
auto& accInfo = lrcInstance_->getAccountInfo(accountId);
auto from = accInfo.contactModel->bestNameForContact(peerUri);
auto contactPhoto = Utils::contactPhoto(lrcInstance_, peerUri, QSize(50, 50), accountId);
auto notifId = QString("%1;%2").arg(accountId).arg(conv);
systemTray_->showNotification(notifId,
tr("Trust request"),
"New request from " + from,
NotificationType::REQUEST,
Utils::QImageToByteArray(contactPhoto));
}
#else
Q_UNUSED(accountId)
Q_UNUSED(peerUri)
#endif
updateConversationFilterData();
}
void
ConversationsAdapter::onTrustRequestTreated(const QString& accountId, const QString& peerUri)
{
#ifdef Q_OS_LINUX
// hide notification
auto notifId = QString("%1;%2").arg(accountId).arg(peerUri);
systemTray_->hideNotification(notifId);
#else
Q_UNUSED(accountId)
Q_UNUSED(peerUri)
#endif
}
void
ConversationsAdapter::onModelChanged()
{
updateConversationFilterData();
}
void
ConversationsAdapter::onProfileUpdated(const QString& contactUri)
{
auto& convInfo = lrcInstance_->getConversationFromPeerUri(contactUri);
if (convInfo.uid.isEmpty())
return;
// notify UI elements
auto row = lrcInstance_->indexOf(convInfo.uid);
const auto index = convSrcModel_->index(row, 0);
Q_EMIT convSrcModel_->dataChanged(index, index);
}
void
ConversationsAdapter::onConversationUpdated(const QString& convId)
{
updateConversationFilterData();
}
void
ConversationsAdapter::onFilterChanged()
{
updateConversationFilterData();
}
void
ConversationsAdapter::onConversationCleared(const QString& convUid)
{
// If currently selected, switch to welcome screen (deselecting
// current smartlist item).
if (convUid == lrcInstance_->get_selectedConvUid()) {
lrcInstance_->deselectConversation();
}
}
void
ConversationsAdapter::onSearchStatusChanged(const QString& status)
{
Q_EMIT showSearchStatus(status);
}
void
ConversationsAdapter::onSearchResultUpdated()
{
// smartlist search results
searchSrcModel_->onSearchResultsUpdated();
}
void
ConversationsAdapter::onConversationReady(const QString& convId)
{
auto convModel = lrcInstance_->getCurrentConversationModel();
auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
auto selectedConvId = lrcInstance_->get_selectedConvUid();
// for one to one conversations including legacy mode, we can prevent
// undesired selection by filtering for a conversation peer match,
// and for all other swarm convs, we can match the conv's id
if (convInfo.isCoreDialog()) {
auto peers = convModel->peersForConversation(convId);
auto selectedPeers = convModel->peersForConversation(selectedConvId);
if (peers != selectedPeers)
return;
} else if (convId != selectedConvId)
return;
updateConversation(convId);
}
void
ConversationsAdapter::onBannedStatusChanged(const QString& uri, bool banned)
{
Q_UNUSED(banned)
auto& convInfo = lrcInstance_->getConversationFromPeerUri(uri);
if (convInfo.uid.isEmpty())
return;
auto row = lrcInstance_->indexOf(convInfo.uid);
const auto index = convSrcModel_->index(row, 0);
Q_EMIT convSrcModel_->dataChanged(index, index);
lrcInstance_->set_selectedConvUid();
}
void
ConversationsAdapter::updateConversation(const QString& convId)
{
// a conversation request has been accepted or a contact has
// been added, so select the conversation and notify the UI to:
// - switch tabs to the conversation filter tab
// - clear search bar
Q_EMIT conversationReady(convId);
lrcInstance_->selectConversation(convId);
}
void
ConversationsAdapter::updateConversationFilterData()
{
// TODO: this may be further spliced to respond separately to
// incoming messages and invites
// total unread message and pending invite counts, and tab selection
auto& accountInfo = lrcInstance_->getCurrentAccountInfo();
int totalUnreadMessages {0};
if (accountInfo.profileInfo.type != profile::Type::SIP) {
auto& convModel = accountInfo.conversationModel;
auto conversations = convModel->getFilteredConversations(FilterType::JAMI, false);
conversations.for_each([&totalUnreadMessages](const conversation::Info& conversation) {
totalUnreadMessages += conversation.unreadMessages;
});
}
set_totalUnreadMessageCount(totalUnreadMessages);
set_pendingRequestCount(accountInfo.conversationModel->pendingRequestCount());
systemTray_->setCount(lrcInstance_->notificationsCount());
if (get_pendingRequestCount() == 0 && get_filterRequests())
set_filterRequests(false);
}
void
ConversationsAdapter::setFilter(const QString& filterString)
{
convModel_->setFilter(filterString);
searchSrcModel_->setFilter(filterString);
Q_EMIT textFilterChanged(filterString);
}
void
ConversationsAdapter::ignoreFiltering(const QVariant& hightlighted)
{
convModel_->ignoreFiltering(hightlighted.toStringList());
}
QVariantMap
ConversationsAdapter::getConvInfoMap(const QString& convId)
{
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
if (convInfo.participants.empty())
return {};
QString peerUri {};
QString bestId {};
const auto& accountInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
if (convInfo.isCoreDialog()) {
try {
peerUri = accountInfo.conversationModel->peersForConversation(convId).at(0);
bestId = accountInfo.contactModel->bestIdForContact(peerUri);
} catch (...) {
}
}
bool isAudioOnly {false};
if (!convInfo.uid.isEmpty()) {
auto* call = lrcInstance_->getCallInfoForConversation(convInfo);
if (call) {
isAudioOnly = call->isAudioOnly;
}
}
bool callStackViewShouldShow {false};
call::Status callState {};
if (!convInfo.callId.isEmpty()) {
auto* callModel = lrcInstance_->getCurrentCallModel();
const auto& call = callModel->getCall(convInfo.callId);
callStackViewShouldShow = callModel->hasCall(convInfo.callId)
&& ((!call.isOutgoing
&& (call.status == call::Status::IN_PROGRESS
|| call.status == call::Status::PAUSED
|| call.status == call::Status::INCOMING_RINGING))
|| (call.isOutgoing && call.status != call::Status::ENDED));
callState = call.status;
}
return {{"convId", convId},
{"bestId", bestId},
{"title", lrcInstance_->getCurrentConversationModel()->title(convId)},
{"description", lrcInstance_->getCurrentConversationModel()->description(convId)},
{"uri", peerUri},
{"uris", accountInfo.conversationModel->peersForConversation(convId)},
{"isSwarm", convInfo.isSwarm()},
{"isRequest", convInfo.isRequest},
{"needsSyncing", convInfo.needsSyncing},
{"isAudioOnly", isAudioOnly},
{"callState", static_cast<int>(callState)},
{"callStackViewShouldShow", callStackViewShouldShow}};
}
void
ConversationsAdapter::restartConversation(const QString& convId)
{
auto& accInfo = lrcInstance_->getCurrentAccountInfo();
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
if (convInfo.uid.isEmpty() || !convInfo.isCoreDialog()) {
return;
}
// get the ONE_TO_ONE conv's peer uri
auto peerUri = accInfo.conversationModel->peersForConversation(convId).at(0);
// store a copy of the original contact so we can re-add them
// Note: we set the profile::Type to TEMPORARY to invoke a full add
// when calling ContactModel::addContact
auto contactInfo = accInfo.contactModel->getContact(peerUri);
contactInfo.profileInfo.type = profile::Type::TEMPORARY;
Utils::oneShotConnect(
accInfo.contactModel.get(),
&ContactModel::contactRemoved,
[this, &accInfo, contactInfo](const QString& peerUri) {
// setup a callback to select another ONE_TO_ONE conversation for this peer
// once the new conversation becomes ready
Utils::oneShotConnect(
accInfo.conversationModel.get(),
&ConversationModel::conversationReady,
[this, peerUri, &accInfo](const QString& convId) {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
// 3. filter for the correct contact-conversation and select it
if (!convInfo.uid.isEmpty() && convInfo.isCoreDialog()
&& peerUri
== accInfo.conversationModel->peersForConversation(convId).at(0)) {
lrcInstance_->selectConversation(convId);
}
});
// 2. add the contact and await the conversationReady signal
accInfo.contactModel->addContact(contactInfo);
});
// 1. remove the contact and await the contactRemoved signal
accInfo.contactModel->removeContact(peerUri);
}
void
ConversationsAdapter::updateConversationTitle(const QString& convId, const QString& newTitle)
{
auto convModel = lrcInstance_->getCurrentConversationModel();
QMap<QString, QString> details;
details["title"] = newTitle;
convModel->updateConversationInfos(convId, details);
}
void
ConversationsAdapter::popFrontError(const QString& convId)
{
auto convModel = lrcInstance_->getCurrentConversationModel();
convModel->popFrontError(convId);
}
void
ConversationsAdapter::updateConversationDescription(const QString& convId,
const QString& newDescription)
{
auto convModel = lrcInstance_->getCurrentConversationModel();
QMap<QString, QString> details;
details["description"] = newDescription;
convModel->updateConversationInfos(convId, details);
}
QString
ConversationsAdapter::dialogId(const QString& peerUri)
{
auto& convInfo = lrcInstance_->getConversationFromPeerUri(peerUri);
if (!convInfo.uid.isEmpty() && convInfo.isCoreDialog())
return convInfo.uid;
return {};
}
void
ConversationsAdapter::openDialogConversationWith(const QString& peerUri)
{
auto& convInfo = lrcInstance_->getConversationFromPeerUri(peerUri);
if (convInfo.uid.isEmpty() || !convInfo.isCoreDialog())
return;
lrcInstance_->selectConversation(convInfo.uid);
}
bool
ConversationsAdapter::connectConversationModel()
{
// Signal connections
auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
QObject::connect(currentConversationModel,
&ConversationModel::modelChanged,
this,
&ConversationsAdapter::onModelChanged,
Qt::UniqueConnection);
QObject::connect(lrcInstance_->getCurrentContactModel(),
&ContactModel::profileUpdated,
this,
&ConversationsAdapter::onProfileUpdated,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::conversationUpdated,
this,
&ConversationsAdapter::onConversationUpdated,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::filterChanged,
this,
&ConversationsAdapter::onFilterChanged,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::conversationCleared,
this,
&ConversationsAdapter::onConversationCleared,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::searchStatusChanged,
this,
&ConversationsAdapter::onSearchStatusChanged,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::searchResultUpdated,
this,
&ConversationsAdapter::onSearchResultUpdated,
Qt::UniqueConnection);
QObject::connect(currentConversationModel,
&ConversationModel::conversationReady,
this,
&ConversationsAdapter::onConversationReady,
Qt::UniqueConnection);
QObject::connect(lrcInstance_->getCurrentContactModel(),
&ContactModel::bannedStatusChanged,
this,
&ConversationsAdapter::onBannedStatusChanged,
Qt::UniqueConnection);
convSrcModel_.reset(new ConversationListModel(lrcInstance_));
convModel_->bindSourceModel(convSrcModel_.get());
searchSrcModel_.reset(new SearchResultsListModel(lrcInstance_));
searchModel_->bindSourceModel(searchSrcModel_.get());
updateConversationFilterData();
return true;
}
void
ConversationsAdapter::createSwarm(const QString& title,
const QString& description,
const QString& avatar,
const VectorString& participants)
{
auto convModel = lrcInstance_->getCurrentConversationModel();
convModel->createConversation(participants,
{{"title", title},
{"description", description},
{"avatar", avatar}});
}