diff --git a/CMakeLists.txt b/CMakeLists.txt index 855358382..727fbba6a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -351,6 +351,8 @@ set(${PROJECT_NAME}_SOURCES src/model/ichatlog.h src/model/sessionchatlog.h src/model/sessionchatlog.cpp + src/model/chathistory.h + src/model/chathistory.cpp src/net/bootstrapnodeupdater.cpp src/net/bootstrapnodeupdater.h src/net/avatarbroadcaster.cpp diff --git a/src/model/chathistory.cpp b/src/model/chathistory.cpp new file mode 100644 index 000000000..3151a5d1f --- /dev/null +++ b/src/model/chathistory.cpp @@ -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 . +*/ + +#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 ChatHistory::getDateIdxs(const QDate& startDate, + size_t maxDates) const +{ + if (canUseHistory()) { + auto counts = history->getNumMessagesForFriendBeforeDateBoundaries(f.getPublicKey(), + startDate, maxDates); + + std::vector 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(); +} diff --git a/src/model/chathistory.h b/src/model/chathistory.h new file mode 100644 index 000000000..15433baf6 --- /dev/null +++ b/src/model/chathistory.h @@ -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 . +*/ + +#ifndef CHAT_HISTORY_H +#define CHAT_HISTORY_H + +#include "ichatlog.h" +#include "sessionchatlog.h" +#include "src/persistence/history.h" + +#include + +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 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 completedMessages; + + // If a message is inserted into history before it gets a completion + // callback it will end up in this map + QMap dispatchedMessageRowIdMap; +}; + +#endif /*CHAT_HISTORY_H*/ diff --git a/src/persistence/history.cpp b/src/persistence/history.cpp index 0d9cb0868..f7bc34327 100644 --- a/src/persistence/history.cpp +++ b/src/persistence/history.cpp @@ -520,6 +520,7 @@ void History::setFileFinished(const QString& fileId, bool success, const QString fileInfos.remove(fileId); } + /** * @brief Fetches chat messages from the database. * @param friendPk Friend publick key to fetch. @@ -592,6 +593,122 @@ QList History::getChatHistoryCounts(const ToxPk& friendPk return counts; } +size_t History::getNumMessagesForFriend(const ToxPk& friendPk) +{ + return getNumMessagesForFriendBeforeDate(friendPk, + // Maximum possible time + QDateTime::fromMSecsSinceEpoch( + std::numeric_limits::max())); +} + +size_t History::getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const QDateTime& date) +{ + QString queryText = QString("SELECT COUNT(history.id) " + "FROM history " + "JOIN peers chat ON chat_id = chat.id " + "WHERE chat.public_key='%1'" + "AND timestamp < %2;") + .arg(friendPk.toString()) + .arg(date.toMSecsSinceEpoch()); + + size_t numMessages = 0; + auto rowCallback = [&numMessages](const QVector& row) { + numMessages = row[0].toLongLong(); + }; + + db->execNow({queryText, rowCallback}); + + return numMessages; +} + +QList History::getMessagesForFriend(const ToxPk& friendPk, size_t firstIdx, + size_t lastIdx) +{ + QList messages; + + // Don't forget to update the rowCallback if you change the selected columns! + QString queryText = + QString("SELECT history.id, faux_offline_pending.id, timestamp, " + "chat.public_key, aliases.display_name, sender.public_key, " + "message, file_transfers.file_restart_id, " + "file_transfers.file_path, file_transfers.file_name, " + "file_transfers.file_size, file_transfers.direction, " + "file_transfers.file_state FROM history " + "LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id " + "JOIN peers chat ON history.chat_id = chat.id " + "JOIN aliases ON sender_alias = aliases.id " + "JOIN peers sender ON aliases.owner = sender.id " + "LEFT JOIN file_transfers ON history.file_id = file_transfers.id " + "WHERE chat.public_key='%1' " + "LIMIT %2 OFFSET %3;") + .arg(friendPk.toString()) + .arg(lastIdx - firstIdx) + .arg(firstIdx); + + auto rowCallback = [&messages](const QVector& row) { + // dispName and message could have null bytes, QString::fromUtf8 + // truncates on null bytes so we strip them + auto id = RowId{row[0].toLongLong()}; + auto isOfflineMessage = row[1].isNull(); + auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong()); + auto friend_key = row[3].toString(); + auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', "")); + auto sender_key = row[5].toString(); + if (row[7].isNull()) { + messages += {id, isOfflineMessage, timestamp, friend_key, + display_name, sender_key, row[6].toString()}; + } else { + ToxFile file; + file.fileKind = TOX_FILE_KIND_DATA; + file.resumeFileId = row[7].toString().toUtf8(); + file.filePath = row[8].toString(); + file.fileName = row[9].toString(); + file.filesize = row[10].toLongLong(); + file.direction = static_cast(row[11].toLongLong()); + file.status = static_cast(row[12].toInt()); + messages += + {id, isOfflineMessage, timestamp, friend_key, display_name, sender_key, file}; + } + }; + + db->execNow({queryText, rowCallback}); + + return messages; +} + +QList History::getUnsentMessagesForFriend(const ToxPk& friendPk) +{ + auto queryText = + QString("SELECT history.id, faux_offline_pending.id, timestamp, chat.public_key, " + "aliases.display_name, sender.public_key, message " + "FROM history " + "JOIN faux_offline_pending ON history.id = faux_offline_pending.id " + "JOIN peers chat on chat.public_key = '%1' " + "JOIN aliases on sender_alias = aliases.id " + "JOIN peers sender on aliases.owner = sender.id;") + .arg(friendPk.toString()); + + QList ret; + auto rowCallback = [&ret](const QVector& row) { + // dispName and message could have null bytes, QString::fromUtf8 + // truncates on null bytes so we strip them + auto id = RowId{row[0].toLongLong()}; + auto isOfflineMessage = row[1].isNull(); + auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong()); + auto friend_key = row[3].toString(); + auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', "")); + auto sender_key = row[5].toString(); + if (row[6].isNull()) { + ret += {id, isOfflineMessage, timestamp, friend_key, + display_name, sender_key, row[6].toString()}; + } + }; + + db->execNow({queryText, rowCallback}); + + return ret; +} + /** * @brief Search phrase in chat messages * @param friendPk Friend public key @@ -681,6 +798,65 @@ QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTi return result; } +/** + * @brief Gets date boundaries in conversation with friendPk. History doesn't model conversation indexes, + * but we can count messages between us and friendPk to effectively give us an index. This function + * returns how many messages have happened between us <-> friendPk each time the date changes + * @param[in] friendPk ToxPk of conversation to retrieve + * @param[in] from Start date to look from + * @param[in] maxNum Maximum number of date boundaries to retrieve + * @note This API may seem a little strange, why not use QDate from and QDate to? The intent is to + * have an API that can be used to get the first item after a date (for search) and to get a list + * of date changes (for loadHistory). We could write two separate queries but the query is fairly + * intricate compared to our other ones so reducing duplication of it is preferable. + */ +QList History::getNumMessagesForFriendBeforeDateBoundaries(const ToxPk& friendPk, + const QDate& from, + size_t maxNum) +{ + auto friendPkString = friendPk.toString(); + + // No guarantee that this is the most efficient way to do this... + // We want to count messages that happened for a friend before a + // certain date. We do this by re-joining our table a second time + // but this time with the only filter being that our id is less than + // the ID of the corresponding row in the table that is grouped by day + auto countMessagesForFriend = + QString("SELECT COUNT(*) - 1 " // Count - 1 corresponds to 0 indexed message id for friend + "FROM history countHistory " // Import unfiltered table as countHistory + "JOIN peers chat ON chat_id = chat.id " // link chat_id to chat.id + "WHERE chat.public_key = '%1'" // filter this conversation + "AND countHistory.id <= history.id") // and filter that our unfiltered table history id only has elements up to history.id + .arg(friendPkString); + + auto limitString = (maxNum) ? QString("LIMIT %1").arg(maxNum) : QString(""); + + auto queryString = QString("SELECT (%1), (timestamp / 1000 / 60 / 60 / 24) AS day " + "FROM history " + "JOIN peers chat ON chat_id = chat.id " + "WHERE chat.public_key = '%2' " + "AND timestamp >= %3 " + "GROUP by day " + "%4;") + .arg(countMessagesForFriend) + .arg(friendPkString) + .arg(QDateTime(from).toMSecsSinceEpoch()) + .arg(limitString); + + QList dateIdxs; + auto rowCallback = [&dateIdxs](const QVector& row) { + DateIdx dateIdx; + dateIdx.numMessagesIn = row[0].toLongLong(); + dateIdx.date = + QDateTime::fromMSecsSinceEpoch(row[1].toLongLong() * 24 * 60 * 60 * 1000).date(); + dateIdxs.append(dateIdx); + }; + + db->execNow({queryString, rowCallback}); + + return dateIdxs; +} + /** * @brief get start date of correspondence * @param friendPk Friend public key diff --git a/src/persistence/history.h b/src/persistence/history.h index fe0a74739..a8d3883cb 100644 --- a/src/persistence/history.h +++ b/src/persistence/history.h @@ -149,6 +149,12 @@ public: uint count; }; + struct DateIdx + { + QDate date; + size_t numMessagesIn; + }; + public: explicit History(std::shared_ptr db); ~History(); @@ -173,8 +179,14 @@ public: const QDateTime& to); QList getChatHistoryDefaultNum(const QString& friendPk); QList getChatHistoryCounts(const ToxPk& friendPk, const QDate& from, const QDate& to); + size_t getNumMessagesForFriend(const ToxPk& friendPk); + size_t getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const QDateTime& date); + QList getMessagesForFriend(const ToxPk& friendPk, size_t firstIdx, size_t lastIdx); + QList getUnsentMessagesForFriend(const ToxPk& friendPk); QDateTime getDateWhereFindPhrase(const QString& friendPk, const QDateTime& from, QString phrase, const ParameterSearch& parameter); + QList getNumMessagesForFriendBeforeDateBoundaries(const ToxPk& friendPk, + const QDate& from, size_t maxNum); QDateTime getStartDateChatHistory(const QString& friendPk); void markAsSent(RowId messageId);