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:
parent
dd7df35720
commit
b7a88cde6e
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user