Newer
Older
* 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 "globalsystemtray.h"
#include "qtutils.h"
#include "utils.h"
#include "webchathelpers.h"
#include <QApplication>
#include <QClipboard>
#include <QDesktopServices>
#include <QFileInfo>
#include <QImageReader>
#include <QList>
#include <QUrl>
MessagesAdapter::MessagesAdapter(QObject* parent)
: QmlAdapterBase(parent)
{}
void
MessagesAdapter::safeInit()
{
connect(&LRCInstance::instance(), &LRCInstance::currentAccountChanged, [this]() {
currentConvUid_.clear();
connectConversationModel();
}
MessagesAdapter::setupChatView(const QString& uid)
auto* convModel = LRCInstance::getCurrentConversationModel();
if (convModel == nullptr) {
return;
}
if (currentConvUid_ == uid)
return;
const auto& convInfo = convModel->getConversationForUID(uid);
if (convInfo.uid.isEmpty() || convInfo.participants.isEmpty()) {
return;
}
QString contactURI = convInfo.participants.at(0);
auto selectedAccountId = LRCInstance::getCurrAccId();
auto& accountInfo = LRCInstance::accountModel().getAccountInfo(selectedAccountId);
lrc::api::profile::Type contactType = lrc::api::profile::Type::INVALID;
try {
auto contactInfo = accountInfo.contactModel->getContact(contactURI);
contactType = contactInfo.profileInfo.type;
} catch (...) {
}
bool shouldShowSendContactRequestBtn = (contactType == lrc::api::profile::Type::PENDING
|| contactType == lrc::api::profile::Type::TEMPORARY);
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&)),
SLOT(slotSendMessageContentSaved(const QString&)));
}
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)
const auto convUid = LRCInstance::getCurrentConvUid();
LRCInstance::getCurrentConversationModel()->sendMessage(convUid, message);
} catch (...) {
qDebug() << "Exception during sendMessage:" << message;
}
}
void
MessagesAdapter::sendImage(const QString& message)
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
{
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.
*/
// TODO: put all QRegExp strings together
QString msg(message);
#ifdef Q_OS_WIN
msg = msg.replace(QRegExp("^file:\\/{2,3}"), "");
msg = msg.replace(QRegExp("^file:\\/{2,3}"), "/");
#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;
}
}
MessagesAdapter::openUrl(const QString& url)
{
if (!QDesktopServices::openUrl(url)) {
qDebug() << "Couldn't open url: " << url;
}
}
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::fromLatin1(ba.toBase64().data());
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:// or file:/// from url.
QString filePath = urlList.at(i).toString().remove(QRegExp("^file:\\/{2,3}"));
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
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 = accInfo->contactModel->bestNameForContact(contactUri);
setInvitation(contact.profileInfo.type == lrc::api::profile::Type::PENDING
|| contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY,
bestName,
contactUri);
if (!contact.profileInfo.avatar.isEmpty()) {
setSenderImage(contactUri, contact.profileInfo.avatar);
} else {
auto avatar = Utils::contactPhoto(convInfo.participants[0]);
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,
{
Q_UNUSED(interactionId);
try {
if (convUid.isEmpty() || convUid != LRCInstance::getCurrentConvUid()) {
auto& accountInfo = LRCInstance::getAccountInfo(accountId);
auto& convModel = accountInfo.conversationModel;
convModel->clearUnreadInteractions(convUid);
printNewInteraction(*convModel, interactionId, interaction);
} catch (...) {
}
}
void
MessagesAdapter::updateDraft()
{
currentConvUid_.clear();
SIGNAL(sendMessageContentSaved(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,
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,
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)
QString param = QString("addImage_base64('%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);
MessagesAdapter::refuseInvitation(const QString& convUid)
const auto currentConvUid = convUid.isEmpty() ? LRCInstance::getCurrentConvUid() : convUid;
LRCInstance::getCurrentConversationModel()->removeConversation(currentConvUid, false);
emit navigateToWelcomePageRequested();
MessagesAdapter::blockConversation(const QString& convUid)
const auto currentConvUid = convUid.isEmpty() ? LRCInstance::getCurrentConvUid() : convUid;
LRCInstance::getCurrentConversationModel()->removeConversation(currentConvUid, true);
emit navigateToWelcomePageRequested();