Skip to content
Snippets Groups Projects
Select Git revision
  • 2333f78379144b253c1f1c6e9ba3a2346ff989c7
  • master default protected
  • release/202005
  • release/202001
  • release/201912
  • release/201911
  • release/releaseWindowsTestOne
  • release/releaseTest
  • release/releaseWindowsTest
  • release/windowsReleaseTest
  • release/201910
  • release/qt/201910
  • release/windows-test/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • 1.0.0
  • 0.3.0
  • 0.2.1
  • 0.2.0
  • 0.1.0
26 results

conversationmodel.cpp

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    conversationmodel.cpp 84.67 KiB
    /****************************************************************************
     *    Copyright (C) 2017-2019 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"
    
    //Qt
    #include <QtCore/QTimer>
    #include <QFileInfo>
    
    // daemon
    #include <account_const.h>
    #include <datatransfer_interface.h>
    
    // std
    #include <algorithm>
    #include <mutex>
    #include <regex>
    #include <fstream>
    
    // LRC
    #include "api/lrc.h"
    #include "api/behaviorcontroller.h"
    #include "api/contactmodel.h"
    #include "api/newcallmodel.h"
    #include "api/newaccountmodel.h"
    #include "api/account.h"
    #include "api/call.h"
    #include "api/datatransfer.h"
    #include "api/datatransfermodel.h"
    #include "callbackshandler.h"
    #include "authority/storagehelper.h"
    
    #include "uri.h"
    
    // Dbus
    #include "dbus/configurationmanager.h"
    #include "dbus/callmanager.h"
    
    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();
    
        /**
         * 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 std::string& uid) const;
        /**
         * return a conversation index from conversations or -1 if no index is found.
         * @param uri of the contact to search.
         * @return an int.
         */
        int indexOfContact(const std::string& uri) const;
        /**
         * Initialize conversations_ and filteredConversations_
         */
        void initConversations();
        /**
         * Sort conversation by last action
         */
        void sortConversations();
        /**
         * Call contactModel.addContact if necessary
         * @param contactUri
         */
        void sendContactRequest(const std::string& contactUri);
        /**
         * Add a conversation with contactUri
         * @param convId
         * @param contactUri
         */
        void addConversationWith(const std::string& convId, const std::string& contactUri);
        /**
         * Add call interaction for conversation with callId
         * @param callId
         * @param duration
         */
        void addOrUpdateCallMessage(const std::string& callId,
                                    const std::string& from = {},
                                    const std::time_t& duration = -1);
        /**
         * Add a new message from a peer in the database
         * @param from the author uri
         * @param body the content of the message
         * @param timestamp the timestamp of the message
         */
        void addIncomingMessage(const std::string& from,
                                const std::string& body,
                                const uint64_t& timestamp = 0);
        /**
         * Change the status of an interaction. Listen from callbacksHandler
         * @param accountId, account linked
         * @param id, interaction to update
         * @param to, peer uri
         * @param status, new status for this interaction
         */
        void slotUpdateInteractionStatus(const std::string& accountId,
                                         const uint64_t id,
                                         const std::string& to, 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 std::string& uid, bool isAudioOnly = false);
    
        /**
         * get number of unread messages
         */
        int getNumberOfUnreadMessagesFor(const std::string& uid);
    
        /**
         * Handle data transfer progression
         */
        void updateTransfer(QTimer* timer, const std::string& conversation, int conversationIdx,
                            int interactionId);
    
        bool usefulDataFromDataTransfer(long long dringId, const datatransfer::Info& info,
                                        int& interactionId, std::string& convId);
    
        /**
         * accept a file transfer
         * @param convUid
         * @param interactionId
         * @param final name of the file
         */
        void acceptTransfer(const std::string& convUid, uint64_t interactionId, const std::string& path);
    
        const ConversationModel& linked;
        Lrc& lrc;
        Database& db;
        const CallbacksHandler& callbacksHandler;
        const BehaviorController& behaviorController;
    
        ConversationModel::ConversationQueue conversations; ///< non-filtered conversations
        ConversationModel::ConversationQueue filteredConversations;
        ConversationModel::ConversationQueue customFilteredConversations;
        std::string filter;
        profile::Type typeFilter;
        profile::Type customTypeFilter;
        std::pair<bool, bool> dirtyConversations {true, true}; ///< true if filteredConversations/customFilteredConversations must be regenerated
        std::map<std::string, std::mutex> interactionsLocks; ///< {convId, mutex}
    
    public Q_SLOTS:
        /**
         * Listen from contactModel when updated (like new alias, avatar, etc.)
         */
        void slotContactModelUpdated(const std::string& uri, bool needsSorted);
        /**
         * Listen from contactModel when a new contact is added
         * @param uri
         */
        void slotContactAdded(const std::string& uri);
        /**
         * Listen from contactModel when a pending contact is accepted
         * @param uri
         */
        void slotPendingContactAccepted(const std::string& uri);
        /**
         * Listen from contactModel when aa new contact is removed
         * @param uri
         */
        void slotContactRemoved(const std::string& uri);
        /**
         * Listen from callmodel for new calls.
         * @param fromId caller uri
         * @param callId
         */
        void slotIncomingCall(const std::string& fromId, const std::string& callId);
        /**
         * Listen from callmodel for calls status changed.
         * @param callId
         */
        void slotCallStatusChanged(const std::string& callId, int code);
        /**
         * Listen from callmodel for writing "Call started"
         * @param callId
         */
        void slotCallStarted(const std::string& callId);
        /**
         * Listen from callmodel for writing "Call ended"
         * @param callId
         */
        void slotCallEnded(const std::string& callId);
        /**
         * Listen from CallbacksHandler for new incoming interactions;
         * @param accountId
         * @param from uri
         * @param payloads body
         */
        void slotNewAccountMessage(std::string& accountId,
                                   std::string& from,
                                   std::map<std::string,std::string> payloads);
        /**
         * Listen from CallbacksHandler for new messages in a SIP call
         * @param callId call linked to the interaction
         * @param from author uri
         * @param body of the message
         */
        void slotIncomingCallMessage(const std::string& callId, const std::string& from, const std::string& body);
        /**
         * Listen from CallModel when a call is added to a conference
         * @param callId
         * @param confId
         */
        void slotCallAddedToConference(const std::string& callId, const std::string& confId);
        /**
         * Listen from CallbacksHandler when a conference is deleted.
         * @param confId
         */
        void slotConferenceRemoved(const std::string& confId);
    
        void slotTransferStatusCreated(long long dringId, api::datatransfer::Info info);
        void slotTransferStatusCanceled(long long dringId, api::datatransfer::Info info);
        void slotTransferStatusAwaitingPeer(long long dringId, api::datatransfer::Info info);
        void slotTransferStatusAwaitingHost(long long dringId, api::datatransfer::Info info);
        void slotTransferStatusOngoing(long long dringId, api::datatransfer::Info info);
        void slotTransferStatusFinished(long long dringId, api::datatransfer::Info info);
        void slotTransferStatusError(long long dringId, api::datatransfer::Info info);
        void slotTransferStatusTimeoutExpired(long long dringId, api::datatransfer::Info info);
        void slotTransferStatusUnjoinable(long long dringId, api::datatransfer::Info info);
        void updateTransferStatus(long long dringId, api::datatransfer::Info info, interaction::Status newStatus);
    };
    
    ConversationModel::ConversationModel(const account::Info& owner,
                                         Lrc& lrc,
                                         Database& db,
                                         const CallbacksHandler& callbacksHandler,
                                         const BehaviorController& behaviorController)
    : QObject()
    , pimpl_(std::make_unique<ConversationModelPimpl>(*this, lrc, db, callbacksHandler, behaviorController))
    , owner(owner)
    {
    
    }
    
    ConversationModel::~ConversationModel()
    {
    
    }
    
    const ConversationModel::ConversationQueue&
    ConversationModel::allFilteredConversations() const
    {
        if (!pimpl_->dirtyConversations.first)
            return pimpl_->filteredConversations;
    
        pimpl_->filteredConversations = pimpl_->conversations;
    
        auto it = std::copy_if(
            pimpl_->conversations.begin(), pimpl_->conversations.end(),
            pimpl_->filteredConversations.begin(),
            [this] (const conversation::Info& entry) {
                try {
                    auto contactInfo = owner.contactModel->getContact(entry.participants.front());
    
                    auto filter = pimpl_->filter;
                    auto uri = URI(QString(filter.c_str()));
                    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;
                    }
    
                    filter = uri.format(flags).toStdString();
    
                    /* Check contact */
                    // If contact is banned, only match if filter is a perfect match
                    if (contactInfo.isBanned) {
                        if (filter == "") return false;
                        return contactInfo.profileInfo.uri == filter
                               || contactInfo.profileInfo.alias == filter
                               || contactInfo.registeredName == filter;
                    }
    
                    std::regex regexFilter;
                    auto isValidReFilter = true;
                    try {
                        regexFilter = std::regex(filter, std::regex_constants::icase);
                    } catch(std::regex_error&) {
                        isValidReFilter = false;
                    }
    
                    auto filterUriAndReg = [regexFilter, isValidReFilter](auto contact, auto filter) {
                        auto result = contact.profileInfo.uri.find(filter) != std::string::npos
                        || contact.registeredName.find(filter) != std::string::npos;
                        if (!result) {
                            auto regexFound = isValidReFilter? (!contact.profileInfo.uri.empty()
                                   && std::regex_search(contact.profileInfo.uri, regexFilter))
                                   || std::regex_search(contact.registeredName, regexFilter) : false;
                            result |= regexFound;
                        }
                        return result;
                    };
    
                    /* Check type */
                    if (pimpl_->typeFilter != profile::Type::PENDING) {
                        // Remove pending contacts and get the temporary item if filter is not empty
                        switch (contactInfo.profileInfo.type) {
                        case profile::Type::COUNT__:
                        case profile::Type::INVALID:
                        case profile::Type::PENDING:
                            return false;
                        case profile::Type::TEMPORARY:
                            return filterUriAndReg(contactInfo, filter);
                        case profile::Type::SIP:
                        case profile::Type::RING:
                            break;
                        }
                    } else {
                        // We only want pending requests matching with the filter
                        if (contactInfo.profileInfo.type != profile::Type::PENDING)
                            return false;
                    }
    
                    // Otherwise perform usual regex search
                    bool result = contactInfo.profileInfo.alias.find(filter) != std::string::npos;
                    if (!result && isValidReFilter) result |= std::regex_search(contactInfo.profileInfo.alias, regexFilter);
                    if (!result) result |= filterUriAndReg(contactInfo, filter);
                    return result;
                } catch (std::out_of_range&) {
                    // getContact() failed
                    return false;
                }
        });
        pimpl_->filteredConversations.resize(std::distance(pimpl_->filteredConversations.begin(), it));
        pimpl_->dirtyConversations.first = false;
        return pimpl_->filteredConversations;
    }
    
    const ConversationModel::ConversationQueue&
    ConversationModel::getFilteredConversations(const profile::Type& filter, bool forceUpdate, const bool includeBanned) const
    {
        if (pimpl_->customTypeFilter == filter && !pimpl_->dirtyConversations.second && !forceUpdate)
            return pimpl_->customFilteredConversations;
    
        pimpl_->customTypeFilter = filter;
        pimpl_->customFilteredConversations = pimpl_->conversations;
    
        auto it = std::copy_if(
            pimpl_->conversations.begin(), pimpl_->conversations.end(),
            pimpl_->customFilteredConversations.begin(),
            [this, &includeBanned] (const conversation::Info& entry) {
                auto contactInfo = owner.contactModel->getContact(entry.participants.front());
                if (!includeBanned && contactInfo.isBanned) return false;
                return (contactInfo.profileInfo.type == pimpl_->customTypeFilter);
            });
        pimpl_->customFilteredConversations.resize(std::distance(pimpl_->customFilteredConversations.begin(), it));
        pimpl_->dirtyConversations.second = false;
        return pimpl_->customFilteredConversations;
    }
    
    conversation::Info
    ConversationModel::filteredConversation(const unsigned int row) const
    {
        const auto& conversations = allFilteredConversations();
        if (row >= conversations.size())
            return conversation::Info();
    
        auto conversationInfo = conversations.at(row);
    
        return conversationInfo;
    }
    
    void
    ConversationModel::makePermanent(const std::string& uid)
    {
        auto conversationIdx = pimpl_->indexOf(uid);
        if (conversationIdx == -1 || !owner.enabled)
            return;
    
        auto& conversation = pimpl_->conversations.at(conversationIdx);
        if (conversation.participants.empty()) {
            // Should not
            qDebug() << "ConversationModel::addConversation can't add a conversation with no participant";
            return;
        }
    
        // Send contact request if non used
        pimpl_->sendContactRequest(conversation.participants.front());
    }
    
    void
    ConversationModel::selectConversation(const std::string& uid) const
    {
        // Get conversation
        auto conversationIdx = pimpl_->indexOf(uid);
    
        if (conversationIdx == -1)
            return;
    
        if (uid.empty() && owner.contactModel->getContact("").profileInfo.uri.empty()) {
            // if we select the temporary contact, check if its a valid contact.
            return;
        }
    
        auto& conversation = pimpl_->conversations.at(conversationIdx);
        bool callEnded = true;
        if (!conversation.callId.empty()) {
            try  {
                auto call = owner.callModel->getCall(conversation.callId);
                callEnded = call.status == call::Status::ENDED;
            } catch (...) {}
        }
    
        if (not callEnded and not conversation.confId.empty()) {
            emit pimpl_->behaviorController.showCallView(owner.id, conversation);
        } else if (callEnded) {
            emit pimpl_->behaviorController.showChatView(owner.id, conversation);
        } 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
                    emit pimpl_->behaviorController.showIncomingCallView(owner.id, conversation);
                    break;
                case call::Status::PAUSED:
                case call::Status::CONNECTED:
                case call::Status::IN_PROGRESS:
                    // We are currently receiving a call
                    emit pimpl_->behaviorController.showCallView(owner.id, conversation);
                    break;
                case call::Status::PEER_BUSY:
                    emit pimpl_->behaviorController.showLeaveMessageView(owner.id, conversation);
                    break;
                case call::Status::TIMEOUT:
                case call::Status::TERMINATING:
                case call::Status::INVALID:
                case call::Status::INACTIVE:
                    // call just ended
                    emit pimpl_->behaviorController.showChatView(owner.id, conversation);
                    break;
                case call::Status::ENDED:
                default: // ENDED
                    // nothing to do
                    break;
                }
            } catch (const std::out_of_range&) {
                // Should not happen
                emit pimpl_->behaviorController.showChatView(owner.id, conversation);
            }
        }
    }
    
    void
    ConversationModel::removeConversation(const std::string& uid, bool banned)
    {
        // Get conversation
        auto conversationIdx = pimpl_->indexOf(uid);
        if (conversationIdx == -1)
            return;
    
        auto& conversation = pimpl_->conversations.at(conversationIdx);
        if (conversation.participants.empty()) {
            // Should not
            qDebug() << "ConversationModel::removeConversation can't remove a conversation without participant";
            return;
        }
    
        // Remove contact from daemon
        // NOTE: this will also remove the conversation into the database.
        for (const auto& participant: conversation.participants)
            owner.contactModel->removeContact(participant, banned);
    }
    
    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
    ConversationModelPimpl::placeCall(const std::string& uid, bool isAudioOnly)
    {
        auto conversationIdx = indexOf(uid);
    
        if (conversationIdx == -1 || !linked.owner.enabled)
            return;
    
        auto& conversation = conversations.at(conversationIdx);
        if (conversation.participants.empty()) {
            // Should not
            qDebug() << "ConversationModel::placeCall can't call a conversation without participant";
            return;
        }
    
        // Disallow multiple call
        if (!conversation.callId.empty()) {
            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 = conversation.participants.front();
        bool isTemporary = participant.empty();
        auto contactInfo = linked.owner.contactModel->getContact(participant);
        auto uri = contactInfo.profileInfo.uri;
    
        if (uri.empty())
            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 = std::function<void(std::string)>(
            [this, isTemporary, uri, isAudioOnly, &conversation](std::string convId) {
                int contactIndex;
                if (isTemporary && (contactIndex = indexOfContact(convId)) < 0) {
                    qDebug() << "Can't place call: Other participant is not a contact (removed while placing call ?)";
                    return;
                }
    
                auto& newConv = isTemporary ? conversations.at(contactIndex) : conversation;
                convId = newConv.uid;
    
                newConv.callId = linked.owner.callModel->createCall(uri, isAudioOnly);
                if (newConv.callId.empty()) {
                    qDebug() << "Can't place call (daemon side failure ?)";
                    return;
                }
    
                dirtyConversations = { true, true };
                emit behaviorController.showIncomingCallView(linked.owner.id, newConv);
            });
    
        if (isTemporary) {
            QMetaObject::Connection* const connection = new QMetaObject::Connection;
            *connection = connect(&this->linked, &ConversationModel::conversationReady,
                [cb, connection](std::string convId) {
                    cb(convId);
                    QObject::disconnect(*connection);
                    if (connection) {
                        delete connection;
                    }
                });
        }
    
        sendContactRequest(participant);
    
        if (!isTemporary) {
            cb(convId);
        }
    }
    
    void
    ConversationModel::placeAudioOnlyCall(const std::string& uid)
    {
        pimpl_->placeCall(uid, true);
    }
    
    void
    ConversationModel::placeCall(const std::string& uid)
    {
        pimpl_->placeCall(uid);
    }
    
    void
    ConversationModel::sendMessage(const std::string& uid, const std::string& body)
    {
        // FIXME potential race condition between index check and at() call
        auto conversationIdx = pimpl_->indexOf(uid);
        if (conversationIdx == -1 || !owner.enabled)
            return;
    
        auto& conversation = pimpl_->conversations.at(conversationIdx);
    
        if (conversation.participants.empty()) {
            // Should not
            qDebug() << "ConversationModel::sendMessage can't send a interaction to a conversation with no participant";
            return;
        }
    
        auto convId = uid;
        bool isTemporary = conversation.participants.front() == "";
    
        /* Make a copy of participants list: if current conversation is temporary,
           it might me destroyed while we are reading it */
        const auto participants = conversation.participants;
    
        auto cb = std::function<void(std::string)>(
            [this, isTemporary, body, &conversation](std::string convId) {
                /* Now we should be able to retrieve the final conversation, in case the previous
                   one was temporary */
                   // FIXME potential race condition between index check and at() call
                int contactIndex;
                if (isTemporary && (contactIndex = pimpl_->indexOfContact(convId)) < 0) {
                    qDebug() << "Can't send message: Other participant is not a contact";
                    return;
                }
    
                uint64_t daemonMsgId = 0;
                auto status = interaction::Status::SENDING;
    
                auto& newConv = isTemporary ? pimpl_->conversations.at(contactIndex) : conversation;
                convId = newConv.uid;
    
                // Send interaction to each participant
                for (const auto& participant : newConv.participants) {
                    auto contactInfo = owner.contactModel->getContact(participant);
    
                    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.empty() and not callLists.contains(newConv.callId.c_str()))
                        newConv.callId.clear();
    
                    if (not newConv.callId.empty()
                        and call::canSendSIPMessage(owner.callModel->getCall(newConv.callId))) {
                        status = interaction::Status::UNKNOWN;
                        owner.callModel->sendSipMessage(newConv.callId, body);
    
                    } else {
                        daemonMsgId = owner.contactModel->sendDhtMessage(contactInfo.profileInfo.uri, body);
                    }
    
                }
    
                // Add interaction to database
                interaction::Info msg {
                    {},
                    body, std::time(nullptr),
                    0,
                    interaction::Type::TEXT,
                    status,
                    true
                };
                int 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, std::to_string(msgId), std::to_string(daemonMsgId));
                }
    
                bool ret = false;
    
                {
                    std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
                    ret = newConv.interactions.insert(std::pair<uint64_t, interaction::Info>(msgId, msg)).second;
                }
    
                if (!ret) {
                    qDebug("ConversationModel::sendMessage failed to send message because an existing key was already present in the database (key = %d)", msgId);
                    return;
                }
    
                newConv.lastMessageUid = msgId;
                pimpl_->dirtyConversations = { true, true };
                // Emit this signal for chatview in the client
                emit newInteraction(convId, msgId, msg);
                // This conversation is now at the top of the list
                pimpl_->sortConversations();
                // The order has changed, informs the client to redraw the list
                emit modelSorted();
            });
    
        if (isTemporary) {
            QMetaObject::Connection* const connection = new QMetaObject::Connection;
            *connection = connect(this, &ConversationModel::conversationReady,
                [cb, connection](std::string convId) {
                    cb(convId);
                    QObject::disconnect(*connection);
                    if (connection) {
                        delete connection;
                    }
                });
        }
    
        /* Check participants list, send contact request if needed.
           NOTE: conferences are not implemented yet, so we have only one participant */
        for (const auto& participant: participants) {
            auto contactInfo = owner.contactModel->getContact(participant);
    
            if (contactInfo.isBanned) {
                qDebug() << "ContactModel::sendMessage: denied, contact is banned";
                return;
            }
    
            pimpl_->sendContactRequest(participant);
        }
    
        if (!isTemporary) {
            cb(convId);
        }
    }
    
    void
    ConversationModel::refreshFilter()
    {
        pimpl_->dirtyConversations = {true, true};
        emit filterChanged();
    }
    
    void
    ConversationModel::setFilter(const std::string& filter)
    {
        pimpl_->filter = filter;
        pimpl_->dirtyConversations = {true, true};
        // Will update the temporary contact in the contactModel
        owner.contactModel->searchContact(filter);
        emit filterChanged();
    }
    
    void
    ConversationModel::setFilter(const profile::Type& filter)
    {
        // Switch between PENDING, RING and SIP contacts.
        pimpl_->typeFilter = filter;
        pimpl_->dirtyConversations = {true, true};
        emit filterChanged();
    }
    
    void
    ConversationModel::joinConversations(const std::string& uidA, const std::string& 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.empty() || conversationB.callId.empty())
            return;
    
        if (conversationA.confId.empty()) {
            if(conversationB.confId.empty()){
                owner.callModel->joinCalls(conversationA.callId, conversationB.callId);
            }else{
                owner.callModel->joinCalls(conversationA.callId, conversationB.confId);
                conversationA.confId = conversationB.confId;
            }
        } else {
            if(conversationB.confId.empty()){
                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 std::string& 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
        {
            std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[uid]);
            conversation.interactions.clear();
        }
        storage::getHistory(pimpl_->db, conversation); // will contains "Conversation started"
        pimpl_->sortConversations();
        emit modelSorted();
        emit conversationCleared(uid);
    }
    
    void
    ConversationModel::clearInteractionFromConversation(const std::string& convId, const uint64_t& interactionId)
    {
        auto conversationIdx = pimpl_->indexOf(convId);
        if (conversationIdx == -1)
            return;
    
        auto erased_keys = 0;
        bool lastInteractionUpdated = false;
        {
            std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
            try
            {
                auto& conversation = pimpl_->conversations.at(conversationIdx);
                storage::clearInteractionFromConversation(pimpl_->db, convId, interactionId);
                erased_keys = conversation.interactions.erase(interactionId);
    
                if (conversation.lastMessageUid == interactionId) {
                    // Update lastMessageUid
                    auto newLastId = 0;
                    if (!conversation.interactions.empty())
                        newLastId = conversation.interactions.rbegin()->first;
                    conversation.lastMessageUid = newLastId;
                    lastInteractionUpdated = true;
                }
    
            } catch (const std::out_of_range& e) {
                qDebug() << "can't clear interaction from conversation: " << e.what();
            }
        }
        if (erased_keys > 0) {
            pimpl_->dirtyConversations.first = true;
            emit interactionRemoved(convId, interactionId);
        }
        if (lastInteractionUpdated) {
            // last interaction as changed, so the order can changes.
            pimpl_->sortConversations();
            emit modelSorted();
        }
    }
    
    void
    ConversationModel::retryInteraction(const std::string& convId, const uint64_t& interactionId)
    {
        auto conversationIdx = pimpl_->indexOf(convId);
        if (conversationIdx == -1)
            return;
    
        auto interactionType = interaction::Type::INVALID;
        auto body = std::string();
        {
            std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
            try {
                auto& conversation = pimpl_->conversations.at(conversationIdx);
    
                auto& interactions = conversation.interactions;
                auto it = interactions.find(interactionId);
                if (it == interactions.end())
                    return;
    
                if (!interaction::isOutgoing(it->second))
                    return;  // Do not retry non outgoing info
    
                if (it->second.type == interaction::Type::TEXT
                || (it->second.type == interaction::Type::DATA_TRANSFER
                && interaction::isOutgoing(it->second))) {
                    body = it->second.body;
                    interactionType = it->second.type;
                } else
                    return;
    
                storage::clearInteractionFromConversation(pimpl_->db, convId, interactionId);
                conversation.interactions.erase(interactionId);
            } catch (const std::out_of_range& e) {
                qDebug() << "can't find interaction from conversation: " << e.what();
                return;
            }
        }
        emit interactionRemoved(convId, interactionId);
    
        // Send a new interaction like the previous one
        if (interactionType == interaction::Type::TEXT) {
            sendMessage(convId, body);
        } else {
            // send file
            QFileInfo f(body.c_str());
            sendFile(convId, body, f.fileName().toStdString());
        }
    }
    
    void
    ConversationModel::clearAllHistory()
    {
        storage::clearAllHistory(pimpl_->db);
    
        for (auto& conversation : pimpl_->conversations) {
            {
                std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[conversation.uid]);
                conversation.interactions.clear();
            }
            storage::getHistory(pimpl_->db, conversation);
        }
        pimpl_->sortConversations();
        emit modelSorted();
    }
    
    void
    ConversationModel::setInteractionRead(const std::string& convId,
                                          const uint64_t& interactionId)
    {
        auto conversationIdx = pimpl_->indexOf(convId);
        if (conversationIdx == -1) {
            return;
        }
        bool emitUpdated = false;
        interaction::Info itCopy;
        {
            std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
            auto& interactions = pimpl_->conversations[conversationIdx].interactions;
            auto it = interactions.find(interactionId);
            if (it != interactions.end()) {
                emitUpdated = true;
                if (it->second.isRead) {
                    return;
                }
                it->second.isRead = true;
                if (pimpl_->conversations[conversationIdx].unreadMessages != 0)
                    pimpl_->conversations[conversationIdx].unreadMessages -= 1;
                itCopy = it->second;
            }
        }
        if (emitUpdated) {
            pimpl_->dirtyConversations = {true, true};
            storage::setInteractionRead(pimpl_->db, interactionId);
            emit interactionStatusUpdated(convId, interactionId, itCopy);
            emit pimpl_->behaviorController.newReadInteraction(owner.id, convId, interactionId);
        }
    }
    
    void
    ConversationModel::clearUnreadInteractions(const std::string& convId) {
        auto conversationIdx = pimpl_->indexOf(convId);
        if (conversationIdx == -1) {
            return;
        }
        bool emitUpdated = false;
        {
            std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
            auto& interactions = pimpl_->conversations[conversationIdx].interactions;
            std::for_each(interactions.begin(), interactions.end(),
                          [&] (decltype(*interactions.begin())& it) {
                              if (!it.second.isRead) {
                                  emitUpdated = true;
                                  it.second.isRead = true;
                                  storage::setInteractionRead(pimpl_->db, it.first);
                              }
                          });
        }
        if (emitUpdated) {
            pimpl_->conversations[conversationIdx].unreadMessages = 0;
            pimpl_->dirtyConversations = {true, true};
            emit conversationUpdated(convId);
        }
    }
    
    ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
                                                   Lrc& lrc,
                                                   Database& db,
                                                   const CallbacksHandler& callbacksHandler,
                                                   const BehaviorController& behaviorController)
    : linked(linked)
    , lrc {lrc}
    , db(db)
    , callbacksHandler(callbacksHandler)
    , typeFilter(profile::Type::INVALID)
    , customTypeFilter(profile::Type::INVALID)
    , behaviorController(behaviorController)
    {
        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::slotUpdateInteractionStatus);
    
        // Call related
        connect(&*linked.owner.callModel, &NewCallModel::newIncomingCall,
                this, &ConversationModelPimpl::slotIncomingCall);
        connect(&*linked.owner.contactModel, &ContactModel::incomingCallFromPending,
                this, &ConversationModelPimpl::slotIncomingCall);
        connect(&*linked.owner.callModel,
                &lrc::api::NewCallModel::callStatusChanged,
                this,
                &ConversationModelPimpl::slotCallStatusChanged);
        connect(&*linked.owner.callModel,
                &lrc::api::NewCallModel::callStarted,
                this,
                &ConversationModelPimpl::slotCallStarted);
        connect(&*linked.owner.callModel,
                &lrc::api::NewCallModel::callEnded,
                this,
                &ConversationModelPimpl::slotCallEnded);
        connect(&*linked.owner.callModel,
                &lrc::api::NewCallModel::callAddedToConference,
                this,
                &ConversationModelPimpl::slotCallAddedToConference);
        connect(&callbacksHandler,
                &CallbacksHandler::conferenceRemoved,
                this,
                &ConversationModelPimpl::slotConferenceRemoved);
    
        // 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);
    }
    
    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::slotUpdateInteractionStatus);
    
        // Call related
        disconnect(&*linked.owner.callModel, &NewCallModel::newIncomingCall,
                   this, &ConversationModelPimpl::slotIncomingCall);
        disconnect(&*linked.owner.contactModel, &ContactModel::incomingCallFromPending,
                   this, &ConversationModelPimpl::slotIncomingCall);
        disconnect(&*linked.owner.callModel, &lrc::api::NewCallModel::callStatusChanged,
                   this, &ConversationModelPimpl::slotCallStatusChanged);
        disconnect(&*linked.owner.callModel, &lrc::api::NewCallModel::callStarted,
                   this, &ConversationModelPimpl::slotCallStarted);
        disconnect(&*linked.owner.callModel, &lrc::api::NewCallModel::callEnded,
                   this, &ConversationModelPimpl::slotCallEnded);
        disconnect(&*linked.owner.callModel, &lrc::api::NewCallModel::callAddedToConference,
                   this, &ConversationModelPimpl::slotCallAddedToConference);
        disconnect(&callbacksHandler, &CallbacksHandler::conferenceRemoved,
                   this, &ConversationModelPimpl::slotConferenceRemoved);
    
        // 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);
    }
    
    void
    ConversationModelPimpl::initConversations()
    {
        const MapStringString accountDetails = ConfigurationManager::instance().getAccountDetails(linked.owner.id.c_str());
        if (accountDetails.empty())
            return;
    
        // Fill conversations
        for (auto const& c : linked.owner.contactModel->getAllContacts())
        {
            auto conv = storage::getConversationsWithPeer(db, c.second.profileInfo.uri);
            if (conv.empty()) {
                // Can't find a conversation with this contact. Start it.
                auto newConversationsId = storage::beginConversationWithPeer(db, c.second.profileInfo.uri, c.second.isTrusted);
                conv.emplace_back(std::move(newConversationsId));
            }
    
            addConversationWith(conv[0], c.first);
    
            auto convIdx = indexOf(conv[0]);
    
            // Check if file transfer interactions were left in an incorrect state
            std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]);
            for(auto& interaction : conversations[convIdx].interactions) {
                if (interaction.second.status == interaction::Status::TRANSFER_CREATED
                    || interaction.second.status == interaction::Status::TRANSFER_AWAITING_HOST
                    || interaction.second.status == interaction::Status::TRANSFER_AWAITING_PEER
                    || interaction.second.status == interaction::Status::TRANSFER_ONGOING
                    || interaction.second.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, interaction.first, interaction::Status::TRANSFER_ERROR);
                    interaction.second.status = interaction::Status::TRANSFER_ERROR;
                }
            }
        }
    
        sortConversations();
        filteredConversations = conversations;
        dirtyConversations.first = false;
    
        // Load all non treated messages for this account
        QVector<Message> messages = ConfigurationManager::instance().getLastMessages(
            linked.owner.id.c_str(),
            storage::getLastTimestamp(db)
        );
        for (const auto& message : messages) {
            uint64_t timestamp = 0;
            try {
                timestamp = static_cast<uint64_t>(message.received);
            } catch (...) {}
            addIncomingMessage(message.from.toStdString(),
                               message.payloads["text/plain"].toStdString(),
                               timestamp);
        }
    }
    
    void
    ConversationModelPimpl::sortConversations()
    {
        std::sort(
            conversations.begin(), conversations.end(),
            [this](const auto& conversationA, const auto& conversationB)
            {
                // A or B is a temporary contact
                if (conversationA.participants.empty()) return true;
                if (conversationB.participants.empty()) return false;
    
                if (conversationA.uid == conversationB.uid) return false;
    
                auto& mtxA = interactionsLocks[conversationA.uid];
                auto& mtxB = interactionsLocks[conversationB.uid];
                std::lock(mtxA, mtxB);
                std::lock_guard<std::mutex> lockConvA(mtxA, std::adopt_lock);
                std::lock_guard<std::mutex> lockConvB(mtxB, std::adopt_lock);
    
                auto historyA = conversationA.interactions;
                auto historyB = conversationB.interactions;
                // A or B is a new conversation (without CONTACT interaction)
                if (conversationA.uid.empty() || conversationB.uid.empty()) return conversationA.uid.empty();
                if (historyA.empty() && historyB.empty()) {
                    // If no information to compare, sort by Ring ID
                    return conversationA.participants.front() > conversationB.participants.front();
                }
                if (historyA.empty()) return false;
                if (historyB.empty()) return true;
                // Sort by last Interaction
                try
                {
                    auto lastMessageA = historyA.at(conversationA.lastMessageUid);
                    auto lastMessageB = historyB.at(conversationB.lastMessageUid);
                    return lastMessageA.timestamp > lastMessageB.timestamp;
                }
                catch (const std::exception& e)
                {
                    qDebug() << "ConversationModel::sortConversations(), can't get lastMessage";
                    return false;
                }
            });
        dirtyConversations = {true, true};
    }
    
    void
    ConversationModelPimpl::sendContactRequest(const std::string& contactUri)
    {
        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);
    }
    
    void
    ConversationModelPimpl::slotContactAdded(const std::string& uri)
    {
        auto type = linked.owner.profileInfo.type;
        try {
            auto contact = linked.owner.contactModel->getContact(uri);
            type =  contact.profileInfo.type;
        } catch (...) {}
        profile::Info profileInfo{ uri, {}, {}, type };
        storage::createOrUpdateProfile(linked.owner.id, profileInfo, true);
        auto conv = storage::getConversationsWithPeer(db, uri);
        if (conv.empty()) {
            // pass conversation UID through only element
            conv.emplace_back(storage::beginConversationWithPeer(db, uri));
        }
        // Add the conversation if not already here
        if (indexOf(conv[0]) == -1) {
            addConversationWith(conv[0], uri);
            emit linked.newConversation(conv[0]);
        }
    
        // delete temporary conversation if it exists and it has the uri of the added contact as uid
        if (indexOf(uri) >= 0) {
            conversations.erase(conversations.begin() + indexOf(uri));
        }
    
        sortConversations();
        emit linked.conversationReady(uri);
        emit linked.modelSorted();
    }
    
    void
    ConversationModelPimpl::slotPendingContactAccepted(const std::string& uri)
    {
        auto type = linked.owner.profileInfo.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()) {
            convs.emplace_back(storage::beginConversationWithPeer(db, uri));
        } else {
            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]);
                {
                    std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]);
                    conversations[convIdx].interactions.emplace(msgId, interaction);
                }
                dirtyConversations = {true, true};
                emit linked.newInteraction(convs[0], msgId, interaction);
            } catch (std::out_of_range& e) {
                qDebug() << "ConversationModelPimpl::slotContactAdded can't find contact";
            }
        }
    }
    
    void
    ConversationModelPimpl::slotContactRemoved(const std::string& uri)
    {
        auto conversationIdx = indexOfContact(uri);
        if (conversationIdx == -1) {
            qDebug() << "ConversationModelPimpl::slotContactRemoved, but conversation not found";
            return; // Not a contact
        }
        auto& conversationUid = conversations[conversationIdx].uid;
        conversations.erase(conversations.begin() + conversationIdx);
        dirtyConversations = {true, true};
        emit linked.conversationRemoved(conversationUid);
        emit linked.modelSorted();
    }
    
    void
    ConversationModelPimpl::slotContactModelUpdated(const std::string& uri, bool needsSorted)
    {
        // We don't create newConversationItem if we already filter on pending
        conversation::Info newConversationItem;
        if (!filter.empty()) {
            // Create a conversation with the temporary item
            conversation::Info conversationInfo;
            auto& temporaryContact = linked.owner.contactModel->getContact("");
            conversationInfo.uid = temporaryContact.profileInfo.uri;
            conversationInfo.participants.emplace_back("");
            conversationInfo.accountId = linked.owner.id;
    
            // if temporary contact is already present, its alias is not empty (namely "Searching ..."),
            // or its registeredName is set because it was found on the nameservice.
            if (not temporaryContact.profileInfo.alias.empty() || not temporaryContact.registeredName.empty()) {
                if (!conversations.empty()) {
                    auto firstContactUri = conversations.front().participants.front();
                    //if first conversation has uri it is already a contact
                    // then we must add temporary item
                    if (not firstContactUri.empty()) {
                        conversations.emplace_front(conversationInfo);
                    } else if (not conversationInfo.uid.empty()) {
                        // If firstContactUri is empty it means that we have to update
                        // this element as it is the temporary.
                        // Only when we have found an uri.
                        conversations.front() = conversationInfo;
                    } else if (not conversations.front().uid.empty()) {
                        //update conversation when uri not found
                        //but conversation have uri from previous search
                        conversations.front() = conversationInfo;
                    }
                } else {
                    // no conversation, add temporaryItem
                    conversations.emplace_front(conversationInfo);
                }
                dirtyConversations = {true, true};
                if (needsSorted) {
                    emit linked.modelSorted();
                } else {
                    emit linked.conversationUpdated(conversations.front().uid);
                }
                return;
            }
        } else {
            // No filter, so we can remove the newConversationItem
            if (!conversations.empty()) {
                auto firstContactUri = conversations.front().participants.front();
    
                if (firstContactUri.empty() && needsSorted) {
                    conversations.pop_front();
                    dirtyConversations = {true, true};
                    emit linked.modelSorted();
                    return;
                }
            }
        }
        // trigger dirtyConversation in all cases to flush emptied temporary element due to filtered contact present in list
        // TL:DR : avoid duplicates and empty elements
        dirtyConversations = {true, true};
        int index = indexOfContact(uri);
        if (index != -1) {
            if (!conversations.empty() && conversations.front().participants.front().empty() &&
                needsSorted) {
                // In this case, contact is present in list, so temporary item does not longer exists
                emit linked.modelSorted();
            } else {
                // In this case, a presence is updated
                emit linked.conversationUpdated(conversations.at(index).uid);
            }
        }
    }
    
    void
    ConversationModelPimpl::addConversationWith(const std::string& convId,
                                                const std::string& contactUri)
    {
        conversation::Info conversation;
        conversation.uid = convId;
        conversation.accountId = linked.owner.id;
        conversation.participants = {contactUri};
        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);
        std::vector<std::function<void(void)>> updateSlots;
        {
            std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
            for (auto& interaction: conversation.interactions) {
                if (interaction.second.status != interaction::Status::SENDING) {
                    continue;
                }
                // Get the message status from daemon, else unknown
                auto id = storage::getDaemonIdByInteractionId(db, std::to_string(interaction.first));
                int status = 0;
                if (id.empty()) {
                    continue;
                }
                try {
                    auto msgId = std::stoull(id);
                    status = ConfigurationManager::instance().getMessageStatus(msgId);
                    updateSlots.emplace_back(
                        [this, msgId, contactUri, status]() -> void {
                            auto accId = linked.owner.id;
                            slotUpdateInteractionStatus(accId, msgId, contactUri, status);
                        });
                } catch (const std::exception& e) {
                    qDebug() << "message id was invalid";
                }
            }
        }
        for (const auto& s: updateSlots) { s(); }
    
        conversation.unreadMessages = getNumberOfUnreadMessagesFor(convId);
        conversations.emplace_back(conversation);
        dirtyConversations = {true, true};
    }
    
    int
    ConversationModelPimpl::indexOf(const std::string& uid) const
    {
        for (unsigned int i = 0; i < conversations.size(); ++i) {
            if (conversations.at(i).uid == uid) return i;
        }
        return -1;
    }
    
    int
    ConversationModelPimpl::indexOfContact(const std::string& uri) const
    {
        for (unsigned int i = 0; i < conversations.size(); ++i) {
            if (conversations.at(i).participants.front() == uri) return i;
        }
        return -1;
    }
    
    void
    ConversationModelPimpl::slotIncomingCall(const std::string& fromId, const std::string& callId)
    {
        auto conversationIdx = indexOfContact(fromId);
    
        if (conversationIdx == -1) {
            qDebug() << "ConversationModelPimpl::slotIncomingCall, but conversation not found";
            return; // Not a contact
        }
    
        auto& conversation = conversations.at(conversationIdx);
    
        qDebug() << "Add call to conversation with " << fromId.c_str();
        conversation.callId = callId;
        dirtyConversations = {true, true};
        emit behaviorController.showIncomingCallView(linked.owner.id, conversation);
    }
    
    void
    ConversationModelPimpl::slotCallStatusChanged(const std::string& 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;
            });
    
        if (i == conversations.end()) {
            // In this case, the user didn't pass through placeCall
            // This means that a participant was invited to a call
            // or a call was placed via dbus.
            // We have to update the model
            try {
                auto call = linked.owner.callModel->getCall(callId);
                for (auto& conversation: conversations) {
                    if (conversation.participants.front() == call.peerUri) {
                        conversation.callId = callId;
                        dirtyConversations = {true, true};
                        emit linked.conversationUpdated(conversation.uid);
                    }
                }
            } catch (std::out_of_range& e) {
                qDebug() << "ConversationModelPimpl::slotCallStatusChanged can't get inexistant call";
            }
            return;
        }
    }
    
    void
    ConversationModelPimpl::slotCallStarted(const std::string& callId)
    {
    
        try {
            auto call = linked.owner.callModel->getCall(callId);
            addOrUpdateCallMessage(callId, (!call.isOutgoing ? call.peerUri : ""));
        } catch (std::out_of_range& e) {
            qDebug() << "ConversationModelPimpl::slotCallStarted can't start inexistant call";
        }
    }
    
    void
    ConversationModelPimpl::slotCallEnded(const std::string& 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.isOutgoing ? call.peerUri : ""), 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
                    dirtyConversations = {true, true};
                    emit linked.conversationUpdated(conversation.uid);
                }
        } catch (std::out_of_range& e) {
            qDebug() << "ConversationModelPimpl::slotCallEnded can't end inexistant call";
        }
    }
    
    void
    ConversationModelPimpl::addOrUpdateCallMessage(const std::string& callId,
                                                   const std::string& from,
                                                   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()) {
            return;
        }
        auto uid = conv_it->uid;
        std::string uriString = storage::prepareUri(from, linked.owner.profileInfo.type);
        auto msg = interaction::Info { uriString, {}, std::time(nullptr), duration,
                                     interaction::Type::CALL, interaction::Status::SUCCESS, true };
        // update the db
        int msgId = storage::addOrUpdateMessage(db, conv_it->uid, msg, callId);
        // now set the formatted call message string in memory only
        msg.body = storage::getCallInteractionString(uriString, duration);
        auto newInteraction = conv_it->interactions.find(msgId) == conv_it->interactions.end();
        if (newInteraction) {
            conv_it->lastMessageUid = msgId;
            std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]);
            conv_it->interactions.emplace(msgId, msg);
        } else {
            std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]);
            conv_it->interactions[msgId] = msg;
        }
        dirtyConversations = { true, true };
        if (newInteraction)
            emit linked.newInteraction(conv_it->uid, msgId, msg);
        else
            emit linked.interactionStatusUpdated(conv_it->uid, msgId, msg);
        sortConversations();
        emit linked.modelSorted();
    }
    
    void
    ConversationModelPimpl::slotNewAccountMessage(std::string& accountId,
                                                  std::string& from,
                                                  std::map<std::string,std::string> payloads)
    {
        if (accountId != linked.owner.id)
            return;
    
        for (const auto &payload : payloads) {
            if (payload.first.find("text/plain") != std::string::npos) {
                addIncomingMessage(from, payload.second);
            }
        }
    }
    
    void
    ConversationModelPimpl::slotIncomingCallMessage(const std::string& callId, const std::string& from, const std::string& body)
    {
        if (not 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);
        }
    
    }
    
    void
    ConversationModelPimpl::addIncomingMessage(const std::string& from,
                                               const std::string& body,
                                               const uint64_t& timestamp)
    {
        auto convIds = storage::getConversationsWithPeer(db, from);
        if (convIds.empty()) {
            convIds.emplace_back(storage::beginConversationWithPeer(db, from, false));
        }
        auto msg = interaction::Info { from, 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);
        auto conversationIdx = indexOf(convIds[0]);
        // Add the conversation if not already here
        if (conversationIdx == -1) {
            addConversationWith(convIds[0], from);
            emit linked.newConversation(convIds[0]);
        } else {
            {
                std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
                conversations[conversationIdx].interactions.emplace(msgId, msg);
            }
            conversations[conversationIdx].lastMessageUid = msgId;
            conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convIds[0]);
        }
        dirtyConversations = {true, true};
        emit behaviorController.newUnreadInteraction(linked.owner.id, convIds[0], msgId, msg);
        emit linked.newInteraction(convIds[0], msgId, msg);
        sortConversations();
        emit linked.modelSorted();
    }
    
    void
    ConversationModelPimpl::slotCallAddedToConference(const std::string& callId, const std::string& confId)
    {
        for (auto& conversation: conversations) {
            if (conversation.callId == callId || conversation.confId == confId) {
                conversation.confId = confId;
                dirtyConversations = {true, true};
                emit linked.selectConversation(conversation.uid);
            }
        }
    }
    
    void
    ConversationModelPimpl::slotUpdateInteractionStatus(const std::string& accountId,
                                                        const uint64_t daemon_id,
                                                        const std::string& peer_uri,
                                                        int status)
    {
        if (accountId != linked.owner.id) {
            return;
        }
        auto newStatus = interaction::Status::INVALID;
        switch (static_cast<DRing::Account::MessageStates>(status))
        {
        case DRing::Account::MessageStates::SENDING:
            newStatus = interaction::Status::SENDING;
            break;
        case DRing::Account::MessageStates::CANCELLED:
            newStatus = interaction::Status::TRANSFER_CANCELED;
            break;
        case DRing::Account::MessageStates::SENT:
        case DRing::Account::MessageStates::READ:
            newStatus = interaction::Status::SUCCESS;
            break;
        case DRing::Account::MessageStates::FAILURE:
            newStatus = interaction::Status::FAILURE;
            break;
        case DRing::Account::MessageStates::UNKNOWN:
        default:
            newStatus = interaction::Status::UNKNOWN;
            break;
        }
        // Update database
        auto interactionId = storage::getInteractionIdByDaemonId(db, std::to_string(daemon_id));
        if (interactionId.empty()) {
            return;
        }
        auto msgId = std::stoull(interactionId);
        storage::updateInteractionStatus(db, msgId, newStatus);
        // Update conversations
        auto convIds = storage::getConversationsWithPeer(db, peer_uri);
        if (!convIds.empty()) {
            auto conversationIdx = indexOf(convIds[0]);
            interaction::Info itCopy;
            bool emitUpdated = false;
            if (conversationIdx != -1) {
                std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
                auto& interactions = conversations[conversationIdx].interactions;
                auto it = interactions.find(msgId);
                if (it != interactions.end()) {
                    it->second.status = newStatus;
                    emitUpdated = true;
                    itCopy = it->second;
                }
            }
            if (emitUpdated) {
                dirtyConversations = { true, true };
                emit linked.interactionStatusUpdated(convIds[0], msgId, itCopy);
            }
        }
    }
    
    void
    ConversationModelPimpl::slotConferenceRemoved(const std::string& confId)
    {
        // Get conversation
        for(auto& i : conversations) {
            if (i.confId == confId) {
                i.confId = "";
            }
        }
    }
    
    int
    ConversationModelPimpl::getNumberOfUnreadMessagesFor(const std::string& uid)
    {
        return storage::countUnreadFromInteractions(db, uid);
    }
    
    void
    ConversationModel::sendFile(const std::string& convUid,
                                const std::string& path,
                                const std::string& filename)
    {
        auto conversationIdx = pimpl_->indexOf(convUid);
        if (conversationIdx == -1 || !owner.enabled)
            return;
    
        const auto peerUri = pimpl_->conversations[conversationIdx].participants.front();
        bool isTemporary = peerUri.empty();
    
        /* 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(peerUri);
    
        auto cb = std::function<void(std::string)>(
            [this, isTemporary, peerUri, path, filename](std::string convId) {
                int contactIndex;
                if (isTemporary && (contactIndex = pimpl_->indexOfContact(convId)) < 0) {
                    qDebug() << "Can't send file: Other participant is not a contact (removed while sending file ?)";
                    return;
                }
    
                // Retrieve final peer uri after creation of the conversation
                const auto& newPeerUri = isTemporary ? pimpl_->conversations.at(contactIndex).participants.front() : peerUri;
    
                auto contactInfo = owner.contactModel->getContact(newPeerUri);
                if (contactInfo.isBanned) {
                    qDebug() << "ContactModel::sendFile: denied, contact is banned";
                    return;
                }
    
                pimpl_->lrc.getDataTransferModel().sendFile(owner.id.c_str(),
                                                            newPeerUri.c_str(),
                                                            path.c_str(),
                                                            filename.c_str());
            });
    
        if (isTemporary) {
            QMetaObject::Connection* const connection = new QMetaObject::Connection;
            *connection = connect(this, &ConversationModel::conversationReady,
                [cb, connection](std::string convId) {
                    cb(convId);
                    QObject::disconnect(*connection);
                    if (connection) {
                        delete connection;
                    }
                });
        } else {
            cb(convUidCopy);
        }
    }
    
    void
    ConversationModel::acceptTransfer(const std::string& convUid, uint64_t interactionId, const std::string& path)
    {
        pimpl_->acceptTransfer(convUid, interactionId, path);
    }
    
    void
    ConversationModel::cancelTransfer(const std::string& convUid, uint64_t interactionId)
    {
        // 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);
        interaction::Info itCopy;
        bool emitUpdated = false;
        if (conversationIdx != -1) {
            std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convUid]);
            auto& interactions = pimpl_->conversations[conversationIdx].interactions;
            auto it = interactions.find(interactionId);
            if (it != interactions.end()) {
                it->second.status = interaction::Status::TRANSFER_CANCELED;
    
                // update information in the db
                storage::updateInteractionStatus(pimpl_->db, interactionId, interaction::Status::TRANSFER_CANCELED);
                emitUpdated = true;
                itCopy = it->second;
            }
        }
        if (emitUpdated) {
            // Forward cancel action to daemon (will invoke slotTransferStatusCanceled)
            pimpl_->lrc.getDataTransferModel().cancel(interactionId);
            pimpl_->dirtyConversations = {true, true};
            emit interactionStatusUpdated(convUid, interactionId, itCopy);
            emit pimpl_->behaviorController.newReadInteraction(owner.id, convUid, interactionId);
        }
    }
    
    void
    ConversationModel::getTransferInfo(uint64_t interactionId, datatransfer::Info& info)
    {
        try {
            auto dringId = pimpl_->lrc.getDataTransferModel().getDringIdFromInteractionId(interactionId);
            pimpl_->lrc.getDataTransferModel().transferInfo(dringId, info);
        } catch (...) {
            info.status = datatransfer::Status::INVALID;
        }
    }
    
    int
    ConversationModel::getNumberOfUnreadMessagesFor(const std::string& convUid)
    {
        return pimpl_->getNumberOfUnreadMessagesFor(convUid);
    }
    
    bool
    ConversationModelPimpl::usefulDataFromDataTransfer(long long dringId, const datatransfer::Info&,
                                                       int& interactionId, std::string& convId)
    {
        try {
            interactionId = lrc.getDataTransferModel().getInteractionIdFromDringId(dringId);
        } catch (const std::out_of_range& e) {
            qWarning() << "Couldn't get interaction from daemon Id: " << dringId;
            return false;
        }
    
        convId = storage::conversationIdFromInteractionId(db, interactionId);
        return true;
    }
    
    void
    ConversationModelPimpl::slotTransferStatusCreated(long long dringId, 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.c_str());
        if (accountDetails.empty())
            return;
        // create a new conversation if needed
        auto convIds = storage::getConversationsWithPeer(db, info.peerUri);
        if (convIds.empty()) {
            convIds.emplace_back(storage::beginConversationWithPeer(db, info.peerUri, false));
        }
    
        // add interaction to the db
        const auto& convId = convIds[0];
        auto interactionId = storage::addDataTransferToConversation(db, convId, info);
    
        // map dringId and interactionId for latter retrivial from client (that only known the interactionId)
        lrc.getDataTransferModel().registerTransferId(dringId, 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);
            emit linked.newConversation(convId);
        } else {
            {
                std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
                conversations[conversationIdx].interactions.emplace(interactionId, interaction);
            }
            conversations[conversationIdx].lastMessageUid = interactionId;
            conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convId);
        }
        dirtyConversations = {true, true};
        emit behaviorController.newUnreadInteraction(linked.owner.id, convId, interactionId, interaction);
        emit linked.newInteraction(convId, interactionId, interaction);
        sortConversations();
        emit linked.modelSorted();
    }
    
    void
    ConversationModelPimpl::slotTransferStatusAwaitingPeer(long long dringId, datatransfer::Info info)
    {
        updateTransferStatus(dringId, info, interaction::Status::TRANSFER_AWAITING_PEER);
    }
    
    void
    ConversationModelPimpl::slotTransferStatusAwaitingHost(long long dringId, datatransfer::Info info)
    {
        int interactionId;
        std::string convId;
        if (not usefulDataFromDataTransfer(dringId, info, interactionId, convId))
            return;
    
        auto newStatus = interaction::Status::TRANSFER_AWAITING_HOST;
        storage::updateInteractionStatus(db, interactionId, newStatus);
    
        auto conversationIdx = indexOf(convId);
        if (conversationIdx != -1) {
            bool emitUpdated = false;
            interaction::Info itCopy;
            {
                std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
                auto& interactions = conversations[conversationIdx].interactions;
                auto it = interactions.find(interactionId);
                if (it != interactions.end()) {
                    emitUpdated = true;
                    it->second.status = newStatus;
                    itCopy = it->second;
                }
            }
            if (emitUpdated) {
                dirtyConversations = {true, true};
                emit linked.interactionStatusUpdated(convId, interactionId, itCopy);
                // If it's an accepted file type and less than 20 MB, accept transfer.
                auto extensionIdx = info.displayName.find_last_of(".");
                if (extensionIdx == std::string::npos) return;
                auto extension = info.displayName.substr(extensionIdx);
                try {
                    auto contactInfo = linked.owner.contactModel->getContact(conversations[conversationIdx].participants.front());
                    // Only accept if contact is added
                    if (contactInfo.profileInfo.type != profile::Type::RING) return;
                } catch (...) {
                    return;
                }
                std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
                auto destinationDir = lrc.getDataTransferModel().downloadDirectory;
                if (info.totalSize < 20 * 1024 * 1024 && !destinationDir.empty()) {
                    auto wantedFilename = destinationDir + info.displayName;
                    auto duplicate = 0;
                    while (std::ifstream(wantedFilename).good()) {
                        ++duplicate;
                        wantedFilename = destinationDir + info.displayName.substr(0, extensionIdx) + " (" + std::to_string(duplicate) + ")" + extension;
                    }
                    acceptTransfer(convId, interactionId, wantedFilename);
                }
            }
        }
    }
    
    void
    ConversationModelPimpl::acceptTransfer(const std::string& convUid, uint64_t interactionId, const std::string& path)
    {
        lrc.getDataTransferModel().accept(interactionId, path, 0);
        storage::updateInteractionBody(db, interactionId, path);
        storage::updateInteractionStatus(db, interactionId, interaction::Status::TRANSFER_ACCEPTED);
    
        // prepare interaction Info and emit signal for the client
        auto conversationIdx = indexOf(convUid);
        interaction::Info itCopy;
        bool emitUpdated = false;
        if (conversationIdx != -1) {
            std::lock_guard<std::mutex> lk(interactionsLocks[convUid]);
            auto& interactions = conversations[conversationIdx].interactions;
            auto it = interactions.find(interactionId);
            if (it != interactions.end()) {
                it->second.body = path;
                it->second.status = interaction::Status::TRANSFER_ACCEPTED;
                emitUpdated = true;
                itCopy = it->second;
            }
        }
        if (emitUpdated) {
            sendContactRequest(conversations[conversationIdx].participants.front());
            dirtyConversations = {true, true};
            emit linked.interactionStatusUpdated(convUid, interactionId, itCopy);
            emit behaviorController.newReadInteraction(linked.owner.id, convUid, interactionId);
        }
    }
    
    void
    ConversationModelPimpl::slotTransferStatusOngoing(long long dringId, datatransfer::Info info)
    {
        int interactionId;
        std::string convId;
        if (not usefulDataFromDataTransfer(dringId, info, interactionId, convId))
            return;
    
        auto newStatus = interaction::Status::TRANSFER_ONGOING;
        storage::updateInteractionStatus(db, interactionId, newStatus);
    
        auto conversationIdx = indexOf(convId);
        if (conversationIdx != -1) {
            bool emitUpdated = false;
            interaction::Info itCopy;
            {
                std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
                auto& interactions = conversations[conversationIdx].interactions;
                auto it = interactions.find(interactionId);
                if (it != interactions.end()) {
                    emitUpdated = true;
                    it->second.status = newStatus;
                    itCopy = it->second;
                }
            }
            if (emitUpdated) {
                auto* timer = new QTimer();
                connect(timer, &QTimer::timeout,
                        [=] { updateTransfer(timer, convId, conversationIdx, interactionId); });
                timer->start(1000);
                dirtyConversations = {true, true};
                emit linked.interactionStatusUpdated(convId, interactionId, itCopy);
            }
        }
    }
    
    void
    ConversationModelPimpl::slotTransferStatusFinished(long long dringId, datatransfer::Info info)
    {
        int interactionId;
        std::string convId;
        if (not usefulDataFromDataTransfer(dringId, info, interactionId, convId))
            return;
    
        // prepare interaction Info and emit signal for the client
        auto conversationIdx = indexOf(convId);
        if (conversationIdx != -1) {
            bool emitUpdated = false;
            auto newStatus = interaction::Status::TRANSFER_FINISHED;
            interaction::Info itCopy;
            {
                std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
                auto& interactions = conversations[conversationIdx].interactions;
                auto it = interactions.find(interactionId);
                if (it != interactions.end()) {
                    // We need to check if current status is ONGOING as CANCELED must not be transformed into FINISHED
                    if (it->second.status == interaction::Status::TRANSFER_ONGOING) {
                        emitUpdated = true;
                        it->second.status = newStatus;
                        itCopy = it->second;
                    }
                }
            }
            if (emitUpdated) {
                dirtyConversations = {true, true};
                storage::updateInteractionStatus(db, interactionId, newStatus);
                emit linked.interactionStatusUpdated(convId, interactionId, itCopy);
            }
        }
    }
    
    void
    ConversationModelPimpl::slotTransferStatusCanceled(long long dringId, datatransfer::Info info)
    {
        updateTransferStatus(dringId, info, interaction::Status::TRANSFER_CANCELED);
    }
    
    void
    ConversationModelPimpl::slotTransferStatusError(long long dringId, datatransfer::Info info)
    {
        updateTransferStatus(dringId, info, interaction::Status::TRANSFER_ERROR);
    }
    
    void
    ConversationModelPimpl::slotTransferStatusUnjoinable(long long dringId, datatransfer::Info info)
    {
        updateTransferStatus(dringId, info, interaction::Status::TRANSFER_UNJOINABLE_PEER);
    }
    
    void
    ConversationModelPimpl::slotTransferStatusTimeoutExpired(long long dringId, datatransfer::Info info)
    {
        updateTransferStatus(dringId, info, interaction::Status::TRANSFER_TIMEOUT_EXPIRED);
    }
    
    void
    ConversationModelPimpl::updateTransferStatus(long long dringId, datatransfer::Info info, interaction::Status newStatus)
    {
        int interactionId;
        std::string convId;
        if (not usefulDataFromDataTransfer(dringId, info, interactionId, convId))
            return;
    
        // update information in the db
        storage::updateInteractionStatus(db, interactionId, newStatus);
    
        // prepare interaction Info and emit signal for the client
        auto conversationIdx = indexOf(convId);
        if (conversationIdx != -1) {
            bool emitUpdated = false;
            interaction::Info itCopy;
            {
                std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
                auto& interactions = conversations[conversationIdx].interactions;
                auto it = interactions.find(interactionId);
                if (it != interactions.end()) {
                    emitUpdated = true;
                    it->second.status = newStatus;
                    itCopy = it->second;
                }
            }
            if (emitUpdated) {
                dirtyConversations = {true, true};
                emit linked.interactionStatusUpdated(convId, interactionId, itCopy);
            }
        }
    }
    
    void
    ConversationModelPimpl::updateTransfer(QTimer* timer, const std::string& conversation,
                                           int conversationIdx, int interactionId)
    {
        try {
            bool emitUpdated = false;
            interaction::Info itCopy;
            {
                std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
                const auto& interactions = conversations[conversationIdx].interactions;
                const auto& it = interactions.find(interactionId);
                if (it != std::cend(interactions)
                    and it->second.status == interaction::Status::TRANSFER_ONGOING) {
                    emitUpdated = true;
                    itCopy = it->second;
                }
            }
            if (emitUpdated) {
                emit linked.interactionStatusUpdated(conversation, interactionId, itCopy);
                return;
            }
        } catch (...) {}
    
        timer->stop();
        delete timer;
    }
    
    } // namespace lrc
    
    #include "api/moc_conversationmodel.cpp"
    #include "conversationmodel.moc"