mirror of https://github.com/qTox/qTox.git
5 changed files with 729 additions and 0 deletions
@ -0,0 +1,460 @@
@@ -0,0 +1,460 @@
|
||||
/*
|
||||
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 "chathistory.h" |
||||
#include "src/persistence/settings.h" |
||||
#include "src/widget/form/chatform.h" |
||||
|
||||
namespace { |
||||
/**
|
||||
* @brief Determines if the given idx needs to be loaded from history |
||||
* @param[in] idx index to check |
||||
* @param[in] sessionChatLog SessionChatLog containing currently loaded items |
||||
* @return True if load is needed |
||||
*/ |
||||
bool needsLoadFromHistory(ChatLogIdx idx, const SessionChatLog& sessionChatLog) |
||||
{ |
||||
return idx < sessionChatLog.getFirstIdx(); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Gets the initial chat log index for a sessionChatLog with 0 items loaded from history. |
||||
* Needed to keep history indexes in sync with chat log indexes |
||||
* @param[in] history |
||||
* @param[in] f |
||||
* @return Initial chat log index |
||||
*/ |
||||
ChatLogIdx getInitialChatLogIdx(History* history, Friend& f) |
||||
{ |
||||
if (!history) { |
||||
return ChatLogIdx(0); |
||||
} |
||||
|
||||
return ChatLogIdx(history->getNumMessagesForFriend(f.getPublicKey())); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Finds the first item in sessionChatLog that contains a message |
||||
* @param[in] sessionChatLog |
||||
* @return index of first message |
||||
*/ |
||||
ChatLogIdx findFirstMessage(const SessionChatLog& sessionChatLog) |
||||
{ |
||||
auto it = sessionChatLog.getFirstIdx(); |
||||
while (it < sessionChatLog.getNextIdx()) { |
||||
if (sessionChatLog.at(it).getContentType() == ChatLogItem::ContentType::message) { |
||||
return it; |
||||
} |
||||
it++; |
||||
} |
||||
return ChatLogIdx(-1); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Handles presence of aciton prefix in content |
||||
* @param[in/out] content |
||||
* @return True if was an action |
||||
*/ |
||||
bool handleActionPrefix(QString& content) |
||||
{ |
||||
// Unfortunately due to legacy reasons we have to continue
|
||||
// inserting and parsing for ACTION_PREFIX in our messages even
|
||||
// though we have the ability to something more intelligent now
|
||||
// that we aren't owned by chatform logic
|
||||
auto isAction = content.startsWith(ChatForm::ACTION_PREFIX, Qt::CaseInsensitive); |
||||
if (isAction) { |
||||
content.remove(0, ChatForm::ACTION_PREFIX.size()); |
||||
} |
||||
|
||||
return isAction; |
||||
} |
||||
} // namespace
|
||||
|
||||
ChatHistory::ChatHistory(Friend& f_, History* history_, const ICoreIdHandler& coreIdHandler, |
||||
const Settings& settings_, IMessageDispatcher& messageDispatcher) |
||||
: f(f_) |
||||
, history(history_) |
||||
, sessionChatLog(getInitialChatLogIdx(history, f), coreIdHandler) |
||||
, settings(settings_) |
||||
, coreIdHandler(coreIdHandler) |
||||
{ |
||||
connect(&messageDispatcher, &IMessageDispatcher::messageSent, this, &ChatHistory::onMessageSent); |
||||
connect(&messageDispatcher, &IMessageDispatcher::messageComplete, this, |
||||
&ChatHistory::onMessageComplete); |
||||
|
||||
if (canUseHistory()) { |
||||
// Defer messageSent callback until we finish firing off all our unsent messages.
|
||||
// If it was connected all our unsent messages would be re-added ot history again
|
||||
dispatchUnsentMessages(messageDispatcher); |
||||
} |
||||
|
||||
// Now that we've fired off our unsent messages we can connect the message
|
||||
connect(&messageDispatcher, &IMessageDispatcher::messageReceived, this, |
||||
&ChatHistory::onMessageReceived); |
||||
|
||||
// NOTE: this has to be done _after_ sending all sent messages since initial
|
||||
// state of the message has to be marked according to our dispatch state
|
||||
constexpr auto defaultNumMessagesToLoad = 100; |
||||
auto firstChatLogIdx = sessionChatLog.getFirstIdx().get() < defaultNumMessagesToLoad |
||||
? ChatLogIdx(0) |
||||
: sessionChatLog.getFirstIdx() - defaultNumMessagesToLoad; |
||||
|
||||
if (canUseHistory()) { |
||||
loadHistoryIntoSessionChatLog(firstChatLogIdx); |
||||
} |
||||
|
||||
// We don't manage any of the item updates ourselves, we just forward along
|
||||
// the underlying sessionChatLog's updates
|
||||
connect(&sessionChatLog, &IChatLog::itemUpdated, this, &IChatLog::itemUpdated); |
||||
} |
||||
|
||||
const ChatLogItem& ChatHistory::at(ChatLogIdx idx) const |
||||
{ |
||||
if (canUseHistory()) { |
||||
ensureIdxInSessionChatLog(idx); |
||||
} |
||||
|
||||
return sessionChatLog.at(idx); |
||||
} |
||||
|
||||
SearchResult ChatHistory::searchForward(SearchPos startIdx, const QString& phrase, |
||||
const ParameterSearch& parameter) const |
||||
{ |
||||
if (startIdx.logIdx >= getNextIdx()) { |
||||
SearchResult res; |
||||
res.found = false; |
||||
return res; |
||||
} |
||||
|
||||
if (canUseHistory()) { |
||||
ensureIdxInSessionChatLog(startIdx.logIdx); |
||||
} |
||||
|
||||
return sessionChatLog.searchForward(startIdx, phrase, parameter); |
||||
} |
||||
|
||||
SearchResult ChatHistory::searchBackward(SearchPos startIdx, const QString& phrase, |
||||
const ParameterSearch& parameter) const |
||||
{ |
||||
auto res = sessionChatLog.searchBackward(startIdx, phrase, parameter); |
||||
|
||||
if (res.found || !canUseHistory()) { |
||||
return res; |
||||
} |
||||
|
||||
auto earliestMessage = findFirstMessage(sessionChatLog); |
||||
|
||||
auto earliestMessageDate = |
||||
(earliestMessage == ChatLogIdx(-1)) |
||||
? QDateTime::currentDateTime() |
||||
: sessionChatLog.at(earliestMessage).getContentAsMessage().message.timestamp; |
||||
|
||||
// Roundabout way of getting the first idx but I don't want to have to
|
||||
// deal with re-implementing so we'll just piece what we want together...
|
||||
//
|
||||
// If the double disk access is real bad we can optimize this by adding
|
||||
// another function to history
|
||||
auto dateWherePhraseFound = |
||||
history->getDateWhereFindPhrase(f.getPublicKey().toString(), earliestMessageDate, phrase, |
||||
parameter); |
||||
|
||||
auto loadIdx = history->getNumMessagesForFriendBeforeDate(f.getPublicKey(), dateWherePhraseFound); |
||||
loadHistoryIntoSessionChatLog(ChatLogIdx(loadIdx)); |
||||
|
||||
// Reset search pos to the message we just loaded to avoid a double search
|
||||
startIdx.logIdx = ChatLogIdx(loadIdx); |
||||
startIdx.numMatches = 0; |
||||
return sessionChatLog.searchBackward(startIdx, phrase, parameter); |
||||
} |
||||
|
||||
ChatLogIdx ChatHistory::getFirstIdx() const |
||||
{ |
||||
if (canUseHistory()) { |
||||
return ChatLogIdx(0); |
||||
} else { |
||||
return sessionChatLog.getFirstIdx(); |
||||
} |
||||
} |
||||
|
||||
ChatLogIdx ChatHistory::getNextIdx() const |
||||
{ |
||||
return sessionChatLog.getNextIdx(); |
||||
} |
||||
|
||||
std::vector<IChatLog::DateChatLogIdxPair> ChatHistory::getDateIdxs(const QDate& startDate, |
||||
size_t maxDates) const |
||||
{ |
||||
if (canUseHistory()) { |
||||
auto counts = history->getNumMessagesForFriendBeforeDateBoundaries(f.getPublicKey(), |
||||
startDate, maxDates); |
||||
|
||||
std::vector<IChatLog::DateChatLogIdxPair> ret; |
||||
std::transform(counts.begin(), counts.end(), std::back_inserter(ret), |
||||
[&](const History::DateIdx& historyDateIdx) { |
||||
DateChatLogIdxPair pair; |
||||
pair.date = historyDateIdx.date; |
||||
pair.idx.get() = historyDateIdx.numMessagesIn; |
||||
return pair; |
||||
}); |
||||
|
||||
// Do not re-search in the session chat log. If we have history the query to the history should have been sufficient
|
||||
return ret; |
||||
} else { |
||||
return sessionChatLog.getDateIdxs(startDate, maxDates); |
||||
} |
||||
} |
||||
|
||||
void ChatHistory::onFileUpdated(const ToxPk& sender, const ToxFile& file) |
||||
{ |
||||
if (canUseHistory()) { |
||||
switch (file.status) { |
||||
case ToxFile::INITIALIZING: { |
||||
// Note: There is some implcit coupling between history and the current
|
||||
// chat log. Both rely on generating a new id based on the state of
|
||||
// initializing. If this is changed in the session chat log we'll end up
|
||||
// with a different order when loading from history
|
||||
history->addNewFileMessage(f.getPublicKey().toString(), file.resumeFileId, file.fileName, |
||||
file.filePath, file.filesize, sender.toString(), |
||||
QDateTime::currentDateTime(), f.getDisplayedName()); |
||||
break; |
||||
} |
||||
case ToxFile::CANCELED: |
||||
case ToxFile::FINISHED: |
||||
case ToxFile::BROKEN: { |
||||
const bool isSuccess = file.status == ToxFile::FINISHED; |
||||
history->setFileFinished(file.resumeFileId, isSuccess, file.filePath, |
||||
file.hashGenerator->result()); |
||||
break; |
||||
} |
||||
case ToxFile::PAUSED: |
||||
case ToxFile::TRANSMITTING: |
||||
default: |
||||
break; |
||||
} |
||||
} |
||||
|
||||
sessionChatLog.onFileUpdated(sender, file); |
||||
} |
||||
|
||||
void ChatHistory::onFileTransferRemotePausedUnpaused(const ToxPk& sender, const ToxFile& file, |
||||
bool paused) |
||||
{ |
||||
sessionChatLog.onFileTransferRemotePausedUnpaused(sender, file, paused); |
||||
} |
||||
|
||||
void ChatHistory::onFileTransferBrokenUnbroken(const ToxPk& sender, const ToxFile& file, bool broken) |
||||
{ |
||||
sessionChatLog.onFileTransferBrokenUnbroken(sender, file, broken); |
||||
} |
||||
|
||||
void ChatHistory::onMessageReceived(const ToxPk& sender, const Message& message) |
||||
{ |
||||
if (canUseHistory()) { |
||||
auto friendPk = f.getPublicKey().toString(); |
||||
auto displayName = f.getDisplayedName(); |
||||
auto content = message.content; |
||||
if (message.isAction) { |
||||
content = ChatForm::ACTION_PREFIX + content; |
||||
} |
||||
|
||||
history->addNewMessage(friendPk, content, friendPk, message.timestamp, true, displayName); |
||||
} |
||||
|
||||
sessionChatLog.onMessageReceived(sender, message); |
||||
} |
||||
|
||||
void ChatHistory::onMessageSent(DispatchedMessageId id, const Message& message) |
||||
{ |
||||
if (canUseHistory()) { |
||||
auto selfPk = coreIdHandler.getSelfPublicKey().toString(); |
||||
auto friendPk = f.getPublicKey().toString(); |
||||
|
||||
auto content = message.content; |
||||
if (message.isAction) { |
||||
content = ChatForm::ACTION_PREFIX + content; |
||||
} |
||||
|
||||
auto username = coreIdHandler.getUsername(); |
||||
|
||||
auto onInsertion = [this, id](RowId historyId) { handleDispatchedMessage(id, historyId); }; |
||||
|
||||
history->addNewMessage(friendPk, content, selfPk, message.timestamp, false, username, |
||||
onInsertion); |
||||
} |
||||
|
||||
sessionChatLog.onMessageSent(id, message); |
||||
} |
||||
|
||||
void ChatHistory::onMessageComplete(DispatchedMessageId id) |
||||
{ |
||||
if (canUseHistory()) { |
||||
completeMessage(id); |
||||
} |
||||
|
||||
sessionChatLog.onMessageComplete(id); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Forces the given index and all future indexes to be in the chatlog |
||||
* @param[in] idx |
||||
* @note Marked const since this doesn't change _external_ state of the class. We |
||||
still have all the same items at all the same indexes, we've just stuckem |
||||
in ram |
||||
*/ |
||||
void ChatHistory::ensureIdxInSessionChatLog(ChatLogIdx idx) const |
||||
{ |
||||
if (needsLoadFromHistory(idx, sessionChatLog)) { |
||||
loadHistoryIntoSessionChatLog(idx); |
||||
} |
||||
} |
||||
/**
|
||||
* @brief Unconditionally loads the given index and all future messages that |
||||
* are not in the session chat log into the session chat log |
||||
* @param[in] idx |
||||
* @note Marked const since this doesn't change _external_ state of the class. We |
||||
still have all the same items at all the same indexes, we've just stuckem |
||||
in ram |
||||
* @note no end idx as we always load from start -> latest. In the future we |
||||
* could have a less contiguous history |
||||
*/ |
||||
void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const |
||||
{ |
||||
if (!needsLoadFromHistory(start, sessionChatLog)) { |
||||
return; |
||||
} |
||||
|
||||
auto end = sessionChatLog.getFirstIdx(); |
||||
|
||||
// We know that both history and us have a start index of 0 so the type
|
||||
// conversion should be safe
|
||||
assert(getFirstIdx() == ChatLogIdx(0)); |
||||
auto messages = history->getMessagesForFriend(f.getPublicKey(), start.get(), end.get()); |
||||
|
||||
assert(messages.size() == end.get() - start.get()); |
||||
ChatLogIdx nextIdx = start; |
||||
|
||||
for (const auto& message : messages) { |
||||
// Note that message.id is _not_ a valid conversion here since it is a
|
||||
// global id not a per-chat id like the ChatLogIdx
|
||||
auto currentIdx = nextIdx++; |
||||
auto sender = ToxId(message.sender).getPublicKey(); |
||||
switch (message.content.getType()) { |
||||
case HistMessageContentType::file: { |
||||
const auto date = message.timestamp; |
||||
const auto file = message.content.asFile(); |
||||
const auto chatLogFile = ChatLogFile{date, file}; |
||||
sessionChatLog.insertFileAtIdx(currentIdx, sender, message.dispName, chatLogFile); |
||||
break; |
||||
} |
||||
case HistMessageContentType::message: { |
||||
auto messageContent = message.content.asMessage(); |
||||
|
||||
auto isAction = handleActionPrefix(messageContent); |
||||
|
||||
// It's okay to skip the message processor here. The processor is
|
||||
// meant to convert between boundaries of our internal
|
||||
// representation. We already had to go through the processor before
|
||||
// we hit IMessageDispatcher's signals which history listens for.
|
||||
// Items added to history have already been sent so we know they already
|
||||
// reflect what was sent/received.
|
||||
auto processedMessage = Message{isAction, messageContent, message.timestamp}; |
||||
|
||||
auto dispatchedMessageIt = |
||||
std::find_if(dispatchedMessageRowIdMap.begin(), dispatchedMessageRowIdMap.end(), |
||||
[&](RowId dispatchedId) { return dispatchedId == message.id; }); |
||||
|
||||
bool isComplete = dispatchedMessageIt == dispatchedMessageRowIdMap.end(); |
||||
|
||||
if (isComplete) { |
||||
auto chatLogMessage = ChatLogMessage{true, processedMessage}; |
||||
sessionChatLog.insertMessageAtIdx(currentIdx, sender, message.dispName, chatLogMessage); |
||||
} else { |
||||
// If the message is incomplete we have to pretend we sent it to ensure
|
||||
// sessionChatLog state is correct
|
||||
sessionChatLog.onMessageSent(dispatchedMessageIt.key(), processedMessage); |
||||
} |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
assert(nextIdx == end); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Sends any unsent messages in history to the underlying message dispatcher |
||||
* @param[in] messageDispatcher |
||||
*/ |
||||
void ChatHistory::dispatchUnsentMessages(IMessageDispatcher& messageDispatcher) |
||||
{ |
||||
auto unsentMessages = history->getUnsentMessagesForFriend(f.getPublicKey()); |
||||
for (auto& message : unsentMessages) { |
||||
// We should only store messages as unsent, if this changes in the
|
||||
// future we need to extend this logic
|
||||
assert(message.content.getType() == HistMessageContentType::message); |
||||
|
||||
auto messageContent = message.content.asMessage(); |
||||
auto isAction = handleActionPrefix(messageContent); |
||||
|
||||
// NOTE: timestamp will be generated in messageDispatcher but we haven't
|
||||
// hooked up our history callback so it will not be shown in our chatlog
|
||||
// with the new timestamp. This is intentional as everywhere else we use
|
||||
// attempted send time (which is whenever the it was initially inserted
|
||||
// into history
|
||||
auto dispatchIds = messageDispatcher.sendMessage(isAction, messageContent); |
||||
|
||||
// We should only send a single message, but in the odd case where we end
|
||||
// up having to split more than when we added the message to history we'll
|
||||
// just associate the last dispatched id with the history message
|
||||
handleDispatchedMessage(dispatchIds.second, message.id); |
||||
|
||||
// We don't add the messages to the underlying chatlog since
|
||||
// 1. We don't even know the ChatLogIdx of this message
|
||||
// 2. We only want to display the latest N messages on boot by default,
|
||||
// even if there are more than N messages that haven't been sent
|
||||
} |
||||
} |
||||
|
||||
void ChatHistory::handleDispatchedMessage(DispatchedMessageId dispatchId, RowId historyId) |
||||
{ |
||||
auto completedMessageIt = completedMessages.find(dispatchId); |
||||
if (completedMessageIt == completedMessages.end()) { |
||||
dispatchedMessageRowIdMap.insert(dispatchId, historyId); |
||||
} else { |
||||
history->markAsSent(historyId); |
||||
completedMessages.erase(completedMessageIt); |
||||
} |
||||
} |
||||
|
||||
void ChatHistory::completeMessage(DispatchedMessageId id) |
||||
{ |
||||
auto dispatchedMessageIt = dispatchedMessageRowIdMap.find(id); |
||||
|
||||
if (dispatchedMessageIt == dispatchedMessageRowIdMap.end()) { |
||||
completedMessages.insert(id); |
||||
} else { |
||||
history->markAsSent(*dispatchedMessageIt); |
||||
dispatchedMessageRowIdMap.erase(dispatchedMessageIt); |
||||
} |
||||
} |
||||
|
||||
bool ChatHistory::canUseHistory() const |
||||
{ |
||||
return history && settings.getEnableLogging(); |
||||
} |
@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
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_HISTORY_H |
||||
#define CHAT_HISTORY_H |
||||
|
||||
#include "ichatlog.h" |
||||
#include "sessionchatlog.h" |
||||
#include "src/persistence/history.h" |
||||
|
||||
#include <QSet> |
||||
|
||||
class Settings; |
||||
|
||||
class ChatHistory : public IChatLog |
||||
{ |
||||
Q_OBJECT |
||||
public: |
||||
ChatHistory(Friend& f_, History* history_, const ICoreIdHandler& coreIdHandler, |
||||
const Settings& settings, IMessageDispatcher& messageDispatcher); |
||||
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; |
||||
|
||||
public slots: |
||||
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 slots: |
||||
void onMessageReceived(const ToxPk& sender, const Message& message); |
||||
void onMessageSent(DispatchedMessageId id, const Message& message); |
||||
void onMessageComplete(DispatchedMessageId id); |
||||
|
||||
private: |
||||
void ensureIdxInSessionChatLog(ChatLogIdx idx) const; |
||||
void loadHistoryIntoSessionChatLog(ChatLogIdx start) const; |
||||
void dispatchUnsentMessages(IMessageDispatcher& messageDispatcher); |
||||
void handleDispatchedMessage(DispatchedMessageId dispatchId, RowId historyId); |
||||
void completeMessage(DispatchedMessageId id); |
||||
bool canUseHistory() const; |
||||
|
||||
Friend& f; |
||||
History* history; |
||||
mutable SessionChatLog sessionChatLog; |
||||
const Settings& settings; |
||||
const ICoreIdHandler& coreIdHandler; |
||||
|
||||
// If a message completes before it's inserted into history it will end up
|
||||
// in this set
|
||||
QSet<DispatchedMessageId> completedMessages; |
||||
|
||||
// If a message is inserted into history before it gets a completion
|
||||
// callback it will end up in this map
|
||||
QMap<DispatchedMessageId, RowId> dispatchedMessageRowIdMap; |
||||
}; |
||||
|
||||
#endif /*CHAT_HISTORY_H*/ |
Loading…
Reference in new issue