Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
messagesadapter.cpp 21.83 KiB
/*
 * Copyright (C) 2020 by Savoir-faire Linux
 * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
 * Author: Anthony L�onard <anthony.leonard@savoirfairelinux.com>
 * Author: Olivier Soldano <olivier.soldano@savoirfairelinux.com>
 * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
 * Author: Isa Nanic <isa.nanic@savoirfairelinux.com>
 * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "messagesadapter.h"
#include "webchathelpers.h"

#include "utils.h"

#include <QDesktopServices>
#include <QFileInfo>
#include <QImageReader>
#include <QList>
#include <QUrl>

MessagesAdapter::MessagesAdapter(QObject *parent)
    : QmlAdapterBase(parent)
{}

MessagesAdapter::~MessagesAdapter() {}

void
MessagesAdapter::initQmlObject() {
    connect(&LRCInstance::instance(),
            &LRCInstance::currentAccountChanged,
            [this](){
                connectConversationModel();
            });
    connectConversationModel();
}

void
MessagesAdapter::setupChatView(const QString &uid)
{

    auto* convModel = LRCInstance::getCurrentConversationModel();
    if (convModel == nullptr) {
        return;
    }
    const auto &convInfo = convModel->getConversationForUID(uid);
    if (convInfo.uid.isEmpty() || convInfo.participants.isEmpty()) {
        return;
    }

    QString contactURI = convInfo.participants.at(0);

    bool isContact = false;
    auto selectedAccountId = LRCInstance::getCurrAccId();
    auto &accountInfo = LRCInstance::accountModel().getAccountInfo(selectedAccountId);

    lrc::api::profile::Type contactType;
    try {
        auto contactInfo = accountInfo.contactModel->getContact(contactURI);
        if (contactInfo.isTrusted) {
            isContact = true;
        }
        contactType = contactInfo.profileInfo.type;
    } catch (...) {
    }

    bool shouldShowSendContactRequestBtn = !isContact
                                           && contactType != lrc::api::profile::Type::SIP;

    QMetaObject::invokeMethod(qmlObj_,
                              "setSendContactRequestButtonVisible",
                              Q_ARG(QVariant, shouldShowSendContactRequestBtn));

    setMessagesVisibility(false);

    /*
     * Type Indicator (contact).
     */
    contactIsComposing(convInfo.uid, "", false);
    connect(LRCInstance::getCurrentConversationModel(),
            &ConversationModel::composingStatusChanged,
            [this](const QString &uid, const QString &contactUri, bool isComposing) {
                contactIsComposing(uid, contactUri, isComposing);
            });

    /*
     * Draft and message content set up.
     */
    Utils::oneShotConnect(qmlObj_,
                          SIGNAL(sendMessageContentSaved(const QString &)),
                          this,
                          SLOT(slotSendMessageContentSaved(const QString &)));

    requestSendMessageContent();
}

void
MessagesAdapter::connectConversationModel()
{
    auto currentConversationModel = LRCInstance::getCurrentConversationModel();

    QObject::disconnect(newInteractionConnection_);
    QObject::disconnect(interactionRemovedConnection_);
    QObject::disconnect(interactionStatusUpdatedConnection_);

    newInteractionConnection_ = QObject::connect(currentConversationModel,
                                                 &lrc::api::ConversationModel::newInteraction,
                                                 [this](const QString &convUid, uint64_t interactionId,
                                                        const lrc::api::interaction::Info &interaction) {
        auto accountId = LRCInstance::getCurrAccId();
        newInteraction(accountId, convUid, interactionId, interaction);
    });

    interactionStatusUpdatedConnection_ = QObject::connect(currentConversationModel,
                                                           &lrc::api::ConversationModel::interactionStatusUpdated,
                                                           [this](const QString &convUid, uint64_t interactionId,
                                                                  const lrc::api::interaction::Info &interaction) {
        auto currentConversationModel = LRCInstance::getCurrentConversationModel();
        currentConversationModel->clearUnreadInteractions(convUid);
        updateInteraction(*currentConversationModel, interactionId, interaction);
    });

    interactionRemovedConnection_
        = QObject::connect(currentConversationModel,
                           &lrc::api::ConversationModel::interactionRemoved,
                           [this](const QString &convUid, uint64_t interactionId) {
                               Q_UNUSED(convUid);
                               removeInteraction(interactionId);
                           });

    currentConversationModel->setFilter("");
}

void
MessagesAdapter::sendContactRequest()
{
    const auto convUid = LRCInstance::getCurrentConvUid();
    if (!convUid.isEmpty()) {
        LRCInstance::getCurrentConversationModel()->makePermanent(convUid);
    }
}

void
MessagesAdapter::updateConversationForAddedContact()
{
    auto* convModel = LRCInstance::getCurrentConversationModel();
    const auto conversation = convModel->getConversationForUID(LRCInstance::getCurrentConvUid());

    clear();
    setConversationProfileData(conversation);
    printHistory(*convModel, conversation.interactions);
}

void
MessagesAdapter::slotSendMessageContentSaved(const QString &content)
{
    if (!LastConvUid_.isEmpty()) {
        LRCInstance::setContentDraft(LastConvUid_, LRCInstance::getCurrAccId(), content);
    }
    LastConvUid_ = LRCInstance::getCurrentConvUid();

    Utils::oneShotConnect(qmlObj_, SIGNAL(messagesCleared()), this, SLOT(slotMessagesCleared()));

    setInvitation(false);
    clear();
    auto restoredContent = LRCInstance::getContentDraft(LRCInstance::getCurrentConvUid(),
                                                        LRCInstance::getCurrAccId());
    setSendMessageContent(restoredContent);
    emit needToUpdateSmartList();
}

void
MessagesAdapter::slotUpdateDraft(const QString &content)
{
    if (!LastConvUid_.isEmpty()) {
        LRCInstance::setContentDraft(LastConvUid_, LRCInstance::getCurrAccId(), content);
    }
    emit needToUpdateSmartList();
}

void
MessagesAdapter::slotMessagesCleared()
{
    auto* convModel = LRCInstance::getCurrentConversationModel();
    const auto convInfo = convModel->getConversationForUID(LRCInstance::getCurrentConvUid());

    printHistory(*convModel, convInfo.interactions);

    Utils::oneShotConnect(qmlObj_, SIGNAL(messagesLoaded()), this, SLOT(slotMessagesLoaded()));

    setConversationProfileData(convInfo);
}

void
MessagesAdapter::slotMessagesLoaded()
{
    setMessagesVisibility(true);
}

void
MessagesAdapter::sendMessage(const QString &message)
{
    try {
        const auto convUid = LRCInstance::getCurrentConvUid();
        LRCInstance::getCurrentConversationModel()->sendMessage(convUid, message);
    } catch (...) {
        qDebug() << "Exception during sendMessage:" << message;
    }
}

void
MessagesAdapter::sendImage(const QString &message)
{
    if (message.startsWith("data:image/png;base64,")) {
        /*
         * Img tag contains base64 data, trim "data:image/png;base64," from data.
         */
        QByteArray data = QByteArray::fromStdString(message.toStdString().substr(22));
        auto img_name_hash = QString::fromStdString(
            QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex().toStdString());
        QString fileName = "\\img_" + img_name_hash + ".png";

        QPixmap image_to_save;
        if (!image_to_save.loadFromData(QByteArray::fromBase64(data))) {
            qDebug().noquote() << "Errors during loadFromData"
                               << "\n";
        }

        QString path = QString(Utils::WinGetEnv("TEMP")) + fileName;
        if (!image_to_save.save(path, "PNG")) {
            qDebug().noquote() << "Errors during QPixmap save"
                               << "\n";
        }

        try {
            auto convUid = LRCInstance::getCurrentConvUid();
            LRCInstance::getCurrentConversationModel()->sendFile(convUid, path, fileName);
        } catch (...) {
            qDebug().noquote() << "Exception during sendFile - base64 img"
                               << "\n";
        }

    } else {
        /*
         * Img tag contains file paths.
         */

        QString msg(message);
#ifdef Q_OS_WIN
        msg = msg.replace("file:///", "");
#else
        msg = msg.replace("file:///", "/");
#endif
        QFileInfo fi(msg);
        QString fileName = fi.fileName();

        try {
            auto convUid = LRCInstance::getCurrentConvUid();
            LRCInstance::getCurrentConversationModel()->sendFile(convUid, msg, fileName);
        } catch (...) {
            qDebug().noquote() << "Exception during sendFile - image from path"
                               << "\n";
        }
    }
}

void
MessagesAdapter::sendFile(const QString &message)
{
    QFileInfo fi(message);
    QString fileName = fi.fileName();
    try {
        auto convUid = LRCInstance::getCurrentConvUid();
        LRCInstance::getCurrentConversationModel()->sendFile(convUid, message, fileName);
    } catch (...) {
        qDebug() << "Exception during sendFile";
    }
}

void
MessagesAdapter::retryInteraction(const QString &arg)
{
    bool ok;
    uint64_t interactionUid = arg.toULongLong(&ok);
    if (ok) {
        LRCInstance::getCurrentConversationModel()
            ->retryInteraction(LRCInstance::getCurrentConvUid(), interactionUid);
    } else {
        qDebug() << "retryInteraction - invalid arg" << arg;
    }
}

void
MessagesAdapter::setNewMessagesContent(const QString &path)
{
    if (path.length() == 0)
        return;
    QByteArray imageFormat = QImageReader::imageFormat(path);

    if (!imageFormat.isEmpty()) {
        setMessagesImageContent(path);
    } else {
        setMessagesFileContent(path);
    }
}

void
MessagesAdapter::deleteInteraction(const QString &arg)
{
    bool ok;
    uint64_t interactionUid = arg.toULongLong(&ok);
    if (ok) {
        LRCInstance::getCurrentConversationModel()
            ->clearInteractionFromConversation(LRCInstance::getCurrentConvUid(), interactionUid);
    } else {
        qDebug() << "DeleteInteraction - invalid arg" << arg;
    }
}

void
MessagesAdapter::openFile(const QString &arg)
{
    QUrl fileUrl("file:///" + arg);
    if (!QDesktopServices::openUrl(fileUrl)) {
        qDebug() << "Couldn't open file: " << fileUrl;
    }
}

void
MessagesAdapter::openUrl(const QString &url)
{
    if (!QDesktopServices::openUrl(url)) {
        qDebug() << "Couldn't open url: " << url;
    }
}

void
MessagesAdapter::acceptFile(const QString &arg)
{
    try {
        auto interactionUid = arg.toLongLong();
        auto convUid = LRCInstance::getCurrentConvUid();
        LRCInstance::getCurrentConversationModel()->acceptTransfer(convUid, interactionUid);
    } catch (...) {
        qDebug() << "JS bridging - exception during acceptFile: " << arg;
    }
}

void
MessagesAdapter::refuseFile(const QString &arg)
{
    try {
        auto interactionUid = arg.toLongLong();
        const auto convUid = LRCInstance::getCurrentConvUid();
        LRCInstance::getCurrentConversationModel()->cancelTransfer(convUid, interactionUid);
    } catch (...) {
        qDebug() << "JS bridging - exception during refuseFile:" << arg;
    }
}

void
MessagesAdapter::pasteKeyDetected()
{
    const QMimeData *mimeData = QApplication::clipboard()->mimeData();

    if (mimeData->hasImage()) {
        /*
         * Save temp data into base64 format.
         */
        QPixmap pixmap = qvariant_cast<QPixmap>(mimeData->imageData());
        QByteArray ba;
        QBuffer bu(&ba);
        bu.open(QIODevice::WriteOnly);
        pixmap.save(&bu, "PNG");
        auto str = QString::fromLocal8Bit(ba.toBase64());

        setMessagesImageContent(str, true);
    } else if (mimeData->hasUrls()) {
        QList<QUrl> urlList = mimeData->urls();
        /*
        * Extract the local paths of the files.
        */
        for (int i = 0; i < urlList.size(); ++i) {
            /*
             * Trim file:/// from url.
             */
            QString filePath = urlList.at(i).toString().remove(0, 8);
            QByteArray imageFormat = QImageReader::imageFormat(filePath);

            /*
             * Check if file is qt supported image file type.
             */
            if (!imageFormat.isEmpty()) {
                setMessagesImageContent(filePath);
            } else {
                setMessagesFileContent(filePath);
            }
        }
    } else {
        QMetaObject::invokeMethod(qmlObj_,
                                  "webViewRunJavaScript",
                                  Q_ARG(QVariant,
                                        QStringLiteral("replaceText(`%1`)").arg(mimeData->text())));
    }
}

void
MessagesAdapter::onComposing(bool isComposing)
{
    LRCInstance::getCurrentConversationModel()->setIsComposing(LRCInstance::getCurrentConvUid(),
                                                               isComposing);
}

void
MessagesAdapter::setConversationProfileData(const lrc::api::conversation::Info &convInfo)
{
    auto* convModel = LRCInstance::getCurrentConversationModel();
    auto accInfo = &LRCInstance::getCurrentAccountInfo();
    const auto conv = convModel->getConversationForUID(convInfo.uid);

    if (conv.participants.isEmpty()) {
        return;
    }

    auto contactUri = conv.participants.front();
    if (contactUri.isEmpty()) {
        return;
    }
    try {
        auto &contact = accInfo->contactModel->getContact(contactUri);
        auto bestName = Utils::bestNameForConversation(convInfo, *convModel);
        setInvitation(contact.profileInfo.type == lrc::api::profile::Type::PENDING,
                      bestName,
                      contactUri);

        if (!contact.profileInfo.avatar.isEmpty()) {
            setSenderImage(contactUri, contact.profileInfo.avatar);
        } else {
            auto avatar = Utils::conversationPhoto(convInfo.uid, *accInfo, true);
            QByteArray ba;
            QBuffer bu(&ba);
            avatar.save(&bu, "PNG");
            setSenderImage(contactUri, QString::fromLocal8Bit(ba.toBase64()));
        }
    } catch (...) {
    }
}

void
MessagesAdapter::newInteraction(const QString &accountId,
                                const QString &convUid,
                                uint64_t interactionId,
                                const interaction::Info &interaction)
{
    Q_UNUSED(interactionId);
    try {
        auto &accountInfo = LRCInstance::getAccountInfo(accountId);
        auto &convModel = accountInfo.conversationModel;
        const auto conversation = convModel->getConversationForUID(convUid);

        if (conversation.uid.isEmpty()) {
            return;
        }
        if (!interaction.authorUri.isEmpty()
            && (!QApplication::focusWindow() || LRCInstance::getCurrAccId() != accountId)) {
            /*
             * TODO: Notification from other accounts.
             */
        }
        if (convUid != LRCInstance::getCurrentConvUid()) {
            return;
        }
        convModel->clearUnreadInteractions(convUid);
        printNewInteraction(*convModel, interactionId, interaction);
    } catch (...) {
    }
}

void
MessagesAdapter::updateDraft()
{
    Utils::oneShotConnect(qmlObj_,
                          SIGNAL(sendMessageContentSaved(const QString &)),
                          this,
                          SLOT(slotUpdateDraft(const QString &)));

    requestSendMessageContent();
}

/*
 * JS invoke.
 */
void
MessagesAdapter::setMessagesVisibility(bool visible)
{
    QString s = QString::fromLatin1(visible ? "showMessagesDiv();" : "hideMessagesDiv();");
    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}

void
MessagesAdapter::requestSendMessageContent()
{
    QString s = QString::fromLatin1("requestSendMessageContent();");
    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}

void
MessagesAdapter::setInvitation(bool show, const QString &contactUri, const QString &contactId)
{
    QString s
        = show
              ? QString::fromLatin1("showInvitation(\"%1\", \"%2\")").arg(contactUri).arg(contactId)
              : QString::fromLatin1("showInvitation()");

    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}

void
MessagesAdapter::clear()
{
    QString s = QString::fromLatin1("clearMessages();");
    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}

void
MessagesAdapter::printHistory(lrc::api::ConversationModel &conversationModel,
                              const std::map<uint64_t, lrc::api::interaction::Info> interactions)
{
    auto interactionsStr = interactionsToJsonArrayObject(conversationModel, interactions).toUtf8();
    QString s = QString::fromLatin1("printHistory(%1);").arg(interactionsStr.constData());
    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}

void
MessagesAdapter::setSenderImage(const QString &sender, const QString &senderImage)
{
    QJsonObject setSenderImageObject = QJsonObject();
    setSenderImageObject.insert("sender_contact_method", QJsonValue(sender));
    setSenderImageObject.insert("sender_image", QJsonValue(senderImage));

    auto setSenderImageObjectString = QString(
        QJsonDocument(setSenderImageObject).toJson(QJsonDocument::Compact));
    QString s = QString::fromLatin1("setSenderImage(%1);")
                    .arg(setSenderImageObjectString.toUtf8().constData());
    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}
void
MessagesAdapter::printNewInteraction(lrc::api::ConversationModel &conversationModel,
                                     uint64_t msgId,
                                     const lrc::api::interaction::Info &interaction)
{
    auto interactionObject
        = interactionToJsonInteractionObject(conversationModel, msgId, interaction).toUtf8();
    if (interactionObject.isEmpty()) {
        return;
    }
    QString s = QString::fromLatin1("addMessage(%1);").arg(interactionObject.constData());
    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}

void
MessagesAdapter::updateInteraction(lrc::api::ConversationModel &conversationModel,
                                   uint64_t msgId,
                                   const lrc::api::interaction::Info &interaction)
{
    auto interactionObject
        = interactionToJsonInteractionObject(conversationModel, msgId, interaction).toUtf8();
    if (interactionObject.isEmpty()) {
        return;
    }
    QString s = QString::fromLatin1("updateMessage(%1);").arg(interactionObject.constData());
    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}

void
MessagesAdapter::setMessagesImageContent(const QString &path, bool isBased64)
{
    if (isBased64) {
        QString param = QString("addImage_base64('file://%1')").arg(path);
        QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
    } else {
        QString param = QString("addImage_path('file://%1')").arg(path);
        QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
    }
}

void
MessagesAdapter::setMessagesFileContent(const QString &path)
{
    qint64 fileSize = QFileInfo(path).size();
    QString fileName = QFileInfo(path).fileName();
    /*
     * If file name is too large, trim it.
     */
    if (fileName.length() > 15) {
        fileName = fileName.remove(12, fileName.length() - 12) + "...";
    }
    QString param = QString("addFile_path('%1','%2','%3')")
                        .arg(path, fileName, Utils::humanFileSize(fileSize));

    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
}

void
MessagesAdapter::removeInteraction(uint64_t interactionId)
{
    QString s = QString::fromLatin1("removeInteraction(%1);").arg(QString::number(interactionId));
    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}

void
MessagesAdapter::setSendMessageContent(const QString &content)
{
    QString s = QString::fromLatin1("setSendMessageContent(`%1`);").arg(content);
    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
}

void
MessagesAdapter::contactIsComposing(const QString &uid, const QString &contactUri, bool isComposing)
{
    if (LRCInstance::getCurrentConvUid() == uid) {
        QString s
            = QString::fromLatin1("showTypingIndicator(`%1`, %2);").arg(contactUri).arg(isComposing);
        QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
    }
}

void
MessagesAdapter::acceptInvitation(const QString &convUid)
{
    const auto currentConvUid = convUid.isEmpty() ? LRCInstance::getCurrentConvUid() : convUid;
    LRCInstance::getCurrentConversationModel()->makePermanent(currentConvUid);
}

void
MessagesAdapter::refuseInvitation(const QString &convUid)
{
    const auto currentConvUid = convUid.isEmpty() ? LRCInstance::getCurrentConvUid() : convUid;
    LRCInstance::getCurrentConversationModel()->removeConversation(currentConvUid, false);
    setInvitation(false);
}

void
MessagesAdapter::blockConversation(const QString &convUid)
{
    const auto currentConvUid = convUid.isEmpty() ? LRCInstance::getCurrentConvUid() : convUid;
    LRCInstance::getCurrentConversationModel()->removeConversation(currentConvUid, true);
    setInvitation(false);
}