mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-07-15 21:15:24 +02:00

- Avoids manually building local file URIs which was causing long load times for conversations on Windows. - Fixes an issue where missing images were caused by a interaction updates erasing the message bodies. Change-Id: I4c65f73cf9f46da5a9ae899940cb205cb34ffae2
4220 lines
164 KiB
C++
4220 lines
164 KiB
C++
/****************************************************************************
|
|
* Copyright (C) 2017-2024 Savoir-faire Linux Inc. *
|
|
* Author: Nicolas Jäger <nicolas.jager@savoirfairelinux.com> *
|
|
* Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> *
|
|
* Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> *
|
|
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> *
|
|
* *
|
|
* This library is free software; you can redistribute it and/or *
|
|
* modify it under the terms of the GNU Lesser General Public *
|
|
* License as published by the Free Software Foundation; either *
|
|
* version 2.1 of the License, or (at your option) any later version. *
|
|
* *
|
|
* This library 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 *
|
|
* Lesser 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 "api/conversationmodel.h"
|
|
|
|
// LRC
|
|
#include "api/lrc.h"
|
|
#include "api/behaviorcontroller.h"
|
|
#include "api/contactmodel.h"
|
|
#include "api/callmodel.h"
|
|
#include "api/accountmodel.h"
|
|
#include "api/account.h"
|
|
#include "api/call.h"
|
|
#include "api/datatransfer.h"
|
|
#include "api/datatransfermodel.h"
|
|
#include "callbackshandler.h"
|
|
#include "containerview.h"
|
|
#include "authority/storagehelper.h"
|
|
#include "uri.h"
|
|
|
|
// Dbus
|
|
#include "dbus/configurationmanager.h"
|
|
#include "dbus/callmanager.h"
|
|
|
|
// daemon
|
|
#include <account_const.h>
|
|
#include <datatransfer_interface.h>
|
|
|
|
// Qt
|
|
#include <QtCore/QTimer>
|
|
#include <QFileInfo>
|
|
|
|
// std
|
|
#include <algorithm>
|
|
#include <mutex>
|
|
#include <regex>
|
|
#include <sstream>
|
|
|
|
namespace lrc {
|
|
|
|
using namespace authority;
|
|
using namespace api;
|
|
|
|
class ConversationModelPimpl : public QObject
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
ConversationModelPimpl(const ConversationModel& linked,
|
|
Lrc& lrc,
|
|
Database& db,
|
|
const CallbacksHandler& callbacksHandler,
|
|
const BehaviorController& behaviorController);
|
|
|
|
~ConversationModelPimpl();
|
|
|
|
using FilterPredicate = std::function<bool(const conversation::Info& convInfo)>;
|
|
|
|
/**
|
|
* return a conversation index from conversations or -1 if no index is found.
|
|
* @param uid of the contact to search.
|
|
* @return an int.
|
|
*/
|
|
int indexOf(const QString& uid) const;
|
|
|
|
/**
|
|
* return a reference to a conversation with given filter
|
|
* @param pred a unary comparison predicate with which to find the conversation
|
|
* @param searchResultIncluded if need to search in contacts and userSearch
|
|
* @return a reference to a conversation with given uid
|
|
*/
|
|
std::reference_wrapper<conversation::Info> getConversation(
|
|
const FilterPredicate& pred, const bool searchResultIncluded = false) const;
|
|
|
|
/**
|
|
* return a reference to a conversation with given uid.
|
|
* @param conversation uid.
|
|
* @param searchResultIncluded if need to search in contacts and userSearch.
|
|
* @return a reference to a conversation with the given uid.
|
|
*/
|
|
std::reference_wrapper<conversation::Info> getConversationForUid(
|
|
const QString& uid, const bool searchResultIncluded = false) const;
|
|
|
|
/**
|
|
* return a reference to a conversation with participant.
|
|
* @param participant uri.
|
|
* @param searchResultIncluded if need to search in contacts and userSearch.
|
|
* @return a reference to a conversation with the given peer uri.
|
|
* @warning we could have multiple swarm conversations for the same peer. This function will
|
|
* return an active one-to-one conversation.
|
|
*/
|
|
std::reference_wrapper<conversation::Info> getConversationForPeerUri(
|
|
const QString& uri, const bool searchResultIncluded = false) const;
|
|
/**
|
|
* return a vector of conversation indices for the given contact uri empty
|
|
* if no index is found
|
|
* @param uri of the contact to search
|
|
* @return an vector of indices
|
|
*/
|
|
std::vector<int> getIndicesForContact(const QString& uri) const;
|
|
/**
|
|
* Initialize conversations_ and filteredConversations_
|
|
*/
|
|
void initConversations();
|
|
/**
|
|
* Filter all conversations
|
|
*/
|
|
bool filter(const conversation::Info& conv);
|
|
/**
|
|
* Sort conversation by last action
|
|
*/
|
|
bool sort(const conversation::Info& convA, const conversation::Info& convB);
|
|
/**
|
|
* Call contactModel.addContact if necessary
|
|
* @param contactUri
|
|
*/
|
|
void sendContactRequest(const QString& contactUri);
|
|
/**
|
|
* Add a conversation with contactUri
|
|
* @param convId
|
|
* @param contactUri
|
|
*/
|
|
void addConversationWith(const QString& convId, const QString& contactUri, bool isRequest);
|
|
/**
|
|
* Add a swarm conversation to conversation list
|
|
* @param convId
|
|
*/
|
|
void addSwarmConversation(const QString& convId);
|
|
/**
|
|
* Add call interaction for conversation with callId
|
|
* @param callId
|
|
* @param duration
|
|
*/
|
|
void addOrUpdateCallMessage(const QString& callId,
|
|
const QString& from,
|
|
bool incoming,
|
|
const std::time_t& duration = -1);
|
|
/**
|
|
* Add a new message from a peer in the database
|
|
* @param peerId the author id
|
|
* @param body the content of the message
|
|
* @param timestamp the timestamp of the message
|
|
* @param daemonId the daemon id
|
|
* @return msgId generated (in db)
|
|
*/
|
|
QString addIncomingMessage(const QString& peerId,
|
|
const QString& body,
|
|
const uint64_t& timestamp = 0,
|
|
const QString& daemonId = "");
|
|
/**
|
|
* Change the status of an interaction. Listen from callbacksHandler
|
|
* @param accountId, account linked
|
|
* @param messageId, interaction to update
|
|
* @param conversationId, conversation
|
|
* @param peerId, peer id
|
|
* @param status, new status for this interaction
|
|
*/
|
|
void updateInteractionStatus(const QString& accountId,
|
|
const QString& conversationId,
|
|
const QString& peerId,
|
|
const QString& messageId,
|
|
int status);
|
|
|
|
/**
|
|
* place a call
|
|
* @param uid, conversation id
|
|
* @param isAudioOnly, allow to specify if the call is only audio. Set to false by default.
|
|
*/
|
|
void placeCall(const QString& uid, bool isAudioOnly = false);
|
|
|
|
/**
|
|
* get number of unread messages
|
|
*/
|
|
int getNumberOfUnreadMessagesFor(const QString& uid);
|
|
|
|
/**
|
|
* Handle data transfer progression
|
|
*/
|
|
void updateTransferProgress(QTimer* timer, int conversationIdx, const QString& interactionId);
|
|
|
|
bool usefulDataFromDataTransfer(const QString& fileId,
|
|
const datatransfer::Info& info,
|
|
QString& interactionId,
|
|
QString& conversationId);
|
|
void awaitingHost(const QString& fileId, datatransfer::Info info);
|
|
|
|
bool hasOneOneSwarmWith(const QString& participant);
|
|
|
|
/**
|
|
* accept a file transfer
|
|
* @param convUid
|
|
* @param interactionId
|
|
*/
|
|
void acceptTransfer(const QString& convUid, const QString& interactionId);
|
|
void handleIncomingFile(const QString& convId, const QString& interactionId, int totalSize);
|
|
void addConversationRequest(const MapStringString& convRequest, bool emitToClient = false);
|
|
void addContactRequest(const QString& contactUri);
|
|
|
|
// filter out ourself from conversation participants.
|
|
const VectorString peersForConversation(const conversation::Info& conversation) const;
|
|
void invalidateModel();
|
|
void emplaceBackConversation(conversation::Info&& conversation);
|
|
void eraseConversation(const QString& convId);
|
|
void eraseConversation(int index);
|
|
|
|
const ConversationModel& linked;
|
|
Lrc& lrc;
|
|
Database& db;
|
|
const CallbacksHandler& callbacksHandler;
|
|
const BehaviorController& behaviorController;
|
|
|
|
ConversationModel::ConversationQueue conversations; ///< non-filtered conversations
|
|
ConversationModel::ConversationQueue searchResults;
|
|
|
|
ConversationModel::ConversationQueueProxy filteredConversations;
|
|
ConversationModel::ConversationQueueProxy customFilteredConversations;
|
|
|
|
QString currentFilter;
|
|
FilterType typeFilter;
|
|
FilterType customTypeFilter;
|
|
|
|
MapStringString transfIdToDbIntId;
|
|
uint32_t mediaResearchRequestId;
|
|
uint32_t msgResearchRequestId;
|
|
|
|
public Q_SLOTS:
|
|
/**
|
|
* Listen from contactModel when updated (like new alias, avatar, etc.)
|
|
*/
|
|
void slotContactModelUpdated(const QString& uri);
|
|
/**
|
|
* Listen from contactModel when a new contact is added
|
|
* @param uri
|
|
*/
|
|
void slotContactAdded(const QString& contactUri);
|
|
/**
|
|
* Listen from contactModel when a pending contact is accepted
|
|
* @param uri
|
|
*/
|
|
void slotPendingContactAccepted(const QString& uri);
|
|
/**
|
|
* Listen from contactModel when aa new contact is removed
|
|
* @param uri
|
|
*/
|
|
void slotContactRemoved(const QString& uri);
|
|
/**
|
|
* Listen from callmodel for new calls.
|
|
* @param fromId caller uri
|
|
* @param callId
|
|
* @param isOutgoing
|
|
* @param toUri
|
|
*/
|
|
void slotNewCall(const QString& fromId,
|
|
const QString& callId,
|
|
bool isOutgoing,
|
|
const QString& toUri);
|
|
/**
|
|
* Listen from callmodel for calls status changed.
|
|
* @param callId
|
|
*/
|
|
void slotCallStatusChanged(const QString& callId, int code);
|
|
/**
|
|
* Listen from callmodel for writing "Call started"
|
|
* @param callId
|
|
*/
|
|
void slotCallStarted(const QString& callId);
|
|
/**
|
|
* Listen from callmodel for writing "Call ended"
|
|
* @param callId
|
|
*/
|
|
void slotCallEnded(const QString& callId);
|
|
/**
|
|
* Listen from CallbacksHandler for new incoming interactions;
|
|
* @param accountId
|
|
* @param msgId
|
|
* @param peerId
|
|
* @param payloads body
|
|
*/
|
|
void slotNewAccountMessage(const QString& accountId,
|
|
const QString& peerId,
|
|
const QString& msgId,
|
|
const MapStringString& payloads);
|
|
/**
|
|
* Listen from CallbacksHandler for new messages in a SIP call
|
|
* @param accountId account linked to the interaction
|
|
* @param callId call linked to the interaction
|
|
* @param from author uri
|
|
* @param body of the message
|
|
*/
|
|
void slotIncomingCallMessage(const QString& accountId,
|
|
const QString& callId,
|
|
const QString& from,
|
|
const QString& body);
|
|
/**
|
|
* Listen from CallModel when a call is added to a conference
|
|
* @param callId
|
|
* @param confId
|
|
*/
|
|
void slotCallAddedToConference(const QString& callId, 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
|
|
* @param contactUri
|
|
* @param isComposing
|
|
*/
|
|
void slotComposingStatusChanged(const QString& accountId,
|
|
const QString& convId,
|
|
const QString& contactUri,
|
|
bool isComposing);
|
|
|
|
void slotTransferStatusCreated(const QString& fileId, api::datatransfer::Info info);
|
|
void slotTransferStatusCanceled(const QString& fileId, api::datatransfer::Info info);
|
|
void slotTransferStatusAwaitingPeer(const QString& fileId, api::datatransfer::Info info);
|
|
void slotTransferStatusAwaitingHost(const QString& fileId, api::datatransfer::Info info);
|
|
void slotTransferStatusOngoing(const QString& fileId, api::datatransfer::Info info);
|
|
void slotTransferStatusFinished(const QString& fileId, api::datatransfer::Info info);
|
|
void slotTransferStatusError(const QString& fileId, api::datatransfer::Info info);
|
|
void slotTransferStatusTimeoutExpired(const QString& fileId, api::datatransfer::Info info);
|
|
void slotTransferStatusUnjoinable(const QString& fileId, api::datatransfer::Info info);
|
|
bool updateTransferStatus(const QString& fileId,
|
|
datatransfer::Info info,
|
|
interaction::Status newStatus,
|
|
bool& updated);
|
|
void slotSwarmLoaded(uint32_t requestId,
|
|
const QString& accountId,
|
|
const QString& conversationId,
|
|
const VectorSwarmMessage& messages);
|
|
/**
|
|
* Listen messageFound signal.
|
|
* Is the search response from MessagesAdapter::getConvMedias()
|
|
* @param requestId token of the request
|
|
* @param accountId
|
|
* @param conversationId
|
|
* @param messages Id of all the messages
|
|
*/
|
|
void slotMessagesFound(uint32_t requestId,
|
|
const QString& accountId,
|
|
const QString& conversationId,
|
|
const VectorMapStringString& messages);
|
|
void slotMessageReceived(const QString& accountId,
|
|
const QString& conversationId,
|
|
const SwarmMessage& message);
|
|
void slotMessageUpdated(const QString& accountId,
|
|
const QString& conversationId,
|
|
const SwarmMessage& message);
|
|
void slotReactionAdded(const QString& accountId,
|
|
const QString& conversationId,
|
|
const QString& messageId,
|
|
const MapStringString& reaction);
|
|
void slotReactionRemoved(const QString& accountId,
|
|
const QString& conversationId,
|
|
const QString& messageId,
|
|
const QString& reactionId);
|
|
void slotConversationProfileUpdated(const QString& accountId,
|
|
const QString& conversationId,
|
|
const MapStringString& profile);
|
|
void slotConversationRequestReceived(const QString& accountId,
|
|
const QString& conversationId,
|
|
const MapStringString& metadatas);
|
|
void slotConversationMemberEvent(const QString& accountId,
|
|
const QString& conversationId,
|
|
const QString& memberUri,
|
|
int event);
|
|
void slotOnConversationError(const QString& accountId,
|
|
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,
|
|
const QString& conversationId,
|
|
const MapStringString& preferences);
|
|
};
|
|
|
|
ConversationModel::ConversationModel(const account::Info& owner,
|
|
Lrc& lrc,
|
|
Database& db,
|
|
const CallbacksHandler& callbacksHandler,
|
|
const BehaviorController& behaviorController)
|
|
: QObject(nullptr)
|
|
, pimpl_(std::make_unique<ConversationModelPimpl>(*this,
|
|
lrc,
|
|
db,
|
|
callbacksHandler,
|
|
behaviorController))
|
|
, owner(owner)
|
|
{}
|
|
|
|
void
|
|
ConversationModel::initConversations()
|
|
{
|
|
pimpl_->initConversations();
|
|
}
|
|
|
|
ConversationModel::~ConversationModel() {}
|
|
|
|
const ConversationModel::ConversationQueue&
|
|
ConversationModel::getConversations() const
|
|
{
|
|
return pimpl_->conversations;
|
|
}
|
|
|
|
const ConversationModel::ConversationQueueProxy&
|
|
ConversationModel::allFilteredConversations() const
|
|
{
|
|
if (!pimpl_->filteredConversations.isDirty())
|
|
return pimpl_->filteredConversations;
|
|
|
|
return pimpl_->filteredConversations.filter().sort().validate();
|
|
}
|
|
|
|
QMap<ConferenceableItem, ConferenceableValue>
|
|
ConversationModel::getConferenceableConversations(const QString& convId, const QString& filter) const
|
|
{
|
|
auto conversationIdx = pimpl_->indexOf(convId);
|
|
if (conversationIdx == -1 || !owner.enabled) {
|
|
return {};
|
|
}
|
|
QMap<ConferenceableItem, ConferenceableValue> result;
|
|
ConferenceableValue callsVector, contactsVector;
|
|
|
|
auto currentConfId = pimpl_->conversations.at(conversationIdx).confId;
|
|
auto currentCallId = pimpl_->conversations.at(conversationIdx).callId;
|
|
auto calls = pimpl_->lrc.getCalls();
|
|
auto conferences = pimpl_->lrc.getConferences(owner.id);
|
|
auto& conversations = pimpl_->conversations;
|
|
auto currentAccountID = pimpl_->linked.owner.id;
|
|
// add contacts for current account
|
|
for (const auto& conv : conversations) {
|
|
// conversations with calls will be added in call section
|
|
// we want to add only contacts non-swarm or one-to-one conversation
|
|
auto& peers = pimpl_->peersForConversation(conv);
|
|
if (!conv.callId.isEmpty() || !conv.confId.isEmpty() || !conv.isCoreDialog()
|
|
|| peers.empty()) {
|
|
continue;
|
|
}
|
|
try {
|
|
auto contact = owner.contactModel->getContact(peers.front());
|
|
if (contact.isBanned || contact.profileInfo.type == profile::Type::PENDING) {
|
|
continue;
|
|
}
|
|
QVector<AccountConversation> cv;
|
|
AccountConversation accConv = {conv.uid, currentAccountID};
|
|
cv.push_back(accConv);
|
|
if (filter.isEmpty()) {
|
|
contactsVector.push_back(cv);
|
|
continue;
|
|
}
|
|
bool result = contact.profileInfo.alias.contains(filter, Qt::CaseInsensitive)
|
|
|| contact.profileInfo.uri.contains(filter, Qt::CaseInsensitive)
|
|
|| contact.registeredName.contains(filter, Qt::CaseInsensitive);
|
|
if (result) {
|
|
contactsVector.push_back(cv);
|
|
}
|
|
} catch (const std::out_of_range& e) {
|
|
qDebug() << e.what();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (calls.empty() && conferences.empty()) {
|
|
result.insert(ConferenceableItem::CONTACT, contactsVector);
|
|
return result;
|
|
}
|
|
|
|
// filter out calls from conference
|
|
for (const auto& c : conferences) {
|
|
for (const auto& subcall : owner.callModel->getConferenceSubcalls(c)) {
|
|
const auto position = std::find(calls.cbegin(), calls.cend(), subcall);
|
|
if (position != calls.cend()) {
|
|
calls.erase(position);
|
|
}
|
|
}
|
|
}
|
|
|
|
// found conversations and account for calls and conferences
|
|
QMap<QString, QVector<AccountConversation>> tempConferences;
|
|
for (const auto& account_id : pimpl_->lrc.getAccountModel().getAccountList()) {
|
|
try {
|
|
auto& accountInfo = pimpl_->lrc.getAccountModel().getAccountInfo(account_id);
|
|
auto type = accountInfo.profileInfo.type == profile::Type::SIP ? FilterType::SIP
|
|
: FilterType::JAMI;
|
|
auto accountConv = accountInfo.conversationModel->getFilteredConversations(type);
|
|
accountConv.for_each([this,
|
|
filter,
|
|
&accountInfo,
|
|
account_id,
|
|
currentCallId,
|
|
currentConfId,
|
|
&conferences,
|
|
&calls,
|
|
&tempConferences,
|
|
&callsVector](const conversation::Info& conv) {
|
|
bool confFilterPredicate = !conv.confId.isEmpty() && conv.confId != currentConfId
|
|
&& std::find(conferences.begin(),
|
|
conferences.end(),
|
|
conv.confId)
|
|
!= conferences.end();
|
|
bool callFilterPredicate = !conv.callId.isEmpty() && conv.callId != currentCallId
|
|
&& std::find(calls.begin(), calls.end(), conv.callId)
|
|
!= calls.end();
|
|
auto& peers = pimpl_->peersForConversation(conv);
|
|
if ((!confFilterPredicate && !callFilterPredicate) || !conv.isCoreDialog()) {
|
|
return;
|
|
}
|
|
|
|
// vector of conversationID accountID pair
|
|
// for call has only one entry, for conference multyple
|
|
QVector<AccountConversation> cv;
|
|
AccountConversation accConv = {conv.uid, account_id};
|
|
cv.push_back(accConv);
|
|
|
|
bool isConference = !conv.confId.isEmpty();
|
|
// call could be added if it is not conference and in active state
|
|
bool shouldAddCall = false;
|
|
if (!isConference && accountInfo.callModel->hasCall(conv.callId)) {
|
|
const auto& call = accountInfo.callModel->getCall(conv.callId);
|
|
shouldAddCall = call.status == lrc::api::call::Status::PAUSED
|
|
|| call.status == lrc::api::call::Status::IN_PROGRESS;
|
|
}
|
|
|
|
auto contact = accountInfo.contactModel->getContact(peers.front());
|
|
// check if contact satisfy filter
|
|
bool result = (filter.isEmpty() || isConference)
|
|
? true
|
|
: (contact.profileInfo.alias.contains(filter)
|
|
|| contact.profileInfo.uri.contains(filter)
|
|
|| contact.registeredName.contains(filter));
|
|
if (!result) {
|
|
return;
|
|
}
|
|
if (isConference && tempConferences.count(conv.confId)) {
|
|
tempConferences.find(conv.confId).value().push_back(accConv);
|
|
} else if (isConference) {
|
|
tempConferences.insert(conv.confId, cv);
|
|
} else if (shouldAddCall) {
|
|
callsVector.push_back(cv);
|
|
}
|
|
});
|
|
} catch (...) {
|
|
}
|
|
}
|
|
for (const auto& it : tempConferences.toStdMap()) {
|
|
if (filter.isEmpty()) {
|
|
callsVector.push_back(it.second);
|
|
continue;
|
|
}
|
|
for (const AccountConversation& accConv : it.second) {
|
|
try {
|
|
auto& account = pimpl_->lrc.getAccountModel().getAccountInfo(accConv.accountId);
|
|
auto& conv = account.conversationModel->getConversationForUid(accConv.convId)->get();
|
|
auto& peers = pimpl_->peersForConversation(conv);
|
|
if (!conv.isCoreDialog()) {
|
|
continue;
|
|
}
|
|
auto cont = account.contactModel->getContact(peers.front());
|
|
if (cont.profileInfo.alias.contains(filter) || cont.profileInfo.uri.contains(filter)
|
|
|| cont.registeredName.contains(filter)) {
|
|
callsVector.push_back(it.second);
|
|
continue;
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
}
|
|
result.insert(ConferenceableItem::CALL, callsVector);
|
|
result.insert(ConferenceableItem::CONTACT, contactsVector);
|
|
return result;
|
|
}
|
|
|
|
const ConversationModel::ConversationQueue&
|
|
ConversationModel::getAllSearchResults() const
|
|
{
|
|
return pimpl_->searchResults;
|
|
}
|
|
|
|
const ConversationModel::ConversationQueueProxy&
|
|
ConversationModel::getFilteredConversations(const FilterType& filter,
|
|
bool forceUpdate,
|
|
const bool includeBanned) const
|
|
{
|
|
if (pimpl_->customTypeFilter == filter && !pimpl_->customFilteredConversations.isDirty()
|
|
&& !forceUpdate)
|
|
return pimpl_->customFilteredConversations;
|
|
|
|
pimpl_->customTypeFilter = filter;
|
|
return pimpl_->customFilteredConversations.reset(pimpl_->conversations)
|
|
.filter([this, &includeBanned](const conversation::Info& entry) {
|
|
try {
|
|
if (entry.isLegacy()) {
|
|
auto& peers = pimpl_->peersForConversation(entry);
|
|
if (peers.isEmpty()) {
|
|
return false;
|
|
}
|
|
auto contactInfo = owner.contactModel->getContact(peers.front());
|
|
// do not check blocked contacts for conversation with many participants
|
|
if (!includeBanned && (contactInfo.isBanned && peers.size() == 1))
|
|
return false;
|
|
}
|
|
switch (pimpl_->customTypeFilter) {
|
|
case FilterType::JAMI:
|
|
// we have conversation with many participants only for JAMI
|
|
return (owner.profileInfo.type == profile::Type::JAMI && !entry.isRequest);
|
|
case FilterType::SIP:
|
|
return (owner.profileInfo.type == profile::Type::SIP && !entry.isRequest);
|
|
case FilterType::REQUEST:
|
|
return entry.isRequest;
|
|
case FilterType::INVALID:
|
|
default:
|
|
break;
|
|
}
|
|
} catch (...) {
|
|
}
|
|
return false;
|
|
})
|
|
.validate();
|
|
}
|
|
|
|
const ConversationModel::ConversationQueueProxy&
|
|
ConversationModel::getFilteredConversations(const profile::Type& profileType,
|
|
bool forceUpdate,
|
|
const bool includeBanned) const
|
|
{
|
|
FilterType filterType = FilterType::INVALID;
|
|
switch (profileType) {
|
|
case lrc::api::profile::Type::JAMI:
|
|
filterType = lrc::api::FilterType::JAMI;
|
|
break;
|
|
case lrc::api::profile::Type::SIP:
|
|
filterType = lrc::api::FilterType::SIP;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return getFilteredConversations(filterType, forceUpdate, includeBanned);
|
|
}
|
|
|
|
OptRef<conversation::Info>
|
|
ConversationModel::getConversationForUid(const QString& uid) const
|
|
{
|
|
if (!pimpl_) {
|
|
qWarning() << "Invalid pimpl_";
|
|
return std::nullopt;
|
|
}
|
|
try {
|
|
return std::make_optional(pimpl_->getConversationForUid(uid, true));
|
|
} catch (const std::out_of_range&) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
OptRef<conversation::Info>
|
|
ConversationModel::getConversationForPeerUri(const QString& uri) const
|
|
{
|
|
try {
|
|
return std::make_optional(pimpl_->getConversation(
|
|
[this, uri](const conversation::Info& conv) -> bool {
|
|
if (!conv.isCoreDialog()) {
|
|
return false;
|
|
}
|
|
if (conv.mode == conversation::Mode::ONE_TO_ONE) {
|
|
return pimpl_->peersForConversation(conv).indexOf(uri) != -1;
|
|
}
|
|
return uri == pimpl_->peersForConversation(conv).front();
|
|
},
|
|
true));
|
|
} catch (const std::out_of_range&) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
OptRef<conversation::Info>
|
|
ConversationModel::getConversationForCallId(const QString& callId) const
|
|
{
|
|
try {
|
|
return std::make_optional(pimpl_->getConversation(
|
|
[callId](const conversation::Info& conv) -> bool {
|
|
return (callId == conv.callId || callId == conv.confId);
|
|
},
|
|
true));
|
|
} catch (const std::out_of_range&) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
OptRef<conversation::Info>
|
|
ConversationModel::filteredConversation(unsigned row) const
|
|
{
|
|
auto conversations = allFilteredConversations();
|
|
if (row >= conversations.get().size())
|
|
return std::nullopt;
|
|
|
|
return std::make_optional(conversations.get().at(row));
|
|
}
|
|
|
|
OptRef<conversation::Info>
|
|
ConversationModel::searchResultForRow(unsigned row) const
|
|
{
|
|
auto& results = pimpl_->searchResults;
|
|
if (row >= results.size())
|
|
return std::nullopt;
|
|
|
|
return std::make_optional(std::ref(results.at(row)));
|
|
}
|
|
|
|
void
|
|
ConversationModel::makePermanent(const QString& uid)
|
|
{
|
|
try {
|
|
auto& conversation = pimpl_->getConversationForUid(uid, true).get();
|
|
|
|
if (conversation.participants.empty()) {
|
|
// Should not
|
|
qDebug() << "ConversationModel::addConversation can't add a conversation with no "
|
|
"participant";
|
|
return;
|
|
}
|
|
|
|
// Send contact request if non used
|
|
auto& peers = pimpl_->peersForConversation(conversation);
|
|
if (peers.size() != 1) {
|
|
return;
|
|
}
|
|
pimpl_->sendContactRequest(peers.front());
|
|
} catch (const std::out_of_range& e) {
|
|
qDebug() << "make permanent failed. conversation not found";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModel::selectConversation(const QString& uid) const
|
|
{
|
|
try {
|
|
auto& conversation = pimpl_->getConversationForUid(uid, true).get();
|
|
|
|
bool callEnded = true;
|
|
if (!conversation.callId.isEmpty()) {
|
|
try {
|
|
auto call = owner.callModel->getCall(conversation.callId);
|
|
callEnded = call.status == call::Status::ENDED;
|
|
} catch (...) {
|
|
}
|
|
}
|
|
if (!conversation.confId.isEmpty() && owner.confProperties.isRendezVous) {
|
|
// If we are on a rendez vous account and we select the conversation,
|
|
// attach to the call.
|
|
CallManager::instance().unholdConference(owner.id, conversation.confId);
|
|
}
|
|
|
|
if (not callEnded and not conversation.confId.isEmpty()) {
|
|
Q_EMIT pimpl_->behaviorController.showCallView(owner.id, conversation.uid);
|
|
} else if (callEnded) {
|
|
Q_EMIT pimpl_->behaviorController.showChatView(owner.id, conversation.uid);
|
|
} else {
|
|
try {
|
|
auto call = owner.callModel->getCall(conversation.callId);
|
|
switch (call.status) {
|
|
case call::Status::INCOMING_RINGING:
|
|
case call::Status::OUTGOING_RINGING:
|
|
case call::Status::CONNECTING:
|
|
case call::Status::SEARCHING:
|
|
// We are currently in a call
|
|
Q_EMIT pimpl_->behaviorController.showIncomingCallView(owner.id,
|
|
conversation.uid);
|
|
break;
|
|
case call::Status::PAUSED:
|
|
case call::Status::CONNECTED:
|
|
case call::Status::IN_PROGRESS:
|
|
// We are currently receiving a call
|
|
Q_EMIT pimpl_->behaviorController.showCallView(owner.id, conversation.uid);
|
|
break;
|
|
case call::Status::PEER_BUSY:
|
|
Q_EMIT pimpl_->behaviorController.showLeaveMessageView(owner.id,
|
|
conversation.uid);
|
|
break;
|
|
case call::Status::TIMEOUT:
|
|
case call::Status::TERMINATING:
|
|
case call::Status::INVALID:
|
|
case call::Status::INACTIVE:
|
|
// call just ended
|
|
Q_EMIT pimpl_->behaviorController.showChatView(owner.id, conversation.uid);
|
|
break;
|
|
case call::Status::ENDED:
|
|
default: // ENDED
|
|
// nothing to do
|
|
break;
|
|
}
|
|
} catch (const std::out_of_range&) {
|
|
// Should not happen
|
|
Q_EMIT pimpl_->behaviorController.showChatView(owner.id, conversation.uid);
|
|
}
|
|
}
|
|
} catch (const std::out_of_range& e) {
|
|
qDebug() << "select conversation failed. conversation not exists";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModel::removeConversation(const QString& uid, bool banned)
|
|
{
|
|
// Get conversation
|
|
auto conversationIdx = pimpl_->indexOf(uid);
|
|
if (conversationIdx == -1)
|
|
return;
|
|
|
|
auto& conversation = pimpl_->conversations.at(conversationIdx);
|
|
// Remove contact from daemon
|
|
// NOTE: this will also remove the conversation into the database for non-swarm and remove
|
|
// conversation repository for one-to-one.
|
|
auto& peers = pimpl_->peersForConversation(conversation);
|
|
if (peers.empty()) {
|
|
// Should not
|
|
qDebug() << "ConversationModel::removeConversation can't remove a conversation without "
|
|
"participant";
|
|
return;
|
|
}
|
|
if (conversation.isSwarm() && !banned && !conversation.isCoreDialog()) {
|
|
if (conversation.isRequest) {
|
|
ConfigurationManager::instance().declineConversationRequest(owner.id, uid);
|
|
} else {
|
|
ConfigurationManager::instance().removeConversation(owner.id, uid);
|
|
}
|
|
} else {
|
|
try {
|
|
auto& contact = owner.contactModel->getContact(peers.front());
|
|
owner.contactModel->removeContact(peers.front(), banned);
|
|
} catch (const std::out_of_range&) {
|
|
qWarning() << "Contact not found: " << peers.front();
|
|
ConfigurationManager::instance().removeConversation(owner.id, uid);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModel::deleteObsoleteHistory(int days)
|
|
{
|
|
if (days < 1)
|
|
return; // unlimited history
|
|
|
|
auto currentTime = static_cast<long int>(std::time(nullptr)); // since epoch, in seconds...
|
|
auto date = currentTime - (days * 86400);
|
|
|
|
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();
|
|
selectConversation(uid);
|
|
Q_EMIT conversationUpdated(uid);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly)
|
|
{
|
|
try {
|
|
auto& conversation = getConversationForUid(uid, true).get();
|
|
if (conversation.participants.empty()) {
|
|
// Should not
|
|
qDebug()
|
|
<< "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();
|
|
linked.selectConversation(conversation.uid);
|
|
Q_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) {
|
|
return;
|
|
}
|
|
// Disallow multiple call
|
|
if (!conversation.callId.isEmpty()) {
|
|
try {
|
|
auto call = linked.owner.callModel->getCall(conversation.callId);
|
|
switch (call.status) {
|
|
case call::Status::INCOMING_RINGING:
|
|
case call::Status::OUTGOING_RINGING:
|
|
case call::Status::CONNECTING:
|
|
case call::Status::SEARCHING:
|
|
case call::Status::PAUSED:
|
|
case call::Status::IN_PROGRESS:
|
|
case call::Status::CONNECTED:
|
|
return;
|
|
case call::Status::INVALID:
|
|
case call::Status::INACTIVE:
|
|
case call::Status::ENDED:
|
|
case call::Status::PEER_BUSY:
|
|
case call::Status::TIMEOUT:
|
|
case call::Status::TERMINATING:
|
|
default:
|
|
break;
|
|
}
|
|
} catch (const std::out_of_range&) {
|
|
}
|
|
}
|
|
|
|
auto convId = uid;
|
|
|
|
auto participant = peers.front();
|
|
bool isTemporary = convId == participant || convId == "SEARCHSIP";
|
|
auto contactInfo = linked.owner.contactModel->getContact(participant);
|
|
auto uri = contactInfo.profileInfo.uri;
|
|
|
|
if (uri.isEmpty())
|
|
return; // Incorrect item
|
|
|
|
// Don't call banned contact
|
|
if (contactInfo.isBanned) {
|
|
qDebug() << "ContactModel::placeCall: denied, contact is banned";
|
|
return;
|
|
}
|
|
|
|
if (linked.owner.profileInfo.type != profile::Type::SIP) {
|
|
uri = "ring:" + uri; // Add the ring: before or it will fail.
|
|
}
|
|
|
|
auto cb = ([this, isTemporary, uri, isAudioOnly, &conversation](QString conversationId) {
|
|
if (indexOf(conversationId) < 0) {
|
|
qDebug() << "Can't place call: conversation not exists";
|
|
return;
|
|
}
|
|
|
|
auto& newConv = isTemporary ? getConversationForUid(conversationId).get()
|
|
: conversation;
|
|
|
|
newConv.callId = linked.owner.callModel->createCall(uri, isAudioOnly);
|
|
if (newConv.callId.isEmpty()) {
|
|
qDebug() << "Can't place call (daemon side failure ?)";
|
|
return;
|
|
}
|
|
|
|
invalidateModel();
|
|
|
|
Q_EMIT behaviorController.showIncomingCallView(linked.owner.id, newConv.uid);
|
|
});
|
|
|
|
if (isTemporary) {
|
|
QMetaObject::Connection* const connection = new QMetaObject::Connection;
|
|
*connection = connect(&this->linked,
|
|
&ConversationModel::conversationReady,
|
|
[cb, connection, convId](QString conversationId,
|
|
QString participantId) {
|
|
if (participantId != convId && convId != "SEARCHSIP") {
|
|
return;
|
|
}
|
|
cb(conversationId);
|
|
QObject::disconnect(*connection);
|
|
if (connection) {
|
|
delete connection;
|
|
}
|
|
});
|
|
}
|
|
|
|
sendContactRequest(participant);
|
|
|
|
if (!isTemporary) {
|
|
cb(convId);
|
|
}
|
|
} catch (const std::out_of_range& e) {
|
|
qDebug() << "could not place call to not existing conversation";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModel::placeAudioOnlyCall(const QString& uid)
|
|
{
|
|
pimpl_->placeCall(uid, true);
|
|
}
|
|
|
|
void
|
|
ConversationModel::placeCall(const QString& uid)
|
|
{
|
|
pimpl_->placeCall(uid);
|
|
}
|
|
|
|
MapStringString
|
|
ConversationModel::getConversationInfos(const QString& conversationId)
|
|
{
|
|
MapStringString ret = ConfigurationManager::instance().conversationInfos(owner.id,
|
|
conversationId);
|
|
return ret;
|
|
}
|
|
|
|
MapStringString
|
|
ConversationModel::getConversationPreferences(const QString& conversationId)
|
|
{
|
|
MapStringString ret = ConfigurationManager::instance()
|
|
.getConversationPreferences(owner.id, conversationId);
|
|
return ret;
|
|
}
|
|
|
|
QString
|
|
ConversationModel::createConversation(const VectorString& participants, const MapStringString& infos)
|
|
{
|
|
auto convUid = ConfigurationManager::instance().startConversation(owner.id);
|
|
pimpl_->addSwarmConversation(convUid);
|
|
if (!infos.isEmpty())
|
|
updateConversationInfos(convUid, infos);
|
|
for (const auto& participant : participants)
|
|
ConfigurationManager::instance().addConversationMember(owner.id, convUid, participant);
|
|
Q_EMIT newConversation(convUid);
|
|
pimpl_->invalidateModel();
|
|
Q_EMIT modelChanged();
|
|
return convUid;
|
|
}
|
|
|
|
void
|
|
ConversationModel::updateConversationInfos(const QString& conversationId,
|
|
const MapStringString infos)
|
|
{
|
|
auto conversationOpt = getConversationForUid(conversationId);
|
|
if (!conversationOpt.has_value())
|
|
return;
|
|
auto& conversation = conversationOpt->get();
|
|
if (conversation.isCoreDialog()) {
|
|
// If 1:1, we override a profile (as the peer will send their new profiles)
|
|
auto peer = pimpl_->peersForConversation(conversation);
|
|
if (!peer.isEmpty())
|
|
owner.contactModel->updateContact(peer.at(0), infos);
|
|
return;
|
|
}
|
|
MapStringString newInfos = infos;
|
|
// Compress avatar as it will be sent in the conversation's request over the DHT
|
|
if (infos.contains("avatar"))
|
|
newInfos["avatar"] = storage::vcard::compressedAvatar(infos["avatar"]);
|
|
ConfigurationManager::instance().updateConversationInfos(owner.id, conversationId, newInfos);
|
|
}
|
|
|
|
void
|
|
ConversationModel::popFrontError(const QString& conversationId)
|
|
{
|
|
auto conversationOpt = getConversationForUid(conversationId);
|
|
if (!conversationOpt.has_value())
|
|
return;
|
|
|
|
auto& conversation = conversationOpt->get();
|
|
conversation.errors.pop_front();
|
|
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)
|
|
{
|
|
ConfigurationManager::instance().setConversationPreferences(owner.id, conversationId, prefs);
|
|
}
|
|
|
|
bool
|
|
ConversationModel::hasPendingRequests() const
|
|
{
|
|
return pendingRequestCount() > 0;
|
|
}
|
|
|
|
int
|
|
ConversationModel::pendingRequestCount() const
|
|
{
|
|
int pendingRequestCount = 0;
|
|
std::for_each(pimpl_->conversations.begin(),
|
|
pimpl_->conversations.end(),
|
|
[&pendingRequestCount](const auto& c) { pendingRequestCount += c.isRequest; });
|
|
return pendingRequestCount;
|
|
}
|
|
|
|
int
|
|
ConversationModel::notificationsCount() const
|
|
{
|
|
int notificationsCount = 0;
|
|
std::for_each(pimpl_->conversations.begin(),
|
|
pimpl_->conversations.end(),
|
|
[¬ificationsCount](const auto& c) {
|
|
if (c.preferences["ignoreNotifications"] == "true") {
|
|
return;
|
|
}
|
|
if (c.isRequest)
|
|
notificationsCount += 1;
|
|
else {
|
|
notificationsCount += c.unreadMessages;
|
|
}
|
|
});
|
|
return notificationsCount;
|
|
}
|
|
|
|
void
|
|
ConversationModel::reloadHistory() const
|
|
{
|
|
std::for_each(pimpl_->conversations.begin(),
|
|
pimpl_->conversations.end(),
|
|
[&](const conversation::Info& c) {
|
|
c.interactions->reloadHistory();
|
|
Q_EMIT conversationUpdated(c.uid);
|
|
Q_EMIT dataChanged(pimpl_->indexOf(c.uid));
|
|
});
|
|
}
|
|
|
|
QString
|
|
ConversationModel::title(const QString& conversationId) const
|
|
{
|
|
auto conversationOpt = getConversationForUid(conversationId);
|
|
if (!conversationOpt.has_value()) {
|
|
return {};
|
|
}
|
|
auto& conversation = conversationOpt->get();
|
|
if (conversation.isCoreDialog()) {
|
|
auto peer = pimpl_->peersForConversation(conversation);
|
|
if (peer.isEmpty())
|
|
return {};
|
|
// In this case, we can just display contact name
|
|
if (peer.at(0) == owner.profileInfo.uri)
|
|
return QObject::tr("%1 (you)").arg(owner.accountModel->bestNameForAccount(owner.id));
|
|
return owner.contactModel->bestNameForContact(peer.at(0));
|
|
}
|
|
if (conversation.infos["title"] != "") {
|
|
return conversation.infos["title"];
|
|
}
|
|
// NOTE: Do not call any daemon method there as title() is called a lot for drawing
|
|
QString title;
|
|
auto idx = 0u;
|
|
auto others = 0;
|
|
for (const auto& member : conversation.participants) {
|
|
QString name;
|
|
if (member.uri == owner.profileInfo.uri) {
|
|
name = QObject::tr("%1 (you)").arg(owner.accountModel->bestNameForAccount(owner.id));
|
|
} else {
|
|
name = owner.contactModel->bestNameForContact(member.uri);
|
|
}
|
|
if (title.length() + name.length() > 32) {
|
|
// Avoid too long titles
|
|
others += 1;
|
|
continue;
|
|
}
|
|
title += name;
|
|
idx += 1;
|
|
if (idx != conversation.participants.size() || others != 0) {
|
|
title += ", ";
|
|
}
|
|
}
|
|
if (others != 0) {
|
|
title += QString("+ %1").arg(others);
|
|
}
|
|
return title;
|
|
}
|
|
|
|
member::Role
|
|
ConversationModel::memberRole(const QString& conversationId, const QString& memberUri) const
|
|
{
|
|
auto conversationOpt = getConversationForUid(conversationId);
|
|
if (!conversationOpt.has_value())
|
|
throw std::out_of_range("Member out of range");
|
|
auto& conversation = conversationOpt->get();
|
|
for (const auto& p : conversation.participants) {
|
|
if (p.uri == memberUri)
|
|
return p.role;
|
|
}
|
|
throw std::out_of_range("Member out of range");
|
|
}
|
|
|
|
QString
|
|
ConversationModel::description(const QString& conversationId) const
|
|
{
|
|
auto conversationOpt = getConversationForUid(conversationId);
|
|
if (!conversationOpt.has_value())
|
|
return {};
|
|
auto& conversation = conversationOpt->get();
|
|
if (conversation.isCoreDialog()) {
|
|
auto peer = pimpl_->peersForConversation(conversation);
|
|
if (peer.isEmpty())
|
|
return {};
|
|
return owner.contactModel->bestIdForContact(peer.front());
|
|
}
|
|
return conversation.infos["description"];
|
|
}
|
|
|
|
QString
|
|
ConversationModel::avatar(const QString& conversationId) const
|
|
{
|
|
auto conversationOpt = getConversationForUid(conversationId);
|
|
if (!conversationOpt.has_value()) {
|
|
return {};
|
|
}
|
|
auto& conversation = conversationOpt->get();
|
|
if (conversation.isCoreDialog()) {
|
|
auto peer = pimpl_->peersForConversation(conversation);
|
|
if (peer.isEmpty())
|
|
return {};
|
|
// In this case, we can just display contact name
|
|
return owner.contactModel->avatar(peer.at(0));
|
|
}
|
|
// We need to strip the whitespace characters for the avatar
|
|
// when it comes from the conversation info.
|
|
return conversation.infos["avatar"].simplified();
|
|
}
|
|
|
|
void
|
|
ConversationModel::sendMessage(const QString& uid, const QString& body, const QString& parentId)
|
|
{
|
|
try {
|
|
auto& conversation = pimpl_->getConversationForUid(uid, true).get();
|
|
if (!conversation.isLegacy()) {
|
|
ConfigurationManager::instance().sendMessage(owner.id,
|
|
uid,
|
|
body,
|
|
parentId,
|
|
static_cast<int>(MessageFlag::Text));
|
|
return;
|
|
}
|
|
|
|
auto& peers = pimpl_->peersForConversation(conversation);
|
|
if (peers.isEmpty()) {
|
|
// Should not
|
|
qDebug() << "ConversationModel::sendMessage can't send a interaction to a conversation "
|
|
"with no participant";
|
|
return;
|
|
}
|
|
auto convId = uid;
|
|
auto& peerId = peers.front();
|
|
bool isTemporary = peerId == convId || convId == "SEARCHSIP";
|
|
|
|
auto cb = ([this, isTemporary, body, &conversation, parentId, convId](
|
|
QString conversationId) {
|
|
if (pimpl_->indexOf(conversationId) < 0) {
|
|
return;
|
|
}
|
|
auto& newConv = isTemporary ? pimpl_->getConversationForUid(conversationId).get()
|
|
: conversation;
|
|
|
|
if (newConv.isSwarm()) {
|
|
ConfigurationManager::instance().sendMessage(owner.id,
|
|
conversationId,
|
|
body,
|
|
parentId,
|
|
static_cast<int>(MessageFlag::Text));
|
|
return;
|
|
}
|
|
auto& peers = pimpl_->peersForConversation(newConv);
|
|
if (peers.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
uint64_t daemonMsgId = 0;
|
|
auto status = interaction::Status::SENDING;
|
|
auto convId = newConv.uid;
|
|
|
|
QStringList callLists = CallManager::instance().getCallList(""); // no auto
|
|
// workaround: sometimes, it may happen that the daemon delete a call, but lrc
|
|
// don't. We check if the call is
|
|
// still valid every time the user want to send a message.
|
|
if (not newConv.callId.isEmpty() and not callLists.contains(newConv.callId))
|
|
newConv.callId.clear();
|
|
|
|
if (not newConv.callId.isEmpty()
|
|
and call::canSendSIPMessage(owner.callModel->getCall(newConv.callId))) {
|
|
status = interaction::Status::UNKNOWN;
|
|
owner.callModel->sendSipMessage(newConv.callId, body);
|
|
|
|
} else {
|
|
daemonMsgId = owner.contactModel->sendDhtMessage(peers.front(), body);
|
|
}
|
|
|
|
// Add interaction to database
|
|
interaction::Info msg {owner.profileInfo.uri,
|
|
body,
|
|
std::time(nullptr),
|
|
0,
|
|
interaction::Type::TEXT,
|
|
status,
|
|
true};
|
|
auto msgId = storage::addMessageToConversation(pimpl_->db, convId, msg);
|
|
|
|
// Update conversation
|
|
if (status == interaction::Status::SENDING) {
|
|
// Because the daemon already give an id for the message, we need to store it.
|
|
storage::addDaemonMsgId(pimpl_->db, msgId, toQString(daemonMsgId));
|
|
}
|
|
|
|
if (!newConv.interactions->append(msgId, msg)) {
|
|
qWarning() << Q_FUNC_INFO << "Append failed: duplicate ID";
|
|
return;
|
|
}
|
|
|
|
newConv.lastSelfMessageId = msgId;
|
|
// Emit this signal for chatview in the client
|
|
Q_EMIT newInteraction(convId, msgId, msg);
|
|
// This conversation is now at the top of the list
|
|
// The order has changed, informs the client to redraw the list
|
|
pimpl_->invalidateModel();
|
|
Q_EMIT modelChanged();
|
|
Q_EMIT dataChanged(pimpl_->indexOf(convId));
|
|
});
|
|
|
|
if (isTemporary) {
|
|
QMetaObject::Connection* const connection = new QMetaObject::Connection;
|
|
*connection = connect(this,
|
|
&ConversationModel::conversationReady,
|
|
[cb, connection, convId](QString conversationId,
|
|
QString participantId) {
|
|
if (participantId != convId && convId != "SEARCHSIP") {
|
|
return;
|
|
}
|
|
cb(conversationId);
|
|
QObject::disconnect(*connection);
|
|
if (connection) {
|
|
delete connection;
|
|
}
|
|
});
|
|
}
|
|
pimpl_->sendContactRequest(peerId);
|
|
if (!isTemporary) {
|
|
cb(convId);
|
|
}
|
|
} catch (const std::out_of_range& e) {
|
|
qDebug() << "could not send message to not existing conversation";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModel::editMessage(const QString& convId,
|
|
const QString& newBody,
|
|
const QString& messageId)
|
|
{
|
|
auto conversationOpt = getConversationForUid(convId);
|
|
if (!conversationOpt.has_value()) {
|
|
return;
|
|
}
|
|
ConfigurationManager::instance().sendMessage(owner.id,
|
|
convId,
|
|
newBody,
|
|
messageId,
|
|
static_cast<int>(MessageFlag::Reply));
|
|
}
|
|
|
|
void
|
|
ConversationModel::reactMessage(const QString& convId,
|
|
const QString& emoji,
|
|
const QString& messageId)
|
|
{
|
|
auto conversationOpt = getConversationForUid(convId);
|
|
if (!conversationOpt.has_value()) {
|
|
return;
|
|
}
|
|
ConfigurationManager::instance().sendMessage(owner.id,
|
|
convId,
|
|
emoji,
|
|
messageId,
|
|
static_cast<int>(MessageFlag::Reaction));
|
|
}
|
|
|
|
void
|
|
ConversationModel::refreshFilter()
|
|
{
|
|
pimpl_->invalidateModel();
|
|
Q_EMIT filterChanged();
|
|
}
|
|
|
|
void
|
|
ConversationModel::updateSearchStatus(const QString& status) const
|
|
{
|
|
Q_EMIT searchStatusChanged(status);
|
|
}
|
|
|
|
void
|
|
ConversationModel::setFilter(const QString& filter)
|
|
{
|
|
pimpl_->currentFilter = filter;
|
|
pimpl_->invalidateModel();
|
|
pimpl_->searchResults.clear();
|
|
Q_EMIT searchResultUpdated();
|
|
owner.contactModel->searchContact(filter);
|
|
Q_EMIT filterChanged();
|
|
}
|
|
|
|
void
|
|
ConversationModel::setFilter(const FilterType& filter)
|
|
{
|
|
// Switch between PENDING, RING and SIP contacts.
|
|
pimpl_->typeFilter = filter;
|
|
pimpl_->invalidateModel();
|
|
Q_EMIT filterChanged();
|
|
}
|
|
|
|
void
|
|
ConversationModel::joinConversations(const QString& uidA, const QString& uidB)
|
|
{
|
|
auto conversationAIdx = pimpl_->indexOf(uidA);
|
|
auto conversationBIdx = pimpl_->indexOf(uidB);
|
|
if (conversationAIdx == -1 || conversationBIdx == -1 || !owner.enabled)
|
|
return;
|
|
auto& conversationA = pimpl_->conversations[conversationAIdx];
|
|
auto& conversationB = pimpl_->conversations[conversationBIdx];
|
|
|
|
if (conversationA.callId.isEmpty() || conversationB.callId.isEmpty())
|
|
return;
|
|
|
|
if (conversationA.confId.isEmpty()) {
|
|
if (conversationB.confId.isEmpty()) {
|
|
owner.callModel->joinCalls(conversationA.callId, conversationB.callId);
|
|
} else {
|
|
owner.callModel->joinCalls(conversationA.callId, conversationB.confId);
|
|
conversationA.confId = conversationB.confId;
|
|
}
|
|
} else {
|
|
if (conversationB.confId.isEmpty()) {
|
|
owner.callModel->joinCalls(conversationA.confId, conversationB.callId);
|
|
conversationB.confId = conversationA.confId;
|
|
} else {
|
|
owner.callModel->joinCalls(conversationA.confId, conversationB.confId);
|
|
conversationB.confId = conversationA.confId;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModel::clearHistory(const QString& uid)
|
|
{
|
|
auto conversationIdx = pimpl_->indexOf(uid);
|
|
if (conversationIdx == -1)
|
|
return;
|
|
|
|
auto& conversation = pimpl_->conversations.at(conversationIdx);
|
|
// Remove all TEXT interactions from database
|
|
storage::clearHistory(pimpl_->db, uid);
|
|
// Update conversation
|
|
conversation.interactions->clear();
|
|
storage::getHistory(pimpl_->db,
|
|
conversation,
|
|
pimpl_->linked.owner.profileInfo.uri); // will contain "Conversation started"
|
|
|
|
Q_EMIT modelChanged();
|
|
Q_EMIT conversationCleared(uid);
|
|
Q_EMIT dataChanged(conversationIdx);
|
|
}
|
|
|
|
bool
|
|
ConversationModel::isLastDisplayed(const QString& convId,
|
|
const QString& interactionId,
|
|
const QString participant)
|
|
{
|
|
auto conversationIdx = pimpl_->indexOf(convId);
|
|
try {
|
|
auto& conversation = pimpl_->conversations.at(conversationIdx);
|
|
return conversation.interactions->getRead(participant) == interactionId;
|
|
} catch (const std::out_of_range& e) {
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void
|
|
ConversationModel::clearAllHistory()
|
|
{
|
|
storage::clearAllHistory(pimpl_->db);
|
|
|
|
for (auto& conversation : pimpl_->conversations) {
|
|
{
|
|
if (conversation.isSwarm()) {
|
|
// WARNING: clear all history is not implemented for swarm
|
|
continue;
|
|
}
|
|
conversation.interactions->clear();
|
|
}
|
|
storage::getHistory(pimpl_->db, conversation, pimpl_->linked.owner.profileInfo.uri);
|
|
Q_EMIT dataChanged(pimpl_->indexOf(conversation.uid));
|
|
}
|
|
Q_EMIT modelChanged();
|
|
}
|
|
|
|
void
|
|
ConversationModel::clearUnreadInteractions(const QString& convId)
|
|
{
|
|
auto conversationOpt = getConversationForUid(convId);
|
|
if (!conversationOpt.has_value()) {
|
|
return;
|
|
}
|
|
auto& conversation = conversationOpt->get();
|
|
bool updated = false;
|
|
QString lastDisplayedId;
|
|
if (conversation.isSwarm()) {
|
|
updated = true;
|
|
conversation.interactions->withLast(
|
|
[&](const QString& id, interaction::Info&) { lastDisplayedId = id; });
|
|
} else {
|
|
conversation.interactions->forEach([&](const QString& id, interaction::Info& interaction) {
|
|
if (interaction.isRead)
|
|
return;
|
|
updated = true;
|
|
interaction.isRead = true;
|
|
if (owner.profileInfo.type != profile::Type::SIP)
|
|
lastDisplayedId = storage::getDaemonIdByInteractionId(pimpl_->db, id);
|
|
storage::setInteractionRead(pimpl_->db, id);
|
|
});
|
|
}
|
|
if (!lastDisplayedId.isEmpty()) {
|
|
auto to = conversation.isSwarm()
|
|
? "swarm:" + convId
|
|
: "jami:" + pimpl_->peersForConversation(conversation).front();
|
|
ConfigurationManager::instance().setMessageDisplayed(owner.id, to, lastDisplayedId, 3);
|
|
}
|
|
if (updated) {
|
|
conversation.unreadMessages = 0;
|
|
pimpl_->invalidateModel();
|
|
Q_EMIT conversationUpdated(convId);
|
|
Q_EMIT dataChanged(pimpl_->indexOf(convId));
|
|
}
|
|
}
|
|
|
|
int
|
|
ConversationModel::loadConversationMessages(const QString& conversationId, const int size)
|
|
{
|
|
auto conversationOpt = getConversationForUid(conversationId);
|
|
if (!conversationOpt.has_value()) {
|
|
return -1;
|
|
}
|
|
auto& conversation = conversationOpt->get();
|
|
if (conversation.allMessagesLoaded) {
|
|
return -1;
|
|
}
|
|
QString lastMsgId;
|
|
conversation.interactions->withLast(
|
|
[&lastMsgId](const QString& id, interaction::Info&) { lastMsgId = id; });
|
|
return ConfigurationManager::instance().loadConversation(owner.id,
|
|
conversationId,
|
|
lastMsgId,
|
|
size);
|
|
}
|
|
|
|
void
|
|
ConversationModel::acceptConversationRequest(const QString& conversationId)
|
|
{
|
|
auto conversationOpt = getConversationForUid(conversationId);
|
|
if (!conversationOpt.has_value())
|
|
return;
|
|
auto& conversation = conversationOpt->get();
|
|
auto& peers = pimpl_->peersForConversation(conversation);
|
|
if (peers.isEmpty())
|
|
return;
|
|
|
|
if (conversation.isSwarm()) {
|
|
conversation.needsSyncing = true;
|
|
Q_EMIT conversationUpdated(conversation.uid);
|
|
pimpl_->invalidateModel();
|
|
Q_EMIT modelChanged();
|
|
ConfigurationManager::instance().acceptConversationRequest(owner.id, conversationId);
|
|
} else {
|
|
pimpl_->sendContactRequest(peers.front());
|
|
try {
|
|
auto contact = owner.contactModel->getContact(peers.front());
|
|
auto notAdded = contact.profileInfo.type == profile::Type::TEMPORARY
|
|
|| contact.profileInfo.type == profile::Type::PENDING;
|
|
if (notAdded) {
|
|
owner.contactModel->addContact(contact);
|
|
return;
|
|
}
|
|
} catch (std::out_of_range& e) {
|
|
qWarning() << e.what();
|
|
}
|
|
}
|
|
}
|
|
|
|
const VectorString
|
|
ConversationModel::peersForConversation(const QString& conversationId)
|
|
{
|
|
const auto conversationOpt = getConversationForUid(conversationId);
|
|
if (!conversationOpt.has_value()) {
|
|
return {};
|
|
}
|
|
const auto& conversation = conversationOpt->get();
|
|
return pimpl_->peersForConversation(conversation);
|
|
}
|
|
|
|
void
|
|
ConversationModel::addConversationMember(const QString& conversationId, const QString& memberId)
|
|
{
|
|
ConfigurationManager::instance().addConversationMember(owner.id, conversationId, memberId);
|
|
}
|
|
|
|
void
|
|
ConversationModel::removeConversationMember(const QString& conversationId, const QString& memberId)
|
|
{
|
|
ConfigurationManager::instance().removeConversationMember(owner.id, conversationId, memberId);
|
|
}
|
|
|
|
ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
|
|
Lrc& lrc,
|
|
Database& db,
|
|
const CallbacksHandler& callbacksHandler,
|
|
const BehaviorController& behaviorController)
|
|
: linked(linked)
|
|
, lrc {lrc}
|
|
, db(db)
|
|
, callbacksHandler(callbacksHandler)
|
|
, typeFilter(FilterType::INVALID)
|
|
, customTypeFilter(FilterType::INVALID)
|
|
, behaviorController(behaviorController)
|
|
{
|
|
filteredConversations.bindSortCallback(this, &ConversationModelPimpl::sort);
|
|
filteredConversations.bindFilterCallback(this, &ConversationModelPimpl::filter);
|
|
|
|
initConversations();
|
|
|
|
// Contact related
|
|
connect(&*linked.owner.contactModel,
|
|
&ContactModel::modelUpdated,
|
|
this,
|
|
&ConversationModelPimpl::slotContactModelUpdated);
|
|
connect(&*linked.owner.contactModel,
|
|
&ContactModel::contactAdded,
|
|
this,
|
|
&ConversationModelPimpl::slotContactAdded);
|
|
connect(&*linked.owner.contactModel,
|
|
&ContactModel::pendingContactAccepted,
|
|
this,
|
|
&ConversationModelPimpl::slotPendingContactAccepted);
|
|
connect(&*linked.owner.contactModel,
|
|
&ContactModel::contactRemoved,
|
|
this,
|
|
&ConversationModelPimpl::slotContactRemoved);
|
|
|
|
// Messages related
|
|
connect(&*linked.owner.contactModel,
|
|
&lrc::ContactModel::newAccountMessage,
|
|
this,
|
|
&ConversationModelPimpl::slotNewAccountMessage);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::incomingCallMessage,
|
|
this,
|
|
&ConversationModelPimpl::slotIncomingCallMessage);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::accountMessageStatusChanged,
|
|
this,
|
|
&ConversationModelPimpl::updateInteractionStatus);
|
|
|
|
// Call related
|
|
connect(&*linked.owner.contactModel,
|
|
&ContactModel::newCall,
|
|
this,
|
|
&ConversationModelPimpl::slotNewCall);
|
|
connect(&*linked.owner.callModel,
|
|
&lrc::api::CallModel::callStatusChanged,
|
|
this,
|
|
&ConversationModelPimpl::slotCallStatusChanged);
|
|
connect(&*linked.owner.callModel,
|
|
&lrc::api::CallModel::callStarted,
|
|
this,
|
|
&ConversationModelPimpl::slotCallStarted);
|
|
connect(&*linked.owner.callModel,
|
|
&lrc::api::CallModel::callEnded,
|
|
this,
|
|
&ConversationModelPimpl::slotCallEnded);
|
|
connect(&*linked.owner.callModel,
|
|
&lrc::api::CallModel::callAddedToConference,
|
|
this,
|
|
&ConversationModelPimpl::slotCallAddedToConference);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::conferenceRemoved,
|
|
this,
|
|
&ConversationModelPimpl::slotConferenceRemoved);
|
|
connect(&ConfigurationManager::instance(),
|
|
&ConfigurationManagerInterface::composingStatusChanged,
|
|
this,
|
|
&ConversationModelPimpl::slotComposingStatusChanged);
|
|
connect(&callbacksHandler, &CallbacksHandler::needsHost, this, [&](auto, auto convId) {
|
|
emit linked.needsHost(convId);
|
|
});
|
|
|
|
// data transfer
|
|
connect(&*linked.owner.contactModel,
|
|
&ContactModel::newAccountTransfer,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusCreated);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusCanceled,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusCanceled);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusAwaitingPeer,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusAwaitingPeer);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusAwaitingHost,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusAwaitingHost);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusOngoing,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusOngoing);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusFinished,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusFinished);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusError,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusError);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusTimeoutExpired,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusTimeoutExpired);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusUnjoinable,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusUnjoinable);
|
|
// swarm conversations
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::swarmLoaded,
|
|
this,
|
|
&ConversationModelPimpl::slotSwarmLoaded);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::messagesFound,
|
|
this,
|
|
&ConversationModelPimpl::slotMessagesFound);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::messageReceived,
|
|
this,
|
|
&ConversationModelPimpl::slotMessageReceived);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::messageUpdated,
|
|
this,
|
|
&ConversationModelPimpl::slotMessageUpdated);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::reactionAdded,
|
|
this,
|
|
&ConversationModelPimpl::slotReactionAdded);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::reactionRemoved,
|
|
this,
|
|
&ConversationModelPimpl::slotReactionRemoved);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::conversationProfileUpdated,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationProfileUpdated);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::conversationRequestReceived,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationRequestReceived);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::conversationRequestDeclined,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationRemoved);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::conversationReady,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationReady);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::conversationRemoved,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationRemoved);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::conversationMemberEvent,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationMemberEvent);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::conversationError,
|
|
this,
|
|
&ConversationModelPimpl::slotOnConversationError);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::conversationPreferencesUpdated,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationPreferencesUpdated);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::activeCallsChanged,
|
|
this,
|
|
&ConversationModelPimpl::slotActiveCallsChanged);
|
|
}
|
|
|
|
ConversationModelPimpl::~ConversationModelPimpl()
|
|
{
|
|
// Contact related
|
|
disconnect(&*linked.owner.contactModel,
|
|
&ContactModel::modelUpdated,
|
|
this,
|
|
&ConversationModelPimpl::slotContactModelUpdated);
|
|
disconnect(&*linked.owner.contactModel,
|
|
&ContactModel::contactAdded,
|
|
this,
|
|
&ConversationModelPimpl::slotContactAdded);
|
|
disconnect(&*linked.owner.contactModel,
|
|
&ContactModel::pendingContactAccepted,
|
|
this,
|
|
&ConversationModelPimpl::slotPendingContactAccepted);
|
|
disconnect(&*linked.owner.contactModel,
|
|
&ContactModel::contactRemoved,
|
|
this,
|
|
&ConversationModelPimpl::slotContactRemoved);
|
|
|
|
// Messages related
|
|
disconnect(&*linked.owner.contactModel,
|
|
&lrc::ContactModel::newAccountMessage,
|
|
this,
|
|
&ConversationModelPimpl::slotNewAccountMessage);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::incomingCallMessage,
|
|
this,
|
|
&ConversationModelPimpl::slotIncomingCallMessage);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::accountMessageStatusChanged,
|
|
this,
|
|
&ConversationModelPimpl::updateInteractionStatus);
|
|
|
|
// Call related
|
|
disconnect(&*linked.owner.contactModel,
|
|
&ContactModel::newCall,
|
|
this,
|
|
&ConversationModelPimpl::slotNewCall);
|
|
disconnect(&*linked.owner.callModel,
|
|
&lrc::api::CallModel::callStatusChanged,
|
|
this,
|
|
&ConversationModelPimpl::slotCallStatusChanged);
|
|
disconnect(&*linked.owner.callModel,
|
|
&lrc::api::CallModel::callStarted,
|
|
this,
|
|
&ConversationModelPimpl::slotCallStarted);
|
|
disconnect(&*linked.owner.callModel,
|
|
&lrc::api::CallModel::callEnded,
|
|
this,
|
|
&ConversationModelPimpl::slotCallEnded);
|
|
disconnect(&*linked.owner.callModel,
|
|
&lrc::api::CallModel::callAddedToConference,
|
|
this,
|
|
&ConversationModelPimpl::slotCallAddedToConference);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::conferenceRemoved,
|
|
this,
|
|
&ConversationModelPimpl::slotConferenceRemoved);
|
|
disconnect(&ConfigurationManager::instance(),
|
|
&ConfigurationManagerInterface::composingStatusChanged,
|
|
this,
|
|
&ConversationModelPimpl::slotComposingStatusChanged);
|
|
|
|
// data transfer
|
|
disconnect(&*linked.owner.contactModel,
|
|
&ContactModel::newAccountTransfer,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusCreated);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusCanceled,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusCanceled);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusAwaitingPeer,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusAwaitingPeer);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusAwaitingHost,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusAwaitingHost);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusOngoing,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusOngoing);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusFinished,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusFinished);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusError,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusError);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusTimeoutExpired,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusTimeoutExpired);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusUnjoinable,
|
|
this,
|
|
&ConversationModelPimpl::slotTransferStatusUnjoinable);
|
|
// swarm conversations
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::swarmLoaded,
|
|
this,
|
|
&ConversationModelPimpl::slotSwarmLoaded);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::messagesFound,
|
|
this,
|
|
&ConversationModelPimpl::slotMessagesFound);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::messageReceived,
|
|
this,
|
|
&ConversationModelPimpl::slotMessageReceived);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::messageUpdated,
|
|
this,
|
|
&ConversationModelPimpl::slotMessageUpdated);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::reactionAdded,
|
|
this,
|
|
&ConversationModelPimpl::slotReactionAdded);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::reactionRemoved,
|
|
this,
|
|
&ConversationModelPimpl::slotReactionRemoved);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::conversationProfileUpdated,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationProfileUpdated);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::conversationRequestReceived,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationRequestReceived);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::conversationRequestDeclined,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationRemoved);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::conversationReady,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationReady);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::conversationRemoved,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationRemoved);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::conversationMemberEvent,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationMemberEvent);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::conversationError,
|
|
this,
|
|
&ConversationModelPimpl::slotOnConversationError);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::activeCallsChanged,
|
|
this,
|
|
&ConversationModelPimpl::slotActiveCallsChanged);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::conversationPreferencesUpdated,
|
|
this,
|
|
&ConversationModelPimpl::slotConversationPreferencesUpdated);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::initConversations()
|
|
{
|
|
const MapStringString accountDetails = ConfigurationManager::instance().getAccountDetails(
|
|
linked.owner.id);
|
|
if (accountDetails.empty())
|
|
return;
|
|
|
|
// Fill swarm conversations
|
|
QStringList swarms = ConfigurationManager::instance().getConversations(linked.owner.id);
|
|
for (auto& swarmConv : swarms) {
|
|
addSwarmConversation(swarmConv);
|
|
}
|
|
|
|
VectorMapStringString conversationsRequests = ConfigurationManager::instance()
|
|
.getConversationRequests(linked.owner.id);
|
|
for (auto& request : conversationsRequests) {
|
|
addConversationRequest(request);
|
|
}
|
|
|
|
// Fill conversations
|
|
for (auto const& c : linked.owner.contactModel->getAllContacts().toStdMap()) {
|
|
auto conv = storage::getConversationsWithPeer(db, c.second.profileInfo.uri);
|
|
if (hasOneOneSwarmWith(c.second.profileInfo.uri))
|
|
continue;
|
|
bool isRequest = c.second.profileInfo.type == profile::Type::PENDING;
|
|
if (conv.empty()) {
|
|
// Can't find a conversation with this contact
|
|
// add pending not swarm conversation
|
|
if (isRequest) {
|
|
addContactRequest(c.second.profileInfo.uri);
|
|
continue;
|
|
}
|
|
conv.push_back(storage::beginConversationWithPeer(db,
|
|
c.second.profileInfo.uri,
|
|
true,
|
|
linked.owner.contactModel->getAddedTs(
|
|
c.second.profileInfo.uri)));
|
|
}
|
|
addConversationWith(conv[0], c.first, isRequest);
|
|
|
|
auto convIdx = indexOf(conv[0]);
|
|
|
|
// Resolve any file transfer interactions were left in an incorrect state
|
|
auto& interactions = conversations[convIdx].interactions;
|
|
interactions->forEach([&](const QString& id, interaction::Info& interaction) {
|
|
if (interaction.status == interaction::Status::TRANSFER_CREATED
|
|
|| interaction.status == interaction::Status::TRANSFER_AWAITING_HOST
|
|
|| interaction.status == interaction::Status::TRANSFER_AWAITING_PEER
|
|
|| interaction.status == interaction::Status::TRANSFER_ONGOING
|
|
|| interaction.status == interaction::Status::TRANSFER_ACCEPTED) {
|
|
// If a datatransfer was left in a non-terminal status in DB, we switch this status
|
|
// to ERROR
|
|
// TODO : Improve for DBus clients as daemon and transfer may still be ongoing
|
|
storage::updateInteractionStatus(db, id, interaction::Status::TRANSFER_ERROR);
|
|
|
|
interaction.status = interaction::Status::TRANSFER_ERROR;
|
|
}
|
|
});
|
|
}
|
|
invalidateModel();
|
|
|
|
filteredConversations.reset(conversations).sort();
|
|
|
|
// Load all non treated messages for this account
|
|
QVector<Message> messages = ConfigurationManager::instance()
|
|
.getLastMessages(linked.owner.id, storage::getLastTimestamp(db));
|
|
for (const auto& message : messages) {
|
|
uint64_t timestamp = 0;
|
|
try {
|
|
timestamp = static_cast<uint64_t>(message.received);
|
|
} catch (...) {
|
|
}
|
|
addIncomingMessage(message.from, message.payloads[TEXT_PLAIN], timestamp);
|
|
}
|
|
}
|
|
|
|
const VectorString
|
|
ConversationModelPimpl::peersForConversation(const conversation::Info& conversation) const
|
|
{
|
|
VectorString result {};
|
|
switch (conversation.mode) {
|
|
case conversation::Mode::NON_SWARM:
|
|
return {conversation.participants[0].uri};
|
|
default:
|
|
break;
|
|
}
|
|
// Note: for one to one, we must return self
|
|
if (conversation.participants.size() == 1)
|
|
return {conversation.participants[0].uri};
|
|
for (const auto& participant : conversation.participants) {
|
|
if (participant.uri.isNull())
|
|
continue;
|
|
if (participant.uri != linked.owner.profileInfo.uri)
|
|
result.push_back(participant.uri);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool
|
|
ConversationModelPimpl::filter(const conversation::Info& entry)
|
|
{
|
|
try {
|
|
// TODO: filter for group?
|
|
// for now group conversation filtered by first peer
|
|
auto& peers = peersForConversation(entry);
|
|
if (peers.size() < 1) {
|
|
return false;
|
|
}
|
|
auto uriPeer = peers.front();
|
|
contact::Info contactInfo;
|
|
try {
|
|
contactInfo = linked.owner.contactModel->getContact(uriPeer);
|
|
} catch (...) {
|
|
// Note: as we search for contacts, when importing a new account,
|
|
// the conversation's request can be there without contact, causing
|
|
// the function to fail.
|
|
contactInfo.profileInfo.uri = uriPeer;
|
|
}
|
|
|
|
auto uri = URI(currentFilter);
|
|
bool stripScheme = (uri.schemeType() < URI::SchemeType::COUNT__);
|
|
FlagPack<URI::Section> flags = URI::Section::USER_INFO | URI::Section::HOSTNAME
|
|
| URI::Section::PORT;
|
|
if (!stripScheme) {
|
|
flags |= URI::Section::SCHEME;
|
|
}
|
|
|
|
currentFilter = uri.format(flags);
|
|
|
|
// Check contact
|
|
// If contact is banned, only match if filter is a perfect match
|
|
// do not check banned contact for conversation with multiple participants
|
|
if (contactInfo.isBanned && peers.size() == 1) {
|
|
if (currentFilter == "")
|
|
return false;
|
|
return contactInfo.profileInfo.uri == currentFilter
|
|
|| contactInfo.profileInfo.alias == currentFilter
|
|
|| contactInfo.registeredName == currentFilter;
|
|
}
|
|
|
|
std::regex regexFilter;
|
|
auto isValidReFilter = true;
|
|
try {
|
|
regexFilter = std::regex(currentFilter.toStdString(), std::regex_constants::icase);
|
|
} catch (std::regex_error&) {
|
|
isValidReFilter = false;
|
|
}
|
|
|
|
auto filterUriAndReg = [regexFilter, isValidReFilter](auto contact, auto filter) {
|
|
auto result = contact.profileInfo.uri.contains(filter)
|
|
|| contact.registeredName.contains(filter);
|
|
if (!result) {
|
|
auto regexFound = isValidReFilter
|
|
? (!contact.profileInfo.uri.isEmpty()
|
|
&& std::regex_search(contact.profileInfo.uri.toStdString(),
|
|
regexFilter))
|
|
|| std::regex_search(contact.registeredName.toStdString(),
|
|
regexFilter)
|
|
: false;
|
|
result |= regexFound;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
// Check type
|
|
switch (typeFilter) {
|
|
case FilterType::JAMI:
|
|
case FilterType::SIP:
|
|
if (entry.isRequest)
|
|
return false;
|
|
if (contactInfo.profileInfo.type == profile::Type::TEMPORARY)
|
|
return filterUriAndReg(contactInfo, currentFilter);
|
|
break;
|
|
case FilterType::REQUEST:
|
|
if (!entry.isRequest)
|
|
return false;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Otherwise perform usual regex search
|
|
bool result = contactInfo.profileInfo.alias.contains(currentFilter);
|
|
if (!result && isValidReFilter)
|
|
result |= std::regex_search(contactInfo.profileInfo.alias.toStdString(), regexFilter);
|
|
if (!result)
|
|
result |= filterUriAndReg(contactInfo, currentFilter);
|
|
return result;
|
|
} catch (std::out_of_range&) {
|
|
// getContact() failed
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool
|
|
ConversationModelPimpl::sort(const conversation::Info& convA, const conversation::Info& convB)
|
|
{
|
|
// A or B is a temporary contact
|
|
if (convA.participants.isEmpty())
|
|
return true;
|
|
if (convB.participants.isEmpty())
|
|
return false;
|
|
|
|
if (convA.uid == convB.uid)
|
|
return false;
|
|
|
|
auto& historyA = convA.interactions;
|
|
auto& historyB = convB.interactions;
|
|
|
|
std::lock(historyA->getMutex(), historyB->getMutex());
|
|
std::lock_guard<std::recursive_mutex> lockConvA(historyA->getMutex(), std::adopt_lock);
|
|
std::lock_guard<std::recursive_mutex> lockConvB(historyB->getMutex(), std::adopt_lock);
|
|
|
|
// A or B is a new conversation (without CONTACT interaction)
|
|
if (convA.uid.isEmpty() || convB.uid.isEmpty())
|
|
return convA.uid.isEmpty();
|
|
|
|
if (historyA->empty() && historyB->empty()) {
|
|
// If no information to compare, sort by Ring ID. For group conversation sort by first peer
|
|
auto& peersForA = peersForConversation(convA);
|
|
auto& peersForB = peersForConversation(convB);
|
|
if (peersForA.isEmpty()) {
|
|
return false;
|
|
}
|
|
if (peersForB.isEmpty()) {
|
|
return true;
|
|
}
|
|
return peersForA.front() > peersForB.front();
|
|
}
|
|
if (historyA->empty())
|
|
return false;
|
|
if (historyB->empty())
|
|
return true;
|
|
// Sort by last Interaction
|
|
time_t timestampA, timestampB;
|
|
historyA->withLast([&](const QString&, const interaction::Info& interaction) {
|
|
timestampA = interaction.timestamp;
|
|
});
|
|
historyB->withLast([&](const QString&, const interaction::Info& interaction) {
|
|
timestampB = interaction.timestamp;
|
|
});
|
|
return timestampA > timestampB;
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::sendContactRequest(const QString& contactUri)
|
|
{
|
|
try {
|
|
auto contact = linked.owner.contactModel->getContact(contactUri);
|
|
auto isNotUsed = contact.profileInfo.type == profile::Type::TEMPORARY
|
|
|| contact.profileInfo.type == profile::Type::PENDING;
|
|
if (isNotUsed)
|
|
linked.owner.contactModel->addContact(contact);
|
|
} catch (std::out_of_range& e) {
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotSwarmLoaded(uint32_t requestId,
|
|
const QString& accountId,
|
|
const QString& conversationId,
|
|
const VectorSwarmMessage& messages)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
auto allLoaded = false;
|
|
try {
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
for (const auto& message : messages) {
|
|
QString msgId = message.id;
|
|
auto msg = interaction::Info(message, linked.owner.profileInfo.uri);
|
|
auto downloadFile = false;
|
|
if (msg.type == interaction::Type::INITIAL) {
|
|
allLoaded = true;
|
|
} else if (msg.type == interaction::Type::DATA_TRANSFER) {
|
|
QString fileId = message.body.value("fileId");
|
|
QString path;
|
|
qlonglong bytesProgress, totalSize;
|
|
linked.owner.dataTransferModel->fileTransferInfo(accountId,
|
|
conversationId,
|
|
fileId,
|
|
path,
|
|
totalSize,
|
|
bytesProgress);
|
|
QFileInfo fi(path);
|
|
if (fi.isSymLink()) {
|
|
msg.body = fi.symLinkTarget();
|
|
} else {
|
|
msg.body = path;
|
|
}
|
|
msg.status = bytesProgress == 0 ? interaction::Status::TRANSFER_AWAITING_HOST
|
|
: bytesProgress == totalSize ? interaction::Status::TRANSFER_FINISHED
|
|
: interaction::Status::TRANSFER_ONGOING;
|
|
linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
|
|
downloadFile = (bytesProgress == 0);
|
|
}
|
|
|
|
// If message is loaded, insert message at beginning
|
|
if (!conversation.interactions->insert(msgId, msg, 0)) {
|
|
qDebug() << Q_FUNC_INFO << "Insert failed: duplicate ID";
|
|
continue;
|
|
}
|
|
|
|
if (downloadFile) {
|
|
handleIncomingFile(conversationId,
|
|
msgId,
|
|
QString(message.body.value("totalSize")).toInt());
|
|
}
|
|
}
|
|
|
|
conversation.lastSelfMessageId = conversation.interactions->lastSelfMessageId(
|
|
linked.owner.profileInfo.uri);
|
|
invalidateModel();
|
|
Q_EMIT linked.modelChanged();
|
|
Q_EMIT linked.newMessagesAvailable(linked.owner.id, conversationId);
|
|
auto conversationIdx = indexOf(conversationId);
|
|
Q_EMIT linked.dataChanged(conversationIdx);
|
|
Q_EMIT linked.conversationMessagesLoaded(requestId, conversationId);
|
|
if (allLoaded) {
|
|
conversation.allMessagesLoaded = true;
|
|
Q_EMIT linked.conversationUpdated(conversationId);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
qWarning() << e.what();
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotMessagesFound(uint32_t requestId,
|
|
const QString& accountId,
|
|
const QString& conversationId,
|
|
const VectorMapStringString& messageIds)
|
|
{
|
|
QMap<QString, interaction::Info> messageDetailedInformation;
|
|
if (requestId == mediaResearchRequestId) {
|
|
Q_FOREACH (const MapStringString& msg, messageIds) {
|
|
auto intInfo = interaction::Info(msg, "");
|
|
if (intInfo.type == interaction::Type::DATA_TRANSFER) {
|
|
auto fileId = msg["fileId"];
|
|
|
|
QString path;
|
|
qlonglong bytesProgress, totalSize;
|
|
linked.owner.dataTransferModel->fileTransferInfo(accountId,
|
|
conversationId,
|
|
fileId,
|
|
path,
|
|
totalSize,
|
|
bytesProgress);
|
|
intInfo.body = path;
|
|
}
|
|
messageDetailedInformation[msg["id"]] = std::move(intInfo);
|
|
}
|
|
} else if (requestId == msgResearchRequestId) {
|
|
Q_FOREACH (const MapStringString& msg, messageIds) {
|
|
auto intInfo = interaction::Info(msg, "");
|
|
if (intInfo.type == interaction::Type::TEXT) {
|
|
messageDetailedInformation[msg["id"]] = std::move(intInfo);
|
|
}
|
|
}
|
|
}
|
|
Q_EMIT linked.messagesFoundProcessed(accountId, messageDetailedInformation);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotMessageReceived(const QString& accountId,
|
|
const QString& conversationId,
|
|
const SwarmMessage& message)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
try {
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
if (message.type == "initial") {
|
|
conversation.allMessagesLoaded = true;
|
|
Q_EMIT linked.conversationUpdated(conversationId);
|
|
if (message.body.find("invited") == message.body.end()) {
|
|
return;
|
|
}
|
|
}
|
|
QString msgId = message.id;
|
|
auto msg = interaction::Info(message, linked.owner.profileInfo.uri);
|
|
|
|
if (msg.type == interaction::Type::CALL) {
|
|
msg.body = interaction::getCallInteractionString(msg.authorUri
|
|
== linked.owner.profileInfo.uri,
|
|
msg);
|
|
} else 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
|
|
QString fileId = message.body.value("fileId");
|
|
QString path;
|
|
qlonglong bytesProgress, totalSize;
|
|
linked.owner.dataTransferModel->fileTransferInfo(accountId,
|
|
conversationId,
|
|
fileId,
|
|
path,
|
|
totalSize,
|
|
bytesProgress);
|
|
QFileInfo fi(path);
|
|
if (fi.isSymLink()) {
|
|
msg.body = fi.symLinkTarget();
|
|
} else {
|
|
msg.body = path;
|
|
}
|
|
msg.status = bytesProgress == 0 ? interaction::Status::TRANSFER_AWAITING_HOST
|
|
: bytesProgress == totalSize ? interaction::Status::TRANSFER_FINISHED
|
|
: interaction::Status::TRANSFER_ONGOING;
|
|
linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
|
|
}
|
|
|
|
if (!conversation.interactions->append(msgId, msg)) {
|
|
qDebug() << Q_FUNC_INFO << "Append failed: duplicate ID" << msgId;
|
|
return;
|
|
}
|
|
|
|
auto updateUnread = msg.authorUri != linked.owner.profileInfo.uri;
|
|
if (updateUnread)
|
|
conversation.unreadMessages++;
|
|
conversation.lastSelfMessageId = conversation.interactions->lastSelfMessageId(
|
|
linked.owner.profileInfo.uri);
|
|
invalidateModel();
|
|
if (!interaction::isOutgoing(msg) && updateUnread) {
|
|
Q_EMIT behaviorController.newUnreadInteraction(linked.owner.id,
|
|
conversationId,
|
|
msgId,
|
|
msg);
|
|
}
|
|
Q_EMIT linked.newInteraction(conversationId, msgId, msg);
|
|
Q_EMIT linked.modelChanged();
|
|
if (msg.status == interaction::Status::TRANSFER_AWAITING_HOST && updateUnread) {
|
|
handleIncomingFile(conversationId,
|
|
msgId,
|
|
QString(message.body.value("totalSize")).toInt());
|
|
}
|
|
Q_EMIT linked.dataChanged(indexOf(conversationId));
|
|
} catch (const std::exception& e) {
|
|
qDebug() << "messages received for not existing conversation";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotMessageUpdated(const QString& accountId,
|
|
const QString& conversationId,
|
|
const SwarmMessage& message)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
try {
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
QString msgId = message.id;
|
|
auto msg = interaction::Info(message, linked.owner.profileInfo.uri);
|
|
|
|
if (!conversation.interactions->update(msgId, msg)) {
|
|
qDebug() << "message not found or could not be reparented";
|
|
return;
|
|
}
|
|
// The conversation is updated, so we need to notify the view.
|
|
invalidateModel();
|
|
Q_EMIT linked.modelChanged();
|
|
Q_EMIT linked.dataChanged(indexOf(conversationId));
|
|
} catch (const std::exception& e) {
|
|
qDebug() << "messages received for not existing conversation";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotReactionAdded(const QString& accountId,
|
|
const QString& conversationId,
|
|
const QString& messageId,
|
|
const MapStringString& reaction)
|
|
{
|
|
if (accountId != linked.owner.id) {
|
|
return;
|
|
}
|
|
try {
|
|
// qInfo() << "Add Reaction to " << messageId << " in " << conversationId;
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
conversation.interactions->addReaction(messageId, reaction);
|
|
} catch (const std::exception& e) {
|
|
qWarning() << e.what();
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotReactionRemoved(const QString& accountId,
|
|
const QString& conversationId,
|
|
const QString& messageId,
|
|
const QString& reactionId)
|
|
{
|
|
if (accountId != linked.owner.id) {
|
|
return;
|
|
}
|
|
try {
|
|
// qInfo() << "Remove Reaction from " << messageId << " in " << conversationId;
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
conversation.interactions->rmReaction(messageId, reactionId);
|
|
} catch (const std::exception& e) {
|
|
qWarning() << e.what();
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotConversationProfileUpdated(const QString& accountId,
|
|
const QString& conversationId,
|
|
const MapStringString& profile)
|
|
{
|
|
if (accountId != linked.owner.id) {
|
|
return;
|
|
}
|
|
try {
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
conversation.infos = profile;
|
|
Q_EMIT linked.profileUpdated(conversationId);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotConversationRequestReceived(const QString& accountId,
|
|
const QString&,
|
|
const MapStringString& metadatas)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
addConversationRequest(metadatas, true);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotConversationReady(const QString& accountId,
|
|
const QString& conversationId)
|
|
{
|
|
// we receive this signal after we accept or after we send a conversation request
|
|
if (accountId != linked.owner.id) {
|
|
return;
|
|
}
|
|
// remove non swarm conversation that was added from slotContactAdded
|
|
const VectorMapStringString& members = ConfigurationManager::instance()
|
|
.getConversationMembers(accountId, conversationId);
|
|
QVector<member::Member> participants;
|
|
// it means conversation with one participant. In this case we could have non swarm conversation
|
|
bool shouldRemoveNonSwarmConversation = members.size() == 2;
|
|
for (const auto& member : members) {
|
|
participants.append({member["uri"], api::member::to_role(member["role"])});
|
|
if (shouldRemoveNonSwarmConversation) {
|
|
try {
|
|
auto& conversation = getConversationForPeerUri(member["uri"]).get();
|
|
// remove non swarm conversation
|
|
if (conversation.isLegacy()) {
|
|
eraseConversation(conversation.uid);
|
|
storage::removeContactConversations(db, member["uri"]);
|
|
invalidateModel();
|
|
Q_EMIT linked.conversationRemoved(conversation.uid);
|
|
Q_EMIT linked.modelChanged();
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
}
|
|
|
|
int conversationIdx = indexOf(conversationId);
|
|
bool conversationExists = conversationIdx >= 0;
|
|
|
|
if (!conversationExists)
|
|
addSwarmConversation(conversationId);
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
if (conversationExists) {
|
|
// if swarm request already exists, update participnts
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
conversation.participants = participants;
|
|
const MapStringString& details = ConfigurationManager::instance()
|
|
.conversationInfos(accountId, conversationId);
|
|
conversation.infos = details;
|
|
const MapStringString& preferences
|
|
= ConfigurationManager::instance().getConversationPreferences(accountId, conversationId);
|
|
conversation.preferences = preferences;
|
|
conversation.mode = conversation::to_mode(details["mode"].toInt());
|
|
conversation.isRequest = false;
|
|
conversation.needsSyncing = false;
|
|
Q_EMIT linked.conversationUpdated(conversationId);
|
|
Q_EMIT linked.dataChanged(conversationIdx);
|
|
ConfigurationManager::instance().loadConversation(linked.owner.id, conversationId, "", 0);
|
|
auto& peers = peersForConversation(conversation);
|
|
if (peers.size() == 1)
|
|
Q_EMIT linked.conversationReady(conversationId, peers.front());
|
|
return;
|
|
}
|
|
invalidateModel();
|
|
// we use conversationReady callback only for conversation with one participant. We could use
|
|
// participants.front()
|
|
auto& peers = peersForConversation(conversation);
|
|
if (peers.size() == 1)
|
|
Q_EMIT linked.conversationReady(conversationId, peers.front());
|
|
Q_EMIT linked.newConversation(conversationId);
|
|
Q_EMIT linked.modelChanged();
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotConversationRemoved(const QString& accountId,
|
|
const QString& conversationId)
|
|
{
|
|
auto conversationIndex = indexOf(conversationId);
|
|
if (accountId != linked.owner.id || conversationIndex < 0)
|
|
return;
|
|
try {
|
|
auto removeConversation = [&]() {
|
|
// remove swarm conversation
|
|
eraseConversation(conversationIndex);
|
|
invalidateModel();
|
|
Q_EMIT linked.conversationRemoved(conversationId);
|
|
};
|
|
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
auto& peers = peersForConversation(conversation);
|
|
if (peers.isEmpty()) {
|
|
removeConversation();
|
|
return;
|
|
}
|
|
auto contactUri = peers.first();
|
|
auto mode = conversation.mode;
|
|
contact::Info contact;
|
|
try {
|
|
contact = linked.owner.contactModel->getContact(contactUri);
|
|
} catch (...) {
|
|
}
|
|
|
|
removeConversation();
|
|
|
|
if (mode == conversation::Mode::ONE_TO_ONE) {
|
|
// If it's a 1:1 conversation and we don't have any more conversation
|
|
// we can remove the contact
|
|
auto contactRemoved = true;
|
|
try {
|
|
auto& conv = getConversationForPeerUri(contactUri).get();
|
|
contactRemoved = !conv.isSwarm();
|
|
} catch (...) {
|
|
}
|
|
|
|
if (contact.isBanned && contactRemoved) {
|
|
// Add 1:1 conv for banned
|
|
auto c = storage::beginConversationWithPeer(db, contactUri);
|
|
addConversationWith(c, contactUri, false);
|
|
Q_EMIT linked.conversationReady(c, contactUri);
|
|
Q_EMIT linked.newConversation(c);
|
|
}
|
|
}
|
|
|
|
} catch (const std::exception& e) {
|
|
qWarning() << e.what();
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotConversationMemberEvent(const QString& accountId,
|
|
const QString& conversationId,
|
|
const QString& memberUri,
|
|
int event)
|
|
{
|
|
if (accountId != linked.owner.id || indexOf(conversationId) < 0) {
|
|
return;
|
|
}
|
|
if (event == 0 /* add */) {
|
|
// clear search result
|
|
for (unsigned int i = 0; i < searchResults.size(); ++i) {
|
|
if (searchResults.at(i).uid == memberUri)
|
|
searchResults.erase(searchResults.begin() + i);
|
|
}
|
|
}
|
|
// update participants
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
const VectorMapStringString& members
|
|
= ConfigurationManager::instance().getConversationMembers(linked.owner.id, conversationId);
|
|
QVector<member::Member> participants;
|
|
VectorString membersRemaining;
|
|
for (auto& member : members) {
|
|
participants.append(member::Member {member["uri"], member::to_role(member["role"])});
|
|
if (member["role"] != "left")
|
|
membersRemaining.append(member["uri"]);
|
|
}
|
|
conversation.participants = participants;
|
|
invalidateModel();
|
|
Q_EMIT linked.modelChanged();
|
|
Q_EMIT linked.conversationUpdated(conversationId);
|
|
Q_EMIT linked.dataChanged(indexOf(conversationId));
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotOnConversationError(const QString& accountId,
|
|
const QString& conversationId,
|
|
int code,
|
|
const QString& what)
|
|
{
|
|
if (accountId != linked.owner.id || indexOf(conversationId) < 0) {
|
|
return;
|
|
}
|
|
try {
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
conversation.errors.push_back({code, what});
|
|
Q_EMIT linked.onConversationErrorsUpdated(conversationId);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
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::slotContactAdded(const QString& contactUri)
|
|
{
|
|
QString convId;
|
|
try {
|
|
convId = linked.owner.contactModel->getContact(contactUri).conversationId;
|
|
} catch (std::out_of_range& e) {
|
|
return;
|
|
}
|
|
|
|
auto isSwarm = !convId.isEmpty();
|
|
auto conv = !isSwarm ? storage::getConversationsWithPeer(db, contactUri)
|
|
: VectorString {convId};
|
|
if (conv.isEmpty()) {
|
|
if (linked.owner.profileInfo.type == profile::Type::SIP) {
|
|
auto convId = storage::beginConversationWithPeer(db,
|
|
contactUri,
|
|
true,
|
|
linked.owner.contactModel->getAddedTs(
|
|
contactUri));
|
|
addConversationWith(convId, contactUri, false);
|
|
Q_EMIT linked.conversationReady(convId, contactUri);
|
|
Q_EMIT linked.newConversation(convId);
|
|
}
|
|
return;
|
|
}
|
|
convId = conv[0];
|
|
try {
|
|
auto& conversation = getConversationForUid(convId).get();
|
|
MapStringString details = ConfigurationManager::instance()
|
|
.conversationInfos(linked.owner.id, conversation.uid);
|
|
bool needsSyncing = details["syncing"] == "true";
|
|
if (conversation.needsSyncing != needsSyncing) {
|
|
conversation.isRequest = false;
|
|
conversation.needsSyncing = needsSyncing;
|
|
Q_EMIT linked.dataChanged(indexOf(conversation.uid));
|
|
Q_EMIT linked.conversationUpdated(conversation.uid);
|
|
invalidateModel();
|
|
Q_EMIT linked.modelChanged();
|
|
}
|
|
} catch (...) {
|
|
if (isSwarm) {
|
|
addSwarmConversation(convId);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::addContactRequest(const QString& contactUri)
|
|
{
|
|
try {
|
|
getConversationForPeerUri(contactUri).get();
|
|
// request from contact already exists, return
|
|
return;
|
|
} catch (std::out_of_range&) {
|
|
// no conversation exists. Add contact request
|
|
conversation::Info conversation(contactUri, &linked.owner);
|
|
conversation.participants = {{contactUri, member::Role::INVITED}};
|
|
conversation.mode = conversation::Mode::NON_SWARM;
|
|
conversation.isRequest = true;
|
|
emplaceBackConversation(std::move(conversation));
|
|
invalidateModel();
|
|
Q_EMIT linked.newConversation(contactUri);
|
|
Q_EMIT linked.modelChanged();
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::addConversationRequest(const MapStringString& convRequest, bool emitToClient)
|
|
{
|
|
auto convId = convRequest["id"];
|
|
auto convIdx = indexOf(convId);
|
|
if (convIdx != -1)
|
|
return;
|
|
|
|
auto peerUri = convRequest["from"];
|
|
auto mode = conversation::to_mode(convRequest["mode"].toInt());
|
|
QString callId, confId;
|
|
const MapStringString& details = ConfigurationManager::instance()
|
|
.conversationInfos(linked.owner.id, convId);
|
|
conversation::Info conversation(convId, &linked.owner);
|
|
conversation.infos = details;
|
|
conversation.callId = callId;
|
|
conversation.confId = confId;
|
|
conversation.participants = {{linked.owner.profileInfo.uri, member::Role::INVITED},
|
|
{peerUri, member::Role::MEMBER}};
|
|
conversation.mode = mode;
|
|
conversation.isRequest = true;
|
|
|
|
MapStringString messageMap = {
|
|
{"type", "initial"},
|
|
{"author", peerUri},
|
|
{"timestamp", convRequest["received"]},
|
|
{"linearizedParent", ""},
|
|
};
|
|
auto msg = interaction::Info(messageMap, linked.owner.profileInfo.uri);
|
|
conversation.interactions->insert(convId, msg);
|
|
|
|
// add the author to the contact model's contact list as a PENDING
|
|
// if they aren't already a contact
|
|
auto isSelf = linked.owner.profileInfo.uri == peerUri;
|
|
if (isSelf)
|
|
return;
|
|
|
|
if (mode == conversation::Mode::ONE_TO_ONE) {
|
|
try {
|
|
profile::Info profileInfo;
|
|
profileInfo.uri = peerUri;
|
|
profileInfo.type = profile::Type::JAMI;
|
|
profileInfo.alias = details["title"];
|
|
profileInfo.avatar = details["avatar"];
|
|
contact::Info contactInfo;
|
|
contactInfo.profileInfo = profileInfo;
|
|
linked.owner.contactModel->addContact(contactInfo);
|
|
} catch (std::out_of_range&) {
|
|
qWarning() << "Couldn't find contact request conversation for" << peerUri;
|
|
}
|
|
}
|
|
|
|
emplaceBackConversation(std::move(conversation));
|
|
invalidateModel();
|
|
Q_EMIT linked.newConversation(convId);
|
|
Q_EMIT linked.modelChanged();
|
|
if (!callId.isEmpty()) {
|
|
// If we replace a non swarm request by a swarm request while having a call.
|
|
linked.selectConversation(convId);
|
|
}
|
|
if (emitToClient)
|
|
Q_EMIT behaviorController.newTrustRequest(linked.owner.id, convId, peerUri);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotPendingContactAccepted(const QString& uri)
|
|
{
|
|
profile::Type type {};
|
|
try {
|
|
type = linked.owner.contactModel->getContact(uri).profileInfo.type;
|
|
} catch (std::out_of_range& e) {
|
|
}
|
|
profile::Info profileInfo {uri, {}, {}, type};
|
|
storage::createOrUpdateProfile(linked.owner.id, profileInfo, true);
|
|
auto convs = storage::getConversationsWithPeer(db, uri);
|
|
if (!convs.empty()) {
|
|
try {
|
|
auto contact = linked.owner.contactModel->getContact(uri);
|
|
auto interaction = interaction::Info {uri,
|
|
{},
|
|
std::time(nullptr),
|
|
0,
|
|
interaction::Type::CONTACT,
|
|
interaction::Status::SUCCESS,
|
|
true};
|
|
auto msgId = storage::addMessageToConversation(db, convs[0], interaction);
|
|
interaction.body = storage::getContactInteractionString(uri,
|
|
interaction::Status::SUCCESS);
|
|
auto convIdx = indexOf(convs[0]);
|
|
if (convIdx >= 0) {
|
|
conversations[convIdx].interactions->append(msgId, interaction);
|
|
}
|
|
filteredConversations.invalidate();
|
|
Q_EMIT linked.newInteraction(convs[0], msgId, interaction);
|
|
Q_EMIT linked.dataChanged(convIdx);
|
|
} catch (std::out_of_range& e) {
|
|
qDebug() << "ConversationModelPimpl::slotContactAdded can't find contact";
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotContactRemoved(const QString& uri)
|
|
{
|
|
std::vector<QString> convIdsToRemove;
|
|
|
|
// save the ids to remove from the list
|
|
for (auto i : getIndicesForContact(uri)) {
|
|
convIdsToRemove.emplace_back(conversations[i].uid);
|
|
}
|
|
|
|
// actually remove them from the list
|
|
for (const auto& id : convIdsToRemove) {
|
|
eraseConversation(id);
|
|
Q_EMIT linked.conversationRemoved(id);
|
|
}
|
|
|
|
invalidateModel();
|
|
Q_EMIT linked.modelChanged();
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotContactModelUpdated(const QString& uri)
|
|
{
|
|
// Update presence for all conversations with this peer
|
|
for (auto& conversation : conversations) {
|
|
auto members = peersForConversation(conversation);
|
|
if (members.indexOf(uri) != -1) {
|
|
invalidateModel();
|
|
Q_EMIT linked.conversationUpdated(conversation.uid);
|
|
Q_EMIT linked.dataChanged(indexOf(conversation.uid));
|
|
}
|
|
}
|
|
|
|
if (currentFilter.isEmpty()) {
|
|
if (searchResults.empty())
|
|
return;
|
|
searchResults.clear();
|
|
Q_EMIT linked.searchResultUpdated();
|
|
return;
|
|
}
|
|
searchResults.clear();
|
|
auto users = linked.owner.contactModel->getSearchResults();
|
|
for (auto& user : users) {
|
|
auto uid = linked.owner.profileInfo.type == profile::Type::SIP ? "SEARCHSIP"
|
|
: user.profileInfo.uri;
|
|
conversation::Info conversationInfo(uid, &linked.owner);
|
|
// For SIP, we always got one search result, so "" is ok as there is no empty uri
|
|
// For Jami accounts, the nameserver can return several results, so we use the uniqueness of
|
|
// the id as id for a temporary conversation.
|
|
conversationInfo.participants.append(
|
|
member::Member {user.profileInfo.uri, member::Role::MEMBER});
|
|
searchResults.emplace_front(std::move(conversationInfo));
|
|
}
|
|
Q_EMIT linked.searchResultUpdated();
|
|
Q_EMIT linked.searchResultEnded();
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::addSwarmConversation(const QString& convId)
|
|
{
|
|
if (Lrc::dbusIsValid()) {
|
|
// Because the daemon may have already loaded interactions
|
|
// we clear them to receive all signals
|
|
ConfigurationManager::instance().clearCache(linked.owner.id, convId);
|
|
}
|
|
QVector<member::Member> participants;
|
|
const VectorMapStringString& members = ConfigurationManager::instance()
|
|
.getConversationMembers(linked.owner.id, convId);
|
|
auto accountURI = linked.owner.profileInfo.uri;
|
|
QString otherMember;
|
|
const MapStringString& details = ConfigurationManager::instance()
|
|
.conversationInfos(linked.owner.id, convId);
|
|
auto mode = conversation::to_mode(details["mode"].toInt());
|
|
conversation::Info conversation(convId, &linked.owner);
|
|
conversation.infos = details;
|
|
VectorMapStringString activeCalls = ConfigurationManager::instance()
|
|
.getActiveCalls(linked.owner.id, convId);
|
|
conversation.activeCalls = activeCalls;
|
|
QString lastRead;
|
|
VectorString membersLeft;
|
|
for (auto& member : members) {
|
|
// this check should be removed once all usage of participants replaced by
|
|
// peersForConversation. We should have ourself in participants list
|
|
// Note: if members.size() == 1, it's a conv with self so we're also the peer
|
|
participants.append(member::Member {member["uri"], member::to_role(member["role"])});
|
|
if (mode == conversation::Mode::ONE_TO_ONE && member["uri"] != accountURI) {
|
|
otherMember = member["uri"];
|
|
} else if (member["uri"] == accountURI) {
|
|
lastRead = member["lastDisplayed"];
|
|
} else if (mode != conversation::Mode::ONE_TO_ONE) {
|
|
// Note: a conversation may be with non contacts.
|
|
// So, refresh bestName to be sure to get quick updates in UI
|
|
linked.owner.contactModel->bestNameForContact(member["uri"]);
|
|
}
|
|
if (member["uri"] != accountURI)
|
|
conversation.interactions->setRead(member["uri"], member["lastDisplayed"]);
|
|
if (member["role"] == "left")
|
|
membersLeft.append(member["uri"]);
|
|
}
|
|
conversation.participants = participants;
|
|
conversation.mode = mode;
|
|
const MapStringString& preferences = ConfigurationManager::instance()
|
|
.getConversationPreferences(linked.owner.id, convId);
|
|
conversation.preferences = preferences;
|
|
conversation.unreadMessages = ConfigurationManager::instance().countInteractions(linked.owner.id,
|
|
convId,
|
|
lastRead,
|
|
"",
|
|
accountURI);
|
|
if (mode == conversation::Mode::ONE_TO_ONE && !otherMember.isEmpty()) {
|
|
try {
|
|
conversation.confId = linked.owner.callModel->getConferenceFromURI(otherMember).id;
|
|
} catch (...) {
|
|
conversation.confId = "";
|
|
}
|
|
try {
|
|
conversation.callId = linked.owner.callModel->getCallFromURI(otherMember).id;
|
|
} catch (...) {
|
|
conversation.callId = "";
|
|
}
|
|
}
|
|
// If conversation has only one peer it is possible that non swarm conversation was created.
|
|
// remove non swarm conversation
|
|
auto& peers = peersForConversation(conversation);
|
|
if (peers.size() == 1) {
|
|
try {
|
|
auto& participantId = peers.front();
|
|
auto& conv = getConversationForPeerUri(participantId).get();
|
|
if (conv.mode == conversation::Mode::NON_SWARM) {
|
|
eraseConversation(conv.uid);
|
|
invalidateModel();
|
|
Q_EMIT linked.conversationRemoved(conv.uid);
|
|
storage::removeContactConversations(db, participantId);
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
if (details["syncing"] == "true") {
|
|
MapStringString messageMap = {
|
|
{"type", "initial"},
|
|
{"author", otherMember},
|
|
{"timestamp", details["created"]},
|
|
{"linearizedParent", ""},
|
|
};
|
|
auto msg = interaction::Info(messageMap, linked.owner.profileInfo.uri);
|
|
conversation.interactions->append(convId, msg);
|
|
conversation.needsSyncing = true;
|
|
Q_EMIT linked.conversationUpdated(conversation.uid);
|
|
Q_EMIT linked.dataChanged(indexOf(conversation.uid));
|
|
}
|
|
emplaceBackConversation(std::move(conversation));
|
|
ConfigurationManager::instance().loadConversation(linked.owner.id, convId, "", 1);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::addConversationWith(const QString& convId,
|
|
const QString& contactUri,
|
|
bool isRequest)
|
|
{
|
|
conversation::Info conversation(convId, &linked.owner);
|
|
conversation.participants = {{contactUri, member::Role::MEMBER}};
|
|
conversation.mode = conversation::Mode::NON_SWARM;
|
|
conversation.needsSyncing = false;
|
|
conversation.isRequest = isRequest;
|
|
|
|
try {
|
|
conversation.confId = linked.owner.callModel->getConferenceFromURI(contactUri).id;
|
|
} catch (...) {
|
|
conversation.confId = "";
|
|
}
|
|
try {
|
|
conversation.callId = linked.owner.callModel->getCallFromURI(contactUri).id;
|
|
} catch (...) {
|
|
conversation.callId = "";
|
|
}
|
|
storage::getHistory(db, conversation, linked.owner.profileInfo.uri);
|
|
|
|
QList<std::function<void(void)>> toUpdate;
|
|
conversation.interactions->forEach([&](const QString& id, interaction::Info& interaction) {
|
|
if (interaction.status != interaction::Status::SENDING) {
|
|
return;
|
|
}
|
|
// Get the message status from daemon, else unknown
|
|
auto daemonId = storage::getDaemonIdByInteractionId(db, id);
|
|
int status = 0;
|
|
if (daemonId.isEmpty()) {
|
|
return;
|
|
}
|
|
try {
|
|
auto msgId = std::stoull(daemonId.toStdString());
|
|
status = ConfigurationManager::instance().getMessageStatus(msgId);
|
|
toUpdate.emplace_back([this, convId, contactUri, daemonId, status]() {
|
|
auto accId = linked.owner.id;
|
|
updateInteractionStatus(accId, convId, contactUri, daemonId, status);
|
|
});
|
|
} catch (const std::exception& e) {
|
|
qWarning() << Q_FUNC_INFO << "Failed: message id was invalid";
|
|
}
|
|
});
|
|
Q_FOREACH (const auto& func, toUpdate)
|
|
func();
|
|
|
|
conversation.unreadMessages = getNumberOfUnreadMessagesFor(convId);
|
|
|
|
emplaceBackConversation(std::move(conversation));
|
|
invalidateModel();
|
|
}
|
|
|
|
int
|
|
ConversationModelPimpl::indexOf(const QString& uid) const
|
|
{
|
|
for (unsigned int i = 0; i < conversations.size(); ++i) {
|
|
if (conversations.at(i).uid == uid)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
std::reference_wrapper<conversation::Info>
|
|
ConversationModelPimpl::getConversation(const FilterPredicate& pred,
|
|
const bool searchResultIncluded) const
|
|
{
|
|
auto conv = std::find_if(conversations.cbegin(), conversations.cend(), pred);
|
|
if (conv != conversations.cend()) {
|
|
return std::remove_const_t<conversation::Info&>(*conv);
|
|
}
|
|
|
|
if (searchResultIncluded) {
|
|
auto sr = std::find_if(searchResults.cbegin(), searchResults.cend(), pred);
|
|
if (sr != searchResults.cend()) {
|
|
return std::remove_const_t<conversation::Info&>(*sr);
|
|
}
|
|
}
|
|
|
|
throw std::out_of_range("Conversation out of range");
|
|
}
|
|
|
|
std::reference_wrapper<conversation::Info>
|
|
ConversationModelPimpl::getConversationForUid(const QString& uid,
|
|
const bool searchResultIncluded) const
|
|
{
|
|
return getConversation([uid](const conversation::Info& conv) -> bool { return uid == conv.uid; },
|
|
searchResultIncluded);
|
|
}
|
|
|
|
std::reference_wrapper<conversation::Info>
|
|
ConversationModelPimpl::getConversationForPeerUri(const QString& uri,
|
|
const bool searchResultIncluded) const
|
|
{
|
|
return getConversation(
|
|
[this, uri](const conversation::Info& conv) -> bool {
|
|
if (!conv.isCoreDialog()) {
|
|
return false;
|
|
}
|
|
auto members = peersForConversation(conv);
|
|
if (members.isEmpty())
|
|
return false;
|
|
return members.indexOf(uri) != -1;
|
|
},
|
|
searchResultIncluded);
|
|
}
|
|
|
|
std::vector<int>
|
|
ConversationModelPimpl::getIndicesForContact(const QString& uri) const
|
|
{
|
|
std::vector<int> ret;
|
|
for (unsigned int i = 0; i < conversations.size(); ++i) {
|
|
const auto& convInfo = conversations.at(i);
|
|
if (!convInfo.isCoreDialog()) {
|
|
continue;
|
|
}
|
|
auto peers = peersForConversation(convInfo);
|
|
if (!peers.isEmpty() && peers.front() == uri) {
|
|
ret.emplace_back(i);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotNewCall(const QString& fromId,
|
|
const QString& callId,
|
|
bool isOutgoing,
|
|
const QString& toUri)
|
|
{
|
|
if (isOutgoing) {
|
|
// search contact
|
|
currentFilter = fromId;
|
|
invalidateModel();
|
|
searchResults.clear();
|
|
Q_EMIT linked.searchResultUpdated();
|
|
linked.owner.contactModel->searchContact(currentFilter);
|
|
Q_EMIT linked.filterChanged();
|
|
}
|
|
|
|
if (toUri == linked.owner.profileInfo.uri) {
|
|
auto convIds = storage::getConversationsWithPeer(db, fromId);
|
|
if (convIds.empty()) {
|
|
// in case if we receive call after removing contact add conversation request;
|
|
try {
|
|
auto contact = linked.owner.contactModel->getContact(fromId);
|
|
if (!isOutgoing && !contact.isBanned && fromId != linked.owner.profileInfo.uri) {
|
|
addContactRequest(fromId);
|
|
}
|
|
if (isOutgoing && contact.profileInfo.type == profile::Type::TEMPORARY) {
|
|
linked.owner.contactModel->addContact(contact);
|
|
}
|
|
} catch (const std::out_of_range&) {
|
|
}
|
|
}
|
|
|
|
auto conversationIndices = getIndicesForContact(fromId);
|
|
if (conversationIndices.empty()) {
|
|
qDebug() << "ConversationModelPimpl::slotNewCall, but conversation not found";
|
|
return; // Not a contact
|
|
}
|
|
|
|
auto& conversation = conversations.at(conversationIndices.at(0));
|
|
qDebug() << "Add call to conversation " << conversation.uid << " - " << callId;
|
|
conversation.callId = callId;
|
|
|
|
addOrUpdateCallMessage(callId, fromId, true);
|
|
Q_EMIT behaviorController.showIncomingCallView(linked.owner.id, conversation.uid);
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotCallStatusChanged(const QString& callId, int code)
|
|
{
|
|
Q_UNUSED(code)
|
|
// Get conversation
|
|
auto i = std::find_if(conversations.begin(),
|
|
conversations.end(),
|
|
[callId](const conversation::Info& conversation) {
|
|
return conversation.callId == callId;
|
|
});
|
|
|
|
try {
|
|
auto call = linked.owner.callModel->getCall(callId);
|
|
if (i != conversations.end()) {
|
|
// Update interaction status
|
|
invalidateModel();
|
|
linked.selectConversation(i->uid);
|
|
Q_EMIT linked.conversationUpdated(i->uid);
|
|
Q_EMIT linked.dataChanged(indexOf(i->uid));
|
|
}
|
|
} catch (std::out_of_range& e) {
|
|
qDebug() << "ConversationModelPimpl::slotCallStatusChanged can't get inexistant call";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotCallStarted(const QString& callId)
|
|
{
|
|
try {
|
|
auto call = linked.owner.callModel->getCall(callId);
|
|
addOrUpdateCallMessage(callId, call.peerUri.remove("ring:"), !call.isOutgoing);
|
|
} catch (std::out_of_range& e) {
|
|
qDebug() << "ConversationModelPimpl::slotCallStarted can't start inexistant call";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotCallEnded(const QString& callId)
|
|
{
|
|
try {
|
|
auto call = linked.owner.callModel->getCall(callId);
|
|
// get duration
|
|
std::time_t duration = 0;
|
|
if (call.startTime.time_since_epoch().count() != 0) {
|
|
auto duration_ns = std::chrono::steady_clock::now() - call.startTime;
|
|
duration = std::chrono::duration_cast<std::chrono::seconds>(duration_ns).count();
|
|
}
|
|
// add or update call interaction with duration
|
|
addOrUpdateCallMessage(callId, call.peerUri.remove("ring:"), !call.isOutgoing, duration);
|
|
/* Reset the callId stored in the conversation.
|
|
Do not call selectConversation() since it is already done in slotCallStatusChanged. */
|
|
for (auto& conversation : conversations)
|
|
if (conversation.callId == callId) {
|
|
conversation.callId = "";
|
|
conversation.confId = ""; // The participant is detached
|
|
invalidateModel();
|
|
Q_EMIT linked.conversationUpdated(conversation.uid);
|
|
Q_EMIT linked.dataChanged(indexOf(conversation.uid));
|
|
}
|
|
} catch (std::out_of_range& e) {
|
|
qDebug() << "ConversationModelPimpl::slotCallEnded can't end inexistant call";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
|
|
const QString& from,
|
|
bool incoming,
|
|
const std::time_t& duration)
|
|
{
|
|
// 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);
|
|
if (contact.profileInfo.type == profile::Type::PENDING) {
|
|
addContactRequest(from);
|
|
storage::beginConversationWithPeer(db, contact.profileInfo.uri);
|
|
}
|
|
} catch (const std::exception&) {
|
|
return;
|
|
}
|
|
try {
|
|
auto& conv = getConversationForPeerUri(from).get();
|
|
if (conv.callId.isEmpty())
|
|
conv.callId = callId;
|
|
} catch (...) {
|
|
return;
|
|
}
|
|
}
|
|
// do not save call interaction for swarm conversation
|
|
if (conv_it->isSwarm())
|
|
return;
|
|
auto uriString = incoming ? storage::prepareUri(from, linked.owner.profileInfo.type)
|
|
: linked.owner.profileInfo.uri;
|
|
auto msg = interaction::Info {uriString,
|
|
{},
|
|
std::time(nullptr),
|
|
duration,
|
|
interaction::Type::CALL,
|
|
interaction::Status::SUCCESS,
|
|
true};
|
|
// 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 = interaction::getCallInteractionString(msg.authorUri == linked.owner.profileInfo.uri,
|
|
msg);
|
|
auto [added, success] = conv_it->interactions->addOrUpdate(msgId, msg);
|
|
if (!success) {
|
|
qWarning() << Q_FUNC_INFO << QString("Failed: to %1 msg").arg(added ? "add" : "update");
|
|
return;
|
|
}
|
|
if (added)
|
|
Q_EMIT linked.newInteraction(conv_it->uid, msgId, msg);
|
|
|
|
invalidateModel();
|
|
Q_EMIT linked.modelChanged();
|
|
Q_EMIT linked.dataChanged(static_cast<int>(std::distance(conversations.begin(), conv_it)));
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotNewAccountMessage(const QString& accountId,
|
|
const QString& peerId,
|
|
const QString& msgId,
|
|
const MapStringString& payloads)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
|
|
for (auto it = payloads.constBegin(); it != payloads.constEnd(); ++it) {
|
|
const auto& payload = it.key();
|
|
if (payload.contains(TEXT_PLAIN)) {
|
|
addIncomingMessage(peerId, it.value(), 0, msgId);
|
|
} else {
|
|
qDebug() << payload;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotIncomingCallMessage(const QString& accountId,
|
|
const QString& callId,
|
|
const QString& from,
|
|
const QString& body)
|
|
{
|
|
if (accountId != linked.owner.id || !linked.owner.callModel->hasCall(callId))
|
|
return;
|
|
|
|
auto& call = linked.owner.callModel->getCall(callId);
|
|
if (call.type == call::Type::CONFERENCE) {
|
|
// Show messages in all conversations for conferences.
|
|
for (const auto& conversation : conversations) {
|
|
if (conversation.confId == callId) {
|
|
if (conversation.participants.empty()) {
|
|
continue;
|
|
}
|
|
addIncomingMessage(from, body);
|
|
}
|
|
}
|
|
} else {
|
|
addIncomingMessage(from, body);
|
|
}
|
|
}
|
|
|
|
QString
|
|
ConversationModelPimpl::addIncomingMessage(const QString& peerId,
|
|
const QString& body,
|
|
const uint64_t& timestamp,
|
|
const QString& daemonId)
|
|
{
|
|
auto convIds = storage::getConversationsWithPeer(db, peerId);
|
|
bool isRequest = false;
|
|
if (convIds.empty()) {
|
|
// in case if we receive a message after removing contact, add a conversation request
|
|
try {
|
|
auto contact = linked.owner.contactModel->getContact(peerId);
|
|
isRequest = contact.profileInfo.type == profile::Type::PENDING;
|
|
// if isSip, it will be a contact!
|
|
auto isSip = linked.owner.profileInfo.type == profile::Type::SIP;
|
|
if (isSip
|
|
|| (isRequest && !contact.isBanned && peerId != linked.owner.profileInfo.uri)) {
|
|
if (!isSip)
|
|
addContactRequest(peerId);
|
|
convIds.push_back(storage::beginConversationWithPeer(db, contact.profileInfo.uri));
|
|
auto& conv = getConversationForPeerUri(contact.profileInfo.uri).get();
|
|
conv.uid = convIds[0];
|
|
} else {
|
|
return "";
|
|
}
|
|
} catch (const std::out_of_range&) {
|
|
return "";
|
|
}
|
|
}
|
|
auto msg = interaction::Info {peerId,
|
|
body,
|
|
timestamp == 0 ? std::time(nullptr)
|
|
: static_cast<time_t>(timestamp),
|
|
0,
|
|
interaction::Type::TEXT,
|
|
interaction::Status::SUCCESS,
|
|
false};
|
|
auto msgId = storage::addMessageToConversation(db, convIds[0], msg);
|
|
if (!daemonId.isEmpty()) {
|
|
storage::addDaemonMsgId(db, msgId, daemonId);
|
|
}
|
|
auto conversationIdx = indexOf(convIds[0]);
|
|
// Add the conversation if not already here
|
|
if (conversationIdx == -1) {
|
|
addConversationWith(convIds[0], peerId, isRequest);
|
|
Q_EMIT linked.newConversation(convIds[0]);
|
|
} else {
|
|
// Maybe check if this is failing?
|
|
conversations[conversationIdx].interactions->append(msgId, msg);
|
|
conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convIds[0]);
|
|
}
|
|
|
|
Q_EMIT behaviorController.newUnreadInteraction(linked.owner.id, convIds[0], msgId, msg);
|
|
Q_EMIT linked.newInteraction(convIds[0], msgId, msg);
|
|
|
|
invalidateModel();
|
|
Q_EMIT linked.modelChanged();
|
|
Q_EMIT linked.dataChanged(conversationIdx);
|
|
|
|
return msgId;
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotCallAddedToConference(const QString& callId, const QString& confId)
|
|
{
|
|
for (auto& conversation : conversations) {
|
|
if (conversation.callId == callId && conversation.confId != confId) {
|
|
conversation.confId = confId;
|
|
invalidateModel();
|
|
// Refresh the conference status only if attached
|
|
MapStringString confDetails = CallManager::instance()
|
|
.getConferenceDetails(linked.owner.id, confId);
|
|
if (confDetails["STATE"] == "ACTIVE_ATTACHED")
|
|
linked.selectConversation(conversation.uid);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::updateInteractionStatus(const QString& accountId,
|
|
const QString& conversationId,
|
|
const QString& peerUri,
|
|
const QString& messageId,
|
|
int status)
|
|
{
|
|
if (accountId != linked.owner.id) {
|
|
return;
|
|
}
|
|
// non-swarm conversation
|
|
if (conversationId.isEmpty() || conversationId == linked.owner.profileInfo.uri) {
|
|
auto convIds = storage::getConversationsWithPeer(db, peerUri);
|
|
if (convIds.empty()) {
|
|
return;
|
|
}
|
|
auto conversationIdx = indexOf(convIds[0]);
|
|
auto& conversation = conversations[conversationIdx];
|
|
auto newStatus = interaction::Status::INVALID;
|
|
switch (static_cast<libjami::Account::MessageStates>(status)) {
|
|
case libjami::Account::MessageStates::SENDING:
|
|
newStatus = interaction::Status::SENDING;
|
|
break;
|
|
case libjami::Account::MessageStates::CANCELLED:
|
|
newStatus = interaction::Status::TRANSFER_CANCELED;
|
|
break;
|
|
case libjami::Account::MessageStates::SENT:
|
|
newStatus = interaction::Status::SUCCESS;
|
|
break;
|
|
case libjami::Account::MessageStates::FAILURE:
|
|
newStatus = interaction::Status::FAILURE;
|
|
break;
|
|
case libjami::Account::MessageStates::DISPLAYED:
|
|
newStatus = interaction::Status::DISPLAYED;
|
|
break;
|
|
case libjami::Account::MessageStates::UNKNOWN:
|
|
default:
|
|
newStatus = interaction::Status::UNKNOWN;
|
|
break;
|
|
}
|
|
auto idString = messageId;
|
|
// for not swarm conversation messageId in hexdesimal string format. Convert to normal string
|
|
// TODO messageId should be received from daemon in string format
|
|
if (static_cast<libjami::Account::MessageStates>(status)
|
|
== libjami::Account::MessageStates::DISPLAYED) {
|
|
std::istringstream ss(messageId.toStdString());
|
|
ss >> std::hex;
|
|
uint64_t id;
|
|
if (!(ss >> id))
|
|
return;
|
|
idString = QString::number(id);
|
|
}
|
|
// Update database
|
|
auto msgId = storage::getInteractionIdByDaemonId(db, idString);
|
|
if (msgId.isEmpty()) {
|
|
return;
|
|
}
|
|
storage::updateInteractionStatus(db, msgId, newStatus);
|
|
// Update conversations
|
|
bool updated = false;
|
|
bool updateDisplayedUid = false;
|
|
QString oldDisplayedUid = 0;
|
|
{
|
|
auto& interactions = conversation.interactions;
|
|
// Try to update the status.
|
|
if (interactions->updateStatus(msgId, newStatus)) {
|
|
updated = true;
|
|
interactions->with(msgId, [&](const QString& id, interaction::Info& interaction) {
|
|
// Determine if the interaction is outgoing and has been displayed.
|
|
bool interactionIsDisplayed = newStatus == interaction::Status::DISPLAYED
|
|
&& interaction::isOutgoing(interaction);
|
|
|
|
// Get the last displayed interaction ID and timestamp for this peer.
|
|
auto [lastIdForPeer, lastTimestampForPeer]
|
|
= interactions->getDisplayedInfoForPeer(peerUri);
|
|
|
|
if (lastIdForPeer.isEmpty()) {
|
|
oldDisplayedUid = "";
|
|
if (peerUri != linked.owner.profileInfo.uri)
|
|
conversation.interactions->setRead(peerUri, msgId);
|
|
updateDisplayedUid = true;
|
|
} else {
|
|
bool interactionIsLast = lastTimestampForPeer < interaction.timestamp;
|
|
updateDisplayedUid = interactionIsDisplayed && interactionIsLast;
|
|
if (updateDisplayedUid) {
|
|
oldDisplayedUid = messageId;
|
|
if (peerUri != linked.owner.profileInfo.uri)
|
|
conversation.interactions->setRead(peerUri, msgId);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (updateDisplayedUid) {
|
|
Q_EMIT linked.displayedInteractionChanged(conversation.uid,
|
|
peerUri,
|
|
oldDisplayedUid,
|
|
msgId);
|
|
}
|
|
if (updated) {
|
|
invalidateModel();
|
|
}
|
|
return;
|
|
}
|
|
// swarm conversation
|
|
try {
|
|
auto& conversation = getConversationForUid(conversationId).get();
|
|
if (conversation.isSwarm()) {
|
|
using namespace libjami::Account;
|
|
auto msgState = static_cast<MessageStates>(status);
|
|
auto& interactions = conversation.interactions;
|
|
interactions->with(messageId,
|
|
[&](const QString& id, const interaction::Info& interaction) {
|
|
if (interaction.type == interaction::Type::TEXT) {
|
|
interaction::Status newState;
|
|
if (msgState == MessageStates::SENDING) {
|
|
newState = interaction::Status::SENDING;
|
|
} else if (msgState == MessageStates::SENT) {
|
|
newState = interaction::Status::SUCCESS;
|
|
} else {
|
|
return;
|
|
}
|
|
interactions->updateStatus(id, newState);
|
|
}
|
|
});
|
|
|
|
if (msgState == MessageStates::DISPLAYED) {
|
|
auto previous = conversation.interactions->getRead(peerUri);
|
|
if (peerUri != linked.owner.profileInfo.uri)
|
|
conversation.interactions->setRead(peerUri, messageId);
|
|
else {
|
|
// 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,
|
|
conversationId,
|
|
messageId,
|
|
"",
|
|
peerUri);
|
|
Q_EMIT linked.dataChanged(indexOf(conversationId));
|
|
}
|
|
Q_EMIT linked.displayedInteractionChanged(conversationId,
|
|
peerUri,
|
|
previous,
|
|
messageId);
|
|
}
|
|
}
|
|
} catch (const std::out_of_range& e) {
|
|
qDebug() << "could not update message status for not existing conversation";
|
|
}
|
|
}
|
|
|
|
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,
|
|
const QString& contactUri,
|
|
bool isComposing)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
|
|
try {
|
|
auto& conversation = getConversationForUid(convId).get();
|
|
if (isComposing)
|
|
conversation.typers.insert(contactUri);
|
|
else
|
|
conversation.typers.remove(contactUri);
|
|
} catch (const std::out_of_range& e) {
|
|
qDebug() << "could not update message status for not existing conversation";
|
|
}
|
|
|
|
Q_EMIT linked.composingStatusChanged(convId, contactUri, isComposing);
|
|
}
|
|
|
|
int
|
|
ConversationModelPimpl::getNumberOfUnreadMessagesFor(const QString& uid)
|
|
{
|
|
return storage::countUnreadFromInteractions(db, uid);
|
|
}
|
|
|
|
void
|
|
ConversationModel::setIsComposing(const QString& convUid, bool isComposing)
|
|
{
|
|
try {
|
|
auto& conversation = pimpl_->getConversationForUid(convUid).get();
|
|
QString to = conversation.mode != conversation::Mode::NON_SWARM
|
|
? "swarm:" + convUid
|
|
: "jami:" + pimpl_->peersForConversation(conversation).front();
|
|
ConfigurationManager::instance().setIsComposing(owner.id, to, isComposing);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModel::sendFile(const QString& convUid,
|
|
const QString& path,
|
|
const QString& filename,
|
|
const QString& parent)
|
|
{
|
|
try {
|
|
auto& conversation = pimpl_->getConversationForUid(convUid, true).get();
|
|
if (conversation.isSwarm()) {
|
|
owner.dataTransferModel->sendFile(owner.id, convUid, path, filename, parent);
|
|
return;
|
|
}
|
|
auto peers = pimpl_->peersForConversation(conversation);
|
|
if (peers.size() < 1) {
|
|
qDebug() << "send file error: could not send file in conversation with no participants";
|
|
return;
|
|
}
|
|
/* isTemporary, and conversationReady callback used only for non-swarm conversation,
|
|
because for swarm, conversation already configured at this point.
|
|
Conversations for new contact from search result are NON_SWARM but after receiving
|
|
conversationReady callback could be updated to ONE_TO_ONE. We still use conversationReady
|
|
callback for one_to_one conversation to check if contact is blocked*/
|
|
const auto peerId = peers.front();
|
|
bool isTemporary = peerId == convUid || convUid == "SEARCHSIP";
|
|
|
|
/* It is necessary to make a copy of convUid since it may very well point to
|
|
a field in the temporary conversation, which is going to be destroyed by
|
|
slotContactAdded() (indirectly triggered by sendContactrequest(). Not doing
|
|
so may result in use after free/crash. */
|
|
auto convUidCopy = convUid;
|
|
|
|
pimpl_->sendContactRequest(peerId);
|
|
|
|
auto cb = ([this, peerId, path, filename, parent](QString conversationId) {
|
|
try {
|
|
auto conversationOpt = getConversationForUid(conversationId);
|
|
if (!conversationOpt.has_value()) {
|
|
qDebug() << "Can't send file";
|
|
return;
|
|
}
|
|
auto contactInfo = owner.contactModel->getContact(peerId);
|
|
if (contactInfo.isBanned) {
|
|
qDebug() << "ContactModel::sendFile: denied, contact is banned";
|
|
return;
|
|
}
|
|
owner.dataTransferModel->sendFile(owner.id, conversationId, path, filename, parent);
|
|
} catch (...) {
|
|
}
|
|
});
|
|
|
|
if (isTemporary) {
|
|
QMetaObject::Connection* const connection = new QMetaObject::Connection;
|
|
*connection = connect(this,
|
|
&ConversationModel::conversationReady,
|
|
[cb, connection, convUidCopy](QString conversationId,
|
|
QString participantId) {
|
|
if (participantId != convUidCopy) {
|
|
return;
|
|
}
|
|
cb(conversationId);
|
|
QObject::disconnect(*connection);
|
|
if (connection) {
|
|
delete connection;
|
|
}
|
|
});
|
|
} else {
|
|
cb(convUidCopy);
|
|
}
|
|
} catch (const std::out_of_range& e) {
|
|
qDebug() << "could not send file to not existing conversation";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModel::getConvMediasInfos(const QString& accountId,
|
|
const QString& conversationId,
|
|
const QString& text,
|
|
bool isMedia)
|
|
{
|
|
if (isMedia)
|
|
pimpl_->mediaResearchRequestId = ConfigurationManager::instance().searchConversation(
|
|
accountId, conversationId, "", "", text, "application/data-transfer+json", 0, 0, 0, 0);
|
|
else
|
|
pimpl_->msgResearchRequestId = ConfigurationManager::instance().searchConversation(
|
|
accountId, conversationId, "", "", text, "text/plain", 0, 0, 0, 0);
|
|
}
|
|
|
|
void
|
|
ConversationModel::acceptTransfer(const QString& convUid, const QString& interactionId)
|
|
{
|
|
lrc::api::datatransfer::Info info = {};
|
|
getTransferInfo(convUid, interactionId, info);
|
|
pimpl_->acceptTransfer(convUid, interactionId);
|
|
}
|
|
|
|
void
|
|
ConversationModel::cancelTransfer(const QString& convUid, const QString& fileId)
|
|
{
|
|
// For this action, we change interaction status before effective canceling as daemon will
|
|
// emit Finished event code immediately (before leaving this method) in non-DBus mode.
|
|
auto conversationIdx = pimpl_->indexOf(convUid);
|
|
bool emitUpdated = false;
|
|
if (conversationIdx != -1) {
|
|
auto& interactions = pimpl_->conversations[conversationIdx].interactions;
|
|
if (interactions->updateStatus(fileId, interaction::Status::TRANSFER_CANCELED)) {
|
|
// update information in the db
|
|
storage::updateInteractionStatus(pimpl_->db,
|
|
fileId,
|
|
interaction::Status::TRANSFER_CANCELED);
|
|
emitUpdated = true;
|
|
}
|
|
}
|
|
if (emitUpdated) {
|
|
// for swarm conversations we need to provide conversation id to accept file, for not swarm
|
|
// conversations we need peer uri
|
|
lrc::api::datatransfer::Info info = {};
|
|
getTransferInfo(convUid, fileId, info);
|
|
// Forward cancel action to daemon (will invoke slotTransferStatusCanceled)
|
|
owner.dataTransferModel->cancel(owner.id, convUid, fileId);
|
|
pimpl_->invalidateModel();
|
|
Q_EMIT pimpl_->behaviorController.newReadInteraction(owner.id, convUid, fileId);
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModel::getTransferInfo(const QString& conversationId,
|
|
const QString& interactionId,
|
|
datatransfer::Info& info) const
|
|
{
|
|
auto convOpt = getConversationForUid(conversationId);
|
|
if (!convOpt)
|
|
return;
|
|
auto fileId = owner.dataTransferModel->getFileIdFromInteractionId(interactionId);
|
|
if (convOpt->get().mode == conversation::Mode::NON_SWARM) {
|
|
return;
|
|
} else {
|
|
QString path;
|
|
qlonglong bytesProgress = 0, totalSize = 0;
|
|
owner.dataTransferModel
|
|
->fileTransferInfo(owner.id, conversationId, fileId, path, totalSize, bytesProgress);
|
|
info.path = path;
|
|
info.totalSize = totalSize;
|
|
info.progress = bytesProgress;
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModel::removeFile(const QString& conversationId,
|
|
const QString& interactionId,
|
|
const QString& path)
|
|
{
|
|
auto convOpt = getConversationForUid(conversationId);
|
|
if (!convOpt)
|
|
return;
|
|
|
|
QFile::remove(path);
|
|
convOpt->get().interactions->updateStatus(interactionId, interaction::Status::TRANSFER_CANCELED);
|
|
}
|
|
|
|
int
|
|
ConversationModel::getNumberOfUnreadMessagesFor(const QString& convUid)
|
|
{
|
|
return pimpl_->getNumberOfUnreadMessagesFor(convUid);
|
|
}
|
|
|
|
bool
|
|
ConversationModelPimpl::usefulDataFromDataTransfer(const QString& fileId,
|
|
const datatransfer::Info& info,
|
|
QString& interactionId,
|
|
QString& conversationId)
|
|
{
|
|
if (info.accountId != linked.owner.id)
|
|
return false;
|
|
try {
|
|
interactionId = linked.owner.dataTransferModel->getInteractionIdFromFileId(fileId);
|
|
conversationId = info.conversationId.isEmpty()
|
|
? storage::conversationIdFromInteractionId(db, interactionId)
|
|
: info.conversationId;
|
|
} catch (const std::out_of_range& e) {
|
|
qWarning() << "Couldn't get interaction from daemon Id: " << fileId;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotTransferStatusCreated(const QString& fileId, datatransfer::Info info)
|
|
{
|
|
// check if transfer is for the current account
|
|
if (info.accountId != linked.owner.id)
|
|
return;
|
|
|
|
const MapStringString accountDetails = ConfigurationManager::instance().getAccountDetails(
|
|
linked.owner.id);
|
|
if (accountDetails.empty())
|
|
return;
|
|
// create a new conversation if needed
|
|
auto convIds = storage::getConversationsWithPeer(db, info.peerUri);
|
|
bool isRequest = false;
|
|
if (convIds.empty()) {
|
|
// in case if we receive file after removing contact add conversation request. If we have
|
|
// swarm request this function will do nothing.
|
|
try {
|
|
auto contact = linked.owner.contactModel->getContact(info.peerUri);
|
|
isRequest = contact.profileInfo.type == profile::Type::PENDING;
|
|
if (isRequest && !contact.isBanned && info.peerUri != linked.owner.profileInfo.uri) {
|
|
addContactRequest(info.peerUri);
|
|
convIds.push_back(storage::beginConversationWithPeer(db, contact.profileInfo.uri));
|
|
auto& conv = getConversationForPeerUri(contact.profileInfo.uri).get();
|
|
conv.uid = convIds[0];
|
|
} else {
|
|
return;
|
|
}
|
|
} catch (const std::out_of_range&) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// add interaction to the db
|
|
const auto& convId = convIds[0];
|
|
auto interactionId = storage::addDataTransferToConversation(db, convId, info);
|
|
|
|
// map fileId and interactionId for latter retrivial from client (that only known the interactionId)
|
|
linked.owner.dataTransferModel->registerTransferId(fileId, interactionId);
|
|
|
|
auto interaction = interaction::Info {info.isOutgoing ? "" : info.peerUri,
|
|
info.isOutgoing ? info.path : info.displayName,
|
|
std::time(nullptr),
|
|
0,
|
|
interaction::Type::DATA_TRANSFER,
|
|
interaction::Status::TRANSFER_CREATED,
|
|
false};
|
|
|
|
// prepare interaction Info and emit signal for the client
|
|
auto conversationIdx = indexOf(convId);
|
|
if (conversationIdx == -1) {
|
|
addConversationWith(convId, info.peerUri, isRequest);
|
|
Q_EMIT linked.newConversation(convId);
|
|
} else {
|
|
conversations[conversationIdx].interactions->append(interactionId, interaction);
|
|
conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convId);
|
|
}
|
|
Q_EMIT behaviorController.newUnreadInteraction(linked.owner.id,
|
|
convId,
|
|
interactionId,
|
|
interaction);
|
|
Q_EMIT linked.newInteraction(convId, interactionId, interaction);
|
|
|
|
invalidateModel();
|
|
Q_EMIT linked.modelChanged();
|
|
Q_EMIT linked.dataChanged(conversationIdx);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotTransferStatusAwaitingPeer(const QString& fileId,
|
|
datatransfer::Info info)
|
|
{
|
|
if (info.accountId != linked.owner.id)
|
|
return;
|
|
bool intUpdated;
|
|
updateTransferStatus(fileId, info, interaction::Status::TRANSFER_AWAITING_PEER, intUpdated);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotTransferStatusAwaitingHost(const QString& fileId,
|
|
datatransfer::Info info)
|
|
{
|
|
if (info.accountId != linked.owner.id)
|
|
return;
|
|
awaitingHost(fileId, info);
|
|
}
|
|
|
|
bool
|
|
ConversationModelPimpl::hasOneOneSwarmWith(const QString& participant)
|
|
{
|
|
try {
|
|
auto& conversation = getConversationForPeerUri(participant).get();
|
|
return conversation.mode == conversation::Mode::ONE_TO_ONE;
|
|
} catch (std::out_of_range&) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::awaitingHost(const QString& fileId, datatransfer::Info info)
|
|
{
|
|
if (info.accountId != linked.owner.id)
|
|
return;
|
|
QString interactionId;
|
|
QString conversationId;
|
|
if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId))
|
|
return;
|
|
|
|
bool intUpdated;
|
|
|
|
if (!updateTransferStatus(fileId,
|
|
info,
|
|
interaction::Status::TRANSFER_AWAITING_HOST,
|
|
intUpdated)) {
|
|
return;
|
|
}
|
|
if (!intUpdated) {
|
|
return;
|
|
}
|
|
auto conversationIdx = indexOf(conversationId);
|
|
auto& peers = peersForConversation(conversations[conversationIdx]);
|
|
handleIncomingFile(conversationId, interactionId, info.totalSize);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::handleIncomingFile(const QString& convId,
|
|
const QString& interactionId,
|
|
int totalSize)
|
|
{
|
|
// If it's an accepted file type and less than 20 MB, accept transfer.
|
|
if (linked.owner.accountModel->autoTransferFromTrusted) {
|
|
if (linked.owner.accountModel->autoTransferSizeThreshold == 0
|
|
|| (totalSize > 0
|
|
&& static_cast<unsigned>(totalSize)
|
|
< linked.owner.accountModel->autoTransferSizeThreshold * 1024 * 1024)) {
|
|
acceptTransfer(convId, interactionId);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::acceptTransfer(const QString& convUid, const QString& interactionId)
|
|
{
|
|
auto& conversation = getConversationForUid(convUid).get();
|
|
if (conversation.isLegacy()) // Ignore legacy
|
|
return;
|
|
|
|
auto& interactions = conversation.interactions;
|
|
if (!interactions->with(interactionId, [&](const QString& id, interaction::Info& interaction) {
|
|
auto fileId = interaction.commit["fileId"];
|
|
if (fileId.isEmpty()) {
|
|
qWarning() << "Cannot download file without fileId";
|
|
return;
|
|
}
|
|
linked.owner.dataTransferModel->download(linked.owner.id,
|
|
convUid,
|
|
interactionId,
|
|
fileId);
|
|
})) {
|
|
qWarning() << "Cannot download file without valid interaction";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::invalidateModel()
|
|
{
|
|
filteredConversations.invalidate();
|
|
customFilteredConversations.invalidate();
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::emplaceBackConversation(conversation::Info&& conversation)
|
|
{
|
|
if (indexOf(conversation.uid) != -1)
|
|
return;
|
|
Q_EMIT linked.beginInsertRows(conversations.size());
|
|
conversations.emplace_back(std::move(conversation));
|
|
Q_EMIT linked.endInsertRows();
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::eraseConversation(const QString& convId)
|
|
{
|
|
eraseConversation(indexOf(convId));
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::eraseConversation(int index)
|
|
{
|
|
Q_EMIT linked.beginRemoveRows(index);
|
|
conversations.erase(conversations.begin() + index);
|
|
Q_EMIT linked.endRemoveRows();
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotTransferStatusOngoing(const QString& fileId, datatransfer::Info info)
|
|
{
|
|
if (info.accountId != linked.owner.id)
|
|
return;
|
|
QString interactionId;
|
|
QString conversationId;
|
|
if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId))
|
|
return;
|
|
bool intUpdated;
|
|
|
|
if (!updateTransferStatus(fileId, info, interaction::Status::TRANSFER_ONGOING, intUpdated)) {
|
|
return;
|
|
}
|
|
if (!intUpdated) {
|
|
return;
|
|
}
|
|
auto conversationIdx = indexOf(conversationId);
|
|
auto* timer = new QTimer();
|
|
connect(timer, &QTimer::timeout, this, [=] {
|
|
updateTransferProgress(timer, conversationIdx, interactionId);
|
|
});
|
|
timer->start(1000);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotTransferStatusFinished(const QString& fileId, datatransfer::Info info)
|
|
{
|
|
if (info.accountId != linked.owner.id)
|
|
return;
|
|
QString interactionId;
|
|
QString conversationId;
|
|
if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId))
|
|
return;
|
|
// prepare interaction Info and emit signal for the client
|
|
auto conversationIdx = indexOf(conversationId);
|
|
if (conversationIdx != -1) {
|
|
bool emitUpdated = false;
|
|
auto newStatus = interaction::Status::TRANSFER_FINISHED;
|
|
auto& interactions = conversations[conversationIdx].interactions;
|
|
interactions->with(interactionId, [&](const QString& id, interaction::Info& interaction) {
|
|
// We need to check if current status is ONGOING as CANCELED must not be
|
|
// transformed into FINISHED
|
|
if (interaction.status == interaction::Status::TRANSFER_ONGOING) {
|
|
emitUpdated = true;
|
|
interactions->updateStatus(id, newStatus);
|
|
}
|
|
});
|
|
if (emitUpdated) {
|
|
invalidateModel();
|
|
if (conversations[conversationIdx].mode != conversation::Mode::NON_SWARM) {
|
|
if (transfIdToDbIntId.find(fileId) != transfIdToDbIntId.end()) {
|
|
auto dbIntId = transfIdToDbIntId[fileId];
|
|
storage::updateInteractionStatus(db, dbIntId, newStatus);
|
|
}
|
|
} else {
|
|
storage::updateInteractionStatus(db, interactionId, newStatus);
|
|
}
|
|
transfIdToDbIntId.remove(fileId);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotTransferStatusCanceled(const QString& fileId, datatransfer::Info info)
|
|
{
|
|
if (info.accountId != linked.owner.id)
|
|
return;
|
|
bool intUpdated;
|
|
updateTransferStatus(fileId, info, interaction::Status::TRANSFER_CANCELED, intUpdated);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotTransferStatusError(const QString& fileId, datatransfer::Info info)
|
|
{
|
|
if (info.accountId != linked.owner.id)
|
|
return;
|
|
bool intUpdated;
|
|
updateTransferStatus(fileId, info, interaction::Status::TRANSFER_ERROR, intUpdated);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotTransferStatusUnjoinable(const QString& fileId, datatransfer::Info info)
|
|
{
|
|
if (info.accountId != linked.owner.id)
|
|
return;
|
|
bool intUpdated;
|
|
updateTransferStatus(fileId, info, interaction::Status::TRANSFER_UNJOINABLE_PEER, intUpdated);
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotTransferStatusTimeoutExpired(const QString& fileId,
|
|
datatransfer::Info info)
|
|
{
|
|
if (info.accountId != linked.owner.id)
|
|
return;
|
|
bool intUpdated;
|
|
updateTransferStatus(fileId, info, interaction::Status::TRANSFER_TIMEOUT_EXPIRED, intUpdated);
|
|
}
|
|
|
|
bool
|
|
ConversationModelPimpl::updateTransferStatus(const QString& fileId,
|
|
datatransfer::Info info,
|
|
interaction::Status newStatus,
|
|
bool& updated)
|
|
{
|
|
QString interactionId;
|
|
QString conversationId;
|
|
if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId)) {
|
|
return false;
|
|
}
|
|
|
|
auto conversationIdx = indexOf(conversationId);
|
|
if (conversationIdx < 0) {
|
|
return false;
|
|
}
|
|
auto& conversation = conversations[conversationIdx];
|
|
if (conversation.isLegacy()) {
|
|
storage::updateInteractionStatus(db, interactionId, newStatus);
|
|
}
|
|
auto& interactions = conversations[conversationIdx].interactions;
|
|
bool emitUpdated = interactions->updateStatus(interactionId,
|
|
newStatus,
|
|
conversation.isSwarm() ? info.path : QString());
|
|
if (emitUpdated) {
|
|
invalidateModel();
|
|
}
|
|
updated = emitUpdated;
|
|
return true;
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::updateTransferProgress(QTimer* timer,
|
|
int conversationIdx,
|
|
const QString& interactionId)
|
|
{
|
|
try {
|
|
bool emitUpdated = false;
|
|
{
|
|
const auto& interactions = conversations[conversationIdx].interactions;
|
|
interactions->with(interactionId, [&](const QString& id, interaction::Info& interaction) {
|
|
if (interaction.status == interaction::Status::TRANSFER_ONGOING) {
|
|
emitUpdated = true;
|
|
interactions->updateStatus(id, interaction::Status::TRANSFER_ONGOING);
|
|
}
|
|
});
|
|
}
|
|
if (emitUpdated)
|
|
return;
|
|
} catch (...) {
|
|
}
|
|
|
|
timer->stop();
|
|
timer->deleteLater();
|
|
}
|
|
|
|
void
|
|
ConversationModelPimpl::slotConversationPreferencesUpdated(const QString&,
|
|
const QString& conversationId,
|
|
const MapStringString& preferences)
|
|
{
|
|
auto conversationIdx = indexOf(conversationId);
|
|
if (conversationIdx < 0)
|
|
return;
|
|
auto& conversation = conversations[conversationIdx];
|
|
conversation.preferences = preferences;
|
|
Q_EMIT linked.conversationPreferencesUpdated(conversationId);
|
|
}
|
|
|
|
} // namespace lrc
|
|
|
|
#include "api/moc_conversationmodel.cpp"
|
|
#include "conversationmodel.moc"
|