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 #5940
reviewable/pr6374/r3
Mick Sayson 2021-02-25 20:41:35 -08:00 committed by Anthony Bilinski
parent a9f7c0ca7e
commit b36a38e716
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
11 changed files with 1176 additions and 358 deletions

View File

@ -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

View File

@ -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 "")

View File

@ -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
);
}

View File

@ -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

View File

@ -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;
};

View File

@ -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

View File

@ -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;

View File

@ -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"));
}

View 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;

View File

@ -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"