mirror of https://github.com/qTox/qTox
feat(chatlog): Re-implement sliding window ChatLog view
* Replace lines/messages with helper class to synchronize state between IChatLog and ChatLog more easily * selection indexes have been replaced with ChatLine::Ptrs, this is to ensure consistency while the view slides around * This has another benefit of removing all the code that has to manually slide the selection boxes around * Replaced all insertion/removal functions with single "insertAtIdx". This helps ensure that mappings between ChatLogIdx and position within the view are captured correctly as items in the view slide around * workerTimeout replaced with more generic name "renderFinished" that is used in synchronous and asynchronous paths * Removed unused function ChatForm::insertChatMessage * Re-implemented "Go to current date" with new ChatLog APIs * Removed unused GenericChatForm::addSystemDateMessage. This is handled by ChatLog now * Resolves #6223 * Resolves #5878 * Resolves #5940reviewable/pr6374/r3
parent
a9f7c0ca7e
commit
b36a38e716
|
@ -201,6 +201,8 @@ set(${PROJECT_NAME}_SOURCES
|
|||
src/chatlog/chatlinecontentproxy.h
|
||||
src/chatlog/chatline.cpp
|
||||
src/chatlog/chatline.h
|
||||
src/chatlog/chatlinestorage.cpp
|
||||
src/chatlog/chatlinestorage.h
|
||||
src/chatlog/chatlog.cpp
|
||||
src/chatlog/chatlog.h
|
||||
src/chatlog/chatmessage.cpp
|
||||
|
|
|
@ -45,6 +45,7 @@ auto_test(core toxid "")
|
|||
auto_test(core toxstring "")
|
||||
auto_test(chatlog textformatter "")
|
||||
auto_test(net bsu "${${PROJECT_NAME}_RESOURCES}") # needs nodes list
|
||||
auto_test(chatlog chatlinestorage "")
|
||||
auto_test(persistence paths "")
|
||||
auto_test(persistence dbschema "")
|
||||
auto_test(persistence offlinemsgengine "")
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
#include "chatlinestorage.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
ChatLineStorage::iterator ChatLineStorage::insertChatMessage(ChatLogIdx idx, QDateTime timestamp, ChatLine::Ptr line)
|
||||
{
|
||||
if (idxInfoMap.find(idx) != idxInfoMap.end()) {
|
||||
qWarning() << "Index is already rendered, not updating";
|
||||
return lines.end();
|
||||
}
|
||||
|
||||
auto linePosIncrementIt = infoIteratorForIdx(idx);
|
||||
|
||||
auto insertionPoint = equivalentLineIterator(linePosIncrementIt);
|
||||
insertionPoint = adjustItForDate(insertionPoint, timestamp);
|
||||
|
||||
insertionPoint = lines.insert(insertionPoint, line);
|
||||
|
||||
// All indexes after the insertion have to be incremented by one
|
||||
incrementLinePosAfter(linePosIncrementIt);
|
||||
|
||||
// Newly inserted index is insertinPoint - start
|
||||
IdxInfo info;
|
||||
info.linePos = std::distance(lines.begin(), insertionPoint);
|
||||
info.timestamp = timestamp;
|
||||
idxInfoMap[idx] = info;
|
||||
|
||||
return insertionPoint;
|
||||
}
|
||||
|
||||
ChatLineStorage::iterator ChatLineStorage::insertDateLine(QDateTime timestamp, ChatLine::Ptr line)
|
||||
{
|
||||
// Assume we only need to render one date line per date. I.e. this does
|
||||
// not handle the case of
|
||||
// * Message inserted Jan 3
|
||||
// * Message inserted Jan 4
|
||||
// * Message inserted Jan 3
|
||||
// In this case the second "Jan 3" message will appear to have been sent
|
||||
// on Jan 4
|
||||
// As of right now this should not be a problem since all items should
|
||||
// be sent/received in order. If we ever implement sender timestamps and
|
||||
// the sender screws us by changing their time we may need to revisit this
|
||||
auto idxMapIt = std::find_if(idxInfoMap.begin(), idxInfoMap.end(), [&] (const IdxInfoMap_t::value_type& v) {
|
||||
return timestamp <= v.second.timestamp;
|
||||
});
|
||||
|
||||
auto insertionPoint = equivalentLineIterator(idxMapIt);
|
||||
insertionPoint = adjustItForDate(insertionPoint, timestamp);
|
||||
|
||||
insertionPoint = lines.insert(insertionPoint, line);
|
||||
|
||||
// All indexes after the insertion have to be incremented by one
|
||||
incrementLinePosAfter(idxMapIt);
|
||||
|
||||
dateMap[line] = timestamp;
|
||||
|
||||
return insertionPoint;
|
||||
}
|
||||
|
||||
bool ChatLineStorage::contains(QDateTime timestamp) const
|
||||
{
|
||||
auto it = std::find_if(dateMap.begin(), dateMap.end(), [&] (DateLineMap_t::value_type v) {
|
||||
return v.second == timestamp;
|
||||
});
|
||||
|
||||
return it != dateMap.end();
|
||||
}
|
||||
|
||||
ChatLineStorage::iterator ChatLineStorage::find(ChatLogIdx idx)
|
||||
{
|
||||
auto infoIt = infoIteratorForIdx(idx);
|
||||
if (infoIt == idxInfoMap.end()) {
|
||||
return lines.end();
|
||||
}
|
||||
|
||||
return lines.begin() + infoIt->second.linePos;
|
||||
|
||||
}
|
||||
|
||||
ChatLineStorage::iterator ChatLineStorage::find(ChatLine::Ptr line)
|
||||
{
|
||||
return std::find(lines.begin(), lines.end(), line);
|
||||
}
|
||||
|
||||
void ChatLineStorage::erase(ChatLogIdx idx)
|
||||
{
|
||||
auto linePosDecrementIt = infoIteratorForIdx(idx);
|
||||
auto lineIt = equivalentLineIterator(linePosDecrementIt);
|
||||
|
||||
erase(lineIt);
|
||||
}
|
||||
|
||||
ChatLineStorage::iterator ChatLineStorage::erase(iterator it)
|
||||
{
|
||||
iterator prevIt = it;
|
||||
|
||||
do {
|
||||
it = prevIt;
|
||||
|
||||
auto infoIterator = equivalentInfoIterator(it);
|
||||
auto dateMapIt = dateMap.find(*it);
|
||||
|
||||
if (dateMapIt != dateMap.end()) {
|
||||
dateMap.erase(dateMapIt);
|
||||
}
|
||||
|
||||
if (infoIterator != idxInfoMap.end()) {
|
||||
infoIterator = idxInfoMap.erase(infoIterator);
|
||||
decrementLinePosAfter(infoIterator);
|
||||
}
|
||||
|
||||
it = lines.erase(it);
|
||||
|
||||
if (it > lines.begin()) {
|
||||
prevIt = std::prev(it);
|
||||
} else {
|
||||
prevIt = lines.end();
|
||||
}
|
||||
} while (shouldRemovePreviousLine(prevIt, it));
|
||||
|
||||
return it;
|
||||
}
|
||||
|
||||
ChatLineStorage::iterator ChatLineStorage::equivalentLineIterator(IdxInfoMap_t::iterator it)
|
||||
{
|
||||
if (it == idxInfoMap.end()) {
|
||||
return lines.end();
|
||||
}
|
||||
|
||||
return std::next(lines.begin(), it->second.linePos);
|
||||
}
|
||||
|
||||
ChatLineStorage::IdxInfoMap_t::iterator ChatLineStorage::equivalentInfoIterator(iterator it)
|
||||
{
|
||||
auto idx = static_cast<size_t>(std::distance(lines.begin(), it));
|
||||
auto equivalentIt = std::find_if(idxInfoMap.begin(), idxInfoMap.end(), [&](const IdxInfoMap_t::value_type& v) {
|
||||
return v.second.linePos >= idx;
|
||||
});
|
||||
|
||||
return equivalentIt;
|
||||
}
|
||||
|
||||
ChatLineStorage::IdxInfoMap_t::iterator ChatLineStorage::infoIteratorForIdx(ChatLogIdx idx)
|
||||
{
|
||||
// If lower_bound proves to be expensive for appending we can try
|
||||
// special casing when idx > idxToLineMap.rbegin()->first
|
||||
|
||||
// If we find an exact match we return that index, otherwise we return
|
||||
// the first item after it. It's up to the caller to check if there's an
|
||||
// exact match first
|
||||
auto it = std::lower_bound(idxInfoMap.begin(), idxInfoMap.end(), idx, [](const IdxInfoMap_t::value_type& v, ChatLogIdx idx) {
|
||||
return v.first < idx;
|
||||
});
|
||||
|
||||
return it;
|
||||
}
|
||||
|
||||
ChatLineStorage::iterator ChatLineStorage::adjustItForDate(iterator it, QDateTime timestamp)
|
||||
{
|
||||
// Continuously move back until either
|
||||
// 1. The dateline found is earlier than our timestamp
|
||||
// 2. There are no more datelines
|
||||
while (it > lines.begin()) {
|
||||
auto possibleDateIt = it - 1;
|
||||
auto dateIt = dateMap.find(*possibleDateIt);
|
||||
if (dateIt == dateMap.end()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (dateIt->second > timestamp) {
|
||||
it = possibleDateIt;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return it;
|
||||
}
|
||||
|
||||
void ChatLineStorage::incrementLinePosAfter(IdxInfoMap_t::iterator inputIt)
|
||||
{
|
||||
for (auto it = inputIt; it != idxInfoMap.end(); ++it) {
|
||||
it->second.linePos++;
|
||||
}
|
||||
}
|
||||
|
||||
void ChatLineStorage::decrementLinePosAfter(IdxInfoMap_t::iterator inputIt)
|
||||
{
|
||||
// All indexes after the insertion have to be incremented by one
|
||||
for (auto it = inputIt; it != idxInfoMap.end(); ++it) {
|
||||
it->second.linePos--;
|
||||
}
|
||||
}
|
||||
|
||||
bool ChatLineStorage::shouldRemovePreviousLine(iterator prevIt, iterator it)
|
||||
{
|
||||
return prevIt != lines.end() && // Previous iterator is valid
|
||||
dateMap.find(*prevIt) != dateMap.end() && // Previous iterator is a date line
|
||||
(
|
||||
it == lines.end() || // Previous iterator is the last line
|
||||
dateMap.find(*it) != dateMap.end() // Adjacent date lines
|
||||
);
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
#pragma once
|
||||
|
||||
#include "src/chatlog/chatline.h"
|
||||
#include "src/model/ichatlog.h"
|
||||
|
||||
#include <QDateTime>
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
/**
|
||||
* Helper class to keep track of what we're currently rendering and in what order
|
||||
* Some constraints that may not be obvious
|
||||
* * Rendered views are not always contiguous. When we clear the chatlog
|
||||
* ongoing file transfers are not removed or else we would have no way to stop
|
||||
* them. If history is loaded after this point then we could actually be
|
||||
* inserting elements both before and in the middle of our existing rendered
|
||||
* items
|
||||
* * We need to be able to go from ChatLogIdx to rendered row index. E.g. if
|
||||
* an SQL query is made for search, we need to map the result back to the
|
||||
* displayed row to send it
|
||||
* * We need to be able to map rows back to ChatLogIdx in order to decide where
|
||||
* to insert newly added messages
|
||||
* * Not all rendered lines will have an associated ChatLogIdx, date lines for
|
||||
* example are not clearly at any ChatLogIdx
|
||||
* * Need to track date messages to ensure that if messages are inserted above
|
||||
* the current position the date line is moved appropriately
|
||||
*
|
||||
* The class is designed to be used like a vector over the currently rendered
|
||||
* items, but with some tweaks for ensuring items tied to the current view are
|
||||
* moved correctly (selection indexes, removal of associated date lines,
|
||||
* mappings of ChatLogIdx -> ChatLine::Ptr, etc.)
|
||||
*/
|
||||
class ChatLineStorage
|
||||
{
|
||||
|
||||
struct IdxInfo
|
||||
{
|
||||
size_t linePos;
|
||||
QDateTime timestamp;
|
||||
};
|
||||
using Lines_t = std::vector<ChatLine::Ptr>;
|
||||
using DateLineMap_t = std::map<ChatLine::Ptr, QDateTime>;
|
||||
using IdxInfoMap_t = std::map<ChatLogIdx, IdxInfo>;
|
||||
|
||||
public:
|
||||
// Types to conform with other containers
|
||||
using size_type = Lines_t::size_type;
|
||||
using reference = Lines_t::reference;
|
||||
using const_reference = Lines_t::const_reference;
|
||||
using const_iterator = Lines_t::const_iterator;
|
||||
using iterator = Lines_t::iterator;
|
||||
|
||||
|
||||
public:
|
||||
iterator insertChatMessage(ChatLogIdx idx, QDateTime timestamp, ChatLine::Ptr line);
|
||||
iterator insertDateLine(QDateTime timestamp, ChatLine::Ptr line);
|
||||
|
||||
ChatLogIdx firstIdx() const { return idxInfoMap.begin()->first; }
|
||||
|
||||
ChatLogIdx lastIdx() const { return idxInfoMap.rbegin()->first; }
|
||||
|
||||
bool contains(ChatLogIdx idx) const { return idxInfoMap.find(idx) != idxInfoMap.end(); }
|
||||
|
||||
bool contains(QDateTime timestamp) const;
|
||||
|
||||
iterator find(ChatLogIdx idx);
|
||||
iterator find(ChatLine::Ptr line);
|
||||
|
||||
const_reference operator[](size_type idx) const { return lines[idx]; }
|
||||
|
||||
const_reference operator[](ChatLogIdx idx) const { return lines[idxInfoMap.at(idx).linePos]; }
|
||||
|
||||
size_type size() const { return lines.size(); }
|
||||
|
||||
iterator begin() { return lines.begin(); }
|
||||
iterator end() { return lines.end(); }
|
||||
|
||||
bool empty() const { return lines.empty(); }
|
||||
|
||||
bool hasIndexedMessage() const { return !idxInfoMap.empty(); }
|
||||
|
||||
void clear()
|
||||
{
|
||||
idxInfoMap.clear();
|
||||
dateMap.clear();
|
||||
return lines.clear();
|
||||
}
|
||||
|
||||
reference front() { return lines.front(); }
|
||||
reference back() { return lines.back(); }
|
||||
|
||||
void erase(ChatLogIdx idx);
|
||||
iterator erase(iterator it);
|
||||
|
||||
private:
|
||||
iterator equivalentLineIterator(IdxInfoMap_t::iterator it);
|
||||
|
||||
IdxInfoMap_t::iterator equivalentInfoIterator(iterator it);
|
||||
|
||||
IdxInfoMap_t::iterator infoIteratorForIdx(ChatLogIdx idx);
|
||||
|
||||
iterator adjustItForDate(iterator it, QDateTime timestamp);
|
||||
|
||||
void incrementLinePosAfter(IdxInfoMap_t::iterator it);
|
||||
void decrementLinePosAfter(IdxInfoMap_t::iterator it);
|
||||
bool shouldRemovePreviousLine(iterator prevIt, iterator it);
|
||||
|
||||
std::vector<ChatLine::Ptr> lines;
|
||||
std::map<ChatLine::Ptr, QDateTime> dateMap;
|
||||
IdxInfoMap_t idxInfoMap;
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -35,6 +35,8 @@ class QTimer;
|
|||
class ChatLineContent;
|
||||
struct ToxFile;
|
||||
|
||||
class ChatLineStorage;
|
||||
|
||||
static const size_t DEF_NUM_MSG_TO_LOAD = 100;
|
||||
class ChatLog : public QGraphicsView
|
||||
{
|
||||
|
@ -43,9 +45,7 @@ public:
|
|||
explicit ChatLog(IChatLog& chatLog, const Core& core, QWidget* parent = nullptr);
|
||||
virtual ~ChatLog();
|
||||
|
||||
void insertChatlineAtBottom(ChatLine::Ptr l);
|
||||
void insertChatlineOnTop(ChatLine::Ptr l);
|
||||
void insertChatlinesOnTop(const QList<ChatLine::Ptr>& newLines);
|
||||
void insertChatlines(std::map<ChatLogIdx, ChatLine::Ptr> chatLines);
|
||||
void clearSelection();
|
||||
void clear();
|
||||
void copySelectedText(bool toSelectionBuffer = false) const;
|
||||
|
@ -68,10 +68,10 @@ public:
|
|||
|
||||
signals:
|
||||
void selectionChanged();
|
||||
void workerTimeoutFinished();
|
||||
void firstVisibleLineChanged(const ChatLine::Ptr& prevLine, const ChatLine::Ptr& firstLine);
|
||||
|
||||
void messageNotFoundShow(SearchDirection direction);
|
||||
void renderFinished();
|
||||
public slots:
|
||||
void forceRelayout();
|
||||
void reloadTheme();
|
||||
|
@ -87,11 +87,15 @@ private slots:
|
|||
void onWorkerTimeout();
|
||||
void onMultiClickTimeout();
|
||||
|
||||
void onMessageUpdated(ChatLogIdx idx);
|
||||
void renderMessage(ChatLogIdx idx);
|
||||
void renderMessages(ChatLogIdx begin, ChatLogIdx end,
|
||||
std::function<void(void)> onCompletion = std::function<void(void)>());
|
||||
void renderMessages(ChatLogIdx begin, ChatLogIdx end);
|
||||
|
||||
void setRenderedWindowStart(ChatLogIdx start);
|
||||
void setRenderedWindowEnd(ChatLogIdx end);
|
||||
|
||||
void onRenderFinished();
|
||||
void onScrollValueChanged(int value);
|
||||
protected:
|
||||
QRectF calculateSceneRect() const;
|
||||
QRect getVisibleRect() const;
|
||||
|
@ -103,7 +107,6 @@ protected:
|
|||
|
||||
qreal useableWidth() const;
|
||||
|
||||
void reposition(int start, int end, qreal deltaY);
|
||||
void updateSceneRect();
|
||||
void checkVisibility();
|
||||
void scrollToBottom();
|
||||
|
@ -116,6 +119,7 @@ protected:
|
|||
void scrollContentsBy(int dx, int dy) final;
|
||||
void resizeEvent(QResizeEvent* ev) final;
|
||||
void showEvent(QShowEvent*) final;
|
||||
void hideEvent(QHideEvent* event) final;
|
||||
void focusInEvent(QFocusEvent* ev) final;
|
||||
void focusOutEvent(QFocusEvent* ev) final;
|
||||
void wheelEvent(QWheelEvent *event) final;
|
||||
|
@ -126,6 +130,8 @@ protected:
|
|||
|
||||
ChatLine::Ptr findLineByPosY(qreal yPos) const;
|
||||
|
||||
void removeLines(ChatLogIdx being, ChatLogIdx end);
|
||||
|
||||
private:
|
||||
void retranslateUi();
|
||||
bool isActiveFileTransfer(ChatLine::Ptr l);
|
||||
|
@ -140,7 +146,8 @@ private:
|
|||
|
||||
void renderItem(const ChatLogItem &item, bool hideName, bool colorizeNames, ChatLine::Ptr &chatMessage);
|
||||
void renderFile(QString displayName, ToxFile file, bool isSelf, QDateTime timestamp, ChatLine::Ptr &chatMessage);
|
||||
bool needsToHideName(ChatLogIdx idx) const;
|
||||
bool needsToHideName(ChatLogIdx idx, bool prevIdxRendered) const;
|
||||
bool shouldRenderMessage(ChatLogIdx idx) const;
|
||||
void disableSearchText();
|
||||
private:
|
||||
enum class SelectionMode
|
||||
|
@ -161,16 +168,22 @@ private:
|
|||
QAction* selectAllAction = nullptr;
|
||||
QGraphicsScene* scene = nullptr;
|
||||
QGraphicsScene* busyScene = nullptr;
|
||||
QVector<ChatLine::Ptr> lines;
|
||||
QList<ChatLine::Ptr> visibleLines;
|
||||
ChatLine::Ptr typingNotification;
|
||||
ChatLine::Ptr busyNotification;
|
||||
|
||||
// selection
|
||||
int selClickedRow = -1; // These 4 are only valid while selectionMode != None
|
||||
|
||||
// For the time being we store these selection indexes as ChatLine::Ptrs. In
|
||||
// order to do multi-selection we do an O(n) search in the chatline storage
|
||||
// to determine the index. This is inefficient but correct with the moving
|
||||
// window of storage. If this proves to cause performance issues we can move
|
||||
// this responsibility into ChatlineStorage and have it coordinate the
|
||||
// shifting of indexes
|
||||
ChatLine::Ptr selClickedRow; // These 4 are only valid while selectionMode != None
|
||||
int selClickedCol = -1;
|
||||
int selFirstRow = -1;
|
||||
int selLastRow = -1;
|
||||
ChatLine::Ptr selFirstRow;
|
||||
ChatLine::Ptr selLastRow;
|
||||
QColor selectionRectColor = Style::getColor(Style::SelectText);
|
||||
SelectionMode selectionMode = SelectionMode::None;
|
||||
QPointF clickPos;
|
||||
|
@ -184,7 +197,7 @@ private:
|
|||
Qt::MouseButton lastClickButton;
|
||||
|
||||
// worker vars
|
||||
int workerLastIndex = 0;
|
||||
size_t workerLastIndex = 0;
|
||||
bool workerStb = false;
|
||||
ChatLine::Ptr workerAnchorLine;
|
||||
|
||||
|
@ -193,8 +206,12 @@ private:
|
|||
qreal lineSpacing = 5.0f;
|
||||
|
||||
IChatLog& chatLog;
|
||||
std::map<ChatLogIdx, ChatLine::Ptr> messages;
|
||||
bool colorizeNames = false;
|
||||
SearchPos searchPos;
|
||||
const Core& core;
|
||||
bool scrollMonitoringEnabled = true;
|
||||
|
||||
std::unique_ptr<ChatLineStorage> chatLineStorage;
|
||||
|
||||
std::vector<std::function<void(void)>> renderCompletionFns;
|
||||
};
|
||||
|
|
|
@ -633,14 +633,6 @@ void ChatForm::sendImageFromPreview()
|
|||
}
|
||||
}
|
||||
|
||||
void ChatForm::insertChatMessage(ChatMessage::Ptr msg)
|
||||
{
|
||||
GenericChatForm::insertChatMessage(msg);
|
||||
if (netcam && bodySplitter->sizes()[1] == 0) {
|
||||
netcam->setShowMessages(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatForm::onCopyStatusMessage()
|
||||
{
|
||||
// make sure to copy not truncated text directly from the friend
|
||||
|
|
|
@ -118,7 +118,6 @@ private:
|
|||
|
||||
protected:
|
||||
std::unique_ptr<NetCamView> createNetcam();
|
||||
void insertChatMessage(ChatMessage::Ptr msg) final;
|
||||
void dragEnterEvent(QDragEnterEvent* ev) final;
|
||||
void dropEvent(QDropEvent* ev) final;
|
||||
void hideEvent(QHideEvent* event) final;
|
||||
|
|
|
@ -216,6 +216,12 @@ GenericChatForm::GenericChatForm(const Core& _core, const Contact* contact, ICha
|
|||
addAction(quoteAction);
|
||||
menu.addSeparator();
|
||||
|
||||
goToCurrentDateAction = menu.addAction(QIcon(), QString(), this, SLOT(goToCurrentDate()),
|
||||
QKeySequence(Qt::CTRL + Qt::Key_G));
|
||||
addAction(goToCurrentDateAction);
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
searchAction = menu.addAction(QIcon(), QString(), this, SLOT(searchFormShow()),
|
||||
QKeySequence(Qt::CTRL + Qt::Key_F));
|
||||
addAction(searchAction);
|
||||
|
@ -501,14 +507,6 @@ void GenericChatForm::addSystemInfoMessage(const QDateTime& datetime, SystemMess
|
|||
chatLog.addSystemMessage(systemMessage);
|
||||
}
|
||||
|
||||
void GenericChatForm::addSystemDateMessage(const QDate& date)
|
||||
{
|
||||
const Settings& s = Settings::getInstance();
|
||||
QString dateText = date.toString(s.getDateFormat());
|
||||
|
||||
insertChatMessage(ChatMessage::createChatInfoMessage(dateText, ChatMessage::INFO, QDateTime()));
|
||||
}
|
||||
|
||||
QDateTime GenericChatForm::getTime(const ChatLine::Ptr &chatLine) const
|
||||
{
|
||||
if (chatLine) {
|
||||
|
@ -553,12 +551,6 @@ void GenericChatForm::onSelectAllClicked()
|
|||
chatWidget->selectAll();
|
||||
}
|
||||
|
||||
void GenericChatForm::insertChatMessage(ChatMessage::Ptr msg)
|
||||
{
|
||||
chatWidget->insertChatlineAtBottom(std::static_pointer_cast<ChatLine>(msg));
|
||||
emit messageInserted();
|
||||
}
|
||||
|
||||
void GenericChatForm::hideEvent(QHideEvent* event)
|
||||
{
|
||||
hideFileMenu();
|
||||
|
@ -694,6 +686,11 @@ void GenericChatForm::onExportChat()
|
|||
file.close();
|
||||
}
|
||||
|
||||
void GenericChatForm::goToCurrentDate()
|
||||
{
|
||||
chatWidget->jumpToIdx(chatLog.getNextIdx());
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -722,6 +719,7 @@ void GenericChatForm::retranslateUi()
|
|||
quoteAction->setText(tr("Quote selected text"));
|
||||
copyLinkAction->setText(tr("Copy link address"));
|
||||
searchAction->setText(tr("Search in text"));
|
||||
goToCurrentDateAction->setText(tr("Go to current date"));
|
||||
loadHistoryAction->setText(tr("Load chat history..."));
|
||||
exportChatAction->setText(tr("Export to file"));
|
||||
}
|
||||
|
|
|
@ -108,6 +108,7 @@ protected slots:
|
|||
void onExportChat();
|
||||
void searchFormShow();
|
||||
void updateShowDateInfo(const ChatLine::Ptr& prevLine, const ChatLine::Ptr& topLine);
|
||||
void goToCurrentDate();
|
||||
|
||||
private:
|
||||
void retranslateUi();
|
||||
|
@ -117,7 +118,6 @@ private:
|
|||
protected:
|
||||
ChatMessage::Ptr createMessage(const ToxPk& author, const QString& message,
|
||||
const QDateTime& datetime, bool isAction, bool isSent, bool colorizeName = false);
|
||||
virtual void insertChatMessage(ChatMessage::Ptr msg);
|
||||
void adjustFileMenuPosition();
|
||||
void hideEvent(QHideEvent* event) override;
|
||||
void showEvent(QShowEvent*) override;
|
||||
|
@ -137,6 +137,7 @@ protected:
|
|||
QAction* quoteAction;
|
||||
QAction* copyLinkAction;
|
||||
QAction* searchAction;
|
||||
QAction* goToCurrentDateAction;
|
||||
QAction* loadHistoryAction;
|
||||
QAction* exportChatAction;
|
||||
|
||||
|
|
|
@ -0,0 +1,335 @@
|
|||
|
||||
#include "src/chatlog/chatlinestorage.h"
|
||||
#include <QTest>
|
||||
|
||||
namespace
|
||||
{
|
||||
class IdxChatLine : public ChatLine
|
||||
{
|
||||
public:
|
||||
explicit IdxChatLine(ChatLogIdx idx)
|
||||
: ChatLine()
|
||||
, idx(idx)
|
||||
{}
|
||||
|
||||
ChatLogIdx get() { return idx; }
|
||||
private:
|
||||
ChatLogIdx idx;
|
||||
|
||||
};
|
||||
|
||||
class TimestampChatLine : public ChatLine
|
||||
{
|
||||
public:
|
||||
explicit TimestampChatLine(QDateTime dateTime)
|
||||
: ChatLine()
|
||||
, timestamp(dateTime)
|
||||
{}
|
||||
|
||||
QDateTime get() { return timestamp; }
|
||||
private:
|
||||
QDateTime timestamp;
|
||||
};
|
||||
|
||||
ChatLogIdx idxFromChatLine(ChatLine::Ptr p) {
|
||||
return std::static_pointer_cast<IdxChatLine>(p)->get();
|
||||
}
|
||||
|
||||
QDateTime timestampFromChatLine(ChatLine::Ptr p) {
|
||||
return std::static_pointer_cast<TimestampChatLine>(p)->get();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
class TestChatLineStorage : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void init();
|
||||
void testChatLogIdxAccess();
|
||||
void testIndexAccess();
|
||||
void testRangeBasedIteration();
|
||||
void testAppendingItems();
|
||||
void testPrependingItems();
|
||||
void testMiddleInsertion();
|
||||
void testIndexRemoval();
|
||||
void testItRemoval();
|
||||
void testDateLineAddition();
|
||||
void testDateLineRemoval();
|
||||
void testInsertionBeforeDates();
|
||||
void testInsertionAfterDate();
|
||||
void testContainsTimestamp();
|
||||
void testContainsIdx();
|
||||
void testEndOfStorageDateRemoval();
|
||||
void testConsecutiveDateLineRemoval();
|
||||
private:
|
||||
ChatLineStorage storage;
|
||||
|
||||
static constexpr size_t initialStartIdx = 10;
|
||||
static constexpr size_t initialEndIdx = 20;
|
||||
static const QDateTime initialTimestamp;
|
||||
|
||||
};
|
||||
|
||||
constexpr size_t TestChatLineStorage::initialStartIdx;
|
||||
constexpr size_t TestChatLineStorage::initialEndIdx;
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
||||
const QDateTime TestChatLineStorage::initialTimestamp = QDate(2021, 01, 01).startOfDay();
|
||||
#else
|
||||
const QDateTime TestChatLineStorage::initialTimestamp(QDate(2021, 01, 01));
|
||||
#endif
|
||||
|
||||
void TestChatLineStorage::init()
|
||||
{
|
||||
storage = ChatLineStorage();
|
||||
|
||||
for (auto idx = ChatLogIdx(initialStartIdx); idx < ChatLogIdx(initialEndIdx); ++idx) {
|
||||
storage.insertChatMessage(idx, initialTimestamp, std::make_shared<IdxChatLine>(idx));
|
||||
}
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testChatLogIdxAccess()
|
||||
{
|
||||
for (auto idx = ChatLogIdx(initialStartIdx); idx < ChatLogIdx(initialEndIdx); ++idx) {
|
||||
QCOMPARE(idxFromChatLine(storage[idx]).get(), idx.get());
|
||||
}
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testIndexAccess()
|
||||
{
|
||||
for (size_t i = 0; i < initialEndIdx - initialStartIdx; ++i) {
|
||||
QCOMPARE(idxFromChatLine(storage[i]).get(), initialStartIdx + i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void TestChatLineStorage::testRangeBasedIteration()
|
||||
{
|
||||
auto idx = ChatLogIdx(initialStartIdx);
|
||||
|
||||
for (const auto& p : storage) {
|
||||
QCOMPARE(idxFromChatLine(p).get(), idx.get());
|
||||
idx = idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testAppendingItems()
|
||||
{
|
||||
for (auto idx = ChatLogIdx(initialEndIdx); idx < ChatLogIdx(initialEndIdx + 10); ++idx) {
|
||||
storage.insertChatMessage(idx, initialTimestamp, std::make_shared<IdxChatLine>(idx));
|
||||
QCOMPARE(storage.lastIdx().get(), idx.get());
|
||||
}
|
||||
|
||||
for (auto idx = ChatLogIdx(initialEndIdx); idx < storage.lastIdx(); ++idx) {
|
||||
QCOMPARE(idxFromChatLine(storage[idx]).get(), idx.get());
|
||||
QCOMPARE(idxFromChatLine(storage[idx.get() - initialStartIdx]).get(), idx.get());
|
||||
}
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testMiddleInsertion()
|
||||
{
|
||||
ChatLogIdx newEnd = ChatLogIdx(initialEndIdx + 5);
|
||||
ChatLogIdx insertIdx = ChatLogIdx(initialEndIdx + 3);
|
||||
|
||||
storage.insertChatMessage(newEnd, initialTimestamp, std::make_shared<IdxChatLine>(newEnd));
|
||||
storage.insertChatMessage(insertIdx, initialTimestamp, std::make_shared<IdxChatLine>(insertIdx));
|
||||
|
||||
QCOMPARE(idxFromChatLine(storage[insertIdx]).get(), insertIdx.get());
|
||||
QCOMPARE(idxFromChatLine(storage[initialEndIdx - initialStartIdx]).get(), insertIdx.get());
|
||||
QCOMPARE(idxFromChatLine(storage[initialEndIdx - initialStartIdx + 1]).get(), newEnd.get());
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testPrependingItems()
|
||||
{
|
||||
for (auto idx = ChatLogIdx(initialStartIdx - 1); idx != ChatLogIdx(-1); idx = idx - 1) {
|
||||
storage.insertChatMessage(idx, initialTimestamp, std::make_shared<IdxChatLine>(idx));
|
||||
QCOMPARE(storage.firstIdx().get(), idx.get());
|
||||
}
|
||||
|
||||
for (auto idx = storage.firstIdx(); idx < storage.lastIdx(); ++idx) {
|
||||
QCOMPARE(idxFromChatLine(storage[idx]).get(), idx.get());
|
||||
QCOMPARE(idxFromChatLine(storage[idx.get()]).get(), idx.get());
|
||||
}
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testIndexRemoval()
|
||||
{
|
||||
QCOMPARE(initialStartIdx, static_cast<size_t>(10));
|
||||
QCOMPARE(initialEndIdx, static_cast<size_t>(20));
|
||||
QCOMPARE(storage.size(), static_cast<size_t>(10));
|
||||
|
||||
storage.erase(ChatLogIdx(11));
|
||||
|
||||
QCOMPARE(storage.size(), static_cast<size_t>(9));
|
||||
|
||||
QCOMPARE(idxFromChatLine(storage[0]).get(), static_cast<size_t>(10));
|
||||
QCOMPARE(idxFromChatLine(storage[1]).get(), static_cast<size_t>(12));
|
||||
|
||||
auto idx = static_cast<size_t>(12);
|
||||
for (auto it = std::next(storage.begin()); it != storage.end(); ++it) {
|
||||
QCOMPARE(idxFromChatLine((*it)).get(), idx++);
|
||||
}
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testItRemoval()
|
||||
{
|
||||
auto it = storage.begin();
|
||||
it = it + 2;
|
||||
|
||||
storage.erase(it);
|
||||
|
||||
QCOMPARE(idxFromChatLine(storage[0]).get(), initialStartIdx);
|
||||
QCOMPARE(idxFromChatLine(storage[1]).get(), initialStartIdx + 1);
|
||||
// Item should have been removed
|
||||
QCOMPARE(idxFromChatLine(storage[2]).get(), initialStartIdx + 3);
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testDateLineAddition()
|
||||
{
|
||||
storage.insertDateLine(initialTimestamp, std::make_shared<TimestampChatLine>(initialTimestamp));
|
||||
auto newTimestamp = initialTimestamp.addDays(1);
|
||||
storage.insertDateLine(newTimestamp, std::make_shared<TimestampChatLine>(newTimestamp));
|
||||
|
||||
QCOMPARE(storage.size(), initialEndIdx - initialStartIdx + 2);
|
||||
QCOMPARE(timestampFromChatLine(storage[0]), initialTimestamp);
|
||||
QCOMPARE(timestampFromChatLine(storage[storage.size() - 1]), newTimestamp);
|
||||
|
||||
for (size_t i = 1; i < storage.size() - 2; ++i)
|
||||
{
|
||||
// Ensure that indexed items all stayed in the right order
|
||||
QCOMPARE(idxFromChatLine(storage[i]).get(), idxFromChatLine(storage[ChatLogIdx(initialStartIdx + i - 1)]).get());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testDateLineRemoval()
|
||||
{
|
||||
// For the time being there is no removal requirement
|
||||
storage.insertDateLine(initialTimestamp, std::make_shared<TimestampChatLine>(initialTimestamp));
|
||||
|
||||
QVERIFY(storage.contains(initialTimestamp));
|
||||
QCOMPARE(timestampFromChatLine(storage[0]), initialTimestamp);
|
||||
|
||||
storage.erase(storage.begin());
|
||||
|
||||
QVERIFY(!storage.contains(initialTimestamp));
|
||||
QCOMPARE(idxFromChatLine(storage[0]).get(), initialStartIdx);
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testInsertionBeforeDates()
|
||||
{
|
||||
storage.insertDateLine(initialTimestamp, std::make_shared<TimestampChatLine>(initialTimestamp));
|
||||
|
||||
auto yesterday = initialTimestamp.addDays(-1);
|
||||
storage.insertDateLine(yesterday, std::make_shared<TimestampChatLine>(yesterday));
|
||||
|
||||
auto firstIdx = ChatLogIdx(initialStartIdx - 2);
|
||||
storage.insertChatMessage(firstIdx, initialTimestamp.addDays(-2), std::make_shared<IdxChatLine>(firstIdx));
|
||||
|
||||
QCOMPARE(idxFromChatLine(storage[0]).get(), firstIdx.get());
|
||||
QCOMPARE(timestampFromChatLine(storage[1]), yesterday);
|
||||
QCOMPARE(timestampFromChatLine(storage[2]), initialTimestamp);
|
||||
QCOMPARE(idxFromChatLine(storage[3]).get(), initialStartIdx);
|
||||
|
||||
auto secondIdx = ChatLogIdx(initialStartIdx - 1);
|
||||
storage.insertChatMessage(secondIdx, initialTimestamp.addDays(-1), std::make_shared<IdxChatLine>(secondIdx));
|
||||
|
||||
QCOMPARE(idxFromChatLine(storage[0]).get(), firstIdx.get());
|
||||
QCOMPARE(timestampFromChatLine(storage[1]), yesterday);
|
||||
QCOMPARE(idxFromChatLine(storage[2]).get(), secondIdx.get());
|
||||
QCOMPARE(timestampFromChatLine(storage[3]), initialTimestamp);
|
||||
QCOMPARE(idxFromChatLine(storage[4]).get(), initialStartIdx);
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testInsertionAfterDate()
|
||||
{
|
||||
auto newTimestamp = initialTimestamp.addDays(1);
|
||||
storage.insertDateLine(newTimestamp, std::make_shared<TimestampChatLine>(newTimestamp));
|
||||
|
||||
QCOMPARE(storage.size(), initialEndIdx - initialStartIdx + 1);
|
||||
QCOMPARE(timestampFromChatLine(storage[initialEndIdx - initialStartIdx]), newTimestamp);
|
||||
|
||||
storage.insertChatMessage(ChatLogIdx(initialEndIdx), newTimestamp, std::make_shared<IdxChatLine>(ChatLogIdx(initialEndIdx)));
|
||||
QCOMPARE(idxFromChatLine(storage[initialEndIdx - initialStartIdx + 1]).get(), initialEndIdx);
|
||||
QCOMPARE(idxFromChatLine(storage[ChatLogIdx(initialEndIdx)]).get(), initialEndIdx);
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testContainsTimestamp()
|
||||
{
|
||||
QCOMPARE(storage.contains(initialTimestamp), false);
|
||||
storage.insertDateLine(initialTimestamp, std::make_shared<TimestampChatLine>(initialTimestamp));
|
||||
QCOMPARE(storage.contains(initialTimestamp), true);
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testContainsIdx()
|
||||
{
|
||||
QCOMPARE(storage.contains(ChatLogIdx(initialEndIdx)), false);
|
||||
QCOMPARE(storage.contains(ChatLogIdx(initialStartIdx)), true);
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testEndOfStorageDateRemoval()
|
||||
{
|
||||
auto tomorrow = initialTimestamp.addDays(1);
|
||||
storage.insertDateLine(tomorrow, std::make_shared<TimestampChatLine>(tomorrow));
|
||||
storage.insertChatMessage(ChatLogIdx(initialEndIdx), tomorrow, std::make_shared<IdxChatLine>(ChatLogIdx(initialEndIdx)));
|
||||
|
||||
QCOMPARE(storage.size(), initialEndIdx - initialStartIdx + 2);
|
||||
|
||||
auto it = storage.begin() + storage.size() - 2;
|
||||
QCOMPARE(timestampFromChatLine(*it++), tomorrow);
|
||||
QCOMPARE(idxFromChatLine(*it).get(), initialEndIdx);
|
||||
|
||||
storage.erase(it);
|
||||
|
||||
QCOMPARE(storage.size(), initialEndIdx - initialStartIdx);
|
||||
|
||||
it = storage.begin() + storage.size() - 1;
|
||||
QCOMPARE(idxFromChatLine(*it++).get(), initialEndIdx - 1);
|
||||
}
|
||||
|
||||
void TestChatLineStorage::testConsecutiveDateLineRemoval()
|
||||
{
|
||||
auto todayPlus1 = initialTimestamp.addDays(1);
|
||||
auto todayPlus2 = initialTimestamp.addDays(2);
|
||||
|
||||
auto todayPlus1Idx = ChatLogIdx(initialEndIdx);
|
||||
auto todayPlus1Idx2 = ChatLogIdx(initialEndIdx + 1);
|
||||
auto todayPlus2Idx = ChatLogIdx(initialEndIdx + 2);
|
||||
|
||||
|
||||
storage.insertDateLine(todayPlus1, std::make_shared<TimestampChatLine>(todayPlus1));
|
||||
storage.insertChatMessage(todayPlus1Idx, todayPlus1, std::make_shared<IdxChatLine>(todayPlus1Idx));
|
||||
storage.insertChatMessage(todayPlus1Idx2, todayPlus1, std::make_shared<IdxChatLine>(todayPlus1Idx2));
|
||||
|
||||
storage.insertDateLine(todayPlus2, std::make_shared<TimestampChatLine>(todayPlus2));
|
||||
storage.insertChatMessage(todayPlus2Idx, todayPlus2, std::make_shared<IdxChatLine>(todayPlus2Idx));
|
||||
|
||||
// 2 date lines and 3 messages were inserted for a total of 5 new lines
|
||||
QCOMPARE(storage.size(), initialEndIdx - initialStartIdx + 5);
|
||||
|
||||
storage.erase(storage.find(todayPlus1Idx2));
|
||||
|
||||
auto newItemIdxStart = initialEndIdx - initialStartIdx;
|
||||
|
||||
// Only the chat message should have been removed
|
||||
QCOMPARE(storage.size(), initialEndIdx - initialStartIdx + 4);
|
||||
|
||||
QCOMPARE(timestampFromChatLine(storage[newItemIdxStart]), todayPlus1);
|
||||
QCOMPARE(idxFromChatLine(storage[newItemIdxStart + 1]).get(), todayPlus1Idx.get());
|
||||
QCOMPARE(timestampFromChatLine(storage[newItemIdxStart + 2]), todayPlus2);
|
||||
QCOMPARE(idxFromChatLine(storage[newItemIdxStart + 3]).get(), todayPlus2Idx.get());
|
||||
|
||||
storage.erase(storage.find(todayPlus1Idx));
|
||||
|
||||
// The chat message + the dateline for it should have been removed as there
|
||||
// were 2 adjacent datelines caused by the removal
|
||||
QCOMPARE(storage.size(), initialEndIdx - initialStartIdx + 2);
|
||||
QCOMPARE(timestampFromChatLine(storage[newItemIdxStart]), todayPlus2);
|
||||
QCOMPARE(idxFromChatLine(storage[newItemIdxStart + 1]).get(), todayPlus2Idx.get());
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TestChatLineStorage);
|
||||
#include "chatlinestorage_test.moc"
|
Loading…
Reference in New Issue