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

refactor(chatlog): Add class to manage underlying chatlog state

This commit is contained in:
Mick Sayson 2019-06-17 18:05:14 -07:00
parent c779d52aef
commit 71f8220925
9 changed files with 1025 additions and 9 deletions

View File

@ -324,6 +324,8 @@ set(${PROJECT_NAME}_SOURCES
src/model/chatroom/groupchatroom.h
src/model/contact.cpp
src/model/contact.h
src/model/chatlogitem.cpp
src/model/chatlogitem.h
src/model/friend.cpp
src/model/friend.h
src/model/message.h
@ -346,6 +348,9 @@ set(${PROJECT_NAME}_SOURCES
src/model/profile/profileinfo.cpp
src/model/profile/profileinfo.h
src/model/dialogs/idialogs.h
src/model/ichatlog.h
src/model/sessionchatlog.h
src/model/sessionchatlog.cpp
src/net/bootstrapnodeupdater.cpp
src/net/bootstrapnodeupdater.h
src/net/avatarbroadcaster.cpp

View File

@ -31,6 +31,7 @@ auto_test(persistence offlinemsgengine)
auto_test(model friendmessagedispatcher)
auto_test(model groupmessagedispatcher)
auto_test(model messageprocessor)
auto_test(model sessionchatlog)
if (UNIX)
auto_test(platform posixsignalnotifier)

136
src/model/chatlogitem.cpp Normal file
View File

@ -0,0 +1,136 @@
#include "chatlogitem.h"
#include "src/core/core.h"
#include "src/friendlist.h"
#include "src/grouplist.h"
#include "src/model/friend.h"
#include "src/model/group.h"
#include <cassert>
namespace {
/**
* Helper template to get the correct deleter function for our type erased unique_ptr
*/
template <typename T>
struct ChatLogItemDeleter
{
static void doDelete(void* ptr)
{
delete static_cast<T*>(ptr);
}
};
QString resolveToxPk(const ToxPk& pk)
{
Friend* f = FriendList::findFriend(pk);
if (f) {
return f->getDisplayedName();
}
for (Group* it : GroupList::getAllGroups()) {
QString res = it->resolveToxId(pk);
if (!res.isEmpty()) {
return res;
}
}
return pk.toString();
}
QString resolveSenderNameFromSender(const ToxPk& sender)
{
// TODO: Remove core instance
const Core* core = Core::getInstance();
// In unit tests we don't have a core instance so we just stringize the key
if (!core) {
return sender.toString();
}
bool isSelf = sender == core->getSelfId().getPublicKey();
QString myNickName = core->getUsername().isEmpty() ? sender.toString() : core->getUsername();
return isSelf ? myNickName : resolveToxPk(sender);
}
} // namespace
ChatLogItem::ChatLogItem(ToxPk sender_, ChatLogFile file_)
: ChatLogItem(std::move(sender_), ContentType::fileTransfer,
ContentPtr(new ChatLogFile(std::move(file_)),
ChatLogItemDeleter<ChatLogFile>::doDelete))
{}
ChatLogItem::ChatLogItem(ToxPk sender_, ChatLogMessage message_)
: ChatLogItem(sender_, ContentType::message,
ContentPtr(new ChatLogMessage(std::move(message_)),
ChatLogItemDeleter<ChatLogMessage>::doDelete))
{}
ChatLogItem::ChatLogItem(ToxPk sender_, ContentType contentType_, ContentPtr content_)
: sender(std::move(sender_))
, displayName(resolveSenderNameFromSender(sender))
, contentType(contentType_)
, content(std::move(content_))
{}
const ToxPk& ChatLogItem::getSender() const
{
return sender;
}
ChatLogItem::ContentType ChatLogItem::getContentType() const
{
return contentType;
}
ChatLogFile& ChatLogItem::getContentAsFile()
{
assert(contentType == ContentType::fileTransfer);
return *static_cast<ChatLogFile*>(content.get());
}
const ChatLogFile& ChatLogItem::getContentAsFile() const
{
assert(contentType == ContentType::fileTransfer);
return *static_cast<ChatLogFile*>(content.get());
}
ChatLogMessage& ChatLogItem::getContentAsMessage()
{
assert(contentType == ContentType::message);
return *static_cast<ChatLogMessage*>(content.get());
}
const ChatLogMessage& ChatLogItem::getContentAsMessage() const
{
assert(contentType == ContentType::message);
return *static_cast<ChatLogMessage*>(content.get());
}
QDateTime ChatLogItem::getTimestamp() const
{
switch (contentType) {
case ChatLogItem::ContentType::message: {
const auto& message = getContentAsMessage();
return message.message.timestamp;
}
case ChatLogItem::ContentType::fileTransfer: {
const auto& file = getContentAsFile();
return file.timestamp;
}
}
assert(false);
return QDateTime();
}
void ChatLogItem::setDisplayName(QString name)
{
displayName = name;
}
const QString& ChatLogItem::getDisplayName() const
{
return displayName;
}

75
src/model/chatlogitem.h Normal file
View File

@ -0,0 +1,75 @@
/*
Copyright © 2019 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CHAT_LOG_ITEM_H
#define CHAT_LOG_ITEM_H
#include "src/core/toxfile.h"
#include "src/core/toxpk.h"
#include "src/model/message.h"
#include <memory>
struct ChatLogMessage
{
bool isComplete;
Message message;
};
struct ChatLogFile
{
QDateTime timestamp;
ToxFile file;
};
class ChatLogItem
{
private:
using ContentPtr = std::unique_ptr<void, void (*)(void*)>;
public:
enum class ContentType
{
message,
fileTransfer,
};
ChatLogItem(ToxPk sender, ChatLogFile file);
ChatLogItem(ToxPk sender, ChatLogMessage message);
const ToxPk& getSender() const;
ContentType getContentType() const;
ChatLogFile& getContentAsFile();
const ChatLogFile& getContentAsFile() const;
ChatLogMessage& getContentAsMessage();
const ChatLogMessage& getContentAsMessage() const;
QDateTime getTimestamp() const;
void setDisplayName(QString name);
const QString& getDisplayName() const;
private:
ChatLogItem(ToxPk sender, ContentType contentType, ContentPtr content);
ToxPk sender;
QString displayName;
ContentType contentType;
ContentPtr content;
};
#endif /*CHAT_LOG_ITEM_H*/

145
src/model/ichatlog.h Normal file
View File

@ -0,0 +1,145 @@
/*
Copyright © 2019 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ICHAT_LOG_H
#define ICHAT_LOG_H
#include "message.h"
#include "src/core/core.h"
#include "src/core/toxfile.h"
#include "src/core/toxpk.h"
#include "src/friendlist.h"
#include "src/grouplist.h"
#include "src/model/chatlogitem.h"
#include "src/model/friend.h"
#include "src/model/group.h"
#include "src/persistence/history.h"
#include "src/util/strongtype.h"
#include "src/widget/searchtypes.h"
#include <cassert>
using ChatLogIdx =
NamedType<size_t, struct ChatLogIdxTag, Orderable, UnderlyingAddable, UnitlessDifferencable, Incrementable>;
Q_DECLARE_METATYPE(ChatLogIdx);
struct SearchPos
{
// Index to the chat log item we want
ChatLogIdx logIdx;
// Number of matches we've had. This is always number of matches from the
// start even if we're searching backwards.
size_t numMatches;
bool operator==(const SearchPos& other) const
{
return tie() == other.tie();
}
bool operator!=(const SearchPos& other) const
{
return tie() != other.tie();
}
bool operator<(const SearchPos& other) const
{
return tie() < other.tie();
}
std::tuple<ChatLogIdx, size_t> tie() const
{
return std::tie(logIdx, numMatches);
}
};
struct SearchResult
{
bool found;
SearchPos pos;
size_t start;
size_t len;
// This is unfortunately needed to shoehorn our API into the highlighting
// API of above classes. They expect to re-search the same thing we did
// for some reason
QRegularExpression exp;
};
class IChatLog : public QObject
{
Q_OBJECT
public:
virtual ~IChatLog() = default;
/**
* @brief Returns reference to item at idx
* @param[in] idx
* @return Variant type referencing either a ToxFile or Message
* @pre idx must be between currentFirstIdx() and currentLastIdx()
*/
virtual const ChatLogItem& at(ChatLogIdx idx) const = 0;
/**
* @brief searches forwards through the chat log until phrase is found according to parameter
* @param[in] startIdx inclusive start idx
* @param[in] phrase phrase to find (may be modified by parameter)
* @param[in] parameter search parameters
*/
virtual SearchResult searchForward(SearchPos startIdx, const QString& phrase,
const ParameterSearch& parameter) const = 0;
/**
* @brief searches backwards through the chat log until phrase is found according to parameter
* @param[in] startIdx inclusive start idx
* @param[in] phrase phrase to find (may be modified by parameter)
* @param[in] parameter search parameters
*/
virtual SearchResult searchBackward(SearchPos startIdx, const QString& phrase,
const ParameterSearch& parameter) const = 0;
/**
* @brief The underlying chat log instance may not want to start at 0
* @return Current first valid index to call at() with
*/
virtual ChatLogIdx getFirstIdx() const = 0;
/**
* @return current last valid index to call at() with
*/
virtual ChatLogIdx getNextIdx() const = 0;
struct DateChatLogIdxPair
{
QDate date;
ChatLogIdx idx;
};
/**
* @brief Gets indexes for each new date starting at startDate
* @param[in] startDate date to start searching from
* @param[in] maxDates maximum number of dates to be returned
*/
virtual std::vector<DateChatLogIdxPair> getDateIdxs(const QDate& startDate,
size_t maxDates) const = 0;
signals:
void itemUpdated(ChatLogIdx idx);
};
#endif /*ICHAT_LOG_H*/

View File

@ -0,0 +1,419 @@
/*
Copyright © 2019 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include "sessionchatlog.h"
#include "src/friendlist.h"
#include <QDebug>
#include <QtGlobal>
#include <mutex>
namespace {
/**
* lower_bound needs two way comparisons. This adaptor allows us to compare
* between a Message and QDateTime in both directions
*/
struct MessageDateAdaptor
{
static const QDateTime invalidDateTime;
MessageDateAdaptor(const std::pair<const ChatLogIdx, ChatLogItem>& item)
: timestamp(item.second.getContentType() == ChatLogItem::ContentType::message
? item.second.getContentAsMessage().message.timestamp
: invalidDateTime)
{}
MessageDateAdaptor(const QDateTime& timestamp)
: timestamp(timestamp)
{}
const QDateTime& timestamp;
};
const QDateTime MessageDateAdaptor::invalidDateTime;
/**
* @brief The search types all can be represented as some regular expression. This function
* takes the input phrase and filter and generates the appropriate regular expression
* @return Regular expression which finds the input
*/
QRegularExpression getRegexpForPhrase(const QString& phrase, FilterSearch filter)
{
constexpr auto regexFlags = QRegularExpression::UseUnicodePropertiesOption;
constexpr auto caseInsensitiveFlags = QRegularExpression::CaseInsensitiveOption;
switch (filter) {
case FilterSearch::Register:
return QRegularExpression(QRegularExpression::escape(phrase), regexFlags);
case FilterSearch::WordsOnly:
return QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase),
caseInsensitiveFlags);
case FilterSearch::RegisterAndWordsOnly:
return QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), regexFlags);
case FilterSearch::RegisterAndRegular:
return QRegularExpression(phrase, regexFlags);
case FilterSearch::Regular:
return QRegularExpression(phrase, caseInsensitiveFlags);
default:
return QRegularExpression(QRegularExpression::escape(phrase), caseInsensitiveFlags);
}
}
/**
* @return True if the given status indicates no future updates will come in
*/
bool toxFileIsComplete(ToxFile::FileStatus status)
{
switch (status) {
case ToxFile::INITIALIZING:
case ToxFile::PAUSED:
case ToxFile::TRANSMITTING:
return false;
case ToxFile::BROKEN:
case ToxFile::CANCELED:
case ToxFile::FINISHED:
default:
return true;
}
}
std::map<ChatLogIdx, ChatLogItem>::const_iterator
firstItemAfterDate(QDate date, const std::map<ChatLogIdx, ChatLogItem>& items)
{
return std::lower_bound(items.begin(), items.end(), QDateTime(date),
[](const MessageDateAdaptor& a, MessageDateAdaptor const& b) {
return a.timestamp.date() < b.timestamp.date();
});
}
} // namespace
SessionChatLog::SessionChatLog(const ICoreIdHandler& coreIdHandler)
: coreIdHandler(coreIdHandler)
{}
/**
* @brief Alternate constructor that allows for an initial index to be set
*/
SessionChatLog::SessionChatLog(ChatLogIdx initialIdx, const ICoreIdHandler& coreIdHandler)
: coreIdHandler(coreIdHandler)
, nextIdx(initialIdx)
{}
SessionChatLog::~SessionChatLog() = default;
const ChatLogItem& SessionChatLog::at(ChatLogIdx idx) const
{
auto item = items.find(idx);
if (item == items.end()) {
std::terminate();
}
return item->second;
}
SearchResult SessionChatLog::searchForward(SearchPos startPos, const QString& phrase,
const ParameterSearch& parameter) const
{
if (startPos.logIdx >= getNextIdx()) {
SearchResult res;
res.found = false;
return res;
}
auto currentPos = startPos;
auto regexp = getRegexpForPhrase(phrase, parameter.filter);
for (auto it = items.find(currentPos.logIdx); it != items.end(); ++it) {
const auto& key = it->first;
const auto& item = it->second;
if (item.getContentType() != ChatLogItem::ContentType::message) {
continue;
}
const auto& content = item.getContentAsMessage();
auto match = regexp.globalMatch(content.message.content, 0);
auto numMatches = 0;
QRegularExpressionMatch lastMatch;
while (match.isValid() && numMatches <= currentPos.numMatches && match.hasNext()) {
lastMatch = match.next();
numMatches++;
}
if (numMatches > currentPos.numMatches) {
SearchResult res;
res.found = true;
res.pos.logIdx = key;
res.pos.numMatches = numMatches;
res.start = lastMatch.capturedStart();
res.len = lastMatch.capturedLength();
return res;
}
// After the first iteration we force this to 0 to search the whole
// message
currentPos.numMatches = 0;
}
// We should have returned from the above loop if we had found anything
SearchResult ret;
ret.found = false;
return ret;
}
SearchResult SessionChatLog::searchBackward(SearchPos startPos, const QString& phrase,
const ParameterSearch& parameter) const
{
auto currentPos = startPos;
auto regexp = getRegexpForPhrase(phrase, parameter.filter);
auto startIt = items.find(currentPos.logIdx);
// If we don't have it we'll start at the end
if (startIt == items.end()) {
startIt = std::prev(items.end());
startPos.numMatches = 0;
}
// Off by 1 due to reverse_iterator api
auto rStartIt = std::reverse_iterator<decltype(startIt)>(std::next(startIt));
auto rEnd = std::reverse_iterator<decltype(startIt)>(items.begin());
for (auto it = rStartIt; it != rEnd; ++it) {
const auto& key = it->first;
const auto& item = it->second;
if (item.getContentType() != ChatLogItem::ContentType::message) {
continue;
}
const auto& content = item.getContentAsMessage();
auto match = regexp.globalMatch(content.message.content, 0);
auto totalMatches = 0;
auto numMatchesBeforePos = 0;
QRegularExpressionMatch lastMatch;
while (match.isValid() && match.hasNext()) {
auto currentMatch = match.next();
totalMatches++;
if (currentPos.numMatches == 0 || currentPos.numMatches > numMatchesBeforePos) {
lastMatch = currentMatch;
numMatchesBeforePos++;
}
}
if ((numMatchesBeforePos < currentPos.numMatches || currentPos.numMatches == 0)
&& numMatchesBeforePos > 0) {
SearchResult res;
res.found = true;
res.pos.logIdx = key;
res.pos.numMatches = numMatchesBeforePos;
res.start = lastMatch.capturedStart();
res.len = lastMatch.capturedLength();
return res;
}
// After the first iteration we force this to 0 to search the whole
// message
currentPos.numMatches = 0;
}
// We should have returned from the above loop if we had found anything
SearchResult ret;
ret.found = false;
return ret;
}
ChatLogIdx SessionChatLog::getFirstIdx() const
{
if (items.empty()) {
return nextIdx;
}
return items.begin()->first;
}
ChatLogIdx SessionChatLog::getNextIdx() const
{
return nextIdx;
}
std::vector<IChatLog::DateChatLogIdxPair> SessionChatLog::getDateIdxs(const QDate& startDate,
size_t maxDates) const
{
std::vector<DateChatLogIdxPair> ret;
auto dateIt = startDate;
while (true) {
auto it = firstItemAfterDate(dateIt, items);
if (it == items.end()) {
break;
}
DateChatLogIdxPair pair;
pair.date = dateIt;
pair.idx = it->first;
ret.push_back(std::move(pair));
dateIt = dateIt.addDays(1);
if (startDate.daysTo(dateIt) > maxDates && maxDates != 0) {
break;
}
}
return ret;
}
void SessionChatLog::insertMessageAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName,
ChatLogMessage message)
{
auto item = ChatLogItem(sender, message);
if (!senderName.isEmpty()) {
item.setDisplayName(senderName);
}
items.emplace(idx, std::move(item));
}
void SessionChatLog::insertFileAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName, ChatLogFile file)
{
auto item = ChatLogItem(sender, file);
if (!senderName.isEmpty()) {
item.setDisplayName(senderName);
}
items.emplace(idx, std::move(item));
}
/**
* @brief Inserts message data into the chatlog buffer
* @note Owner of SessionChatLog is in charge of attaching this to the appropriate IMessageDispatcher
*/
void SessionChatLog::onMessageReceived(const ToxPk& sender, const Message& message)
{
auto messageIdx = nextIdx++;
ChatLogMessage chatLogMessage;
chatLogMessage.isComplete = true;
chatLogMessage.message = message;
items.emplace(messageIdx, ChatLogItem(sender, chatLogMessage));
emit this->itemUpdated(messageIdx);
}
/**
* @brief Inserts message data into the chatlog buffer
* @note Owner of SessionChatLog is in charge of attaching this to the appropriate IMessageDispatcher
*/
void SessionChatLog::onMessageSent(DispatchedMessageId id, const Message& message)
{
auto messageIdx = nextIdx++;
ChatLogMessage chatLogMessage;
chatLogMessage.isComplete = false;
chatLogMessage.message = message;
items.emplace(messageIdx, ChatLogItem(coreIdHandler.getSelfPublicKey(), chatLogMessage));
outgoingMessages.insert(id, messageIdx);
emit this->itemUpdated(messageIdx);
}
/**
* @brief Marks the associated message as complete and notifies any listeners
* @note Owner of SessionChatLog is in charge of attaching this to the appropriate IMessageDispatcher
*/
void SessionChatLog::onMessageComplete(DispatchedMessageId id)
{
auto chatLogIdxIt = outgoingMessages.find(id);
if (chatLogIdxIt == outgoingMessages.end()) {
qWarning() << "Failed to find outgoing message";
return;
}
const auto& chatLogIdx = *chatLogIdxIt;
auto messageIt = items.find(chatLogIdx);
if (messageIt == items.end()) {
qWarning() << "Failed to look up message in chat log";
return;
}
messageIt->second.getContentAsMessage().isComplete = true;
emit this->itemUpdated(messageIt->first);
}
/**
* @brief Updates file state in the chatlog
* @note The files need to be pre-filtered for the current chat since we do no validation
* @note This should be attached to any CoreFile signal that fits the signature
*/
void SessionChatLog::onFileUpdated(const ToxPk& sender, const ToxFile& file)
{
auto fileIt =
std::find_if(currentFileTransfers.begin(), currentFileTransfers.end(),
[&](const CurrentFileTransfer& transfer) { return transfer.file == file; });
ChatLogIdx messageIdx;
if (fileIt == currentFileTransfers.end() && file.status == ToxFile::INITIALIZING) {
assert(file.status == ToxFile::INITIALIZING);
CurrentFileTransfer currentTransfer;
currentTransfer.file = file;
currentTransfer.idx = nextIdx++;
currentFileTransfers.push_back(currentTransfer);
const auto chatLogFile = ChatLogFile{QDateTime::currentDateTime(), file};
items.emplace(currentTransfer.idx, ChatLogItem(sender, chatLogFile));
messageIdx = currentTransfer.idx;
} else if (fileIt != currentFileTransfers.end()) {
messageIdx = fileIt->idx;
fileIt->file = file;
items.at(messageIdx).getContentAsFile().file = file;
} else {
// This may be a file unbroken message that we don't handle ATM
return;
}
if (toxFileIsComplete(file.status)) {
currentFileTransfers.erase(fileIt);
}
emit this->itemUpdated(messageIdx);
}
void SessionChatLog::onFileTransferRemotePausedUnpaused(const ToxPk& sender, const ToxFile& file,
bool /*paused*/)
{
onFileUpdated(sender, file);
}
void SessionChatLog::onFileTransferBrokenUnbroken(const ToxPk& sender, const ToxFile& file,
bool /*broken*/)
{
onFileUpdated(sender, file);
}

View File

@ -0,0 +1,88 @@
/*
Copyright © 2019 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SESSION_CHAT_LOG_H
#define SESSION_CHAT_LOG_H
#include "ichatlog.h"
#include "imessagedispatcher.h"
#include <QList>
#include <QObject>
struct SessionChatLogMetadata;
class SessionChatLog : public IChatLog
{
Q_OBJECT
public:
SessionChatLog(const ICoreIdHandler& coreIdHandler);
SessionChatLog(ChatLogIdx initialIdx, const ICoreIdHandler& coreIdHandler);
~SessionChatLog();
const ChatLogItem& at(ChatLogIdx idx) const override;
SearchResult searchForward(SearchPos startIdx, const QString& phrase,
const ParameterSearch& parameter) const override;
SearchResult searchBackward(SearchPos startIdx, const QString& phrase,
const ParameterSearch& parameter) const override;
ChatLogIdx getFirstIdx() const override;
ChatLogIdx getNextIdx() const override;
std::vector<DateChatLogIdxPair> getDateIdxs(const QDate& startDate, size_t maxDates) const override;
void insertMessageAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName, ChatLogMessage message);
void insertFileAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName, ChatLogFile file);
public slots:
void onMessageReceived(const ToxPk& sender, const Message& message);
void onMessageSent(DispatchedMessageId id, const Message& message);
void onMessageComplete(DispatchedMessageId id);
void onFileUpdated(const ToxPk& sender, const ToxFile& file);
void onFileTransferRemotePausedUnpaused(const ToxPk& sender, const ToxFile& file, bool paused);
void onFileTransferBrokenUnbroken(const ToxPk& sender, const ToxFile& file, bool broken);
private:
const ICoreIdHandler& coreIdHandler;
ChatLogIdx nextIdx = ChatLogIdx(0);
std::map<ChatLogIdx, ChatLogItem> items;
struct CurrentFileTransfer
{
ChatLogIdx idx;
ToxFile file;
};
/**
* Short list of active file transfers in given log. This is to make it
* so we don't have to search through all files that have ever been transferred
* in order to find our existing transfers
*/
std::vector<CurrentFileTransfer> currentFileTransfers;
/**
* Maps DispatchedMessageIds back to ChatLogIdxs. Messages are removed when the message
* is marked as completed
*/
QMap<DispatchedMessageId, ChatLogIdx> outgoingMessages;
};
#endif /*SESSION_CHAT_LOG_H*/

View File

@ -28,7 +28,30 @@ struct Addable
T operator+(T const& other) const { return static_cast<T const&>(*this).get() + other.get(); };
};
template <typename T>
template <typename T, typename Underlying>
struct UnderlyingAddable
{
T operator+(Underlying const& other) const
{
return T(static_cast<T const&>(*this).get() + other);
};
};
template <typename T, typename Underlying>
struct UnitlessDifferencable
{
T operator-(Underlying const& other) const
{
return T(static_cast<T const&>(*this).get() - other);
};
Underlying operator-(T const& other) const
{
return static_cast<T const&>(*this).get() - other.get();
}
};
template <typename T, typename>
struct Incrementable
{
T& operator++()
@ -46,23 +69,28 @@ struct Incrementable
}
};
template <typename T>
template <typename T, typename>
struct EqualityComparible
{
bool operator==(const T& other) const { return static_cast<T const&>(*this).get() == other.get(); };
bool operator!=(const T& other) const
{
return static_cast<T const&>(*this).get() != other.get();
};
};
template <typename T>
template <typename T, typename Underlying>
struct Hashable
{
friend uint qHash(const Hashable<T>& key, uint seed = 0)
friend uint qHash(const Hashable<T, Underlying>& key, uint seed = 0)
{
return qHash(static_cast<T const&>(*key).get(), seed);
}
};
template <typename T>
struct Orderable : EqualityComparible<T>
template <typename T, typename Underlying>
struct Orderable : EqualityComparible<T, Underlying>
{
bool operator<(const T& rhs) const { return static_cast<T const&>(*this).get() < rhs.get(); }
bool operator>(const T& rhs) const { return static_cast<T const&>(*this).get() > rhs.get(); }
@ -82,10 +110,12 @@ struct Orderable : EqualityComparible<T>
* qRegisterMetaType<ReceiptNum>();
*/
template <typename T, typename Tag, template <typename> class... Properties>
class NamedType : public Properties<NamedType<T, Tag, Properties...>>...
template <typename T, typename Tag, template <typename, typename> class... Properties>
class NamedType : public Properties<NamedType<T, Tag, Properties...>, T>...
{
public:
using UnderlyingType = T;
NamedType() {}
explicit NamedType(T const& value) : value_(value) {}
T& get() { return value_; }
@ -94,7 +124,7 @@ private:
T value_;
};
template <typename T, typename Tag, template <typename> class... Properties>
template <typename T, typename Tag, template <typename, typename> class... Properties>
uint qHash(const NamedType<T, Tag, Properties...>& key, uint seed = 0)
{
return qHash(key.get(), seed);

View File

@ -0,0 +1,117 @@
#include "src/model/ichatlog.h"
#include "src/model/imessagedispatcher.h"
#include "src/model/sessionchatlog.h"
#include <QtTest/QtTest>
namespace {
Message createMessage(const QString& content)
{
Message message;
message.content = content;
message.isAction = false;
message.timestamp = QDateTime::currentDateTime();
return message;
}
class MockCoreIdHandler : public ICoreIdHandler
{
public:
ToxId getSelfId() const override
{
std::terminate();
return ToxId();
}
ToxPk getSelfPublicKey() const override
{
static uint8_t id[TOX_PUBLIC_KEY_SIZE] = {5};
return ToxPk(id);
}
QString getUsername() const override
{
std::terminate();
return QString();
}
};
} // namespace
class TestSessionChatLog : public QObject
{
Q_OBJECT
public:
TestSessionChatLog(){};
private slots:
void init();
void testSanity();
private:
MockCoreIdHandler idHandler;
std::unique_ptr<SessionChatLog> chatLog;
};
/**
* @brief Test initialiation, resets the chatlog
*/
void TestSessionChatLog::init()
{
chatLog = std::unique_ptr<SessionChatLog>(new SessionChatLog(idHandler));
}
/**
* @brief Quick sanity test that the chatlog is working as expected. Tests basic insertion, retrieval, and searching of messages
*/
void TestSessionChatLog::testSanity()
{
/* ChatLogIdx(0) */ chatLog->onMessageSent(DispatchedMessageId(0), createMessage("test"));
/* ChatLogIdx(1) */ chatLog->onMessageSent(DispatchedMessageId(1), createMessage("test test"));
/* ChatLogIdx(2) */ chatLog->onMessageReceived(ToxPk(), createMessage("test2"));
/* ChatLogIdx(3) */ chatLog->onFileUpdated(ToxPk(), ToxFile());
/* ChatLogIdx(4) */ chatLog->onMessageSent(DispatchedMessageId(2), createMessage("test3"));
/* ChatLogIdx(5) */ chatLog->onMessageSent(DispatchedMessageId(3), createMessage("test4"));
/* ChatLogIdx(6) */ chatLog->onMessageSent(DispatchedMessageId(4), createMessage("test"));
/* ChatLogIdx(7) */ chatLog->onMessageReceived(ToxPk(), createMessage("test5"));
QVERIFY(chatLog->getNextIdx() == ChatLogIdx(8));
QVERIFY(chatLog->at(ChatLogIdx(3)).getContentType() == ChatLogItem::ContentType::fileTransfer);
QVERIFY(chatLog->at(ChatLogIdx(7)).getContentType() == ChatLogItem::ContentType::message);
auto searchPos = SearchPos{ChatLogIdx(1), 0};
auto searchResult = chatLog->searchForward(searchPos, "test", ParameterSearch());
QVERIFY(searchResult.found);
QVERIFY(searchResult.len == 4);
QVERIFY(searchResult.pos.logIdx == ChatLogIdx(1));
QVERIFY(searchResult.start == 0);
searchPos = searchResult.pos;
searchResult = chatLog->searchForward(searchPos, "test", ParameterSearch());
QVERIFY(searchResult.found);
QVERIFY(searchResult.len == 4);
QVERIFY(searchResult.pos.logIdx == ChatLogIdx(1));
QVERIFY(searchResult.start == 5);
searchPos = searchResult.pos;
searchResult = chatLog->searchForward(searchPos, "test", ParameterSearch());
QVERIFY(searchResult.found);
QVERIFY(searchResult.len == 4);
QVERIFY(searchResult.pos.logIdx == ChatLogIdx(2));
QVERIFY(searchResult.start == 0);
searchPos = searchResult.pos;
searchResult = chatLog->searchBackward(searchPos, "test", ParameterSearch());
QVERIFY(searchResult.found);
QVERIFY(searchResult.len == 4);
QVERIFY(searchResult.pos.logIdx == ChatLogIdx(1));
QVERIFY(searchResult.start == 5);
}
QTEST_GUILESS_MAIN(TestSessionChatLog)
#include "sessionchatlog_test.moc"