-
Andreas Traczyk authored
If we detect that the current account has been deleted, remove the proxy model source models immediately. Gitlab: #1557 Change-Id: Iaf84198438b54e44d31a46870acdfa9569be6daa
Andreas Traczyk authoredIf we detect that the current account has been deleted, remove the proxy model source models immediately. Gitlab: #1557 Change-Id: Iaf84198438b54e44d31a46870acdfa9569be6daa
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
conversationsadapter.cpp 25.14 KiB
/*
* Copyright (C) 2020-2024 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 "qtutils.h"
#include "systemtray.h"
#ifdef Q_OS_LINUX
#include "namedirectory.h"
#endif
#include <QApplication>
#include <QJsonObject>
using namespace lrc::api;
ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
LRCInstance* instance,
ConversationListProxyModel* convProxyModel,
SelectableListProxyModel* searchProxyModel,
QObject* parent)
: QmlAdapterBase(instance, parent)
, systemTray_(systemTray)
, convSrcModel_(new ConversationListModel(lrcInstance_))
, convModel_(convProxyModel)
, searchSrcModel_(new SearchResultsListModel(lrcInstance_))
, searchModel_(searchProxyModel)
{
convModel_->bindSourceModel(convSrcModel_.get());
searchModel_->bindSourceModel(searchSrcModel_.get());
set_convListProxyModel(QVariant::fromValue(convModel_));
set_searchListProxyModel(QVariant::fromValue(searchModel_));
// this will trigger when the invite filter tab is selected
connect(this, &ConversationsAdapter::filterRequestsChanged, [this]() {
convModel_->setFilterRequests(filterRequests_);
});
connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, this, [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() || convInfo.accountId != lrcInstance_->get_currentAccountId())
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, [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,
[this](const QString& accountId, const QString& convUid) {
Q_EMIT lrcInstance_->notificationClicked();
lrcInstance_->selectConversation(convUid, accountId);
});
connect(systemTray_,
&SystemTray::acceptPendingActivated,
this,
[this](const QString& accountId, const QString& convUid) {
auto& accInfo = lrcInstance_->getAccountInfo(accountId);
accInfo.conversationModel->acceptConversationRequest(convUid);
});
connect(systemTray_,
&SystemTray::refusePendingActivated,
this,
[this](const QString& accountId, const QString& convUid) {
auto& accInfo = lrcInstance_->getAccountInfo(accountId);
accInfo.conversationModel->removeConversation(convUid);
});
#endif
connect(&lrcInstance_->behaviorController(),
&BehaviorController::newUnreadInteraction,
this,
&ConversationsAdapter::onNewUnreadInteraction);
connect(&lrcInstance_->behaviorController(),
&BehaviorController::newReadInteraction,
this,
&ConversationsAdapter::onNewReadInteraction);
connect(&lrcInstance_->behaviorController(),
&BehaviorController::newTrustRequest,
this,
&ConversationsAdapter::onNewTrustRequest);
connect(&lrcInstance_->behaviorController(),
&BehaviorController::trustRequestTreated,
this,
&ConversationsAdapter::onTrustRequestTreated);
connect(lrcInstance_,
&LRCInstance::currentAccountIdChanged,
this,
&ConversationsAdapter::onCurrentAccountIdChanged);
connect(lrcInstance_,
&LRCInstance::currentAccountRemoved,
this,
&ConversationsAdapter::onCurrentAccountRemoved,
Qt::DirectConnection);
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 (!QApplication::focusWindow() || accountId != lrcInstance_->get_currentAccountId()
|| convUid != lrcInstance_->get_selectedConvUid()) {
auto& accountInfo = lrcInstance_->getAccountInfo(accountId);
if (interaction.authorUri == accountInfo.profileInfo.uri)
return;
auto from = accountInfo.contactModel->bestNameForContact(interaction.authorUri);
auto body_ = interaction.body;
if (interaction.type == interaction::Type::DATA_TRANSFER) {
body_ = interaction.commit.value("displayName");
}
auto preferences = accountInfo.conversationModel->getConversationPreferences(convUid);
// Ignore notifications for this conversation
if (preferences["ignoreNotifications"] == "true")
return;
#ifdef Q_OS_LINUX
auto to = lrcInstance_->accountModel().bestNameForAccount(accountId);
auto contactPhoto = Utils::contactPhoto(lrcInstance_,
interaction.authorUri,
QSize(50, 50),
accountId);
auto notifId = QString("%1;%2;%3").arg(accountId, convUid, interactionId);
systemTray_->showNotification(notifId,
tr("%1 received a new message").arg(to),
from + ": " + body_,
SystemTray::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, convUid, 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 to = lrcInstance_->accountModel().bestNameForAccount(accountId);
auto cb = [this, to, accountId, conv, peerUri](QString peerBestName) {
auto contactPhoto = Utils::contactPhoto(lrcInstance_, peerUri, QSize(50, 50), accountId);
auto notifId = QString("%1;%2").arg(accountId, conv);
systemTray_->showNotification(notifId,
tr("%1 received a new trust request").arg(to),
"New request from " + peerBestName,
SystemTray::NotificationType::REQUEST,
Utils::QImageToByteArray(contactPhoto));
};
// This peer is not yet a contact, so we don't have a name for it,
// but we can attempt to look it up using the name service before
// falling back to the bestNameForContact.
Utils::oneShotConnect(&NameDirectory::instance(),
&NameDirectory::registeredNameFound,
this,
[this, accountId, peerUri, cb](NameDirectory::LookupStatus status,
const QString& address,
const QString& name) {
if (address == peerUri) {
if (status == NameDirectory::LookupStatus::SUCCESS)
cb(name);
else {
auto& accInfo = lrcInstance_->getAccountInfo(accountId);
cb(accInfo.contactModel->bestNameForContact(peerUri));
}
}
});
std::ignore = NameDirectory::instance().lookupAddress(accountId, peerUri);
}
#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, 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::onConversationRemoved(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::onSearchResultEnded()
{
if (selectFirst_.exchange(false)) {
convModel_->select(0);
searchModel_->select(0);
}
}
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_->onNotificationCountChanged(lrcInstance_->notificationsCount());
if (get_pendingRequestCount() == 0 && get_filterRequests())
set_filterRequests(false);
}
void
ConversationsAdapter::setFilterAndSelect(const QString& filterString)
{
selectFirst_ = true;
setFilter(filterString);
}
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::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)
{
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);
}
void
ConversationsAdapter::onCurrentAccountRemoved()
{
// Unbind proxy model source models.
convModel_->bindSourceModel(nullptr);
searchModel_->bindSourceModel(nullptr);
}
void
ConversationsAdapter::connectConversationModel()
{
// Signal connections
auto model = lrcInstance_->getCurrentConversationModel();
if (!model) {
return;
}
auto connectObjectSignal = [this](auto obj, auto signal, auto slot) {
connect(obj, signal, this, slot, Qt::UniqueConnection);
};
connectObjectSignal(model,
&ConversationModel::modelChanged,
&ConversationsAdapter::onModelChanged);
connectObjectSignal(model,
&ConversationModel::profileUpdated,
&ConversationsAdapter::onProfileUpdated);
connectObjectSignal(model,
&ConversationModel::conversationUpdated,
&ConversationsAdapter::onConversationUpdated);
connectObjectSignal(model,
&ConversationModel::conversationRemoved,
&ConversationsAdapter::onConversationRemoved);
connectObjectSignal(model,
&ConversationModel::filterChanged,
&ConversationsAdapter::onFilterChanged);
connectObjectSignal(model,
&ConversationModel::conversationCleared,
&ConversationsAdapter::onConversationCleared);
connectObjectSignal(model,
&ConversationModel::searchStatusChanged,
&ConversationsAdapter::onSearchStatusChanged);
connectObjectSignal(model,
&ConversationModel::searchResultUpdated,
&ConversationsAdapter::onSearchResultUpdated);
connectObjectSignal(model,
&ConversationModel::searchResultEnded,
&ConversationsAdapter::onSearchResultEnded);
connectObjectSignal(model,
&ConversationModel::conversationReady,
&ConversationsAdapter::onConversationReady);
connectObjectSignal(lrcInstance_->getCurrentContactModel(),
&ContactModel::bannedStatusChanged,
&ConversationsAdapter::onBannedStatusChanged);
convSrcModel_.reset(new ConversationListModel(lrcInstance_));
convModel_->bindSourceModel(convSrcModel_.get());
searchSrcModel_.reset(new SearchResultsListModel(lrcInstance_));
searchModel_->bindSourceModel(searchSrcModel_.get());
updateConversationFilterData();
}
QString
ConversationsAdapter::createSwarm(const QString& title,
const QString& description,
const QString& avatar,
const VectorString& participants)
{
auto convModel = lrcInstance_->getCurrentConversationModel();
MapStringString details;
if (!title.isEmpty())
details["title"] = title;
if (!description.isEmpty())
details["description"] = description;
if (!avatar.isEmpty())
details["avatar"] = avatar;
return convModel->createConversation(participants, details);
}