1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00

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
This commit is contained in:
Mick Sayson 2021-02-18 19:08:16 -08:00 committed by Anthony Bilinski
parent dd7df35720
commit b7a88cde6e
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
4 changed files with 394 additions and 383 deletions

View File

@ -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 <QAction>
#include <QApplication>
@ -39,11 +40,9 @@
#include <algorithm>
#include <cassert>
/**
* @var ChatLog::repNameAfter
* @brief repetition interval sender name (sec)
*/
namespace
{
template <class T>
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<ChatLogIdx, ChatMessage::Ptr>& 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<Text*>(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<void(void)> onCompletion)
{
QList<ChatLine::Ptr> beforeLines;
QList<ChatLine::Ptr> 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<ChatLine::Ptr>* 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<QMetaObject::Connection>();
*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<ChatLineContentProxy*>(chatMessage->getContent(1));
assert(proxy->getWidgetType() == ChatLineContentProxy::FileTransferWidgetType);
auto ftWidget = static_cast<FileTransferWidget*>(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<Text*>(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]);
}

View File

@ -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<void(void)> onCompletion = std::function<void(void)>());
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<ChatLogIdx, ChatMessage::Ptr> messages;
bool colorizeNames = false;
SearchPos searchPos;
const Core& core;
};

View File

@ -132,108 +132,6 @@ QPushButton* createButton(const QString& name, T* self, Fun onClickSlot)
return btn;
}
ChatMessage::Ptr getChatMessageForIdx(ChatLogIdx idx,
const std::map<ChatLogIdx, ChatMessage::Ptr>& 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<ChatLineContentProxy*>(chatMessage->getContent(1));
assert(proxy->getWidgetType() == ChatLineContentProxy::FileTransferWidgetType);
auto ftWidget = static_cast<FileTransferWidget*>(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<Text*>(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<Text*>(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<void(void)> onCompletion)
{
QList<ChatLine::Ptr> beforeLines;
QList<ChatLine::Ptr> 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<ChatLine::Ptr>* 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<QMetaObject::Connection>();
*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

View File

@ -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<void(void)> onCompletion = std::function<void(void)>());
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<int, int> 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<ChatLogIdx, ChatMessage::Ptr> messages;
bool colorizeNames = false;
};