mirror of https://github.com/qTox/qTox.git
9 changed files with 1025 additions and 9 deletions
@ -0,0 +1,136 @@
@@ -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; |
||||
} |
@ -0,0 +1,75 @@
@@ -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*/ |
@ -0,0 +1,145 @@
@@ -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*/ |
@ -0,0 +1,419 @@
@@ -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); |
||||
} |
@ -0,0 +1,88 @@
@@ -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*/ |
@ -0,0 +1,117 @@
@@ -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" |
Loading…
Reference in new issue