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/gui.h"
|
||||||
#include "src/widget/translator.h"
|
#include "src/widget/translator.h"
|
||||||
#include "src/widget/style.h"
|
#include "src/widget/style.h"
|
||||||
|
#include "src/persistence/settings.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
@ -39,11 +40,9 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
/**
|
|
||||||
* @var ChatLog::repNameAfter
|
|
||||||
* @brief repetition interval sender name (sec)
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
template <class T>
|
template <class T>
|
||||||
T clamp(T x, T min, T max)
|
T clamp(T x, T min, T max)
|
||||||
{
|
{
|
||||||
|
@ -54,8 +53,115 @@ T clamp(T x, T min, T max)
|
||||||
return x;
|
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)
|
: QGraphicsView(parent)
|
||||||
|
, chatLog(chatLog)
|
||||||
|
, core(core)
|
||||||
{
|
{
|
||||||
// Create the scene
|
// Create the scene
|
||||||
busyScene = new QGraphicsScene(this);
|
busyScene = new QGraphicsScene(this);
|
||||||
|
@ -63,6 +169,10 @@ ChatLog::ChatLog(QWidget* parent)
|
||||||
scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex);
|
scene->setItemIndexMethod(QGraphicsScene::BspTreeIndex);
|
||||||
setScene(scene);
|
setScene(scene);
|
||||||
|
|
||||||
|
busyNotification = ChatMessage::createBusyNotification();
|
||||||
|
busyNotification->addToScene(busyScene);
|
||||||
|
busyNotification->visibilityChanged(true);
|
||||||
|
|
||||||
// Cfg.
|
// Cfg.
|
||||||
setInteractive(true);
|
setInteractive(true);
|
||||||
setAcceptDrops(false);
|
setAcceptDrops(false);
|
||||||
|
@ -128,7 +238,11 @@ ChatLog::ChatLog(QWidget* parent)
|
||||||
reloadTheme();
|
reloadTheme();
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
Translator::registerHandler(std::bind(&ChatLog::retranslateUi, this), this);
|
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()
|
ChatLog::~ChatLog()
|
||||||
|
@ -576,6 +690,8 @@ void ChatLog::clear()
|
||||||
insertChatlineAtBottom(l);
|
insertChatlineAtBottom(l);
|
||||||
|
|
||||||
updateSceneRect();
|
updateSceneRect();
|
||||||
|
|
||||||
|
messages.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatLog::copySelectedText(bool toSelectionBuffer) const
|
void ChatLog::copySelectedText(bool toSelectionBuffer) const
|
||||||
|
@ -587,16 +703,6 @@ void ChatLog::copySelectedText(bool toSelectionBuffer) const
|
||||||
clipboard->setText(text, toSelectionBuffer ? QClipboard::Selection : QClipboard::Clipboard);
|
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)
|
void ChatLog::setTypingNotificationVisible(bool visible)
|
||||||
{
|
{
|
||||||
if (typingNotification.get()) {
|
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()
|
void ChatLog::forceRelayout()
|
||||||
{
|
{
|
||||||
startResizeWorker();
|
startResizeWorker();
|
||||||
|
@ -770,11 +950,9 @@ void ChatLog::updateTypingNotification()
|
||||||
|
|
||||||
void ChatLog::updateBusyNotification()
|
void ChatLog::updateBusyNotification()
|
||||||
{
|
{
|
||||||
if (busyNotification.get()) {
|
|
||||||
// repoisition the busy notification (centered)
|
// repoisition the busy notification (centered)
|
||||||
busyNotification->layout(useableWidth(), getVisibleRect().topLeft()
|
busyNotification->layout(useableWidth(), getVisibleRect().topLeft()
|
||||||
+ QPointF(0, getVisibleRect().height() / 2.0));
|
+ QPointF(0, getVisibleRect().height() / 2.0));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatLine::Ptr ChatLog::findLineByPosY(qreal yPos) const
|
ChatLine::Ptr ChatLog::findLineByPosY(qreal yPos) const
|
||||||
|
@ -857,6 +1035,58 @@ void ChatLog::onMultiClickTimeout()
|
||||||
clickCount = 0;
|
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()
|
void ChatLog::handleMultiClickEvent()
|
||||||
{
|
{
|
||||||
// Ignore single or double clicks
|
// Ignore single or double clicks
|
||||||
|
@ -916,7 +1146,7 @@ void ChatLog::focusOutEvent(QFocusEvent* ev)
|
||||||
void ChatLog::wheelEvent(QWheelEvent *event)
|
void ChatLog::wheelEvent(QWheelEvent *event)
|
||||||
{
|
{
|
||||||
QGraphicsView::wheelEvent(event);
|
QGraphicsView::wheelEvent(event);
|
||||||
checkVisibility(true);
|
checkVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatLog::retranslateUi()
|
void ChatLog::retranslateUi()
|
||||||
|
@ -1047,3 +1277,109 @@ void ChatLog::setTypingNotification()
|
||||||
typingNotification->addToScene(scene);
|
typingNotification->addToScene(scene);
|
||||||
updateTypingNotification();
|
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 "chatline.h"
|
||||||
#include "chatmessage.h"
|
#include "chatmessage.h"
|
||||||
#include "src/widget/style.h"
|
#include "src/widget/style.h"
|
||||||
|
#include "src/model/ichatlog.h"
|
||||||
|
|
||||||
class QGraphicsScene;
|
class QGraphicsScene;
|
||||||
class QGraphicsRectItem;
|
class QGraphicsRectItem;
|
||||||
|
@ -39,7 +40,7 @@ class ChatLog : public QGraphicsView
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit ChatLog(QWidget* parent = nullptr);
|
explicit ChatLog(IChatLog& chatLog, const Core& core, QWidget* parent = nullptr);
|
||||||
virtual ~ChatLog();
|
virtual ~ChatLog();
|
||||||
|
|
||||||
void insertChatlineAtBottom(ChatLine::Ptr l);
|
void insertChatlineAtBottom(ChatLine::Ptr l);
|
||||||
|
@ -48,7 +49,6 @@ public:
|
||||||
void clearSelection();
|
void clearSelection();
|
||||||
void clear();
|
void clear();
|
||||||
void copySelectedText(bool toSelectionBuffer = false) const;
|
void copySelectedText(bool toSelectionBuffer = false) const;
|
||||||
void setBusyNotification(ChatLine::Ptr notification);
|
|
||||||
void setTypingNotificationVisible(bool visible);
|
void setTypingNotificationVisible(bool visible);
|
||||||
void setTypingNotificationName(const QString& displayName);
|
void setTypingNotificationName(const QString& displayName);
|
||||||
void scrollToLine(ChatLine::Ptr line);
|
void scrollToLine(ChatLine::Ptr line);
|
||||||
|
@ -62,20 +62,36 @@ public:
|
||||||
ChatLineContent* getContentFromGlobalPos(QPoint pos) const;
|
ChatLineContent* getContentFromGlobalPos(QPoint pos) const;
|
||||||
const uint repNameAfter = 5 * 60;
|
const uint repNameAfter = 5 * 60;
|
||||||
|
|
||||||
|
void setColorizedNames(bool enable) { colorizeNames = enable; };
|
||||||
|
void jumpToDate(QDate date);
|
||||||
|
void jumpToIdx(ChatLogIdx idx);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void selectionChanged();
|
void selectionChanged();
|
||||||
void workerTimeoutFinished();
|
void workerTimeoutFinished();
|
||||||
void firstVisibleLineChanged(const ChatLine::Ptr& prevLine, const ChatLine::Ptr& firstLine);
|
void firstVisibleLineChanged(const ChatLine::Ptr& prevLine, const ChatLine::Ptr& firstLine);
|
||||||
|
|
||||||
|
void messageNotFoundShow(SearchDirection direction);
|
||||||
public slots:
|
public slots:
|
||||||
void forceRelayout();
|
void forceRelayout();
|
||||||
void reloadTheme();
|
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:
|
private slots:
|
||||||
void onSelectionTimerTimeout();
|
void onSelectionTimerTimeout();
|
||||||
void onWorkerTimeout();
|
void onWorkerTimeout();
|
||||||
void onMultiClickTimeout();
|
void onMultiClickTimeout();
|
||||||
|
|
||||||
|
void renderMessage(ChatLogIdx idx);
|
||||||
|
void renderMessages(ChatLogIdx begin, ChatLogIdx end,
|
||||||
|
std::function<void(void)> onCompletion = std::function<void(void)>());
|
||||||
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QRectF calculateSceneRect() const;
|
QRectF calculateSceneRect() const;
|
||||||
QRect getVisibleRect() const;
|
QRect getVisibleRect() const;
|
||||||
|
@ -122,6 +138,10 @@ private:
|
||||||
void moveMultiSelectionDown(int offset);
|
void moveMultiSelectionDown(int offset);
|
||||||
void setTypingNotification();
|
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:
|
private:
|
||||||
enum class SelectionMode
|
enum class SelectionMode
|
||||||
{
|
{
|
||||||
|
@ -171,4 +191,10 @@ private:
|
||||||
// layout
|
// layout
|
||||||
QMargins margins = QMargins(10, 10, 10, 10);
|
QMargins margins = QMargins(10, 10, 10, 10);
|
||||||
qreal lineSpacing = 5.0f;
|
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;
|
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
|
} // namespace
|
||||||
|
|
||||||
GenericChatForm::GenericChatForm(const Core& _core, const Contact* contact, IChatLog& chatLog,
|
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();
|
headWidget = new ChatFormHeader();
|
||||||
searchForm = new SearchForm();
|
searchForm = new SearchForm();
|
||||||
dateInfo = new QLabel(this);
|
dateInfo = new QLabel(this);
|
||||||
chatWidget = new ChatLog(this);
|
chatWidget = new ChatLog(chatLog, core, this);
|
||||||
chatWidget->setBusyNotification(ChatMessage::createBusyNotification());
|
|
||||||
searchForm->hide();
|
searchForm->hide();
|
||||||
dateInfo->setAlignment(Qt::AlignHCenter);
|
dateInfo->setAlignment(Qt::AlignHCenter);
|
||||||
dateInfo->setVisible(false);
|
dateInfo->setVisible(false);
|
||||||
|
@ -344,13 +241,11 @@ GenericChatForm::GenericChatForm(const Core& _core, const Contact* contact, ICha
|
||||||
&GenericChatForm::onChatContextMenuRequested);
|
&GenericChatForm::onChatContextMenuRequested);
|
||||||
connect(chatWidget, &ChatLog::firstVisibleLineChanged, this, &GenericChatForm::updateShowDateInfo);
|
connect(chatWidget, &ChatLog::firstVisibleLineChanged, this, &GenericChatForm::updateShowDateInfo);
|
||||||
|
|
||||||
connect(searchForm, &SearchForm::searchInBegin, this, &GenericChatForm::searchInBegin);
|
connect(searchForm, &SearchForm::searchInBegin, chatWidget, &ChatLog::startSearch);
|
||||||
connect(searchForm, &SearchForm::searchUp, this, &GenericChatForm::onSearchUp);
|
connect(searchForm, &SearchForm::searchUp, chatWidget, &ChatLog::onSearchUp);
|
||||||
connect(searchForm, &SearchForm::searchDown, this, &GenericChatForm::onSearchDown);
|
connect(searchForm, &SearchForm::searchDown, chatWidget, &ChatLog::onSearchDown);
|
||||||
connect(searchForm, &SearchForm::visibleChanged, this, &GenericChatForm::onSearchTriggered);
|
connect(searchForm, &SearchForm::visibleChanged, chatWidget, &ChatLog::removeSearchPhrase);
|
||||||
connect(this, &GenericChatForm::messageNotFoundShow, searchForm, &SearchForm::showMessageNotFound);
|
connect(chatWidget, &ChatLog::messageNotFoundShow, searchForm, &SearchForm::showMessageNotFound);
|
||||||
|
|
||||||
connect(&chatLog, &IChatLog::itemUpdated, this, &GenericChatForm::renderMessage);
|
|
||||||
|
|
||||||
connect(msgEdit, &ChatTextEdit::enterPressed, this, &GenericChatForm::onSendTriggered);
|
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
|
// update header on name/title change
|
||||||
connect(contact, &Contact::displayedNameChanged, this, &GenericChatForm::setName);
|
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()
|
GenericChatForm::~GenericChatForm()
|
||||||
|
@ -381,21 +272,6 @@ GenericChatForm::~GenericChatForm()
|
||||||
delete searchForm;
|
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()
|
void GenericChatForm::adjustFileMenuPosition()
|
||||||
{
|
{
|
||||||
QPoint pos = fileButton->mapTo(bodySplitter, QPoint());
|
QPoint pos = fileButton->mapTo(bodySplitter, QPoint());
|
||||||
|
@ -561,33 +437,6 @@ void GenericChatForm::onSendTriggered()
|
||||||
messageDispatcher.sendMessage(isAction, msg);
|
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()
|
void GenericChatForm::onEmoteButtonClicked()
|
||||||
{
|
{
|
||||||
|
@ -639,7 +488,7 @@ void GenericChatForm::onChatMessageFontChanged(const QFont& font)
|
||||||
|
|
||||||
void GenericChatForm::setColorizedNames(bool enable)
|
void GenericChatForm::setColorizedNames(bool enable)
|
||||||
{
|
{
|
||||||
colorizeNames = enable;
|
chatWidget->setColorizedNames(enable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenericChatForm::addSystemInfoMessage(const QDateTime& datetime, SystemMessageType messageType,
|
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()
|
void GenericChatForm::clearChatArea()
|
||||||
{
|
{
|
||||||
clearChatArea(/* confirm = */ true, /* inform = */ true);
|
clearChatArea(/* confirm = */ true, /* inform = */ true);
|
||||||
|
@ -706,8 +546,6 @@ void GenericChatForm::clearChatArea(bool confirm, bool inform)
|
||||||
|
|
||||||
if (inform)
|
if (inform)
|
||||||
addSystemInfoMessage(QDateTime::currentDateTime(), SystemMessageType::cleared, {});
|
addSystemInfoMessage(QDateTime::currentDateTime(), SystemMessageType::cleared, {});
|
||||||
|
|
||||||
messages.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenericChatForm::onSelectAllClicked()
|
void GenericChatForm::onSelectAllClicked()
|
||||||
|
@ -821,9 +659,7 @@ void GenericChatForm::onLoadHistory()
|
||||||
{
|
{
|
||||||
LoadHistoryDialog dlg(&chatLog);
|
LoadHistoryDialog dlg(&chatLog);
|
||||||
if (dlg.exec()) {
|
if (dlg.exec()) {
|
||||||
QDateTime time = dlg.getFromDate();
|
chatWidget->jumpToDate(dlg.getFromDate().date());
|
||||||
auto idx = firstItemAfterDate(dlg.getFromDate().date(), chatLog);
|
|
||||||
renderMessages(idx, chatLog.getNextIdx());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -858,176 +694,6 @@ void GenericChatForm::onExportChat()
|
||||||
file.close();
|
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)
|
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
|
// If the dateInfo is visible we need to pretend the top line is the one
|
||||||
|
|
|
@ -82,7 +82,6 @@ public:
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void messageInserted();
|
void messageInserted();
|
||||||
void messageNotFoundShow(SearchDirection direction);
|
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void focusInput();
|
void focusInput();
|
||||||
|
@ -108,28 +107,16 @@ protected slots:
|
||||||
void onLoadHistory();
|
void onLoadHistory();
|
||||||
void onExportChat();
|
void onExportChat();
|
||||||
void searchFormShow();
|
void searchFormShow();
|
||||||
void onSearchTriggered();
|
|
||||||
void updateShowDateInfo(const ChatLine::Ptr& prevLine, const ChatLine::Ptr& topLine);
|
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:
|
private:
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
void addSystemDateMessage(const QDate& date);
|
void addSystemDateMessage(const QDate& date);
|
||||||
QDateTime getTime(const ChatLine::Ptr& chatLine) const;
|
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:
|
protected:
|
||||||
ChatMessage::Ptr createMessage(const ToxPk& author, const QString& message,
|
ChatMessage::Ptr createMessage(const ToxPk& author, const QString& message,
|
||||||
const QDateTime& datetime, bool isAction, bool isSent, bool colorizeName = false);
|
const QDateTime& datetime, bool isAction, bool isSent, bool colorizeName = false);
|
||||||
bool needsToHideName(ChatLogIdx idx) const;
|
|
||||||
virtual void insertChatMessage(ChatMessage::Ptr msg);
|
virtual void insertChatMessage(ChatMessage::Ptr msg);
|
||||||
void adjustFileMenuPosition();
|
void adjustFileMenuPosition();
|
||||||
void hideEvent(QHideEvent* event) override;
|
void hideEvent(QHideEvent* event) override;
|
||||||
|
@ -137,7 +124,6 @@ protected:
|
||||||
bool event(QEvent*) final;
|
bool event(QEvent*) final;
|
||||||
void resizeEvent(QResizeEvent* event) final;
|
void resizeEvent(QResizeEvent* event) final;
|
||||||
bool eventFilter(QObject* object, QEvent* event) final;
|
bool eventFilter(QObject* object, QEvent* event) final;
|
||||||
void disableSearchText();
|
|
||||||
bool searchInText(const QString& phrase, const ParameterSearch& parameter, SearchDirection direction);
|
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);
|
std::pair<int, int> indexForSearchInLine(const QString& txt, const QString& phrase, const ParameterSearch& parameter, SearchDirection direction);
|
||||||
|
|
||||||
|
@ -178,7 +164,4 @@ protected:
|
||||||
|
|
||||||
IChatLog& chatLog;
|
IChatLog& chatLog;
|
||||||
IMessageDispatcher& messageDispatcher;
|
IMessageDispatcher& messageDispatcher;
|
||||||
SearchPos searchPos;
|
|
||||||
std::map<ChatLogIdx, ChatMessage::Ptr> messages;
|
|
||||||
bool colorizeNames = false;
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user