From ed514d71667d87c30acd1f41213d8ec8c6041b04 Mon Sep 17 00:00:00 2001 From: Mick Sayson Date: Sat, 25 May 2019 17:11:44 -0700 Subject: [PATCH] refactor(chatform): Remove message handling logic from gui path Functional changes * Offline messages are still sent when the chat log is cleared * Spinner now does not wait for history to be complete, just a receipt from our friend * Export chat and load chat history are now available in group chats * Merged save chat log and export chat log * Note that we lost the info messages in the process NonFunctional Changes * FileTransferWidget slots only called for correct file * Settings::getEnableGroupChatsColor now embedded in GenericChatForm::colorizeNames * Settings::setEnableGroupChatscolor now emits signal connected to GenericChatForm::setColorizedNames to keep state in sync * Chatlog history not reloaded on setPassword() * I am pretty sure this had no purpose * Removed a lot of responsibility from ChatForm * History moved to ChatHistory implementation of IChatLog * OfflineMsgEngine moved to FriendMessageDispatcher * Export chat and load chat history moved to GenericChatLog * Backed by IChatLog so can be used generically * Message processing moved to FriendMessageDispatcher * The action of sending files to coreFile is still handled by ChatForm, but displaying of the sent messages is done through IChatLog -> GenericChatForm * Search moved to ChatHistory/SessionChatLog * All insertion of chat log elements should be handled by GenericChatForm now * Removed overlapping responsibilities from GroupChatForm * Search and message sending goes through ichatlog/messagedispatcher too * Lots of search functionality pushed down into IChatLog * Some of the file logic was moved into Widget. This is mostly to avoid scope increase of this PR even further. * History APIs removed that were no longer used --- src/chatlog/chatmessage.cpp | 2 +- src/chatlog/content/filetransferwidget.cpp | 90 +-- src/chatlog/content/filetransferwidget.h | 11 +- src/model/about/aboutfriend.cpp | 2 +- src/nexus.cpp | 1 + src/persistence/history.cpp | 173 +---- src/persistence/history.h | 17 +- src/persistence/profile.cpp | 2 - src/persistence/settings.cpp | 5 +- src/persistence/settings.h | 1 + src/widget/form/chatform.cpp | 606 +---------------- src/widget/form/chatform.h | 62 +- src/widget/form/genericchatform.cpp | 745 ++++++++++----------- src/widget/form/genericchatform.h | 47 +- src/widget/form/groupchatform.cpp | 58 +- src/widget/form/groupchatform.h | 9 +- src/widget/form/loadhistorydialog.cpp | 19 +- src/widget/form/loadhistorydialog.h | 5 +- src/widget/widget.cpp | 244 +++++-- src/widget/widget.h | 29 +- 20 files changed, 665 insertions(+), 1463 deletions(-) diff --git a/src/chatlog/chatmessage.cpp b/src/chatlog/chatmessage.cpp index 247d768b7..7054a860d 100644 --- a/src/chatlog/chatmessage.cpp +++ b/src/chatlog/chatmessage.cpp @@ -88,7 +88,7 @@ ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QSt authorFont.setBold(true); QColor color = Style::getColor(Style::MainText); - if (colorizeName && Settings::getInstance().getEnableGroupChatsColor()) { + if (colorizeName) { QByteArray hash = QCryptographicHash::hash((sender.toUtf8()), QCryptographicHash::Sha256); quint8 *data = (quint8*)hash.data(); diff --git a/src/chatlog/content/filetransferwidget.cpp b/src/chatlog/content/filetransferwidget.cpp index ceba1b2b2..a4d19f585 100644 --- a/src/chatlog/content/filetransferwidget.cpp +++ b/src/chatlog/content/filetransferwidget.cpp @@ -88,20 +88,6 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file) CoreFile* coreFile = Core::getInstance()->getCoreFile(); - connect(coreFile, &CoreFile::fileTransferInfo, this, - &FileTransferWidget::onFileTransferInfo); - connect(coreFile, &CoreFile::fileTransferAccepted, this, - &FileTransferWidget::onFileTransferAccepted); - connect(coreFile, &CoreFile::fileTransferCancelled, this, - &FileTransferWidget::onFileTransferCancelled); - connect(coreFile, &CoreFile::fileTransferPaused, this, - &FileTransferWidget::onFileTransferPaused); - connect(coreFile, &CoreFile::fileTransferFinished, this, - &FileTransferWidget::onFileTransferFinished); - connect(coreFile, &CoreFile::fileTransferRemotePausedUnpaused, this, - &FileTransferWidget::fileTransferRemotePausedUnpaused); - connect(coreFile, &CoreFile::fileTransferBrokenUnbroken, this, - &FileTransferWidget::fileTransferBrokenUnbroken); connect(ui->leftButton, &QPushButton::clicked, this, &FileTransferWidget::onLeftButtonClicked); connect(ui->rightButton, &QPushButton::clicked, this, &FileTransferWidget::onRightButtonClicked); connect(ui->previewButton, &QPushButton::clicked, this, @@ -133,30 +119,9 @@ bool FileTransferWidget::tryRemoveFile(const QString& filepath) return writable; } -void FileTransferWidget::autoAcceptTransfer(const QString& path) +void FileTransferWidget::onFileTransferUpdate(ToxFile file) { - QString filepath; - int number = 0; - - QString suffix = QFileInfo(fileInfo.fileName).completeSuffix(); - QString base = QFileInfo(fileInfo.fileName).baseName(); - - do { - filepath = QString("%1/%2%3.%4") - .arg(path, base, - number > 0 ? QString(" (%1)").arg(QString::number(number)) : QString(), - suffix); - ++number; - } while (QFileInfo(filepath).exists()); - - // Do not automatically accept the file-transfer if the path is not writable. - // The user can still accept it manually. - if (tryRemoveFile(filepath)) { - CoreFile* coreFile = Core::getInstance()->getCoreFile(); - coreFile->acceptFileRecvRequest(fileInfo.friendId, fileInfo.fileNum, filepath); - } else { - qWarning() << "Cannot write to " << filepath; - } + updateWidget(file); } bool FileTransferWidget::isActive() const @@ -265,53 +230,6 @@ void FileTransferWidget::paintEvent(QPaintEvent*) } } -void FileTransferWidget::onFileTransferInfo(ToxFile file) -{ - updateWidget(file); -} - -void FileTransferWidget::onFileTransferAccepted(ToxFile file) -{ - updateWidget(file); -} - -void FileTransferWidget::onFileTransferCancelled(ToxFile file) -{ - updateWidget(file); -} - -void FileTransferWidget::onFileTransferPaused(ToxFile file) -{ - updateWidget(file); -} - -void FileTransferWidget::onFileTransferResumed(ToxFile file) -{ - updateWidget(file); -} - -void FileTransferWidget::onFileTransferFinished(ToxFile file) -{ - updateWidget(file); -} - -void FileTransferWidget::fileTransferRemotePausedUnpaused(ToxFile file, bool paused) -{ - if (paused) { - onFileTransferPaused(file); - } else { - onFileTransferResumed(file); - } -} - -void FileTransferWidget::fileTransferBrokenUnbroken(ToxFile file, bool broken) -{ - // TODO: Handle broken transfer differently once we have resuming code - if (broken) { - onFileTransferCancelled(file); - } -} - QString FileTransferWidget::getHumanReadableSize(qint64 size) { static const char* suffix[] = {"B", "kiB", "MiB", "GiB", "TiB"}; @@ -737,9 +655,7 @@ void FileTransferWidget::applyTransformation(const int orientation, QImage& imag void FileTransferWidget::updateWidget(ToxFile const& file) { - if (fileInfo != file) { - return; - } + assert(file == fileInfo); fileInfo = file; diff --git a/src/chatlog/content/filetransferwidget.h b/src/chatlog/content/filetransferwidget.h index 717548252..84a0f0c67 100644 --- a/src/chatlog/content/filetransferwidget.h +++ b/src/chatlog/content/filetransferwidget.h @@ -42,19 +42,10 @@ class FileTransferWidget : public QWidget public: explicit FileTransferWidget(QWidget* parent, ToxFile file); virtual ~FileTransferWidget(); - void autoAcceptTransfer(const QString& path); bool isActive() const; static QString getHumanReadableSize(qint64 size); -protected slots: - void onFileTransferInfo(ToxFile file); - void onFileTransferAccepted(ToxFile file); - void onFileTransferCancelled(ToxFile file); - void onFileTransferPaused(ToxFile file); - void onFileTransferResumed(ToxFile file); - void onFileTransferFinished(ToxFile file); - void fileTransferRemotePausedUnpaused(ToxFile file, bool paused); - void fileTransferBrokenUnbroken(ToxFile file, bool broken); + void onFileTransferUpdate(ToxFile file); protected: void updateWidgetColor(ToxFile const& file); diff --git a/src/model/about/aboutfriend.cpp b/src/model/about/aboutfriend.cpp index 98720416a..eaa66b8ab 100644 --- a/src/model/about/aboutfriend.cpp +++ b/src/model/about/aboutfriend.cpp @@ -116,7 +116,7 @@ bool AboutFriend::isHistoryExistence() History* const history = Nexus::getProfile()->getHistory(); if (history) { const ToxPk pk = f->getPublicKey(); - return history->isHistoryExistence(pk.toString()); + return history->historyExists(pk); } return false; diff --git a/src/nexus.cpp b/src/nexus.cpp index bfc0bd0cc..c88da24db 100644 --- a/src/nexus.cpp +++ b/src/nexus.cpp @@ -208,6 +208,7 @@ void Nexus::showMainGUI() connect(core, &Core::friendStatusMessageChanged, widget, &Widget::onFriendStatusMessageChanged); connect(core, &Core::friendRequestReceived, widget, &Widget::onFriendRequestReceived); connect(core, &Core::friendMessageReceived, widget, &Widget::onFriendMessageReceived); + connect(core, &Core::receiptRecieved, widget, &Widget::onReceiptReceived); connect(core, &Core::groupInviteReceived, widget, &Widget::onGroupInviteReceived); connect(core, &Core::groupMessageReceived, widget, &Widget::onGroupMessageReceived); connect(core, &Core::groupPeerlistChanged, widget, &Widget::onGroupPeerlistChanged); diff --git a/src/persistence/history.cpp b/src/persistence/history.cpp index f7bc34327..604961416 100644 --- a/src/persistence/history.cpp +++ b/src/persistence/history.cpp @@ -224,9 +224,9 @@ bool History::isValid() * @param friendPk * @return True if has, false otherwise. */ -bool History::isHistoryExistence(const QString& friendPk) +bool History::historyExists(const ToxPk& friendPk) { - return !getChatHistoryDefaultNum(friendPk).isEmpty(); + return !getMessagesForFriend(friendPk, 0, 1).empty(); } /** @@ -521,78 +521,6 @@ void History::setFileFinished(const QString& fileId, bool success, const QString fileInfos.remove(fileId); } -/** - * @brief Fetches chat messages from the database. - * @param friendPk Friend publick key to fetch. - * @param from Start of period to fetch. - * @param to End of period to fetch. - * @return List of messages. - */ -QList History::getChatHistoryFromDate(const QString& friendPk, - const QDateTime& from, const QDateTime& to) -{ - if (!isValid()) { - return {}; - } - return getChatHistory(friendPk, from, to, 0); -} - -/** - * @brief Fetches the latest set amount of messages from the database. - * @param friendPk Friend public key to fetch. - * @return List of messages. - */ -QList History::getChatHistoryDefaultNum(const QString& friendPk) -{ - if (!isValid()) { - return {}; - } - return getChatHistory(friendPk, QDateTime::fromMSecsSinceEpoch(0), QDateTime::currentDateTime(), - NUM_MESSAGES_DEFAULT); -} - - -/** - * @brief Fetches chat messages counts for each day from the database. - * @param friendPk Friend public key to fetch. - * @param from Start of period to fetch. - * @param to End of period to fetch. - * @return List of structs containing days offset and message count for that day. - */ -QList History::getChatHistoryCounts(const ToxPk& friendPk, const QDate& from, - const QDate& to) -{ - if (!isValid()) { - return {}; - } - QDateTime fromTime(from); - QDateTime toTime(to); - - QList counts; - - auto rowCallback = [&counts](const QVector& row) { - DateMessages app; - app.count = row[0].toUInt(); - app.offsetDays = row[1].toUInt(); - counts.append(app); - }; - - QString queryText = - QString("SELECT COUNT(history.id), ((timestamp / 1000 / 60 / 60 / 24) - %4 ) AS day " - "FROM history " - "JOIN peers chat ON chat_id = chat.id " - "WHERE timestamp BETWEEN %1 AND %2 AND chat.public_key='%3'" - "GROUP BY day;") - .arg(fromTime.toMSecsSinceEpoch()) - .arg(toTime.toMSecsSinceEpoch()) - .arg(friendPk.toString()) - .arg(QDateTime::fromMSecsSinceEpoch(0).daysTo(fromTime)); - - db->execNow({queryText, rowCallback}); - - return counts; -} - size_t History::getNumMessagesForFriend(const ToxPk& friendPk) { return getNumMessagesForFriendBeforeDate(friendPk, @@ -857,31 +785,6 @@ QList History::getNumMessagesForFriendBeforeDateBoundaries(con return dateIdxs; } -/** - * @brief get start date of correspondence - * @param friendPk Friend public key - * @return start date of correspondence - */ -QDateTime History::getStartDateChatHistory(const QString& friendPk) -{ - QDateTime result; - auto rowCallback = [&result](const QVector& row) { - result = QDateTime::fromMSecsSinceEpoch(row[0].toLongLong()); - }; - - QString queryText = - QStringLiteral("SELECT timestamp " - "FROM history " - "LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id " - "JOIN peers chat ON chat_id = chat.id " - "WHERE chat.public_key='%1' ORDER BY timestamp ASC LIMIT 1;") - .arg(friendPk); - - db->execNow({queryText, rowCallback}); - - return result; -} - /** * @brief Marks a message as sent. * Removing message from the faux-offline pending messages list. @@ -895,74 +798,4 @@ void History::markAsSent(RowId messageId) } db->execLater(QString("DELETE FROM faux_offline_pending WHERE id=%1;").arg(messageId.get())); -} - - -/** - * @brief Fetches chat messages from the database. - * @param friendPk Friend publick key to fetch. - * @param from Start of period to fetch. - * @param to End of period to fetch. - * @param numMessages max number of messages to fetch. - * @return List of messages. - */ -QList History::getChatHistory(const QString& friendPk, const QDateTime& from, - const QDateTime& to, int numMessages) -{ - QList messages; - - auto rowCallback = [&messages](const QVector& row) { - // dispName and message could have null bytes, QString::fromUtf8 - // truncates on null bytes so we strip them - auto id = RowId{row[0].toLongLong()}; - auto isOfflineMessage = row[1].isNull(); - auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong()); - auto friend_key = row[3].toString(); - auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', "")); - auto sender_key = row[5].toString(); - if (row[7].isNull()) { - messages += - {id, isOfflineMessage, timestamp, friend_key, display_name, sender_key, row[6].toString()}; - } else { - ToxFile file; - file.fileKind = TOX_FILE_KIND_DATA; - file.resumeFileId = row[7].toString().toUtf8(); - file.filePath = row[8].toString(); - file.fileName = row[9].toString(); - file.filesize = row[10].toLongLong(); - file.direction = static_cast(row[11].toLongLong()); - file.status = static_cast(row[12].toInt()); - messages += - {id, isOfflineMessage, timestamp, friend_key, display_name, sender_key, file}; - } - }; - - // Don't forget to update the rowCallback if you change the selected columns! - QString queryText = - QString("SELECT history.id, faux_offline_pending.id, timestamp, " - "chat.public_key, aliases.display_name, sender.public_key, " - "message, file_transfers.file_restart_id, " - "file_transfers.file_path, file_transfers.file_name, " - "file_transfers.file_size, file_transfers.direction, " - "file_transfers.file_state FROM history " - "LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id " - "JOIN peers chat ON history.chat_id = chat.id " - "JOIN aliases ON sender_alias = aliases.id " - "JOIN peers sender ON aliases.owner = sender.id " - "LEFT JOIN file_transfers ON history.file_id = file_transfers.id " - "WHERE timestamp BETWEEN %1 AND %2 AND chat.public_key='%3'") - .arg(from.toMSecsSinceEpoch()) - .arg(to.toMSecsSinceEpoch()) - .arg(friendPk); - if (numMessages) { - queryText = - "SELECT * FROM (" + queryText - + QString(" ORDER BY history.id DESC limit %1) AS T1 ORDER BY T1.id ASC;").arg(numMessages); - } else { - queryText = queryText + ";"; - } - - db->execNow({queryText, rowCallback}); - - return messages; -} +} \ No newline at end of file diff --git a/src/persistence/history.h b/src/persistence/history.h index a8d3883cb..53e583ffc 100644 --- a/src/persistence/history.h +++ b/src/persistence/history.h @@ -143,12 +143,6 @@ public: HistMessageContent content; }; - struct DateMessages - { - uint offsetDays; - uint count; - }; - struct DateIdx { QDate date; @@ -161,7 +155,7 @@ public: bool isValid(); - bool isHistoryExistence(const QString& friendPk); + bool historyExists(const ToxPk& friendPk); void eraseHistory(); void removeFriendHistory(const QString& friendPk); @@ -174,11 +168,6 @@ public: const QString& sender, const QDateTime& time, QString const& dispName); void setFileFinished(const QString& fileId, bool success, const QString& filePath, const QByteArray& fileHash); - - QList getChatHistoryFromDate(const QString& friendPk, const QDateTime& from, - const QDateTime& to); - QList getChatHistoryDefaultNum(const QString& friendPk); - QList getChatHistoryCounts(const ToxPk& friendPk, const QDate& from, const QDate& to); size_t getNumMessagesForFriend(const ToxPk& friendPk); size_t getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const QDateTime& date); QList getMessagesForFriend(const ToxPk& friendPk, size_t firstIdx, size_t lastIdx); @@ -187,7 +176,6 @@ public: const ParameterSearch& parameter); QList getNumMessagesForFriendBeforeDateBoundaries(const ToxPk& friendPk, const QDate& from, size_t maxNum); - QDateTime getStartDateChatHistory(const QString& friendPk); void markAsSent(RowId messageId); @@ -206,9 +194,6 @@ private slots: void onFileInserted(RowId dbId, QString fileId); private: - QList getChatHistory(const QString& friendPk, const QDateTime& from, - const QDateTime& to, int numMessages); - static RawDatabase::Query generateFileFinished(RowId fileId, bool success, const QString& filePath, const QByteArray& fileHash); std::shared_ptr db; diff --git a/src/persistence/profile.cpp b/src/persistence/profile.cpp index 28d655ca1..b35fa9024 100644 --- a/src/persistence/profile.cpp +++ b/src/persistence/profile.cpp @@ -853,8 +853,6 @@ QString Profile::setPassword(const QString& newPassword) "password."); } - Nexus::getDesktopGUI()->reloadHistory(); - QByteArray avatar = loadAvatarData(core->getSelfId().getPublicKey()); saveAvatar(core->getSelfId().getPublicKey(), avatar); diff --git a/src/persistence/settings.cpp b/src/persistence/settings.cpp index da8b108fa..df375137a 100644 --- a/src/persistence/settings.cpp +++ b/src/persistence/settings.cpp @@ -2331,7 +2331,10 @@ void Settings::setAutoLogin(bool state) void Settings::setEnableGroupChatsColor(bool state) { QMutexLocker locker{&bigLock}; - nameColors = state; + if (state != nameColors) { + nameColors = state; + emit nameColorsChanged(nameColors); + } } bool Settings::getEnableGroupChatsColor() const diff --git a/src/persistence/settings.h b/src/persistence/settings.h index e360b4fde..4028b7869 100644 --- a/src/persistence/settings.h +++ b/src/persistence/settings.h @@ -201,6 +201,7 @@ signals: // GUI void autoLoginChanged(bool enabled); + void nameColorsChanged(bool enabled); void separateWindowChanged(bool enabled); void showSystemTrayChanged(bool enabled); bool minimizeOnCloseChanged(bool enabled); diff --git a/src/widget/form/chatform.cpp b/src/widget/form/chatform.cpp index 3e0c9db48..2964576a8 100644 --- a/src/widget/form/chatform.cpp +++ b/src/widget/form/chatform.cpp @@ -102,36 +102,11 @@ namespace return cD + res.sprintf("%02ds", seconds); } - - void completeMessage(ChatMessage::Ptr ma, RowId rowId) - { - auto profile = Nexus::getProfile(); - if (profile->isHistoryEnabled()) { - profile->getHistory()->markAsSent(rowId); - } - - // force execution on the gui thread - QTimer::singleShot(0, QCoreApplication::instance(), [ma] { - ma->markAsSent(QDateTime::currentDateTime()); - }); - } - - struct CompleteMessageFunctor - { - void operator()() const - { - completeMessage(ma, rowId); - } - - ChatMessage::Ptr ma; - RowId rowId; - }; } // namespace -ChatForm::ChatForm(Friend* chatFriend, History* history) - : GenericChatForm(chatFriend) +ChatForm::ChatForm(Friend* chatFriend, IChatLog& chatLog, IMessageDispatcher& messageDispatcher) + : GenericChatForm(chatFriend, chatLog, messageDispatcher) , f(chatFriend) - , history{history} , isTyping{false} , lastCallIsVideo{false} { @@ -146,8 +121,6 @@ ChatForm::ChatForm(Friend* chatFriend, History* history) statusMessageLabel->setTextFormat(Qt::PlainText); statusMessageLabel->setContextMenuPolicy(Qt::CustomContextMenu); - offlineEngine = new OfflineMsgEngine(f, Core::getInstance()); - typingTimer.setSingleShot(true); callDurationTimer = nullptr; @@ -161,29 +134,18 @@ ChatForm::ChatForm(Friend* chatFriend, History* history) headWidget->addWidget(callDuration, 1, Qt::AlignCenter); callDuration->hide(); - loadHistoryAction = menu.addAction(QString(), this, SLOT(onLoadHistory())); copyStatusAction = statusMessageMenu.addAction(QString(), this, SLOT(onCopyStatusMessage())); - exportChatAction = - menu.addAction(QIcon::fromTheme("document-save"), QString(), this, SLOT(onExportChat())); - const Core* core = Core::getInstance(); const Profile* profile = Nexus::getProfile(); const CoreFile* coreFile = core->getCoreFile(); - connect(coreFile, &CoreFile::fileReceiveRequested, this, &ChatForm::onFileRecvRequest); connect(profile, &Profile::friendAvatarChanged, this, &ChatForm::onAvatarChanged); - connect(coreFile, &CoreFile::fileSendStarted, this, &ChatForm::startFileSend); - connect(coreFile, &CoreFile::fileTransferFinished, this, &ChatForm::onFileTransferFinished); - connect(coreFile, &CoreFile::fileTransferCancelled, this, &ChatForm::onFileTransferCancelled); - connect(coreFile, &CoreFile::fileTransferBrokenUnbroken, this, &ChatForm::onFileTransferBrokenUnbroken); - connect(coreFile, &CoreFile::fileSendFailed, this, &ChatForm::onFileSendFailed); - connect(core, &Core::receiptRecieved, this, &ChatForm::onReceiptReceived); - connect(core, &Core::friendMessageReceived, this, &ChatForm::onFriendMessageReceived); + connect(coreFile, &CoreFile::fileReceiveRequested, this, &ChatForm::updateFriendActivityForFile); + connect(coreFile, &CoreFile::fileSendStarted, this, &ChatForm::updateFriendActivityForFile); connect(core, &Core::friendTypingChanged, this, &ChatForm::onFriendTypingChanged); connect(core, &Core::friendStatusChanged, this, &ChatForm::onFriendStatusChanged); connect(coreFile, &CoreFile::fileNameChanged, this, &ChatForm::onFileNameChanged); - const CoreAV* av = core->getAv(); connect(av, &CoreAV::avInvite, this, &ChatForm::onAvInvite); connect(av, &CoreAV::avStart, this, &ChatForm::onAvStart); @@ -194,7 +156,8 @@ ChatForm::ChatForm(Friend* chatFriend, History* history) connect(headWidget, &ChatFormHeader::micMuteToggle, this, &ChatForm::onMicMuteToggle); connect(headWidget, &ChatFormHeader::volMuteToggle, this, &ChatForm::onVolMuteToggle); - connect(msgEdit, &ChatTextEdit::enterPressed, this, &ChatForm::onSendTriggered); + connect(sendButton, &QPushButton::pressed, this, &ChatForm::updateFriendActivity); + connect(msgEdit, &ChatTextEdit::enterPressed, this, &ChatForm::updateFriendActivity); connect(msgEdit, &ChatTextEdit::textChanged, this, &ChatForm::onTextEditChanged); connect(msgEdit, &ChatTextEdit::pasteImage, this, &ChatForm::sendImage); connect(statusMessageLabel, &CroppingLabel::customContextMenuRequested, this, @@ -218,9 +181,6 @@ ChatForm::ChatForm(Friend* chatFriend, History* history) connect(headWidget, &ChatFormHeader::callRejected, this, &ChatForm::onRejectCallTriggered); updateCallButtons(); - if (Nexus::getProfile()->isHistoryEnabled()) { - loadHistoryDefaultNum(true); - } setAcceptDrops(true); retranslateUi(); @@ -232,7 +192,6 @@ ChatForm::~ChatForm() Translator::unregister(this); delete netcam; netcam = nullptr; - delete offlineEngine; } void ChatForm::setStatusMessage(const QString& newMessage) @@ -242,11 +201,22 @@ void ChatForm::setStatusMessage(const QString& newMessage) statusMessageLabel->setToolTip(Qt::convertFromPlainText(newMessage, Qt::WhiteSpaceNormal)); } -void ChatForm::onSendTriggered() +void ChatForm::updateFriendActivity() { - SendMessageStr(msgEdit->toPlainText()); - msgEdit->clear(); + // TODO: Remove Widget::getInstance() + Widget::getInstance()->updateFriendActivity(f); } + +void ChatForm::updateFriendActivityForFile(const ToxFile& file) +{ + if (file.friendId != f->getId()) { + return; + } + + // TODO: Remove Widget::getInstance() + Widget::getInstance()->updateFriendActivity(f); +} + void ChatForm::onFileNameChanged(const ToxPk& friendPk) { if (friendPk != f->getPublicKey()) { @@ -311,101 +281,6 @@ void ChatForm::onAttachClicked() } } -void ChatForm::startFileSend(ToxFile file) -{ - if (file.friendId != f->getId()) { - return; - } - - QString name; - const Core* core = Core::getInstance(); - ToxPk self = core->getSelfId().getPublicKey(); - if (previousId != self) { - name = core->getUsername(); - previousId = self; - } - - insertChatMessage( - ChatMessage::createFileTransferMessage(name, file, true, QDateTime::currentDateTime())); - - if (history && Settings::getInstance().getEnableLogging()) { - auto selfPk = Core::getInstance()->getSelfId().toString(); - auto pk = f->getPublicKey().toString(); - auto name = Core::getInstance()->getUsername(); - history->addNewFileMessage(pk, file.resumeFileId, file.fileName, file.filePath, - file.filesize, selfPk, QDateTime::currentDateTime(), name); - } - - Widget::getInstance()->updateFriendActivity(f); -} - -void ChatForm::onFileTransferFinished(ToxFile file) -{ - history->setFileFinished(file.resumeFileId, true, file.filePath, file.hashGenerator->result()); -} - -void ChatForm::onFileTransferBrokenUnbroken(ToxFile file, bool broken) -{ - if (broken) { - history->setFileFinished(file.resumeFileId, false, file.filePath, file.hashGenerator->result()); - } -} - -void ChatForm::onFileTransferCancelled(ToxFile file) -{ - history->setFileFinished(file.resumeFileId, false, file.filePath, file.hashGenerator->result()); -} - -void ChatForm::onFileRecvRequest(ToxFile file) -{ - if (file.friendId != f->getId()) { - return; - } - - Widget::getInstance()->newFriendMessageAlert(f->getPublicKey(), - file.fileName + - " (" + FileTransferWidget::getHumanReadableSize(file.filesize) + ")", - true, true); - QString name; - ToxPk friendId = f->getPublicKey(); - if (friendId != previousId) { - name = f->getDisplayedName(); - previousId = friendId; - } - - ChatMessage::Ptr msg = - ChatMessage::createFileTransferMessage(name, file, false, QDateTime::currentDateTime()); - - insertChatMessage(msg); - - if (history && Settings::getInstance().getEnableLogging()) { - auto pk = f->getPublicKey().toString(); - auto name = f->getDisplayedName(); - history->addNewFileMessage(pk, file.resumeFileId, file.fileName, file.filePath, - file.filesize, pk, QDateTime::currentDateTime(), name); - } - ChatLineContentProxy* proxy = static_cast(msg->getContent(1)); - - assert(proxy->getWidgetType() == ChatLineContentProxy::FileTransferWidgetType); - FileTransferWidget* tfWidget = static_cast(proxy->getWidget()); - - const Settings& settings = Settings::getInstance(); - QString autoAcceptDir = settings.getAutoAcceptDir(f->getPublicKey()); - - if (autoAcceptDir.isEmpty() && settings.getAutoSaveEnabled()) { - autoAcceptDir = settings.getGlobalAutoAcceptDir(); - } - - auto maxAutoAcceptSize = settings.getMaxAutoAcceptSize(); - bool autoAcceptSizeCheckPassed = maxAutoAcceptSize == 0 || maxAutoAcceptSize >= file.filesize; - - if (!autoAcceptDir.isEmpty() && autoAcceptSizeCheckPassed) { - tfWidget->autoAcceptTransfer(autoAcceptDir); - } - - Widget::getInstance()->updateFriendActivity(f); -} - void ChatForm::onAvInvite(uint32_t friendId, bool video) { if (friendId != f->getId()) { @@ -554,95 +429,6 @@ void ChatForm::onVolMuteToggle() updateMuteVolButton(); } -void ChatForm::searchInBegin(const QString& phrase, const ParameterSearch& parameter) -{ - disableSearchText(); - - searchPoint = QPoint(1, -1); - - const bool isFirst = (parameter.period == PeriodSearch::WithTheFirst); - const bool isAfter = (parameter.period == PeriodSearch::AfterDate); - if (isFirst || isAfter) { - if (isFirst || (isAfter && parameter.date < getFirstTime().date())) { - const QString pk = f->getPublicKey().toString(); - if ((isFirst || parameter.date >= history->getStartDateChatHistory(pk).date()) - && loadHistory(phrase, parameter)) { - - return; - } - } - - onSearchDown(phrase, parameter); - } else { - if (parameter.period == PeriodSearch::BeforeDate && parameter.date < getFirstTime().date()) { - const QString pk = f->getPublicKey().toString(); - if (parameter.date >= history->getStartDateChatHistory(pk).date() - && loadHistory(phrase, parameter)) { - return; - } - } - - onSearchUp(phrase, parameter); - } -} - -void ChatForm::onSearchUp(const QString& phrase, const ParameterSearch& parameter) -{ - if (phrase.isEmpty()) { - disableSearchText(); - } - - QVector lines = chatWidget->getLines(); - int numLines = lines.size(); - - int startLine; - - if (searchAfterLoadHistory) { - startLine = 1; - searchAfterLoadHistory = false; - } else { - startLine = numLines - searchPoint.x(); - } - - if (startLine == 0 && loadHistory(phrase, parameter)) { - return; - } - - const bool isSearch = searchInText(phrase, parameter, SearchDirection::Up); - - if (!isSearch) { - const QString pk = f->getPublicKey().toString(); - const QDateTime newBaseDate = - history->getDateWhereFindPhrase(pk, earliestMessage, phrase, parameter); - - if (!newBaseDate.isValid()) { - emit messageNotFoundShow(SearchDirection::Up); - return; - } - - searchPoint.setX(numLines); - searchAfterLoadHistory = true; - loadHistoryByDateRange(newBaseDate); - } -} - -void ChatForm::onSearchDown(const QString& phrase, const ParameterSearch& parameter) -{ - if (!searchInText(phrase, parameter, SearchDirection::Down)) { - emit messageNotFoundShow(SearchDirection::Down); - } -} - -void ChatForm::onFileSendFailed(uint32_t friendId, const QString& fname) -{ - if (friendId != f->getId()) { - return; - } - - addSystemInfoMessage(tr("Failed to send file \"%1\"").arg(fname), ChatMessage::ERROR, - QDateTime::currentDateTime()); -} - void ChatForm::onFriendStatusChanged(uint32_t friendId, Status::Status status) { // Disable call buttons if friend is offline @@ -653,8 +439,6 @@ void ChatForm::onFriendStatusChanged(uint32_t friendId, Status::Status status) if (!f->isOnline()) { // Hide the "is typing" message when a friend goes offline setFriendTyping(false); - } else { - offlineEngine->deliverOfflineMsgs(); } updateCallButtons(); @@ -682,16 +466,6 @@ void ChatForm::onFriendNameChanged(const QString& name) } } -void ChatForm::onFriendMessageReceived(quint32 friendId, const QString& message, bool isAction) -{ - if (friendId != f->getId()) { - return; - } - - QDateTime timestamp = QDateTime::currentDateTime(); - addMessage(f->getPublicKey(), message, timestamp, isAction); -} - void ChatForm::onStatusMessage(const QString& message) { if (sender() == f) { @@ -699,13 +473,6 @@ void ChatForm::onStatusMessage(const QString& message) } } -void ChatForm::onReceiptReceived(quint32 friendId, ReceiptNum receipt) -{ - if (friendId == f->getId()) { - offlineEngine->onReceiptReceived(receipt); - } -} - void ChatForm::onAvatarChanged(const ToxPk& friendPk, const QPixmap& pic) { if (friendPk != f->getPublicKey()) { @@ -751,7 +518,8 @@ void ChatForm::dropEvent(QDropEvent* ev) QString urlString = url.toString(); if (url.isValid() && !url.isLocalFile() && urlString.length() < static_cast(tox_max_message_length())) { - SendMessageStr(urlString); + messageDispatcher.sendMessage(false, urlString); + continue; } @@ -783,190 +551,6 @@ void ChatForm::dropEvent(QDropEvent* ev) void ChatForm::clearChatArea() { GenericChatForm::clearChatArea(/* confirm = */ false, /* inform = */ true); - offlineEngine->removeAllMessages(); -} - -QString getMsgAuthorDispName(const ToxPk& authorPk, const QString& dispName) -{ - QString authorStr; - const Core* core = Core::getInstance(); - bool isSelf = authorPk == core->getSelfId().getPublicKey(); - - if (!dispName.isEmpty()) { - authorStr = dispName; - } else if (isSelf) { - authorStr = core->getUsername(); - } else { - authorStr = ChatForm::resolveToxPk(authorPk); - } - return authorStr; -} - -void ChatForm::loadHistoryDefaultNum(bool processUndelivered) -{ - const QString pk = f->getPublicKey().toString(); - QList msgs = history->getChatHistoryDefaultNum(pk); - if (!msgs.isEmpty()) { - earliestMessage = msgs.first().timestamp; - } - handleLoadedMessages(msgs, processUndelivered); -} - -void ChatForm::loadHistoryByDateRange(const QDateTime& since, bool processUndelivered) -{ - QDateTime now = QDateTime::currentDateTime(); - if (since > now) { - return; - } - - if (!earliestMessage.isNull()) { - if (earliestMessage < since) { - return; - } - - if (earliestMessage < now) { - now = earliestMessage; - now = now.addMSecs(-1); - } - } - - QString pk = f->getPublicKey().toString(); - earliestMessage = since; - QList msgs = history->getChatHistoryFromDate(pk, since, now); - handleLoadedMessages(msgs, processUndelivered); -} - -void ChatForm::handleLoadedMessages(QList newHistMsgs, bool processUndelivered) -{ - ToxPk prevIdBackup = previousId; - previousId = ToxPk{}; - QList chatLines; - QDate lastDate(1, 0, 0); - for (const auto& histMessage : newHistMsgs) { - MessageMetadata const metadata = getMessageMetadata(histMessage); - lastDate = addDateLineIfNeeded(chatLines, lastDate, histMessage, metadata); - auto msg = chatMessageFromHistMessage(histMessage, metadata); - - if (!msg) { - continue; - } - - if (processUndelivered) { - sendLoadedMessage(msg, metadata); - } - chatLines.append(msg); - previousId = metadata.authorPk; - prevMsgDateTime = metadata.msgDateTime; - } - previousId = prevIdBackup; - insertChatlines(chatLines); - if (searchAfterLoadHistory && chatLines.isEmpty()) { - onContinueSearch(); - } -} - -void ChatForm::insertChatlines(QList chatLines) -{ - QScrollBar* verticalBar = chatWidget->verticalScrollBar(); - int savedSliderPos = verticalBar->maximum() - verticalBar->value(); - chatWidget->insertChatlinesOnTop(chatLines); - savedSliderPos = verticalBar->maximum() - savedSliderPos; - verticalBar->setValue(savedSliderPos); -} - -QDate ChatForm::addDateLineIfNeeded(QList& msgs, QDate const& lastDate, - History::HistMessage const& newMessage, - MessageMetadata const& metadata) -{ - // Show the date every new day - QDate newDate = metadata.msgDateTime.date(); - if (newDate > lastDate) { - QString dateText = newDate.toString(Settings::getInstance().getDateFormat()); - auto msg = ChatMessage::createChatInfoMessage(dateText, ChatMessage::INFO, QDateTime()); - msgs.append(msg); - return newDate; - } - return lastDate; -} - -ChatForm::MessageMetadata ChatForm::getMessageMetadata(History::HistMessage const& histMessage) -{ - const ToxPk authorPk = ToxId(histMessage.sender).getPublicKey(); - const QDateTime msgDateTime = histMessage.timestamp.toLocalTime(); - const bool isSelf = Core::getInstance()->getSelfId().getPublicKey() == authorPk; - const bool needSending = !histMessage.isSent && isSelf; - const bool isAction = - histMessage.content.getType() == HistMessageContentType::message - && histMessage.content.asMessage().startsWith(ACTION_PREFIX, Qt::CaseInsensitive); - const RowId id = histMessage.id; - return {isSelf, needSending, isAction, id, authorPk, msgDateTime}; -} - -ChatMessage::Ptr ChatForm::chatMessageFromHistMessage(History::HistMessage const& histMessage, - MessageMetadata const& metadata) -{ - ToxPk authorPk(ToxId(histMessage.sender).getPublicKey()); - QString authorStr = getMsgAuthorDispName(authorPk, histMessage.dispName); - QDateTime dateTime = metadata.needSending ? QDateTime() : metadata.msgDateTime; - - - ChatMessage::Ptr msg; - - switch (histMessage.content.getType()) { - case HistMessageContentType::message: { - ChatMessage::MessageType type = metadata.isAction ? ChatMessage::ACTION : ChatMessage::NORMAL; - auto& message = histMessage.content.asMessage(); - QString messageText = metadata.isAction ? message.mid(ACTION_PREFIX.length()) : message; - - msg = ChatMessage::createChatMessage(authorStr, messageText, type, metadata.isSelf, dateTime); - break; - } - case HistMessageContentType::file: { - auto& file = histMessage.content.asFile(); - bool isMe = file.direction == ToxFile::SENDING; - msg = ChatMessage::createFileTransferMessage(authorStr, file, isMe, dateTime); - break; - } - default: - qCritical() << "Invalid HistMessageContentType"; - assert(false); - } - - if (!metadata.isAction && needsToHideName(authorPk, metadata.msgDateTime)) { - msg->hideSender(); - } - return msg; -} - -void ChatForm::sendLoadedMessage(ChatMessage::Ptr chatMsg, MessageMetadata const& metadata) -{ - if (!metadata.needSending) { - return; - } - - ReceiptNum receipt; - bool messageSent{false}; - QString stringMsg = chatMsg->toString(); - if (f->isOnline()) { - Core* core = Core::getInstance(); - uint32_t friendId = f->getId(); - messageSent = metadata.isAction ? core->sendAction(friendId, stringMsg, receipt) - : core->sendMessage(friendId, stringMsg, receipt); - if (!messageSent) { - qWarning() << "Failed to send loaded message, adding to offline messaging"; - } - } - - auto onCompletion = CompleteMessageFunctor{}; - onCompletion.ma = chatMsg; - onCompletion.rowId = metadata.id; - - auto modelMsg = Message{metadata.isAction, stringMsg, QDateTime::currentDateTime()}; - if (messageSent) { - getOfflineMsgEngine()->addSentMessage(receipt, modelMsg, onCompletion); - } else { - getOfflineMsgEngine()->addUnsentMessage(modelMsg, onCompletion); - } } void ChatForm::onScreenshotClicked() @@ -1012,19 +596,6 @@ void ChatForm::sendImage(const QPixmap& pixmap) } } -void ChatForm::onLoadHistory() -{ - if (!history) { - return; - } - - LoadHistoryDialog dlg(f->getPublicKey()); - if (dlg.exec()) { - QDateTime fromTime = dlg.getFromDate(); - loadHistoryByDateRange(fromTime); - } -} - void ChatForm::insertChatMessage(ChatMessage::Ptr msg) { GenericChatForm::insertChatMessage(msg); @@ -1131,107 +702,9 @@ void ChatForm::hideEvent(QHideEvent* event) GenericChatForm::hideEvent(event); } -OfflineMsgEngine* ChatForm::getOfflineMsgEngine() -{ - return offlineEngine; -} - -void ChatForm::SendMessageStr(QString msg) -{ - if (msg.isEmpty()) { - return; - } - - bool isAction = msg.startsWith(ACTION_PREFIX, Qt::CaseInsensitive); - if (isAction) { - msg.remove(0, ACTION_PREFIX.length()); - } - - QStringList splittedMsg = Core::splitMessage(msg, tox_max_message_length()); - QDateTime timestamp = QDateTime::currentDateTime(); - - for (const QString& part : splittedMsg) { - QString historyPart = part; - if (isAction) { - historyPart = ACTION_PREFIX + part; - } - - ReceiptNum receipt; - bool messageSent{false}; - if (f->isOnline()) { - Core* core = Core::getInstance(); - uint32_t friendId = f->getId(); - messageSent = isAction ? core->sendAction(friendId, part, receipt) : core->sendMessage(friendId, part, receipt); - if (!messageSent) { - qCritical() << "Failed to send message, adding to offline messaging"; - } - } - - ChatMessage::Ptr ma = createSelfMessage(part, timestamp, isAction, false); - - Message modelMsg{isAction, part, timestamp}; - - - if (history && Settings::getInstance().getEnableLogging()) { - auto* offMsgEngine = getOfflineMsgEngine(); - QString selfPk = Core::getInstance()->getSelfId().toString(); - QString pk = f->getPublicKey().toString(); - QString name = Core::getInstance()->getUsername(); - bool const isSent = false; // This forces history to add it to the offline messages table - - // Use functor to avoid having to declare a lambda in a lambda - CompleteMessageFunctor onCompletion; - onCompletion.ma = ma; - - history->addNewMessage(pk, historyPart, selfPk, timestamp, isSent, name, - [messageSent, offMsgEngine, receipt, modelMsg, - onCompletion](RowId id) mutable { - onCompletion.rowId = id; - if (messageSent) { - offMsgEngine->addSentMessage(receipt, modelMsg, - onCompletion); - } else { - offMsgEngine->addUnsentMessage(modelMsg, onCompletion); - } - }); - } else { - if (messageSent) { - offlineEngine->addSentMessage(receipt, modelMsg, - [ma] { ma->markAsSent(QDateTime::currentDateTime()); }); - } else { - offlineEngine->addUnsentMessage(modelMsg, [ma] { - ma->markAsSent(QDateTime::currentDateTime()); - }); - } - } - - // set last message only when sending it - msgEdit->setLastMessage(msg); - Widget::getInstance()->updateFriendActivity(f); - } -} - -bool ChatForm::loadHistory(const QString& phrase, const ParameterSearch& parameter) -{ - const QString pk = f->getPublicKey().toString(); - const QDateTime newBaseDate = - history->getDateWhereFindPhrase(pk, earliestMessage, phrase, parameter); - - if (newBaseDate.isValid() && getFirstTime().isValid() && newBaseDate.date() < getFirstTime().date()) { - searchAfterLoadHistory = true; - loadHistoryByDateRange(newBaseDate); - - return true; - } - - return false; -} - void ChatForm::retranslateUi() { - loadHistoryAction->setText(tr("Load chat history...")); copyStatusAction->setText(tr("Copy")); - exportChatAction->setText(tr("Export to file")); updateMuteMicButton(); updateMuteVolButton(); @@ -1240,38 +713,3 @@ void ChatForm::retranslateUi() netcam->setShowMessages(chatWidget->isVisible()); } } - -void ChatForm::onExportChat() -{ - QString pk = f->getPublicKey().toString(); - QDateTime epochStart = QDateTime::fromMSecsSinceEpoch(0); - QDateTime now = QDateTime::currentDateTime(); - QList msgs = history->getChatHistoryFromDate(pk, epochStart, now); - - QString path = QFileDialog::getSaveFileName(Q_NULLPTR, tr("Save chat log")); - if (path.isEmpty()) { - return; - } - - QFile file(path); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - return; - } - - QString buffer; - for (const auto& it : msgs) { - if (it.content.getType() != HistMessageContentType::message) { - continue; - } - QString timestamp = it.timestamp.time().toString("hh:mm:ss"); - QString datestamp = it.timestamp.date().toString("yyyy-MM-dd"); - ToxPk authorPk(ToxId(it.sender).getPublicKey()); - QString author = getMsgAuthorDispName(authorPk, it.dispName); - - buffer = buffer - % QString{datestamp % '\t' % timestamp % '\t' % author % '\t' - % it.content.asMessage() % '\n'}; - } - file.write(buffer.toUtf8()); - file.close(); -} diff --git a/src/widget/form/chatform.h b/src/widget/form/chatform.h index 3a34676b7..fe64553d0 100644 --- a/src/widget/form/chatform.h +++ b/src/widget/form/chatform.h @@ -27,8 +27,10 @@ #include "genericchatform.h" #include "src/core/core.h" -#include "src/persistence/history.h" +#include "src/model/ichatlog.h" +#include "src/model/imessagedispatcher.h" #include "src/model/status.h" +#include "src/persistence/history.h" #include "src/widget/tool/screenshotgrabber.h" class CallConfirmWidget; @@ -44,15 +46,11 @@ class ChatForm : public GenericChatForm { Q_OBJECT public: - ChatForm(Friend* chatFriend, History* history); + ChatForm(Friend* chatFriend, IChatLog& chatLog, IMessageDispatcher& messageDispatcher); ~ChatForm(); void setStatusMessage(const QString& newMessage); - void loadHistoryByDateRange(const QDateTime& since, bool processUndelivered = false); - void loadHistoryDefaultNum(bool processUndelivered = false); - void dischargeReceipt(int receipt); void setFriendTyping(bool isTyping); - OfflineMsgEngine* getOfflineMsgEngine(); virtual void show(ContentLayout* contentLayout) final override; virtual void reloadTheme() final override; @@ -69,11 +67,6 @@ signals: void acceptCall(uint32_t friendId); public slots: - void startFileSend(ToxFile file); - void onFileTransferFinished(ToxFile file); - void onFileTransferCancelled(ToxFile file); - void onFileTransferBrokenUnbroken(ToxFile file, bool broken); - void onFileRecvRequest(ToxFile file); void onAvInvite(uint32_t friendId, bool video); void onAvStart(uint32_t friendId, bool video); void onAvEnd(uint32_t friendId, bool error); @@ -81,13 +74,9 @@ public slots: void onFileNameChanged(const ToxPk& friendPk); void clearChatArea(); -protected slots: - void searchInBegin(const QString& phrase, const ParameterSearch& parameter) override; - void onSearchUp(const QString& phrase, const ParameterSearch& parameter) override; - void onSearchDown(const QString& phrase, const ParameterSearch& parameter) override; - private slots: - void onSendTriggered() override; + void updateFriendActivity(); + void updateFriendActivityForFile(const ToxFile& file); void onAttachClicked() override; void onScreenshotClicked() override; @@ -99,47 +88,16 @@ private slots: void onMicMuteToggle(); void onVolMuteToggle(); - void onFileSendFailed(uint32_t friendId, const QString& fname); void onFriendStatusChanged(quint32 friendId, Status::Status status); void onFriendTypingChanged(quint32 friendId, bool isTyping); void onFriendNameChanged(const QString& name); - void onFriendMessageReceived(quint32 friendId, const QString& message, bool isAction); void onStatusMessage(const QString& message); - void onReceiptReceived(quint32 friendId, ReceiptNum receipt); - void onLoadHistory(); void onUpdateTime(); void sendImage(const QPixmap& pixmap); void doScreenshot(); void onCopyStatusMessage(); - void onExportChat(); private: - struct MessageMetadata - { - const bool isSelf; - const bool needSending; - const bool isAction; - const RowId id; - const ToxPk authorPk; - const QDateTime msgDateTime; - MessageMetadata(bool isSelf, bool needSending, bool isAction, RowId id, ToxPk authorPk, - QDateTime msgDateTime) - : isSelf{isSelf} - , needSending{needSending} - , isAction{isAction} - , id{id} - , authorPk{authorPk} - , msgDateTime{msgDateTime} - {} - }; - void handleLoadedMessages(QList newHistMsgs, bool processUndelivered); - QDate addDateLineIfNeeded(QList& msgs, QDate const& lastDate, - History::HistMessage const& newMessage, MessageMetadata const& metadata); - MessageMetadata getMessageMetadata(History::HistMessage const& histMessage); - ChatMessage::Ptr chatMessageFromHistMessage(History::HistMessage const& histMessage, - MessageMetadata const& metadata); - void sendLoadedMessage(ChatMessage::Ptr chatMsg, MessageMetadata const& metadata); - void insertChatlines(QList chatLines); void updateMuteMicButton(); void updateMuteVolButton(); void retranslateUi(); @@ -147,8 +105,6 @@ private: void startCounter(); void stopCounter(bool error = false); void updateCallButtons(); - void SendMessageStr(QString msg); - bool loadHistory(const QString& phrase, const ParameterSearch& parameter); protected: GenericNetCamView* createNetcam() final override; @@ -166,13 +122,7 @@ private: QTimer* callDurationTimer; QTimer typingTimer; QElapsedTimer timeElapsed; - OfflineMsgEngine* offlineEngine; - QAction* loadHistoryAction; QAction* copyStatusAction; - QAction* exportChatAction; - - History* history; - QHash ftransWidgets; bool isTyping; bool lastCallIsVideo; }; diff --git a/src/widget/form/genericchatform.cpp b/src/widget/form/genericchatform.cpp index 2f195b089..8308f7bf3 100644 --- a/src/widget/form/genericchatform.cpp +++ b/src/widget/form/genericchatform.cpp @@ -19,13 +19,15 @@ #include "genericchatform.h" +#include "src/chatlog/chatlinecontentproxy.h" #include "src/chatlog/chatlog.h" +#include "src/chatlog/content/filetransferwidget.h" #include "src/chatlog/content/timestamp.h" #include "src/core/core.h" -#include "src/model/friend.h" #include "src/friendlist.h" -#include "src/model/group.h" #include "src/grouplist.h" +#include "src/model/friend.h" +#include "src/model/group.h" #include "src/persistence/settings.h" #include "src/persistence/smileypack.h" #include "src/video/genericnetcamview.h" @@ -34,13 +36,15 @@ #include "src/widget/contentdialogmanager.h" #include "src/widget/contentlayout.h" #include "src/widget/emoticonswidget.h" +#include "src/widget/form/chatform.h" +#include "src/widget/form/loadhistorydialog.h" #include "src/widget/maskablepixmapwidget.h" +#include "src/widget/searchform.h" #include "src/widget/style.h" #include "src/widget/tool/chattextedit.h" #include "src/widget/tool/flyoutoverlaywidget.h" #include "src/widget/translator.h" #include "src/widget/widget.h" -#include "src/widget/searchform.h" #include #include @@ -127,13 +131,129 @@ QPushButton* createButton(const QString& name, T* self, Fun onClickSlot) return btn; } +ChatMessage::Ptr getChatMessageForIdx(ChatLogIdx idx, + const std::map& messages) +{ + auto existingMessageIt = messages.find(idx); + + if (existingMessageIt == messages.end()) { + return ChatMessage::Ptr(); + } + + return existingMessageIt->second; } -GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent) +bool shouldRenderDate(ChatLogIdx idxToRender, const IChatLog& chatLog) +{ + if (idxToRender == chatLog.getFirstIdx()) + return true; + + return chatLog.at(idxToRender - 1).getTimestamp().date() + != chatLog.at(idxToRender).getTimestamp().date(); +} + +ChatMessage::Ptr dateMessageForItem(const ChatLogItem& item) +{ + const auto& s = Settings::getInstance(); + const auto date = item.getTimestamp().date(); + auto dateText = date.toString(s.getDateFormat()); + return ChatMessage::createChatInfoMessage(dateText, ChatMessage::INFO, QDateTime()); +} + +ChatMessage::Ptr createMessage(const QString& displayName, bool isSelf, bool colorizeNames, + const ChatLogMessage& chatLogMessage) +{ + auto messageType = chatLogMessage.message.isAction ? ChatMessage::MessageType::ACTION + : ChatMessage::MessageType::NORMAL; + + const bool bSelfMentioned = + std::any_of(chatLogMessage.message.metadata.begin(), chatLogMessage.message.metadata.end(), + [](const MessageMetadata& metadata) { + return metadata.type == MessageMetadataType::selfMention; + }); + + if (bSelfMentioned) { + messageType = ChatMessage::MessageType::ALERT; + } + + // Spinner is displayed by passing in an empty date + auto timestamp = chatLogMessage.isComplete ? chatLogMessage.message.timestamp : QDateTime(); + + return ChatMessage::createChatMessage(displayName, chatLogMessage.message.content, messageType, + isSelf, timestamp, colorizeNames); +} + +void renderMessage(const QString& displayName, bool isSelf, bool colorizeNames, + const ChatLogMessage& chatLogMessage, ChatMessage::Ptr& chatMessage) +{ + + if (chatMessage) { + if (chatLogMessage.isComplete) { + chatMessage->markAsSent(chatLogMessage.message.timestamp); + } + } else { + chatMessage = createMessage(displayName, isSelf, colorizeNames, chatLogMessage); + } +} + +void renderFile(QString displayName, ToxFile file, bool isSelf, QDateTime timestamp, + ChatMessage::Ptr& chatMessage) +{ + if (!chatMessage) { + chatMessage = ChatMessage::createFileTransferMessage(displayName, file, isSelf, timestamp); + } else { + auto proxy = static_cast(chatMessage->getContent(1)); + assert(proxy->getWidgetType() == ChatLineContentProxy::FileTransferWidgetType); + auto ftWidget = static_cast(proxy->getWidget()); + ftWidget->onFileTransferUpdate(file); + } +} + +void renderItem(const ChatLogItem& item, bool hideName, bool colorizeNames, ChatMessage::Ptr& chatMessage) +{ + const auto& sender = item.getSender(); + + const Core* core = Core::getInstance(); + bool isSelf = sender == core->getSelfId().getPublicKey(); + + switch (item.getContentType()) { + case ChatLogItem::ContentType::message: { + const auto& chatLogMessage = item.getContentAsMessage(); + + renderMessage(item.getDisplayName(), isSelf, colorizeNames, chatLogMessage, chatMessage); + + break; + } + case ChatLogItem::ContentType::fileTransfer: { + const auto& file = item.getContentAsFile(); + renderFile(item.getDisplayName(), file.file, isSelf, item.getTimestamp(), chatMessage); + break; + } + } + + if (hideName) { + chatMessage->hideSender(); + } +} + +ChatLogIdx firstItemAfterDate(QDate date, const IChatLog& chatLog) +{ + auto idxs = chatLog.getDateIdxs(date, 1); + if (idxs.size()) { + return idxs[0].idx; + } else { + return chatLog.getNextIdx(); + } +} +} // namespace + +GenericChatForm::GenericChatForm(const Contact* contact, IChatLog& chatLog, + IMessageDispatcher& messageDispatcher, QWidget* parent) : QWidget(parent, Qt::Window) , audioInputFlag(false) , audioOutputFlag(false) - , searchAfterLoadHistory(false) + , chatLog(chatLog) + , messageDispatcher(messageDispatcher) { curRow = 0; headWidget = new ChatFormHeader(); @@ -219,8 +339,6 @@ GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent) menu.addActions(chatWidget->actions()); menu.addSeparator(); - saveChatAction = menu.addAction(QIcon::fromTheme("document-save"), QString(), - this, SLOT(onSaveLogClicked())); clearAction = menu.addAction(QIcon::fromTheme("edit-clear"), QString(), this, SLOT(clearChatArea()), QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_L)); @@ -229,6 +347,10 @@ GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent) copyLinkAction = menu.addAction(QIcon(), QString(), this, SLOT(copyLink())); menu.addSeparator(); + loadHistoryAction = menu.addAction(QIcon(), QString(), this, SLOT(onLoadHistory())); + exportChatAction = + menu.addAction(QIcon::fromTheme("document-save"), QString(), this, SLOT(onExportChat())); + connect(chatWidget, &ChatLog::customContextMenuRequested, this, &GenericChatForm::onChatContextMenuRequested); connect(chatWidget, &ChatLog::firstVisibleLineChanged, this, &GenericChatForm::updateShowDateInfo); @@ -239,7 +361,9 @@ GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent) connect(searchForm, &SearchForm::visibleChanged, this, &GenericChatForm::onSearchTriggered); connect(this, &GenericChatForm::messageNotFoundShow, searchForm, &SearchForm::showMessageNotFound); - connect(chatWidget, &ChatLog::workerTimeoutFinished, this, &GenericChatForm::onContinueSearch); + connect(&chatLog, &IChatLog::itemUpdated, this, &GenericChatForm::renderMessage); + + connect(msgEdit, &ChatTextEdit::enterPressed, this, &GenericChatForm::onSendTriggered); reloadTheme(); @@ -254,6 +378,11 @@ GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent) // update header on name/title change connect(contact, &Contact::displayedNameChanged, this, &GenericChatForm::setName); + auto chatLogIdxRange = chatLog.getNextIdx() - chatLog.getFirstIdx(); + auto firstChatLogIdx = (chatLogIdxRange < 100) ? chatLog.getFirstIdx() : chatLog.getNextIdx() - 100; + + renderMessages(firstChatLogIdx, chatLog.getNextIdx()); + netcam = nullptr; } @@ -373,101 +502,52 @@ void GenericChatForm::onChatContextMenuRequested(QPoint pos) menu.exec(pos); } +void GenericChatForm::onSendTriggered() +{ + auto msg = msgEdit->toPlainText(); + + if (msg.isEmpty()) { + return; + } + + msgEdit->setLastMessage(msg); + msgEdit->clear(); + + bool isAction = msg.startsWith(ChatForm::ACTION_PREFIX, Qt::CaseInsensitive); + if (isAction) { + msg.remove(0, ChatForm::ACTION_PREFIX.length()); + } + + messageDispatcher.sendMessage(isAction, msg); +} + /** * @brief Show, is it needed to hide message author name or not * @param messageAuthor Author of the sent message * @oaran messageTime DateTime of the sent message * @return True if it's needed to hide name, false otherwise */ -bool GenericChatForm::needsToHideName(const ToxPk& messageAuthor, const QDateTime& messageTime) const +bool GenericChatForm::needsToHideName(ChatLogIdx idx) const { - qint64 messagesTimeDiff = prevMsgDateTime.secsTo(messageTime); - return messageAuthor == previousId && messagesTimeDiff < chatWidget->repNameAfter; -} - -/** - * @brief Creates ChatMessage shared object and inserts it into ChatLog - * @param author Author of the message - * @param message Message text - * @param dt Date and time when message was sent - * @param isAction True if this is an action message, false otherwise - * @param isSent True if message was received by your friend - * @return ChatMessage object - */ -ChatMessage::Ptr GenericChatForm::createMessage(const ToxPk& author, const QString& message, - const QDateTime& dt, bool isAction, bool isSent, bool colorizeName) -{ - const Core* core = Core::getInstance(); - bool isSelf = author == core->getSelfId().getPublicKey(); - QString myNickName = core->getUsername().isEmpty() ? author.toString() : core->getUsername(); - QString authorStr = isSelf ? myNickName : resolveToxPk(author); - const auto now = QDateTime::currentDateTime(); - if (getLatestTime().date() != now.date()) { - addSystemDateMessage(); + // If the previous message is not rendered we should show the name + // regardless of other constraints + auto itemBefore = messages.find(idx - 1); + if (itemBefore == messages.end()) { + return false; } - ChatMessage::Ptr msg; - if (isAction) { - msg = ChatMessage::createChatMessage(authorStr, message, ChatMessage::ACTION, isSelf, QDateTime(), colorizeName); - previousId = ToxPk{}; - } else { - msg = ChatMessage::createChatMessage(authorStr, message, ChatMessage::NORMAL, isSelf, QDateTime(), colorizeName); - if (needsToHideName(author, now)) { - msg->hideSender(); - } + const auto& prevItem = chatLog.at(idx - 1); + const auto& currentItem = chatLog.at(idx); - previousId = author; - prevMsgDateTime = now; + // Always show the * in the name field for action messages + if (currentItem.getContentType() == ChatLogItem::ContentType::message + && currentItem.getContentAsMessage().message.isAction) { + return false; } - if (isSent) { - msg->markAsSent(dt); - } - - insertChatMessage(msg); - return msg; -} - -/** - * @brief Same, as createMessage, but creates message that you will send to someone - */ -ChatMessage::Ptr GenericChatForm::createSelfMessage(const QString& message, const QDateTime& dt, - bool isAction, bool isSent) -{ - ToxPk selfPk = Core::getInstance()->getSelfId().getPublicKey(); - return createMessage(selfPk, message, dt, isAction, isSent); -} - -/** - * @brief Inserts message into ChatLog - */ -void GenericChatForm::addMessage(const ToxPk& author, const QString& message, const QDateTime& dt, - bool isAction, bool colorizeName) -{ - createMessage(author, message, dt, isAction, true, colorizeName); -} - -/** - * @brief Inserts int ChatLog message that you have sent - */ -void GenericChatForm::addSelfMessage(const QString& message, const QDateTime& datetime, bool isAction) -{ - createSelfMessage(message, datetime, isAction, true); -} - -void GenericChatForm::addAlertMessage(const ToxPk& author, const QString& msg, const QDateTime& dt, bool colorizeName) -{ - QString authorStr = resolveToxPk(author); - bool isSelf = author == Core::getInstance()->getSelfId().getPublicKey(); - auto chatMsg = ChatMessage::createChatMessage(authorStr, msg, ChatMessage::ALERT, isSelf, dt, colorizeName); - const QDateTime newMsgDateTime = QDateTime::currentDateTime(); - if (needsToHideName(author, newMsgDateTime)) { - chatMsg->hideSender(); - } - - insertChatMessage(chatMsg); - previousId = author; - prevMsgDateTime = newMsgDateTime; + qint64 messagesTimeDiff = prevItem.getTimestamp().secsTo(currentItem.getTimestamp()); + return currentItem.getSender() == prevItem.getSender() + && messagesTimeDiff < chatWidget->repNameAfter; } void GenericChatForm::onEmoteButtonClicked() @@ -498,37 +578,6 @@ void GenericChatForm::onEmoteInsertRequested(QString str) msgEdit->setFocus(); // refocus so that we can continue typing } -void GenericChatForm::onSaveLogClicked() -{ - QString path = QFileDialog::getSaveFileName(Q_NULLPTR, tr("Save chat log")); - if (path.isEmpty()) - return; - - QFile file(path); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) - return; - - QString plainText; - auto lines = chatWidget->getLines(); - for (ChatLine::Ptr l : lines) { - Timestamp* rightCol = qobject_cast(l->getContent(2)); - - ChatLineContent* middleCol = l->getContent(1); - ChatLineContent* leftCol = l->getContent(0); - - QString nick = leftCol->getText().isNull() ? tr("[System message]") : leftCol->getText(); - - QString msg = middleCol->getText(); - - QString timestamp = (rightCol == nullptr) ? tr("Not sent") : rightCol->getText(); - - plainText += QString{nick % "\t" % timestamp % "\t" % msg % "\n"}; - } - - file.write(plainText.toUtf8()); - file.close(); -} - void GenericChatForm::onCopyLogClicked() { chatWidget->copySelectedText(); @@ -549,21 +598,22 @@ void GenericChatForm::onChatMessageFontChanged(const QFont& font) + fontToCss(font, "QTextEdit")); } +void GenericChatForm::setColorizedNames(bool enable) +{ + colorizeNames = enable; +} + void GenericChatForm::addSystemInfoMessage(const QString& message, ChatMessage::SystemMessageType type, const QDateTime& datetime) { - if (getLatestTime().date() != QDate::currentDate()) { - addSystemDateMessage(); - } - previousId = ToxPk(); insertChatMessage(ChatMessage::createChatInfoMessage(message, type, datetime)); } -void GenericChatForm::addSystemDateMessage() +void GenericChatForm::addSystemDateMessage(const QDate& date) { const Settings& s = Settings::getInstance(); - QString dateText = QDate::currentDate().toString(s.getDateFormat()); + QString dateText = date.toString(s.getDateFormat()); previousId = ToxPk(); insertChatMessage(ChatMessage::createChatInfoMessage(dateText, ChatMessage::INFO, QDateTime())); @@ -584,258 +634,16 @@ QDateTime GenericChatForm::getTime(const ChatLine::Ptr &chatLine) const return QDateTime(); } + void GenericChatForm::disableSearchText() { - if (searchPoint != QPoint(1, -1)) { - QVector lines = chatWidget->getLines(); - int numLines = lines.size(); - int index = numLines - searchPoint.x(); - if (index >= 0 && numLines > index) { - ChatLine::Ptr l = lines[index]; - if (l->getColumnCount() >= 2) { - ChatLineContent* content = l->getContent(1); - Text* text = static_cast(content); - text->deselectText(); - } - } + auto msgIt = messages.find(searchPos.logIdx); + if (msgIt != messages.end()) { + auto text = qobject_cast(msgIt->second->getContent(1)); + text->deselectText(); } } -bool GenericChatForm::searchInText(const QString& phrase, const ParameterSearch& parameter, SearchDirection direction) -{ - bool isSearch = false; - - if (phrase.isEmpty()) { - disableSearchText(); - } - - auto lines = chatWidget->getLines(); - - if (lines.isEmpty()) { - return isSearch; - } - - int numLines = lines.size(); - - int startLine = -1; - - if (parameter.period == PeriodSearch::WithTheEnd || parameter.period == PeriodSearch::None) { - startLine = numLines - searchPoint.x(); - } else if (parameter.period == PeriodSearch::WithTheFirst) { - startLine = 0; - } else if (parameter.period == PeriodSearch::AfterDate) { - const auto lambda = [=](const ChatLine::Ptr& item) { - const auto d = getTime(item).date(); - return d.isValid() && parameter.date <= d; - }; - - const auto find = std::find_if(lines.begin(), lines.end(), lambda); - - if (find != lines.end()) { - startLine = static_cast(std::distance(lines.begin(), find)); - } - } else if (parameter.period == PeriodSearch::BeforeDate) { -#if QT_VERSION > QT_VERSION_CHECK(5, 6, 0) - const auto lambda = [=](const ChatLine::Ptr& item) { - const auto d = getTime(item).date(); - return d.isValid() && parameter.date >= d; - }; - - const auto find = std::find_if(lines.rbegin(), lines.rend(), lambda); - - if (find != lines.rend()) { - startLine = static_cast(std::distance(find, lines.rend())) - 1; - } -#else - for (int i = lines.size() - 1; i >= 0; --i) { - auto d = getTime(lines[i]).date(); - if (d.isValid() && parameter.date >= d) { - startLine = i; - break; - } - } -#endif - } - - if (startLine < 0 || startLine >= numLines) { - return isSearch; - } - - const bool searchUp = (direction == SearchDirection::Up); - for (int i = startLine; searchUp ? i >= 0 : i < numLines; searchUp ? --i : ++i) { - ChatLine::Ptr l = lines[i]; - - if (l->getColumnCount() < 2) { - continue; - } - - ChatLineContent* content = l->getContent(1); - Text* text = static_cast(content); - - if (searchUp && searchPoint.y() == 0) { - text->deselectText(); - searchPoint.setY(-1); - - continue; - } - - QString txt = content->getText(); - - bool find = false; - QRegularExpression exp; - QRegularExpressionMatch match; - - auto flagIns = QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption; - auto flag = QRegularExpression::UseUnicodePropertiesOption; - switch (parameter.filter) { - case FilterSearch::Register: - find = txt.contains(phrase, Qt::CaseSensitive); - break; - case FilterSearch::WordsOnly: - exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flagIns); - find = txt.contains(exp); - break; - case FilterSearch::RegisterAndWordsOnly: - exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flag); - find = txt.contains(exp); - break; - case FilterSearch::RegisterAndRegular: - exp = QRegularExpression(phrase, flag); - find = txt.contains(exp); - break; - case FilterSearch::Regular: - exp = QRegularExpression(phrase, flagIns); - find = txt.contains(exp); - break; - default: - find = txt.contains(phrase, Qt::CaseInsensitive); - break; - } - - if (!find) { - continue; - } - - auto point = indexForSearchInLine(txt, phrase, parameter, direction); - if ((point.first == -1 && searchPoint.y() > -1)) { - text->deselectText(); - searchPoint.setY(-1); - } else { - chatWidget->scrollToLine(l); - text->deselectText(); - - if (exp.pattern().isEmpty()) { - text->selectText(phrase, point); - } else { - text->selectText(exp, point); - } - - searchPoint = QPoint(numLines - i, point.first); - isSearch = true; - - break; - } - } - - return isSearch; -} - -std::pair GenericChatForm::indexForSearchInLine(const QString& txt, const QString& phrase, const ParameterSearch& parameter, SearchDirection direction) -{ - int index = -1; - int size = 0; - - QRegularExpression exp; - auto flagIns = QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption; - auto flag = QRegularExpression::UseUnicodePropertiesOption; - if (direction == SearchDirection::Up) { - int startIndex = -1; - if (searchPoint.y() > -1) { - startIndex = searchPoint.y() - 1; - } - - switch (parameter.filter) { - case FilterSearch::Register: - index = txt.lastIndexOf(phrase, startIndex, Qt::CaseSensitive); - break; - case FilterSearch::WordsOnly: - exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flagIns); - break; - case FilterSearch::RegisterAndWordsOnly: - exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flag); - break; - case FilterSearch::RegisterAndRegular: - exp = QRegularExpression(phrase, flag); - break; - case FilterSearch::Regular: - exp = QRegularExpression(phrase, flagIns); - break; - default: - index = txt.lastIndexOf(phrase, startIndex, Qt::CaseInsensitive); - break; - } - - if (!exp.pattern().isEmpty()) { - auto matchIt = exp.globalMatch(txt); - - while (matchIt.hasNext()) { - const auto match = matchIt.next(); - - int sizeItem = match.capturedLength(); - int indexItem = match.capturedStart(); - - if (startIndex == -1 || indexItem < startIndex) { - index = indexItem; - size = sizeItem; - } else { - break; - } - } - } else { - size = phrase.size(); - } - - } else { - int startIndex = 0; - if (searchPoint.y() > -1) { - startIndex = searchPoint.y() + 1; - } - - switch (parameter.filter) { - case FilterSearch::Register: - index = txt.indexOf(phrase, startIndex, Qt::CaseSensitive); - break; - case FilterSearch::WordsOnly: - exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flagIns); - break; - case FilterSearch::RegisterAndWordsOnly: - exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flag); - break; - case FilterSearch::RegisterAndRegular: - exp = QRegularExpression(phrase, flag); - break; - case FilterSearch::Regular: - exp = QRegularExpression(phrase, flagIns); - break; - default: - index = txt.indexOf(phrase, startIndex, Qt::CaseInsensitive); - break; - } - - if (!exp.pattern().isEmpty()) { - const auto match = exp.match(txt, startIndex); - if (match.hasMatch()) { - size = match.capturedLength(0); - index = match.capturedEnd() - size; - } - } else { - size = phrase.size(); - } - } - - return std::make_pair(index, size); -} - void GenericChatForm::clearChatArea() { clearChatArea(/* confirm = */ true, /* inform = */ true); @@ -859,7 +667,7 @@ void GenericChatForm::clearChatArea(bool confirm, bool inform) if (inform) addSystemInfoMessage(tr("Cleared"), ChatMessage::INFO, QDateTime::currentDateTime()); - earliestMessage = QDateTime(); // null + messages.clear(); } void GenericChatForm::onSelectAllClicked() @@ -987,15 +795,177 @@ void GenericChatForm::searchFormShow() } } +void GenericChatForm::onLoadHistory() +{ + LoadHistoryDialog dlg(&chatLog); + if (dlg.exec()) { + QDateTime time = dlg.getFromDate(); + auto idx = firstItemAfterDate(dlg.getFromDate().date(), chatLog); + renderMessages(idx, chatLog.getNextIdx()); + } +} + +void GenericChatForm::onExportChat() +{ + QString path = QFileDialog::getSaveFileName(Q_NULLPTR, tr("Save chat log")); + if (path.isEmpty()) { + return; + } + + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + return; + } + + QString buffer; + for (auto i = chatLog.getFirstIdx(); i < chatLog.getNextIdx(); ++i) { + const auto& item = chatLog.at(i); + if (item.getContentType() != ChatLogItem::ContentType::message) { + continue; + } + + QString timestamp = item.getTimestamp().time().toString("hh:mm:ss"); + QString datestamp = item.getTimestamp().date().toString("yyyy-MM-dd"); + QString author = item.getDisplayName(); + + buffer = buffer + % QString{datestamp % '\t' % timestamp % '\t' % author % '\t' + % item.getContentAsMessage().message.content % '\n'}; + } + file.write(buffer.toUtf8()); + file.close(); +} + void GenericChatForm::onSearchTriggered() { if (searchForm->isHidden()) { searchForm->removeSearchPhrase(); + } + disableSearchText(); +} - disableSearchText(); +void GenericChatForm::searchInBegin(const QString& phrase, const ParameterSearch& parameter) +{ + disableSearchText(); + + bool bForwardSearch = false; + switch (parameter.period) { + case PeriodSearch::WithTheFirst: { + bForwardSearch = true; + searchPos.logIdx = chatLog.getFirstIdx(); + searchPos.numMatches = 0; + break; + } + case PeriodSearch::WithTheEnd: + case PeriodSearch::None: { + bForwardSearch = false; + searchPos.logIdx = chatLog.getNextIdx(); + searchPos.numMatches = 0; + break; + } + case PeriodSearch::AfterDate: { + bForwardSearch = true; + searchPos.logIdx = firstItemAfterDate(parameter.date, chatLog); + searchPos.numMatches = 0; + break; + } + case PeriodSearch::BeforeDate: { + bForwardSearch = false; + searchPos.logIdx = firstItemAfterDate(parameter.date, chatLog); + searchPos.numMatches = 0; + break; + } + } + + if (bForwardSearch) { + onSearchDown(phrase, parameter); } else { - searchPoint = QPoint(1, -1); - searchAfterLoadHistory = false; + onSearchUp(phrase, parameter); + } +} + +void GenericChatForm::onSearchUp(const QString& phrase, const ParameterSearch& parameter) +{ + auto result = chatLog.searchBackward(searchPos, phrase, parameter); + handleSearchResult(result, SearchDirection::Up); +} + +void GenericChatForm::onSearchDown(const QString& phrase, const ParameterSearch& parameter) +{ + auto result = chatLog.searchForward(searchPos, phrase, parameter); + handleSearchResult(result, SearchDirection::Down); +} + +void GenericChatForm::handleSearchResult(SearchResult result, SearchDirection direction) +{ + if (!result.found) { + emit messageNotFoundShow(direction); + return; + } + + disableSearchText(); + + searchPos = result.pos; + + auto const firstRenderedIdx = (messages.empty()) ? chatLog.getNextIdx() : messages.begin()->first; + + renderMessages(searchPos.logIdx, firstRenderedIdx, [this, result] { + auto msg = messages.at(searchPos.logIdx); + chatWidget->scrollToLine(msg); + + auto text = qobject_cast(msg->getContent(1)); + text->selectText(result.exp, std::make_pair(result.start, result.len)); + }); +} + +void GenericChatForm::renderMessage(ChatLogIdx idx) +{ + renderMessages(idx, idx + 1); +} + +void GenericChatForm::renderMessages(ChatLogIdx begin, ChatLogIdx end, + std::function onCompletion) +{ + QList beforeLines; + QList afterLines; + + for (auto i = begin; i < end; ++i) { + auto chatMessage = getChatMessageForIdx(i, messages); + renderItem(chatLog.at(i), needsToHideName(i), colorizeNames, chatMessage); + + if (messages.find(i) == messages.end()) { + QList* lines = + (messages.empty() || i > messages.rbegin()->first) ? &afterLines : &beforeLines; + + messages.insert({i, chatMessage}); + + if (shouldRenderDate(i, chatLog)) { + lines->push_back(dateMessageForItem(chatLog.at(i))); + } + lines->push_back(chatMessage); + } + } + + for (auto const& line : afterLines) { + chatWidget->insertChatlineAtBottom(line); + } + + if (!beforeLines.empty()) { + // Rendering upwards is expensive and has async behavior for chatWidget. + // Once rendering completes we call our completion callback once and + // then disconnect the signal + if (onCompletion) { + auto connection = std::make_shared(); + *connection = connect(chatWidget, &ChatLog::workerTimeoutFinished, + [onCompletion, connection, this] { + onCompletion(); + disconnect(*connection); + }); + } + + chatWidget->insertChatlinesOnTop(beforeLines); + } else if (onCompletion) { + onCompletion(); } } @@ -1012,31 +982,18 @@ void GenericChatForm::updateShowDateInfo(const ChatLine::Ptr& line) } } -void GenericChatForm::onContinueSearch() -{ - const QString phrase = searchForm->getSearchPhrase(); - const ParameterSearch parameter = searchForm->getParameterSearch(); - if (!phrase.isEmpty() && searchAfterLoadHistory) { - if (parameter.period == PeriodSearch::WithTheFirst || parameter.period == PeriodSearch::AfterDate) { - searchAfterLoadHistory = false; - onSearchDown(phrase, parameter); - } else { - onSearchUp(phrase, parameter); - } - } -} - void GenericChatForm::retranslateUi() { sendButton->setToolTip(tr("Send message")); emoteButton->setToolTip(tr("Smileys")); fileButton->setToolTip(tr("Send file(s)")); screenshotButton->setToolTip(tr("Send a screenshot")); - saveChatAction->setText(tr("Save chat log")); clearAction->setText(tr("Clear displayed messages")); quoteAction->setText(tr("Quote selected text")); copyLinkAction->setText(tr("Copy link address")); searchAction->setText(tr("Search in text")); + loadHistoryAction->setText(tr("Load chat history...")); + exportChatAction->setText(tr("Export to file")); } void GenericChatForm::showNetcam() diff --git a/src/widget/form/genericchatform.h b/src/widget/form/genericchatform.h index 6edfc5031..8dece3bfd 100644 --- a/src/widget/form/genericchatform.h +++ b/src/widget/form/genericchatform.h @@ -22,6 +22,7 @@ #include "src/chatlog/chatmessage.h" #include "src/core/toxpk.h" +#include "src/model/ichatlog.h" #include "src/widget/searchtypes.h" #include @@ -51,6 +52,9 @@ class QSplitter; class QToolButton; class QVBoxLayout; +class IMessageDispatcher; +class Message; + namespace Ui { class MainWindow; } @@ -65,7 +69,8 @@ class GenericChatForm : public QWidget { Q_OBJECT public: - explicit GenericChatForm(const Contact* contact, QWidget* parent = nullptr); + GenericChatForm(const Contact* contact, IChatLog& chatLog, + IMessageDispatcher& messageDispatcher, QWidget* parent = nullptr); ~GenericChatForm() override; void setName(const QString& newName); @@ -75,34 +80,28 @@ public: virtual void show(ContentLayout* contentLayout); virtual void reloadTheme(); - void addMessage(const ToxPk& author, const QString& message, const QDateTime& datetime, - bool isAction, bool colorizeName = false); - void addSelfMessage(const QString& message, const QDateTime& datetime, bool isAction); void addSystemInfoMessage(const QString& message, ChatMessage::SystemMessageType type, const QDateTime& datetime); - void addAlertMessage(const ToxPk& author, const QString& message, const QDateTime& datetime, bool colorizeName = false); static QString resolveToxPk(const ToxPk& pk); QDateTime getLatestTime() const; QDateTime getFirstTime() const; signals: - void sendMessage(uint32_t, QString); - void sendAction(uint32_t, QString); void messageInserted(); void messageNotFoundShow(SearchDirection direction); public slots: void focusInput(); void onChatMessageFontChanged(const QFont& font); + void setColorizedNames(bool enable); protected slots: void onChatContextMenuRequested(QPoint pos); virtual void onScreenshotClicked() = 0; - virtual void onSendTriggered() = 0; + void onSendTriggered(); virtual void onAttachClicked() = 0; void onEmoteButtonClicked(); void onEmoteInsertRequested(QString str); - void onSaveLogClicked(); void onCopyLogClicked(); void clearChatArea(); void clearChatArea(bool confirm, bool inform); @@ -113,26 +112,29 @@ protected slots: void onSplitterMoved(int pos, int index); void quoteSelectedText(); void copyLink(); + void onLoadHistory(); + void onExportChat(); void searchFormShow(); void onSearchTriggered(); void updateShowDateInfo(const ChatLine::Ptr& line); - virtual void searchInBegin(const QString& phrase, const ParameterSearch& parameter) = 0; - virtual void onSearchUp(const QString& phrase, const ParameterSearch& parameter) = 0; - virtual void onSearchDown(const QString& phrase, const ParameterSearch& parameter) = 0; - void onContinueSearch(); + void searchInBegin(const QString& phrase, const ParameterSearch& parameter); + void onSearchUp(const QString& phrase, const ParameterSearch& parameter); + void onSearchDown(const QString& phrase, const ParameterSearch& parameter); + void handleSearchResult(SearchResult result, SearchDirection direction); + void renderMessage(ChatLogIdx idx); + void renderMessages(ChatLogIdx begin, ChatLogIdx end, + std::function onCompletion = std::function()); private: void retranslateUi(); - void addSystemDateMessage(); + void addSystemDateMessage(const QDate& date); QDateTime getTime(const ChatLine::Ptr& chatLine) const; protected: ChatMessage::Ptr createMessage(const ToxPk& author, const QString& message, const QDateTime& datetime, bool isAction, bool isSent, bool colorizeName = false); - ChatMessage::Ptr createSelfMessage(const QString& message, const QDateTime& datetime, - bool isAction, bool isSent); - bool needsToHideName(const ToxPk& messageAuthor, const QDateTime& messageTime) const; + bool needsToHideName(ChatLogIdx idx) const; void showNetcam(); void hideNetcam(); virtual GenericNetCamView* createNetcam() = 0; @@ -152,15 +154,15 @@ protected: bool audioOutputFlag; int curRow; - QAction* saveChatAction; QAction* clearAction; QAction* quoteAction; QAction* copyLinkAction; QAction* searchAction; + QAction* loadHistoryAction; + QAction* exportChatAction; ToxPk previousId; - QDateTime prevMsgDateTime; QDateTime earliestMessage; QMenu menu; @@ -185,8 +187,11 @@ protected: GenericNetCamView* netcam; Widget* parent; - QPoint searchPoint; - bool searchAfterLoadHistory; + IChatLog& chatLog; + IMessageDispatcher& messageDispatcher; + SearchPos searchPos; + std::map messages; + bool colorizeNames = false; }; #endif // GENERICCHATFORM_H diff --git a/src/widget/form/groupchatform.cpp b/src/widget/form/groupchatform.cpp index 8f3b8ca0f..b617fdbc9 100644 --- a/src/widget/form/groupchatform.cpp +++ b/src/widget/form/groupchatform.cpp @@ -82,8 +82,8 @@ QString editName(const QString& name) * @brief Timeout = peer stopped sending audio. */ -GroupChatForm::GroupChatForm(Group* chatGroup) - : GenericChatForm (chatGroup) +GroupChatForm::GroupChatForm(Group* chatGroup, IChatLog& chatLog, IMessageDispatcher& messageDispatcher) + : GenericChatForm(chatGroup, chatLog, messageDispatcher) , group(chatGroup) , inCall(false) { @@ -118,8 +118,6 @@ GroupChatForm::GroupChatForm(Group* chatGroup) //nameLabel->setMinimumHeight(12); nusersLabel->setMinimumHeight(12); - connect(sendButton, SIGNAL(clicked()), this, SLOT(onSendTriggered())); - connect(msgEdit, SIGNAL(enterPressed()), this, SLOT(onSendTriggered())); connect(msgEdit, &ChatTextEdit::tabPressed, tabber, &TabCompleter::complete); connect(msgEdit, &ChatTextEdit::keyPressed, tabber, &TabCompleter::reset); connect(headWidget, &ChatFormHeader::callTriggered, this, &GroupChatForm::onCallClicked); @@ -143,31 +141,6 @@ GroupChatForm::~GroupChatForm() Translator::unregister(this); } -void GroupChatForm::onSendTriggered() -{ - QString msg = msgEdit->toPlainText(); - if (msg.isEmpty()) - return; - - msgEdit->setLastMessage(msg); - msgEdit->clear(); - - if (group->getPeersCount() != 1) { - if (msg.startsWith(ChatForm::ACTION_PREFIX, Qt::CaseInsensitive)) { - msg.remove(0, ChatForm::ACTION_PREFIX.length()); - emit sendAction(group->getId(), msg); - } else { - emit sendMessage(group->getId(), msg); - } - } else { - if (msg.startsWith(ChatForm::ACTION_PREFIX, Qt::CaseInsensitive)) - addSelfMessage(msg.mid(ChatForm::ACTION_PREFIX.length()), QDateTime::currentDateTime(), - true); - else - addSelfMessage(msg, QDateTime::currentDateTime(), false); - } -} - void GroupChatForm::onTitleChanged(const QString& author, const QString& title) { if (author.isEmpty()) { @@ -179,33 +152,6 @@ void GroupChatForm::onTitleChanged(const QString& author, const QString& title) addSystemInfoMessage(message, ChatMessage::INFO, curTime); } -void GroupChatForm::searchInBegin(const QString& phrase, const ParameterSearch& parameter) -{ - disableSearchText(); - - searchPoint = QPoint(1, -1); - - if (parameter.period == PeriodSearch::WithTheFirst || parameter.period == PeriodSearch::AfterDate) { - onSearchDown(phrase, parameter); - } else { - onSearchUp(phrase, parameter); - } -} - -void GroupChatForm::onSearchUp(const QString& phrase, const ParameterSearch& parameter) -{ - if (!searchInText(phrase, parameter, SearchDirection::Up)) { - emit messageNotFoundShow(SearchDirection::Up); - } -} - -void GroupChatForm::onSearchDown(const QString& phrase, const ParameterSearch& parameter) -{ - if (!searchInText(phrase, parameter, SearchDirection::Down)) { - emit messageNotFoundShow(SearchDirection::Down); - } -} - void GroupChatForm::onScreenshotClicked() { // Unsupported diff --git a/src/widget/form/groupchatform.h b/src/widget/form/groupchatform.h index f7091d69f..1e376e875 100644 --- a/src/widget/form/groupchatform.h +++ b/src/widget/form/groupchatform.h @@ -32,18 +32,19 @@ class TabCompleter; class FlowLayout; class QTimer; class GroupId; +class IMessageDispatcher; +class Message; class GroupChatForm : public GenericChatForm { Q_OBJECT public: - explicit GroupChatForm(Group* chatGroup); + explicit GroupChatForm(Group* chatGroup, IChatLog& chatLog, IMessageDispatcher& messageDispatcher); ~GroupChatForm(); void peerAudioPlaying(ToxPk peerPk); private slots: - void onSendTriggered() override; void onScreenshotClicked() override; void onAttachClicked() override; void onMicMuteToggle(); @@ -53,9 +54,6 @@ private slots: void onUserLeft(const ToxPk& user, const QString& name); void onPeerNameChanged(const ToxPk& peer, const QString& oldName, const QString& newName); void onTitleChanged(const QString& author, const QString& title); - void searchInBegin(const QString& phrase, const ParameterSearch& parameter) override; - void onSearchUp(const QString& phrase, const ParameterSearch& parameter) override; - void onSearchDown(const QString& phrase, const ParameterSearch& parameter) override; void onLabelContextMenuRequested(const QPoint& localPos); protected: @@ -70,7 +68,6 @@ private: void retranslateUi(); void updateUserCount(int numPeers); void updateUserNames(); - void sendJoinLeaveMessages(); void leaveGroupCall(); private: diff --git a/src/widget/form/loadhistorydialog.cpp b/src/widget/form/loadhistorydialog.cpp index d82628d90..83cc2d074 100644 --- a/src/widget/form/loadhistorydialog.cpp +++ b/src/widget/form/loadhistorydialog.cpp @@ -19,17 +19,18 @@ #include "loadhistorydialog.h" #include "ui_loadhistorydialog.h" +#include "src/model/ichatlog.h" #include "src/nexus.h" #include "src/persistence/history.h" #include "src/persistence/profile.h" +#include #include #include -#include -LoadHistoryDialog::LoadHistoryDialog(const ToxPk& friendPk, QWidget* parent) +LoadHistoryDialog::LoadHistoryDialog(const IChatLog* chatLog, QWidget* parent) : QDialog(parent) , ui(new Ui::LoadHistoryDialog) - , friendPk(friendPk) + , chatLog(chatLog) { ui->setupUi(this); highlightDates(QDate::currentDate().year(), QDate::currentDate().month()); @@ -76,15 +77,17 @@ void LoadHistoryDialog::highlightDates(int year, int month) History* history = Nexus::getProfile()->getHistory(); QDate monthStart(year, month, 1); QDate monthEnd(year, month + 1, 1); - QList counts = - history->getChatHistoryCounts(this->friendPk, monthStart, monthEnd); + + // Max 31 days in a month + auto dateIdxs = chatLog->getDateIdxs(monthStart, 31); QTextCharFormat format; format.setFontWeight(QFont::Bold); QCalendarWidget* calendar = ui->fromDate; - for (History::DateMessages p : counts) { - format.setToolTip(tr("%1 messages").arg(p.count)); - calendar->setDateTextFormat(monthStart.addDays(p.offsetDays), format); + for (const auto& item : dateIdxs) { + if (item.date < monthEnd) { + calendar->setDateTextFormat(item.date, format); + } } } diff --git a/src/widget/form/loadhistorydialog.h b/src/widget/form/loadhistorydialog.h index a93b36f4e..b2b79d38b 100644 --- a/src/widget/form/loadhistorydialog.h +++ b/src/widget/form/loadhistorydialog.h @@ -27,13 +27,14 @@ namespace Ui { class LoadHistoryDialog; } +class IChatLog; class LoadHistoryDialog : public QDialog { Q_OBJECT public: - explicit LoadHistoryDialog(const ToxPk& friendPk, QWidget* parent = nullptr); + explicit LoadHistoryDialog(const IChatLog* chatLog, QWidget* parent = nullptr); explicit LoadHistoryDialog(QWidget* parent = nullptr); ~LoadHistoryDialog(); @@ -46,7 +47,7 @@ public slots: private: Ui::LoadHistoryDialog* ui; - const ToxPk friendPk; + const IChatLog* chatLog; }; #endif // LOADHISTORYDIALOG_H diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index 887d089fd..7982b0528 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -49,11 +49,13 @@ #include "systemtrayicon.h" #include "form/groupchatform.h" #include "src/audio/audio.h" +#include "src/chatlog/content/filetransferwidget.h" #include "src/core/core.h" #include "src/core/coreav.h" #include "src/core/corefile.h" #include "src/friendlist.h" #include "src/grouplist.h" +#include "src/model/chathistory.h" #include "src/model/chatroom/friendchatroom.h" #include "src/model/chatroom/groupchatroom.h" #include "src/model/friend.h" @@ -92,6 +94,48 @@ bool toxActivateEventHandler(const QByteArray&) return true; } +namespace { + +/** + * @brief Dangerous way to find out if a path is writable. + * @param filepath Path to file which should be deleted. + * @return True, if file writeable, false otherwise. + */ +bool tryRemoveFile(const QString& filepath) +{ + QFile tmp(filepath); + bool writable = tmp.open(QIODevice::WriteOnly); + tmp.remove(); + return writable; +} + +void acceptFileTransfer(const ToxFile& file, const QString& path) +{ + QString filepath; + int number = 0; + + QString suffix = QFileInfo(file.fileName).completeSuffix(); + QString base = QFileInfo(file.fileName).baseName(); + + do { + filepath = QString("%1/%2%3.%4") + .arg(path, base, + number > 0 ? QString(" (%1)").arg(QString::number(number)) : QString(), + suffix); + ++number; + } while (QFileInfo(filepath).exists()); + + // Do not automatically accept the file-transfer if the path is not writable. + // The user can still accept it manually. + if (tryRemoveFile(filepath)) { + CoreFile* coreFile = Core::getInstance()->getCoreFile(); + coreFile->acceptFileRecvRequest(file.friendId, file.fileNum, filepath); + } else { + qWarning() << "Cannot write to " << filepath; + } +} +} // namespace + Widget* Widget::instance{nullptr}; Widget::Widget(IAudioControl& audio, QWidget* parent) @@ -251,6 +295,7 @@ void Widget::init() connect(profile, &Profile::selfAvatarChanged, profileForm, &ProfileForm::onSelfAvatarLoaded); + connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::onFileReceiveRequested); connect(coreFile, &CoreFile::fileDownloadFinished, filesForm, &FilesForm::onFileDownloadComplete); connect(coreFile, &CoreFile::fileUploadFinished, filesForm, &FilesForm::onFileUploadComplete); connect(ui->addButton, &QPushButton::clicked, this, &Widget::onAddClicked); @@ -271,6 +316,21 @@ void Widget::init() connect(filterDisplayGroup, &QActionGroup::triggered, this, &Widget::changeDisplayMode); connect(ui->friendList, &QWidget::customContextMenuRequested, this, &Widget::friendListContextMenu); + connect(coreFile, &CoreFile::fileSendStarted, this, &Widget::dispatchFile); + connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::dispatchFile); + connect(coreFile, &CoreFile::fileTransferAccepted, this, &Widget::dispatchFile); + connect(coreFile, &CoreFile::fileTransferCancelled, this, &Widget::dispatchFile); + connect(coreFile, &CoreFile::fileTransferFinished, this, &Widget::dispatchFile); + connect(coreFile, &CoreFile::fileTransferPaused, this, &Widget::dispatchFile); + connect(coreFile, &CoreFile::fileTransferInfo, this, &Widget::dispatchFile); + connect(coreFile, &CoreFile::fileTransferRemotePausedUnpaused, this, &Widget::dispatchFileWithBool); + connect(coreFile, &CoreFile::fileTransferBrokenUnbroken, this, &Widget::dispatchFileWithBool); + connect(coreFile, &CoreFile::fileSendFailed, this, &Widget::dispatchFileSendFailed); + // NOTE: We intentionally do not connect the fileUploadFinished and fileDownloadFinished signals + // because they are duplicates of fileTransferFinished NOTE: We don't hook up the + // fileNameChanged signal since it is only emitted before a fileReceiveRequest. We get the + // initial request with the sanitized name so there is no work for us to do + // keyboard shortcuts new QShortcut(Qt::CTRL + Qt::Key_Q, this, SLOT(close())); new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab, this, SLOT(previousContact())); @@ -904,10 +964,7 @@ void Widget::setUsername(const QString& username) Qt::convertFromPlainText(username, Qt::WhiteSpaceNormal)); // for overlength names } - QString sanename = username; - sanename.remove(QRegExp("[\\t\\n\\v\\f\\r\\x0000]")); - nameMention = QRegExp("\\b" + QRegExp::escape(username) + "\\b", Qt::CaseInsensitive); - sanitizedNameMention = nameMention; + sharedMessageProcessorParams.onUserNameSet(username); } void Widget::onStatusMessageChanged(const QString& newStatusMessage) @@ -924,13 +981,6 @@ void Widget::setStatusMessage(const QString& statusMessage) ui->statusLabel->setToolTip("

" + statusMessage.toHtmlEscaped() + "

"); } -void Widget::reloadHistory() -{ - for (auto f : FriendList::getAllFriends()) { - chatForms[f->getPublicKey()]->loadHistoryDefaultNum(true); - } -} - /** * @brief Plays a sound via the audioNotification AudioSink * @param sound Sound to play @@ -989,6 +1039,60 @@ void Widget::onStopNotification() audioNotification.reset(); } +/** + * @brief Dispatches file to the appropriate chatlog and accepts the transfer if necessary + */ +void Widget::dispatchFile(ToxFile file) +{ + const auto& friendId = FriendList::id2Key(file.friendId); + Friend* f = FriendList::findFriend(friendId); + if (!f) { + return; + } + + auto pk = f->getPublicKey(); + + if (file.status == ToxFile::INITIALIZING && file.direction == ToxFile::RECEIVING) { + auto sender = + (file.direction == ToxFile::SENDING) ? Core::getInstance()->getSelfPublicKey() : pk; + + const Settings& settings = Settings::getInstance(); + QString autoAcceptDir = settings.getAutoAcceptDir(f->getPublicKey()); + + if (autoAcceptDir.isEmpty() && settings.getAutoSaveEnabled()) { + autoAcceptDir = settings.getGlobalAutoAcceptDir(); + } + + auto maxAutoAcceptSize = settings.getMaxAutoAcceptSize(); + bool autoAcceptSizeCheckPassed = maxAutoAcceptSize == 0 || maxAutoAcceptSize >= file.filesize; + + if (!autoAcceptDir.isEmpty() && autoAcceptSizeCheckPassed) { + acceptFileTransfer(file, autoAcceptDir); + } + } + + const auto senderPk = (file.direction == ToxFile::SENDING) ? core->getSelfPublicKey() : pk; + friendChatLogs[pk]->onFileUpdated(senderPk, file); +} + +void Widget::dispatchFileWithBool(ToxFile file, bool) +{ + dispatchFile(file); +} + +void Widget::dispatchFileSendFailed(uint32_t friendId, const QString& fileName) +{ + const auto& friendPk = FriendList::id2Key(friendId); + + auto chatForm = chatForms.find(friendPk); + if (chatForm == chatForms.end()) { + return; + } + + chatForm.value()->addSystemInfoMessage(tr("Failed to send file \"%1\"").arg(fileName), + ChatMessage::ERROR, QDateTime::currentDateTime()); +} + void Widget::onRejectCall(uint32_t friendId) { CoreAV* const av = core->getAv(); @@ -1006,8 +1110,20 @@ void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk) const auto compact = settings.getCompactLayout(); auto widget = new FriendWidget(chatroom, compact); auto history = Nexus::getProfile()->getHistory(); - auto friendForm = new ChatForm(newfriend, history); + auto messageProcessor = MessageProcessor(sharedMessageProcessorParams); + auto friendMessageDispatcher = + std::make_shared(*newfriend, std::move(messageProcessor), *core); + + // Note: We do not have to connect the message dispatcher signals since + // ChatHistory hooks them up in a very specific order + auto chatHistory = + std::make_shared(*newfriend, history, *core, Settings::getInstance(), + *friendMessageDispatcher); + auto friendForm = new ChatForm(newfriend, *chatHistory, *friendMessageDispatcher); + + friendMessageDispatchers[friendPk] = friendMessageDispatcher; + friendChatLogs[friendPk] = chatHistory; friendChatrooms[friendPk] = chatroom; friendWidgets[friendPk] = widget; chatForms[friendPk] = friendForm; @@ -1021,6 +1137,20 @@ void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk) contactListWidget->addFriendWidget(widget, Status::Status::Offline, settings.getFriendCircleID(friendPk)); + + auto notifyReceivedCallback = [this, friendPk](const ToxPk& author, const Message& message) { + auto isTargeted = std::any_of(message.metadata.begin(), message.metadata.end(), + [](MessageMetadata metadata) { + return metadata.type == MessageMetadataType::selfMention; + }); + newFriendMessageAlert(friendPk, message.content); + }; + + auto notifyReceivedConnection = + connect(friendMessageDispatcher.get(), &IMessageDispatcher::messageReceived, + notifyReceivedCallback); + + friendAlertConnections.insert(friendPk, notifyReceivedConnection); connect(newfriend, &Friend::aliasChanged, this, &Widget::onFriendAliasChanged); connect(newfriend, &Friend::displayedNameChanged, this, &Widget::onFriendDisplayedNameChanged); @@ -1228,19 +1358,18 @@ void Widget::onFriendMessageReceived(uint32_t friendnumber, const QString& messa return; } - QDateTime timestamp = QDateTime::currentDateTime(); - Profile* profile = Nexus::getProfile(); - if (profile->isHistoryEnabled()) { - QString publicKey = f->getPublicKey().toString(); - QString name = f->getDisplayedName(); - QString text = message; - if (isAction) { - text = ChatForm::ACTION_PREFIX + text; - } - profile->getHistory()->addNewMessage(publicKey, text, publicKey, timestamp, true, name); + friendMessageDispatchers[f->getPublicKey()]->onMessageReceived(isAction, message); +} + +void Widget::onReceiptReceived(int friendId, ReceiptNum receipt) +{ + const auto& friendKey = FriendList::id2Key(friendId); + Friend* f = FriendList::findFriend(friendKey); + if (!f) { + return; } - newFriendMessageAlert(friendId, message); + friendMessageDispatchers[f->getPublicKey()]->onReceiptReceived(receipt); } void Widget::addFriendDialog(const Friend* frnd, ContentDialog* dialog) @@ -1526,6 +1655,15 @@ void Widget::onFriendRequestReceived(const ToxPk& friendPk, const QString& messa } } +void Widget::onFileReceiveRequested(const ToxFile& file) +{ + const ToxPk& friendPk = FriendList::id2Key(file.friendId); + newFriendMessageAlert(friendPk, + file.fileName + " (" + + FileTransferWidget::getHumanReadableSize(file.filesize) + ")", + true, true); +} + void Widget::updateFriendActivity(const Friend* frnd) { const ToxPk& pk = frnd->getPublicKey(); @@ -1560,6 +1698,8 @@ void Widget::removeFriend(Friend* f, bool fake) onAddClicked(); } + friendAlertConnections.remove(friendPk); + contactListWidget->removeFriendWidget(widget); ContentDialog* lastDialog = ContentDialogManager::getInstance()->getFriendDialog(friendPk); @@ -1790,26 +1930,8 @@ void Widget::onGroupMessageReceived(int groupnumber, int peernumber, const QStri assert(g); ToxPk author = core->getGroupPeerPk(groupnumber, peernumber); - bool isSelf = author == core->getSelfId().getPublicKey(); - if (settings.getBlackList().contains(author.toString())) { - qDebug() << "onGroupMessageReceived: Filtered:" << author.toString(); - return; - } - - const auto mention = !core->getUsername().isEmpty() - && (message.contains(nameMention) || message.contains(sanitizedNameMention)); - const auto targeted = !isSelf && mention; - const auto date = QDateTime::currentDateTime(); - auto form = groupChatForms[groupId].data(); - - if (targeted && !isAction) { - form->addAlertMessage(author, message, date, true); - } else { - form->addMessage(author, message, date, isAction, true); - } - - newGroupMessageAlert(groupId, author, message, targeted || settings.getGroupAlwaysNotify()); + groupMessageDispatchers[groupId]->onMessageReceived(author, isAction, message); } void Widget::onGroupPeerlistChanged(uint32_t groupnumber) @@ -1902,6 +2024,8 @@ void Widget::removeGroup(Group* g, bool fake) onAddClicked(); } + groupAlertConnections.remove(groupId); + contactListWidget->reDraw(); } @@ -1928,7 +2052,37 @@ Group* Widget::createGroup(uint32_t groupnumber, const GroupId& groupId) const auto compact = settings.getCompactLayout(); auto widget = new GroupWidget(chatroom, compact); - auto form = new GroupChatForm(newgroup); + auto messageProcessor = MessageProcessor(sharedMessageProcessorParams); + auto messageDispatcher = + std::make_shared(*newgroup, std::move(messageProcessor), *core, + *core, Settings::getInstance()); + auto groupChatLog = std::make_shared(*core); + + connect(messageDispatcher.get(), &IMessageDispatcher::messageReceived, groupChatLog.get(), + &SessionChatLog::onMessageReceived); + connect(messageDispatcher.get(), &IMessageDispatcher::messageSent, groupChatLog.get(), + &SessionChatLog::onMessageSent); + connect(messageDispatcher.get(), &IMessageDispatcher::messageComplete, groupChatLog.get(), + &SessionChatLog::onMessageComplete); + + auto notifyReceivedCallback = [this, groupId](const ToxPk& author, const Message& message) { + auto isTargeted = std::any_of(message.metadata.begin(), message.metadata.end(), + [](MessageMetadata metadata) { + return metadata.type == MessageMetadataType::selfMention; + }); + newGroupMessageAlert(groupId, author, message.content, + isTargeted || settings.getGroupAlwaysNotify()); + }; + + auto notifyReceivedConnection = + connect(messageDispatcher.get(), &IMessageDispatcher::messageReceived, notifyReceivedCallback); + groupAlertConnections.insert(groupId, notifyReceivedConnection); + + auto form = new GroupChatForm(newgroup, *groupChatLog, *messageDispatcher); + connect(&settings, &Settings::nameColorsChanged, form, &GenericChatForm::setColorizedNames); + form->setColorizedNames(settings.getEnableGroupChatsColor()); + groupMessageDispatchers[groupId] = messageDispatcher; + groupChatLogs[groupId] = groupChatLog; groupWidgets[groupId] = widget; groupChatrooms[groupId] = chatroom; groupChatForms[groupId] = QSharedPointer(form); @@ -1947,8 +2101,6 @@ Group* Widget::createGroup(uint32_t groupnumber, const GroupId& groupId) connect(widget, &GroupWidget::removeGroup, this, widgetRemoveGroup); connect(widget, &GroupWidget::middleMouseClicked, this, [=]() { removeGroup(groupId); }); connect(widget, &GroupWidget::chatroomWidgetClicked, form, &ChatForm::focusInput); - connect(form, &GroupChatForm::sendMessage, core, &Core::sendGroupMessage); - connect(form, &GroupChatForm::sendAction, core, &Core::sendGroupAction); connect(newgroup, &Group::titleChangedByUser, this, &Widget::titleChangedByUser); connect(this, &Widget::changeGroupTitle, core, &Core::changeGroupTitle); connect(core, &Core::usernameSet, newgroup, &Group::setSelfName); @@ -2220,7 +2372,7 @@ void Widget::clearAllReceipts() { QList frnds = FriendList::getAllFriends(); for (Friend* f : frnds) { - chatForms[f->getPublicKey()]->getOfflineMsgEngine()->removeAllMessages(); + friendMessageDispatchers[f->getPublicKey()]->clearOutgoingMessages(); } } diff --git a/src/widget/widget.h b/src/widget/widget.h index 6e5148247..020b14cec 100644 --- a/src/widget/widget.h +++ b/src/widget/widget.h @@ -36,6 +36,8 @@ #include "src/core/toxfile.h" #include "src/core/toxid.h" #include "src/core/toxpk.h" +#include "src/model/friendmessagedispatcher.h" +#include "src/model/groupmessagedispatcher.h" #if DESKTOP_NOTIFICATIONS #include "src/platform/desktop_notifications/desktopnotify.h" #endif @@ -79,6 +81,8 @@ class SystemTrayIcon; class VideoSurface; class UpdateCheck; class Settings; +class IChatLog; +class ChatHistory; class Widget final : public QMainWindow { @@ -135,7 +139,6 @@ public: static void confirmExecutableOpen(const QFileInfo& file); void clearAllReceipts(); - void reloadHistory(); void reloadTheme(); static inline QIcon prepareIcon(QString path, int w = 0, int h = 0); @@ -167,7 +170,9 @@ public slots: void onFriendUsernameChanged(int friendId, const QString& username); void onFriendAliasChanged(const ToxPk& friendId, const QString& alias); void onFriendMessageReceived(uint32_t friendnumber, const QString& message, bool isAction); + void onReceiptReceived(int friendId, ReceiptNum receipt); void onFriendRequestReceived(const ToxPk& friendPk, const QString& message); + void onFileReceiveRequested(const ToxFile& file); void updateFriendActivity(const Friend* frnd); void onEmptyGroupCreated(uint32_t groupnumber, const GroupId& groupId, const QString& title); void onGroupJoined(int groupNum, const GroupId& groupId); @@ -230,6 +235,9 @@ private slots: void incomingNotification(uint32_t friendId); void onRejectCall(uint32_t friendId); void onStopNotification(); + void dispatchFile(ToxFile file); + void dispatchFileWithBool(ToxFile file, bool); + void dispatchFileSendFailed(uint32_t friendId, const QString& fileName); private: // QMainWindow overrides @@ -305,7 +313,6 @@ private: bool notify(QObject* receiver, QEvent* event); bool autoAwayActive = false; QTimer* timer; - QRegExp nameMention, sanitizedNameMention; bool eventFlag; bool eventIcon; bool wasMaximized = false; @@ -319,14 +326,32 @@ private: Settings& settings; QMap friendWidgets; + // Shared pointer because qmap copies stuff all over the place + QMap> friendMessageDispatchers; + // Stop gap method of linking our friend messages back to a group id. + // Eventual goal is to have a notification manager that works on + // Messages hooked up to message dispatchers but we aren't there + // yet + QMap friendAlertConnections; + QMap> friendChatLogs; QMap> friendChatrooms; QMap chatForms; QMap groupWidgets; + QMap> groupMessageDispatchers; + + // Stop gap method of linking our group messages back to a group id. + // Eventual goal is to have a notification manager that works on + // Messages hooked up to message dispatchers but we aren't there + // yet + QMap groupAlertConnections; + QMap> groupChatLogs; QMap> groupChatrooms; QMap> groupChatForms; Core* core = nullptr; + + MessageProcessor::SharedParams sharedMessageProcessorParams; #if DESKTOP_NOTIFICATIONS DesktopNotify notifier; #endif