From b7a88cde6ecc9c7dce892875bc547416ec15bd93 Mon Sep 17 00:00:00 2001 From: Mick Sayson Date: Thu, 18 Feb 2021 19:08:16 -0800 Subject: [PATCH] refactor(chatlog): Move rendering of messages from GenericChatForm -> ChatLog * Simplifies reasoning about who owns what functionality between GenericChatForm and ChatLog. GenericChatForm is now just a layout class and ChatLog handles all interactions with retrieving and displaying messages from the model * Reasoning for work is described in more detail in #6223 --- src/chatlog/chatlog.cpp | 380 ++++++++++++++++++++++++++-- src/chatlog/chatlog.h | 30 ++- src/widget/form/genericchatform.cpp | 350 +------------------------ src/widget/form/genericchatform.h | 17 -- 4 files changed, 394 insertions(+), 383 deletions(-) diff --git a/src/chatlog/chatlog.cpp b/src/chatlog/chatlog.cpp index cee5c97d6..8d41ed15a 100644 --- a/src/chatlog/chatlog.cpp +++ b/src/chatlog/chatlog.cpp @@ -26,6 +26,7 @@ #include "src/widget/gui.h" #include "src/widget/translator.h" #include "src/widget/style.h" +#include "src/persistence/settings.h" #include #include @@ -39,11 +40,9 @@ #include #include -/** - * @var ChatLog::repNameAfter - * @brief repetition interval sender name (sec) - */ +namespace +{ template T clamp(T x, T min, T max) { @@ -54,8 +53,115 @@ T clamp(T x, T min, T max) return x; } -ChatLog::ChatLog(QWidget* parent) +ChatMessage::Ptr getChatMessageForIdx(ChatLogIdx idx, + const std::map& messages) +{ + auto existingMessageIt = messages.find(idx); + + if (existingMessageIt == messages.end()) { + return ChatMessage::Ptr(); + } + + return existingMessageIt->second; +} + +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; + } + + const auto timestamp = chatLogMessage.message.timestamp; + return ChatMessage::createChatMessage(displayName, chatLogMessage.message.content, messageType, + isSelf, chatLogMessage.state, timestamp, colorizeNames); +} + +void renderMessageRaw(const QString& displayName, bool isSelf, bool colorizeNames, + const ChatLogMessage& chatLogMessage, ChatMessage::Ptr& chatMessage) +{ + + if (chatMessage) { + if (chatLogMessage.state == MessageState::complete) { + chatMessage->markAsDelivered(chatLogMessage.message.timestamp); + } else if (chatLogMessage.state == MessageState::broken) { + chatMessage->markAsBroken(); + } + } else { + chatMessage = createMessage(displayName, isSelf, colorizeNames, chatLogMessage); + } +} + +/** + * @return Chat message message type (info/warning) for the given system message + * @param[in] systemMessage + */ +ChatMessage::SystemMessageType getChatMessageType(const SystemMessage& systemMessage) +{ + switch (systemMessage.messageType) + { + case SystemMessageType::fileSendFailed: + case SystemMessageType::messageSendFailed: + case SystemMessageType::unexpectedCallEnd: + return ChatMessage::ERROR; + case SystemMessageType::userJoinedGroup: + case SystemMessageType::userLeftGroup: + case SystemMessageType::peerNameChanged: + case SystemMessageType::peerStateChange: + case SystemMessageType::titleChanged: + case SystemMessageType::cleared: + case SystemMessageType::outgoingCall: + case SystemMessageType::incomingCall: + case SystemMessageType::callEnd: + return ChatMessage::INFO; + } + + return ChatMessage::INFO; +} + +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 + + +ChatLog::ChatLog(IChatLog& chatLog, const Core& core, QWidget* parent) : QGraphicsView(parent) + , chatLog(chatLog) + , core(core) { // Create the scene busyScene = new QGraphicsScene(this); @@ -63,6 +169,10 @@ ChatLog::ChatLog(QWidget* parent) scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex); setScene(scene); + busyNotification = ChatMessage::createBusyNotification(); + busyNotification->addToScene(busyScene); + busyNotification->visibilityChanged(true); + // Cfg. setInteractive(true); setAcceptDrops(false); @@ -128,7 +238,11 @@ ChatLog::ChatLog(QWidget* parent) reloadTheme(); retranslateUi(); Translator::registerHandler(std::bind(&ChatLog::retranslateUi, this), this); - scrollToBottom(); + + auto chatLogIdxRange = chatLog.getNextIdx() - chatLog.getFirstIdx(); + auto firstChatLogIdx = (chatLogIdxRange < 100) ? chatLog.getFirstIdx() : chatLog.getNextIdx() - 100; + + renderMessages(firstChatLogIdx, chatLog.getNextIdx()); } ChatLog::~ChatLog() @@ -576,6 +690,8 @@ void ChatLog::clear() insertChatlineAtBottom(l); updateSceneRect(); + + messages.clear(); } void ChatLog::copySelectedText(bool toSelectionBuffer) const @@ -587,16 +703,6 @@ void ChatLog::copySelectedText(bool toSelectionBuffer) const clipboard->setText(text, toSelectionBuffer ? QClipboard::Selection : QClipboard::Clipboard); } -void ChatLog::setBusyNotification(ChatLine::Ptr notification) -{ - if (!notification.get()) - return; - - busyNotification = notification; - busyNotification->addToScene(busyScene); - busyNotification->visibilityChanged(true); -} - void ChatLog::setTypingNotificationVisible(bool visible) { if (typingNotification.get()) { @@ -663,6 +769,80 @@ void ChatLog::reloadTheme() } } +void ChatLog::startSearch(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 { + onSearchUp(phrase, parameter); + } +} + +void ChatLog::onSearchUp(const QString& phrase, const ParameterSearch& parameter) +{ + auto result = chatLog.searchBackward(searchPos, phrase, parameter); + handleSearchResult(result, SearchDirection::Up); +} + +void ChatLog::onSearchDown(const QString& phrase, const ParameterSearch& parameter) +{ + auto result = chatLog.searchForward(searchPos, phrase, parameter); + handleSearchResult(result, SearchDirection::Down); +} + +void ChatLog::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); + scrollToLine(msg); + + auto text = qobject_cast(msg->getContent(1)); + text->selectText(result.exp, std::make_pair(result.start, result.len)); + }); +} + void ChatLog::forceRelayout() { startResizeWorker(); @@ -770,11 +950,9 @@ void ChatLog::updateTypingNotification() void ChatLog::updateBusyNotification() { - if (busyNotification.get()) { - // repoisition the busy notification (centered) - busyNotification->layout(useableWidth(), getVisibleRect().topLeft() - + QPointF(0, getVisibleRect().height() / 2.0)); - } + // repoisition the busy notification (centered) + busyNotification->layout(useableWidth(), getVisibleRect().topLeft() + + QPointF(0, getVisibleRect().height() / 2.0)); } ChatLine::Ptr ChatLog::findLineByPosY(qreal yPos) const @@ -857,6 +1035,58 @@ void ChatLog::onMultiClickTimeout() clickCount = 0; } +void ChatLog::renderMessage(ChatLogIdx idx) +{ + renderMessages(idx, idx + 1); +} + +void ChatLog::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) { + 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(this, &ChatLog::workerTimeoutFinished, + [this, onCompletion, connection] { + onCompletion(); + this->disconnect(*connection); + }); + } + + insertChatlinesOnTop(beforeLines); + } else if (onCompletion) { + onCompletion(); + } +} + + void ChatLog::handleMultiClickEvent() { // Ignore single or double clicks @@ -916,7 +1146,7 @@ void ChatLog::focusOutEvent(QFocusEvent* ev) void ChatLog::wheelEvent(QWheelEvent *event) { QGraphicsView::wheelEvent(event); - checkVisibility(true); + checkVisibility(); } void ChatLog::retranslateUi() @@ -1047,3 +1277,109 @@ void ChatLog::setTypingNotification() typingNotification->addToScene(scene); updateTypingNotification(); } + +void ChatLog::renderItem(const ChatLogItem& item, bool hideName, bool colorizeNames, ChatMessage::Ptr& chatMessage) +{ + const auto& sender = item.getSender(); + + bool isSelf = sender == core.getSelfId().getPublicKey(); + + switch (item.getContentType()) { + case ChatLogItem::ContentType::message: { + const auto& chatLogMessage = item.getContentAsMessage(); + + renderMessageRaw(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; + } + case ChatLogItem::ContentType::systemMessage: { + const auto& systemMessage = item.getContentAsSystemMessage(); + + auto chatMessageType = getChatMessageType(systemMessage); + chatMessage = ChatMessage::createChatInfoMessage(systemMessage.toString(), chatMessageType, QDateTime::currentDateTime()); + // Ignore caller's decision to hide the name. We show the icon in the + // slot of the sender's name so we always want it visible + hideName = false; + break; + } + } + + if (hideName) { + chatMessage->hideSender(); + } +} + +void ChatLog::renderFile(QString displayName, ToxFile file, bool isSelf, QDateTime timestamp, + ChatMessage::Ptr& chatMessage) +{ + if (!chatMessage) { + CoreFile* coreFile = core.getCoreFile(); + assert(coreFile); + chatMessage = ChatMessage::createFileTransferMessage(displayName, *coreFile, 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); + } +} + +/** + * @brief Show, is it needed to hide message author name or not + * @param idx ChatLogIdx of the message + * @return True if the name should be hidden, false otherwise + */ +bool ChatLog::needsToHideName(ChatLogIdx idx) const +{ + // 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; + } + + const auto& prevItem = chatLog.at(idx - 1); + const auto& currentItem = chatLog.at(idx); + + // Always show the * in the name field for action messages + if (currentItem.getContentType() == ChatLogItem::ContentType::message + && currentItem.getContentAsMessage().message.isAction) { + return false; + } + + qint64 messagesTimeDiff = prevItem.getTimestamp().secsTo(currentItem.getTimestamp()); + return currentItem.getSender() == prevItem.getSender() + && messagesTimeDiff < repNameAfter; +} + +void ChatLog::disableSearchText() +{ + auto msgIt = messages.find(searchPos.logIdx); + if (msgIt != messages.end()) { + auto text = qobject_cast(msgIt->second->getContent(1)); + text->deselectText(); + } +} + +void ChatLog::removeSearchPhrase() +{ + disableSearchText(); +} + +void ChatLog::jumpToDate(QDate date) { + auto idx = firstItemAfterDate(date, chatLog); + jumpToIdx(idx); +} + +void ChatLog::jumpToIdx(ChatLogIdx idx) { + if (messages.find(idx) == messages.end()) { + renderMessages(idx, chatLog.getNextIdx()); + } + + scrollToLine(messages[idx]); +} diff --git a/src/chatlog/chatlog.h b/src/chatlog/chatlog.h index c3011a200..2ef615d7e 100644 --- a/src/chatlog/chatlog.h +++ b/src/chatlog/chatlog.h @@ -26,6 +26,7 @@ #include "chatline.h" #include "chatmessage.h" #include "src/widget/style.h" +#include "src/model/ichatlog.h" class QGraphicsScene; class QGraphicsRectItem; @@ -39,7 +40,7 @@ class ChatLog : public QGraphicsView { Q_OBJECT public: - explicit ChatLog(QWidget* parent = nullptr); + explicit ChatLog(IChatLog& chatLog, const Core& core, QWidget* parent = nullptr); virtual ~ChatLog(); void insertChatlineAtBottom(ChatLine::Ptr l); @@ -48,7 +49,6 @@ public: void clearSelection(); void clear(); void copySelectedText(bool toSelectionBuffer = false) const; - void setBusyNotification(ChatLine::Ptr notification); void setTypingNotificationVisible(bool visible); void setTypingNotificationName(const QString& displayName); void scrollToLine(ChatLine::Ptr line); @@ -62,20 +62,36 @@ public: ChatLineContent* getContentFromGlobalPos(QPoint pos) const; const uint repNameAfter = 5 * 60; + void setColorizedNames(bool enable) { colorizeNames = enable; }; + void jumpToDate(QDate date); + void jumpToIdx(ChatLogIdx idx); + signals: void selectionChanged(); void workerTimeoutFinished(); void firstVisibleLineChanged(const ChatLine::Ptr& prevLine, const ChatLine::Ptr& firstLine); + void messageNotFoundShow(SearchDirection direction); public slots: void forceRelayout(); void reloadTheme(); + void startSearch(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 removeSearchPhrase(); + private slots: void onSelectionTimerTimeout(); void onWorkerTimeout(); void onMultiClickTimeout(); + void renderMessage(ChatLogIdx idx); + void renderMessages(ChatLogIdx begin, ChatLogIdx end, + std::function onCompletion = std::function()); + + protected: QRectF calculateSceneRect() const; QRect getVisibleRect() const; @@ -122,6 +138,10 @@ private: void moveMultiSelectionDown(int offset); void setTypingNotification(); + void renderItem(const ChatLogItem &item, bool hideName, bool colorizeNames, ChatMessage::Ptr &chatMessage); + void renderFile(QString displayName, ToxFile file, bool isSelf, QDateTime timestamp, ChatMessage::Ptr &chatMessage); + bool needsToHideName(ChatLogIdx idx) const; + void disableSearchText(); private: enum class SelectionMode { @@ -171,4 +191,10 @@ private: // layout QMargins margins = QMargins(10, 10, 10, 10); qreal lineSpacing = 5.0f; + + IChatLog& chatLog; + std::map messages; + bool colorizeNames = false; + SearchPos searchPos; + const Core& core; }; diff --git a/src/widget/form/genericchatform.cpp b/src/widget/form/genericchatform.cpp index 83c9cc370..22683119f 100644 --- a/src/widget/form/genericchatform.cpp +++ b/src/widget/form/genericchatform.cpp @@ -132,108 +132,6 @@ 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; -} - -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; - } - - const auto timestamp = chatLogMessage.message.timestamp; - return ChatMessage::createChatMessage(displayName, chatLogMessage.message.content, messageType, - isSelf, chatLogMessage.state, timestamp, colorizeNames); -} - -void renderMessageRaw(const QString& displayName, bool isSelf, bool colorizeNames, - const ChatLogMessage& chatLogMessage, ChatMessage::Ptr& chatMessage) -{ - - if (chatMessage) { - if (chatLogMessage.state == MessageState::complete) { - chatMessage->markAsDelivered(chatLogMessage.message.timestamp); - } else if (chatLogMessage.state == MessageState::broken) { - chatMessage->markAsBroken(); - } - } else { - chatMessage = createMessage(displayName, isSelf, colorizeNames, chatLogMessage); - } -} - - - -ChatLogIdx firstItemAfterDate(QDate date, const IChatLog& chatLog) -{ - auto idxs = chatLog.getDateIdxs(date, 1); - if (idxs.size()) { - return idxs[0].idx; - } else { - return chatLog.getNextIdx(); - } -} - -/** - * @return Chat message message type (info/warning) for the given system message - * @param[in] systemMessage - */ -ChatMessage::SystemMessageType getChatMessageType(const SystemMessage& systemMessage) -{ - switch (systemMessage.messageType) { - case SystemMessageType::fileSendFailed: - case SystemMessageType::messageSendFailed: - case SystemMessageType::unexpectedCallEnd: - return ChatMessage::ERROR; - case SystemMessageType::userJoinedGroup: - case SystemMessageType::userLeftGroup: - case SystemMessageType::peerNameChanged: - case SystemMessageType::peerStateChange: - case SystemMessageType::titleChanged: - case SystemMessageType::cleared: - case SystemMessageType::outgoingCall: - case SystemMessageType::incomingCall: - case SystemMessageType::callEnd: - return ChatMessage::INFO; - } - - return ChatMessage::INFO; -} } // namespace GenericChatForm::GenericChatForm(const Core& _core, const Contact* contact, IChatLog& chatLog, @@ -249,8 +147,7 @@ GenericChatForm::GenericChatForm(const Core& _core, const Contact* contact, ICha headWidget = new ChatFormHeader(); searchForm = new SearchForm(); dateInfo = new QLabel(this); - chatWidget = new ChatLog(this); - chatWidget->setBusyNotification(ChatMessage::createBusyNotification()); + chatWidget = new ChatLog(chatLog, core, this); searchForm->hide(); dateInfo->setAlignment(Qt::AlignHCenter); dateInfo->setVisible(false); @@ -344,13 +241,11 @@ GenericChatForm::GenericChatForm(const Core& _core, const Contact* contact, ICha &GenericChatForm::onChatContextMenuRequested); connect(chatWidget, &ChatLog::firstVisibleLineChanged, this, &GenericChatForm::updateShowDateInfo); - connect(searchForm, &SearchForm::searchInBegin, this, &GenericChatForm::searchInBegin); - connect(searchForm, &SearchForm::searchUp, this, &GenericChatForm::onSearchUp); - connect(searchForm, &SearchForm::searchDown, this, &GenericChatForm::onSearchDown); - connect(searchForm, &SearchForm::visibleChanged, this, &GenericChatForm::onSearchTriggered); - connect(this, &GenericChatForm::messageNotFoundShow, searchForm, &SearchForm::showMessageNotFound); - - connect(&chatLog, &IChatLog::itemUpdated, this, &GenericChatForm::renderMessage); + connect(searchForm, &SearchForm::searchInBegin, chatWidget, &ChatLog::startSearch); + connect(searchForm, &SearchForm::searchUp, chatWidget, &ChatLog::onSearchUp); + connect(searchForm, &SearchForm::searchDown, chatWidget, &ChatLog::onSearchDown); + connect(searchForm, &SearchForm::visibleChanged, chatWidget, &ChatLog::removeSearchPhrase); + connect(chatWidget, &ChatLog::messageNotFoundShow, searchForm, &SearchForm::showMessageNotFound); connect(msgEdit, &ChatTextEdit::enterPressed, this, &GenericChatForm::onSendTriggered); @@ -369,10 +264,6 @@ GenericChatForm::GenericChatForm(const Core& _core, const Contact* contact, ICha // 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()); } GenericChatForm::~GenericChatForm() @@ -381,21 +272,6 @@ GenericChatForm::~GenericChatForm() delete searchForm; } -void GenericChatForm::renderFile(QString displayName, ToxFile file, bool isSelf, QDateTime timestamp, - ChatMessage::Ptr& chatMessage) -{ - if (!chatMessage) { - CoreFile* coreFile = core.getCoreFile(); - assert(coreFile); - chatMessage = ChatMessage::createFileTransferMessage(displayName, *coreFile, 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 GenericChatForm::adjustFileMenuPosition() { QPoint pos = fileButton->mapTo(bodySplitter, QPoint()); @@ -561,33 +437,6 @@ void GenericChatForm::onSendTriggered() messageDispatcher.sendMessage(isAction, msg); } -/** - * @brief Show, is it needed to hide message author name or not - * @param idx ChatLogIdx of the message - * @return True if the name should be hidden, false otherwise - */ -bool GenericChatForm::needsToHideName(ChatLogIdx idx) const -{ - // 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; - } - - const auto& prevItem = chatLog.at(idx - 1); - const auto& currentItem = chatLog.at(idx); - - // Always show the * in the name field for action messages - if (currentItem.getContentType() == ChatLogItem::ContentType::message - && currentItem.getContentAsMessage().message.isAction) { - return false; - } - - qint64 messagesTimeDiff = prevItem.getTimestamp().secsTo(currentItem.getTimestamp()); - return currentItem.getSender() == prevItem.getSender() - && messagesTimeDiff < chatWidget->repNameAfter; -} void GenericChatForm::onEmoteButtonClicked() { @@ -639,7 +488,7 @@ void GenericChatForm::onChatMessageFontChanged(const QFont& font) void GenericChatForm::setColorizedNames(bool enable) { - colorizeNames = enable; + chatWidget->setColorizedNames(enable); } void GenericChatForm::addSystemInfoMessage(const QDateTime& datetime, SystemMessageType messageType, @@ -676,15 +525,6 @@ QDateTime GenericChatForm::getTime(const ChatLine::Ptr &chatLine) const } -void GenericChatForm::disableSearchText() -{ - auto msgIt = messages.find(searchPos.logIdx); - if (msgIt != messages.end()) { - auto text = qobject_cast(msgIt->second->getContent(1)); - text->deselectText(); - } -} - void GenericChatForm::clearChatArea() { clearChatArea(/* confirm = */ true, /* inform = */ true); @@ -706,8 +546,6 @@ void GenericChatForm::clearChatArea(bool confirm, bool inform) if (inform) addSystemInfoMessage(QDateTime::currentDateTime(), SystemMessageType::cleared, {}); - - messages.clear(); } void GenericChatForm::onSelectAllClicked() @@ -821,9 +659,7 @@ void GenericChatForm::onLoadHistory() { LoadHistoryDialog dlg(&chatLog); if (dlg.exec()) { - QDateTime time = dlg.getFromDate(); - auto idx = firstItemAfterDate(dlg.getFromDate().date(), chatLog); - renderMessages(idx, chatLog.getNextIdx()); + chatWidget->jumpToDate(dlg.getFromDate().date()); } } @@ -858,176 +694,6 @@ void GenericChatForm::onExportChat() file.close(); } -void GenericChatForm::onSearchTriggered() -{ - if (searchForm->isHidden()) { - searchForm->removeSearchPhrase(); - } - 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 { - 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::renderItem(const ChatLogItem& item, bool hideName, bool colorizeNames, ChatMessage::Ptr& chatMessage) -{ - const auto& sender = item.getSender(); - - bool isSelf = sender == core.getSelfId().getPublicKey(); - - switch (item.getContentType()) { - case ChatLogItem::ContentType::message: { - const auto& chatLogMessage = item.getContentAsMessage(); - - renderMessageRaw(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; - } - case ChatLogItem::ContentType::systemMessage: { - const auto& systemMessage = item.getContentAsSystemMessage(); - - auto chatMessageType = getChatMessageType(systemMessage); - chatMessage = ChatMessage::createChatInfoMessage(systemMessage.toString(), chatMessageType, - QDateTime::currentDateTime()); - // Ignore caller's decision to hide the name. We show the icon in the - // slot of the sender's name so we always want it visible - hideName = false; - break; - } - } - - if (hideName) { - chatMessage->hideSender(); - } -} - -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, - [this, onCompletion, connection] { - onCompletion(); - this->disconnect(*connection); - }); - } - - chatWidget->insertChatlinesOnTop(beforeLines); - } else if (onCompletion) { - onCompletion(); - } -} - void GenericChatForm::updateShowDateInfo(const ChatLine::Ptr& prevLine, const ChatLine::Ptr& topLine) { // If the dateInfo is visible we need to pretend the top line is the one diff --git a/src/widget/form/genericchatform.h b/src/widget/form/genericchatform.h index 4fe4f30e7..440fdece1 100644 --- a/src/widget/form/genericchatform.h +++ b/src/widget/form/genericchatform.h @@ -82,7 +82,6 @@ public: signals: void messageInserted(); - void messageNotFoundShow(SearchDirection direction); public slots: void focusInput(); @@ -108,28 +107,16 @@ protected slots: void onLoadHistory(); void onExportChat(); void searchFormShow(); - void onSearchTriggered(); void updateShowDateInfo(const ChatLine::Ptr& prevLine, const ChatLine::Ptr& topLine); - 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(const QDate& date); QDateTime getTime(const ChatLine::Ptr& chatLine) const; - void renderItem(const ChatLogItem &item, bool hideName, bool colorizeNames, ChatMessage::Ptr &chatMessage); - void renderFile(QString displayName, ToxFile file, bool isSelf, QDateTime timestamp, ChatMessage::Ptr &chatMessage); protected: ChatMessage::Ptr createMessage(const ToxPk& author, const QString& message, const QDateTime& datetime, bool isAction, bool isSent, bool colorizeName = false); - bool needsToHideName(ChatLogIdx idx) const; virtual void insertChatMessage(ChatMessage::Ptr msg); void adjustFileMenuPosition(); void hideEvent(QHideEvent* event) override; @@ -137,7 +124,6 @@ protected: bool event(QEvent*) final; void resizeEvent(QResizeEvent* event) final; bool eventFilter(QObject* object, QEvent* event) final; - void disableSearchText(); bool searchInText(const QString& phrase, const ParameterSearch& parameter, SearchDirection direction); std::pair indexForSearchInLine(const QString& txt, const QString& phrase, const ParameterSearch& parameter, SearchDirection direction); @@ -178,7 +164,4 @@ protected: IChatLog& chatLog; IMessageDispatcher& messageDispatcher; - SearchPos searchPos; - std::map messages; - bool colorizeNames = false; };