blob: 1aa925be52a2261cf61f4ec34d1f49cd46380845 [file] [log] [blame]
Sébastien Blin1f915762020-08-03 13:27:42 -04001/*
2 * Copyright (C) 2020 by Savoir-faire Linux
3 * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
4 * Author: Anthony L�onard <anthony.leonard@savoirfairelinux.com>
5 * Author: Olivier Soldano <olivier.soldano@savoirfairelinux.com>
6 * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
7 * Author: Isa Nanic <isa.nanic@savoirfairelinux.com>
8 * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 3 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 */
23
24#include "messagesadapter.h"
25#include "webchathelpers.h"
26
27#include "utils.h"
28
29#include <QDesktopServices>
30#include <QFileInfo>
31#include <QImageReader>
32#include <QList>
33#include <QUrl>
34
35MessagesAdapter::MessagesAdapter(QObject *parent)
36 : QmlAdapterBase(parent)
37{}
38
39MessagesAdapter::~MessagesAdapter() {}
40
41void
42MessagesAdapter::initQmlObject()
43{
44 connectConversationModel();
45}
46
47void
48MessagesAdapter::setupChatView(const QString &uid)
49{
50 auto &convInfo = LRCInstance::getConversationFromConvUid(uid);
51 if (convInfo.uid.isEmpty()) {
52 return;
53 }
54
55 QString contactURI = convInfo.participants.at(0);
56
57 bool isContact = false;
58 auto selectedAccountId = LRCInstance::getCurrAccId();
59 auto &accountInfo = LRCInstance::accountModel().getAccountInfo(selectedAccountId);
60
61 lrc::api::profile::Type contactType;
62 try {
63 auto contactInfo = accountInfo.contactModel->getContact(contactURI);
64 if (contactInfo.isTrusted) {
65 isContact = true;
66 }
67 contactType = contactInfo.profileInfo.type;
68 } catch (...) {
69 }
70
71 bool shouldShowSendContactRequestBtn = !isContact
72 && contactType != lrc::api::profile::Type::SIP;
73
74 QMetaObject::invokeMethod(qmlObj_,
75 "setSendContactRequestButtonVisible",
76 Q_ARG(QVariant, shouldShowSendContactRequestBtn));
77
78 setMessagesVisibility(false);
79
80 /*
81 * Type Indicator (contact).
82 */
83 contactIsComposing(convInfo.uid, "", false);
84 connect(LRCInstance::getCurrentConversationModel(),
85 &ConversationModel::composingStatusChanged,
86 [this](const QString &uid, const QString &contactUri, bool isComposing) {
87 contactIsComposing(uid, contactUri, isComposing);
88 });
89
90 /*
91 * Draft and message content set up.
92 */
93 Utils::oneShotConnect(qmlObj_,
94 SIGNAL(sendMessageContentSaved(const QString &)),
95 this,
96 SLOT(slotSendMessageContentSaved(const QString &)));
97
98 requestSendMessageContent();
99}
100
101void
102MessagesAdapter::connectConversationModel()
103{
104 auto currentConversationModel = LRCInstance::getCurrentAccountInfo().conversationModel.get();
105
106 QObject::disconnect(newInteractionConnection_);
107 QObject::disconnect(interactionRemovedConnection_);
108 QObject::disconnect(interactionStatusUpdatedConnection_);
109
110 newInteractionConnection_
111 = QObject::connect(currentConversationModel,
112 &lrc::api::ConversationModel::newInteraction,
113 [this](const QString &convUid,
114 uint64_t interactionId,
115 const lrc::api::interaction::Info &interaction) {
116 auto accountId = LRCInstance::getCurrAccId();
117 newInteraction(accountId, convUid, interactionId, interaction);
118 });
119
120 interactionStatusUpdatedConnection_ = QObject::connect(
121 currentConversationModel,
122 &lrc::api::ConversationModel::interactionStatusUpdated,
123 [this](const QString &convUid,
124 uint64_t interactionId,
125 const lrc::api::interaction::Info &interaction) {
126 if (convUid != LRCInstance::getCurrentConvUid()) {
127 return;
128 }
129 auto &currentAccountInfo = LRCInstance::getCurrentAccountInfo();
130 auto currentConversationModel = currentAccountInfo.conversationModel.get();
131 currentConversationModel->clearUnreadInteractions(convUid);
132 updateInteraction(*currentConversationModel, interactionId, interaction);
133 });
134
135 interactionRemovedConnection_
136 = QObject::connect(currentConversationModel,
137 &lrc::api::ConversationModel::interactionRemoved,
138 [this](const QString &convUid, uint64_t interactionId) {
139 Q_UNUSED(convUid);
140 removeInteraction(interactionId);
141 });
142
143 currentConversationModel->setFilter("");
144}
145
146void
147MessagesAdapter::sendContactRequest()
148{
149 auto convInfo = LRCInstance::getCurrentConversation();
150 if (!convInfo.uid.isEmpty()) {
151 LRCInstance::getCurrentConversationModel()->makePermanent(convInfo.uid);
152 }
153}
154
155void
156MessagesAdapter::accountChangedSetUp(const QString &accoountId)
157{
158 Q_UNUSED(accoountId)
159
160 connectConversationModel();
161}
162
163void
164MessagesAdapter::updateConversationForAddedContact()
165{
166 auto conversation = LRCInstance::getCurrentConversation();
167 auto convModel = LRCInstance::getCurrentConversationModel();
168
169 clear();
170 setConversationProfileData(conversation);
171 printHistory(*convModel, conversation.interactions);
172}
173
174void
175MessagesAdapter::slotSendMessageContentSaved(const QString &content)
176{
177 if (!LastConvUid_.isEmpty()) {
178 LRCInstance::setContentDraft(LastConvUid_, LRCInstance::getCurrAccId(), content);
179 }
180 LastConvUid_ = LRCInstance::getCurrentConvUid();
181
182 Utils::oneShotConnect(qmlObj_, SIGNAL(messagesCleared()), this, SLOT(slotMessagesCleared()));
183
184 setInvitation(false);
185 clear();
186 auto restoredContent = LRCInstance::getContentDraft(LRCInstance::getCurrentConvUid(),
187 LRCInstance::getCurrAccId());
188 setSendMessageContent(restoredContent);
189 emit needToUpdateSmartList();
190}
191
192void
193MessagesAdapter::slotUpdateDraft(const QString &content)
194{
195 if (!LastConvUid_.isEmpty()) {
196 LRCInstance::setContentDraft(LastConvUid_, LRCInstance::getCurrAccId(), content);
197 }
198 emit needToUpdateSmartList();
199}
200
201void
202MessagesAdapter::slotMessagesCleared()
203{
204 auto &convInfo = LRCInstance::getConversationFromConvUid(LRCInstance::getCurrentConvUid());
205 auto convModel = LRCInstance::getCurrentConversationModel();
206
207 printHistory(*convModel, convInfo.interactions);
208
209 Utils::oneShotConnect(qmlObj_, SIGNAL(messagesLoaded()), this, SLOT(slotMessagesLoaded()));
210
211 setConversationProfileData(convInfo);
212}
213
214void
215MessagesAdapter::slotMessagesLoaded()
216{
217 setMessagesVisibility(true);
218}
219
220void
221MessagesAdapter::sendMessage(const QString &message)
222{
223 try {
224 auto convUid = LRCInstance::getCurrentConvUid();
225 LRCInstance::getCurrentConversationModel()->sendMessage(convUid, message);
226 } catch (...) {
227 qDebug() << "Exception during sendMessage:" << message;
228 }
229}
230
231void
232MessagesAdapter::sendImage(const QString &message)
233{
234 if (message.startsWith("data:image/png;base64,")) {
235 /*
236 * Img tag contains base64 data, trim "data:image/png;base64," from data.
237 */
238 QByteArray data = QByteArray::fromStdString(message.toStdString().substr(22));
239 auto img_name_hash = QString::fromStdString(
240 QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex().toStdString());
241 QString fileName = "\\img_" + img_name_hash + ".png";
242
243 QPixmap image_to_save;
244 if (!image_to_save.loadFromData(QByteArray::fromBase64(data))) {
245 qDebug().noquote() << "Errors during loadFromData"
246 << "\n";
247 }
248
249 QString path = QString(Utils::WinGetEnv("TEMP")) + fileName;
250 if (!image_to_save.save(path, "PNG")) {
251 qDebug().noquote() << "Errors during QPixmap save"
252 << "\n";
253 }
254
255 try {
256 auto convUid = LRCInstance::getCurrentConvUid();
257 LRCInstance::getCurrentConversationModel()->sendFile(convUid, path, fileName);
258 } catch (...) {
259 qDebug().noquote() << "Exception during sendFile - base64 img"
260 << "\n";
261 }
262
263 } else {
264 /*
265 * Img tag contains file paths.
266 */
267
268 QString msg(message);
269#ifdef Q_OS_WIN
270 msg = msg.replace("file:///", "");
271#else
272 msg = msg.replace("file:///", "/");
273#endif
274 QFileInfo fi(msg);
275 QString fileName = fi.fileName();
276
277 try {
278 auto convUid = LRCInstance::getCurrentConvUid();
279 LRCInstance::getCurrentConversationModel()->sendFile(convUid, msg, fileName);
280 } catch (...) {
281 qDebug().noquote() << "Exception during sendFile - image from path"
282 << "\n";
283 }
284 }
285}
286
287void
288MessagesAdapter::sendFile(const QString &message)
289{
290 QFileInfo fi(message);
291 QString fileName = fi.fileName();
292 try {
293 auto convUid = LRCInstance::getCurrentConvUid();
294 LRCInstance::getCurrentConversationModel()->sendFile(convUid, message, fileName);
295 } catch (...) {
296 qDebug() << "Exception during sendFile";
297 }
298}
299
300void
301MessagesAdapter::retryInteraction(const QString &arg)
302{
303 bool ok;
304 uint64_t interactionUid = arg.toULongLong(&ok);
305 if (ok) {
306 LRCInstance::getCurrentConversationModel()
307 ->retryInteraction(LRCInstance::getCurrentConvUid(), interactionUid);
308 } else {
309 qDebug() << "retryInteraction - invalid arg" << arg;
310 }
311}
312
313void
314MessagesAdapter::setNewMessagesContent(const QString &path)
315{
316 if (path.length() == 0)
317 return;
318 QByteArray imageFormat = QImageReader::imageFormat(path);
319
320 if (!imageFormat.isEmpty()) {
321 setMessagesImageContent(path);
322 } else {
323 setMessagesFileContent(path);
324 }
325}
326
327void
328MessagesAdapter::deleteInteraction(const QString &arg)
329{
330 bool ok;
331 uint64_t interactionUid = arg.toULongLong(&ok);
332 if (ok) {
333 LRCInstance::getCurrentConversationModel()
334 ->clearInteractionFromConversation(LRCInstance::getCurrentConvUid(), interactionUid);
335 } else {
336 qDebug() << "DeleteInteraction - invalid arg" << arg;
337 }
338}
339
340void
341MessagesAdapter::openFile(const QString &arg)
342{
343 QUrl fileUrl("file:///" + arg);
344 if (!QDesktopServices::openUrl(fileUrl)) {
345 qDebug() << "Couldn't open file: " << fileUrl;
346 }
347}
348
349void
350MessagesAdapter::acceptFile(const QString &arg)
351{
352 try {
353 auto interactionUid = arg.toLongLong();
354 auto convUid = LRCInstance::getCurrentConvUid();
355 LRCInstance::getCurrentConversationModel()->acceptTransfer(convUid, interactionUid);
356 } catch (...) {
357 qDebug() << "JS bridging - exception during acceptFile: " << arg;
358 }
359}
360
361void
362MessagesAdapter::refuseFile(const QString &arg)
363{
364 try {
365 auto interactionUid = arg.toLongLong();
366 auto convUid = LRCInstance::getCurrentConvUid();
367 LRCInstance::getCurrentConversationModel()->cancelTransfer(convUid, interactionUid);
368 } catch (...) {
369 qDebug() << "JS bridging - exception during refuseFile:" << arg;
370 }
371}
372
373void
374MessagesAdapter::pasteKeyDetected()
375{
376 const QMimeData *mimeData = QApplication::clipboard()->mimeData();
377
378 if (mimeData->hasImage()) {
379 /*
380 * Save temp data into base64 format.
381 */
382 QPixmap pixmap = qvariant_cast<QPixmap>(mimeData->imageData());
383 QByteArray ba;
384 QBuffer bu(&ba);
385 bu.open(QIODevice::WriteOnly);
386 pixmap.save(&bu, "PNG");
387 auto str = QString::fromLocal8Bit(ba.toBase64());
388
389 setMessagesImageContent(str, true);
390 } else if (mimeData->hasUrls()) {
391 QList<QUrl> urlList = mimeData->urls();
392 /*
393 * Extract the local paths of the files.
394 */
395 for (int i = 0; i < urlList.size(); ++i) {
396 /*
397 * Trim file:/// from url.
398 */
399 QString filePath = urlList.at(i).toString().remove(0, 8);
400 QByteArray imageFormat = QImageReader::imageFormat(filePath);
401
402 /*
403 * Check if file is qt supported image file type.
404 */
405 if (!imageFormat.isEmpty()) {
406 setMessagesImageContent(filePath);
407 } else {
408 setMessagesFileContent(filePath);
409 }
410 }
411 } else {
412 QMetaObject::invokeMethod(qmlObj_,
413 "webViewRunJavaScript",
414 Q_ARG(QVariant,
415 QStringLiteral("replaceText(`%1`)").arg(mimeData->text())));
416 }
417}
418
419void
420MessagesAdapter::onComposing(bool isComposing)
421{
422 LRCInstance::getCurrentConversationModel()->setIsComposing(LRCInstance::getCurrentConvUid(),
423 isComposing);
424}
425
426void
427MessagesAdapter::setConversationProfileData(const lrc::api::conversation::Info &convInfo)
428{
429 auto convModel = LRCInstance::getCurrentConversationModel();
430 auto accInfo = &LRCInstance::getCurrentAccountInfo();
431 auto contactUri = convInfo.participants.front();
432
433 if (contactUri.isEmpty()) {
434 return;
435 }
436 try {
437 auto &contact = accInfo->contactModel->getContact(contactUri);
438 auto bestName = Utils::bestNameForConversation(convInfo, *convModel);
439 setInvitation(contact.profileInfo.type == lrc::api::profile::Type::PENDING,
440 bestName,
441 contactUri);
442
443 if (!contact.profileInfo.avatar.isEmpty()) {
444 setSenderImage(contactUri, contact.profileInfo.avatar);
445 } else {
446 auto avatar = Utils::conversationPhoto(convInfo.uid, *accInfo, true);
447 QByteArray ba;
448 QBuffer bu(&ba);
449 avatar.save(&bu, "PNG");
450 setSenderImage(contactUri, QString::fromLocal8Bit(ba.toBase64()));
451 }
452 } catch (...) {
453 }
454}
455
456void
457MessagesAdapter::newInteraction(const QString &accountId,
458 const QString &convUid,
459 uint64_t interactionId,
460 const interaction::Info &interaction)
461{
462 Q_UNUSED(interactionId);
463 try {
464 auto &accountInfo = LRCInstance::getAccountInfo(accountId);
465 auto &convModel = accountInfo.conversationModel;
466 auto &conversation = LRCInstance::getConversationFromConvUid(convUid, accountId);
467
468 if (conversation.uid.isEmpty()) {
469 return;
470 }
471 if (!interaction.authorUri.isEmpty()
472 && (!QApplication::focusWindow() || LRCInstance::getCurrAccId() != accountId)) {
473 /*
474 * TODO: Notification from other accounts.
475 */
476 }
477 if (convUid != LRCInstance::getCurrentConvUid()) {
478 return;
479 }
480 convModel->clearUnreadInteractions(convUid);
481 printNewInteraction(*convModel, interactionId, interaction);
482 } catch (...) {
483 }
484}
485
486void
487MessagesAdapter::updateDraft()
488{
489 Utils::oneShotConnect(qmlObj_,
490 SIGNAL(sendMessageContentSaved(const QString &)),
491 this,
492 SLOT(slotUpdateDraft(const QString &)));
493
494 requestSendMessageContent();
495}
496
497/*
498 * JS invoke.
499 */
500void
501MessagesAdapter::setMessagesVisibility(bool visible)
502{
503 QString s = QString::fromLatin1(visible ? "showMessagesDiv();" : "hideMessagesDiv();");
504 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
505}
506
507void
508MessagesAdapter::requestSendMessageContent()
509{
510 QString s = QString::fromLatin1("requestSendMessageContent();");
511 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
512}
513
514void
515MessagesAdapter::setInvitation(bool show, const QString &contactUri, const QString &contactId)
516{
517 QString s
518 = show
519 ? QString::fromLatin1("showInvitation(\"%1\", \"%2\")").arg(contactUri).arg(contactId)
520 : QString::fromLatin1("showInvitation()");
521
522 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
523}
524
525void
526MessagesAdapter::clear()
527{
528 QString s = QString::fromLatin1("clearMessages();");
529 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
530}
531
532void
533MessagesAdapter::printHistory(lrc::api::ConversationModel &conversationModel,
534 const std::map<uint64_t, lrc::api::interaction::Info> interactions)
535{
536 auto interactionsStr = interactionsToJsonArrayObject(conversationModel, interactions).toUtf8();
537 QString s = QString::fromLatin1("printHistory(%1);").arg(interactionsStr.constData());
538 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
539}
540
541void
542MessagesAdapter::setSenderImage(const QString &sender, const QString &senderImage)
543{
544 QJsonObject setSenderImageObject = QJsonObject();
545 setSenderImageObject.insert("sender_contact_method", QJsonValue(sender));
546 setSenderImageObject.insert("sender_image", QJsonValue(senderImage));
547
548 auto setSenderImageObjectString = QString(
549 QJsonDocument(setSenderImageObject).toJson(QJsonDocument::Compact));
550 QString s = QString::fromLatin1("setSenderImage(%1);")
551 .arg(setSenderImageObjectString.toUtf8().constData());
552 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
553}
554
555void
556MessagesAdapter::printNewInteraction(lrc::api::ConversationModel &conversationModel,
557 uint64_t msgId,
558 const lrc::api::interaction::Info &interaction)
559{
560 auto interactionObject
561 = interactionToJsonInteractionObject(conversationModel, msgId, interaction).toUtf8();
562 if (interactionObject.isEmpty()) {
563 return;
564 }
565 QString s = QString::fromLatin1("addMessage(%1);").arg(interactionObject.constData());
566 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
567}
568
569void
570MessagesAdapter::updateInteraction(lrc::api::ConversationModel &conversationModel,
571 uint64_t msgId,
572 const lrc::api::interaction::Info &interaction)
573{
574 auto interactionObject
575 = interactionToJsonInteractionObject(conversationModel, msgId, interaction).toUtf8();
576 if (interactionObject.isEmpty()) {
577 return;
578 }
579 QString s = QString::fromLatin1("updateMessage(%1);").arg(interactionObject.constData());
580 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
581}
582
583void
584MessagesAdapter::setMessagesImageContent(const QString &path, bool isBased64)
585{
586 if (isBased64) {
587 QString param = QString("addImage_base64('file://%1')").arg(path);
588 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
589 } else {
590 QString param = QString("addImage_path('file://%1')").arg(path);
591 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
592 }
593}
594
595void
596MessagesAdapter::setMessagesFileContent(const QString &path)
597{
598 qint64 fileSize = QFileInfo(path).size();
599 QString fileName = QFileInfo(path).fileName();
600 /*
601 * If file name is too large, trim it.
602 */
603 if (fileName.length() > 15) {
604 fileName = fileName.remove(12, fileName.length() - 12) + "...";
605 }
606 QString param = QString("addFile_path('%1','%2','%3')")
607 .arg(path, fileName, Utils::humanFileSize(fileSize));
608
609 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
610}
611
612void
613MessagesAdapter::removeInteraction(uint64_t interactionId)
614{
615 QString s = QString::fromLatin1("removeInteraction(%1);").arg(QString::number(interactionId));
616 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
617}
618
619void
620MessagesAdapter::setSendMessageContent(const QString &content)
621{
622 QString s = QString::fromLatin1("setSendMessageContent(`%1`);").arg(content);
623 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
624}
625
626void
627MessagesAdapter::contactIsComposing(const QString &uid, const QString &contactUri, bool isComposing)
628{
629 if (LRCInstance::getCurrentConvUid() == uid) {
630 QString s
631 = QString::fromLatin1("showTypingIndicator(`%1`, %2);").arg(contactUri).arg(isComposing);
632 QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
633 }
634}
635
636void
637MessagesAdapter::acceptInvitation()
638{
639 auto convUid = LRCInstance::getCurrentConvUid();
640 LRCInstance::getCurrentConversationModel()->makePermanent(convUid);
641}
642
643void
644MessagesAdapter::refuseInvitation()
645{
646 auto convUid = LRCInstance::getCurrentConvUid();
647 LRCInstance::getCurrentConversationModel()->removeConversation(convUid, false);
648 setInvitation(false);
649}
650
651void
652MessagesAdapter::blockConversation()
653{
654 auto convUid = LRCInstance::getCurrentConvUid();
655 LRCInstance::getCurrentConversationModel()->removeConversation(convUid, true);
656 setInvitation(false);
657}