mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
Merge pull request #5703
Mick Sayson (8): refactor(messages): Create class to manage sending/receiving friend messages from core refactor(core): Remove length parameter from splitMessages refactor(messages): Create class to manage sending/receiving group messages from core fix(messages): Fix broken sanitized name for notifications/alert messages refactor(messages): Replace QRegExp with QRegularExpression refactor(chatlog): Add class to manage underlying chatlog state refactor(chatlog): Add a class to manage history through the IChatLog interface refactor(chatform): Remove message handling logic from gui path
This commit is contained in:
commit
033f28e67e
|
@ -324,8 +324,19 @@ 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
|
||||
src/model/message.cpp
|
||||
src/model/imessagedispatcher.h
|
||||
src/model/friendmessagedispatcher.h
|
||||
src/model/friendmessagedispatcher.cpp
|
||||
src/model/groupmessagedispatcher.h
|
||||
src/model/groupmessagedispatcher.cpp
|
||||
src/model/message.h
|
||||
src/model/message.cpp
|
||||
src/model/groupinvite.cpp
|
||||
src/model/groupinvite.h
|
||||
src/model/group.cpp
|
||||
|
@ -337,6 +348,11 @@ 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/model/chathistory.h
|
||||
src/model/chathistory.cpp
|
||||
src/net/bootstrapnodeupdater.cpp
|
||||
src/net/bootstrapnodeupdater.h
|
||||
src/net/avatarbroadcaster.cpp
|
||||
|
|
|
@ -28,6 +28,10 @@ auto_test(net bsu)
|
|||
auto_test(persistence paths)
|
||||
auto_test(persistence dbschema)
|
||||
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)
|
||||
|
|
|
@ -88,7 +88,7 @@ ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QSt
|
|||
authorFont.setBold(true);
|
||||
|
||||
QColor color = Style::getColor(Style::MainText);
|
||||
if (colorizeName && Settings::getInstance().getEnableGroupChatsColor()) {
|
||||
if (colorizeName) {
|
||||
QByteArray hash = QCryptographicHash::hash((sender.toUtf8()), QCryptographicHash::Sha256);
|
||||
quint8 *data = (quint8*)hash.data();
|
||||
|
||||
|
|
|
@ -88,20 +88,6 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file)
|
|||
|
||||
CoreFile* coreFile = Core::getInstance()->getCoreFile();
|
||||
|
||||
connect(coreFile, &CoreFile::fileTransferInfo, this,
|
||||
&FileTransferWidget::onFileTransferInfo);
|
||||
connect(coreFile, &CoreFile::fileTransferAccepted, this,
|
||||
&FileTransferWidget::onFileTransferAccepted);
|
||||
connect(coreFile, &CoreFile::fileTransferCancelled, this,
|
||||
&FileTransferWidget::onFileTransferCancelled);
|
||||
connect(coreFile, &CoreFile::fileTransferPaused, this,
|
||||
&FileTransferWidget::onFileTransferPaused);
|
||||
connect(coreFile, &CoreFile::fileTransferFinished, this,
|
||||
&FileTransferWidget::onFileTransferFinished);
|
||||
connect(coreFile, &CoreFile::fileTransferRemotePausedUnpaused, this,
|
||||
&FileTransferWidget::fileTransferRemotePausedUnpaused);
|
||||
connect(coreFile, &CoreFile::fileTransferBrokenUnbroken, this,
|
||||
&FileTransferWidget::fileTransferBrokenUnbroken);
|
||||
connect(ui->leftButton, &QPushButton::clicked, this, &FileTransferWidget::onLeftButtonClicked);
|
||||
connect(ui->rightButton, &QPushButton::clicked, this, &FileTransferWidget::onRightButtonClicked);
|
||||
connect(ui->previewButton, &QPushButton::clicked, this,
|
||||
|
@ -133,30 +119,9 @@ bool FileTransferWidget::tryRemoveFile(const QString& filepath)
|
|||
return writable;
|
||||
}
|
||||
|
||||
void FileTransferWidget::autoAcceptTransfer(const QString& path)
|
||||
void FileTransferWidget::onFileTransferUpdate(ToxFile file)
|
||||
{
|
||||
QString filepath;
|
||||
int number = 0;
|
||||
|
||||
QString suffix = QFileInfo(fileInfo.fileName).completeSuffix();
|
||||
QString base = QFileInfo(fileInfo.fileName).baseName();
|
||||
|
||||
do {
|
||||
filepath = QString("%1/%2%3.%4")
|
||||
.arg(path, base,
|
||||
number > 0 ? QString(" (%1)").arg(QString::number(number)) : QString(),
|
||||
suffix);
|
||||
++number;
|
||||
} while (QFileInfo(filepath).exists());
|
||||
|
||||
// Do not automatically accept the file-transfer if the path is not writable.
|
||||
// The user can still accept it manually.
|
||||
if (tryRemoveFile(filepath)) {
|
||||
CoreFile* coreFile = Core::getInstance()->getCoreFile();
|
||||
coreFile->acceptFileRecvRequest(fileInfo.friendId, fileInfo.fileNum, filepath);
|
||||
} else {
|
||||
qWarning() << "Cannot write to " << filepath;
|
||||
}
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
bool FileTransferWidget::isActive() const
|
||||
|
@ -265,53 +230,6 @@ void FileTransferWidget::paintEvent(QPaintEvent*)
|
|||
}
|
||||
}
|
||||
|
||||
void FileTransferWidget::onFileTransferInfo(ToxFile file)
|
||||
{
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::onFileTransferAccepted(ToxFile file)
|
||||
{
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::onFileTransferCancelled(ToxFile file)
|
||||
{
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::onFileTransferPaused(ToxFile file)
|
||||
{
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::onFileTransferResumed(ToxFile file)
|
||||
{
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::onFileTransferFinished(ToxFile file)
|
||||
{
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::fileTransferRemotePausedUnpaused(ToxFile file, bool paused)
|
||||
{
|
||||
if (paused) {
|
||||
onFileTransferPaused(file);
|
||||
} else {
|
||||
onFileTransferResumed(file);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferWidget::fileTransferBrokenUnbroken(ToxFile file, bool broken)
|
||||
{
|
||||
// TODO: Handle broken transfer differently once we have resuming code
|
||||
if (broken) {
|
||||
onFileTransferCancelled(file);
|
||||
}
|
||||
}
|
||||
|
||||
QString FileTransferWidget::getHumanReadableSize(qint64 size)
|
||||
{
|
||||
static const char* suffix[] = {"B", "kiB", "MiB", "GiB", "TiB"};
|
||||
|
@ -737,9 +655,7 @@ void FileTransferWidget::applyTransformation(const int orientation, QImage& imag
|
|||
|
||||
void FileTransferWidget::updateWidget(ToxFile const& file)
|
||||
{
|
||||
if (fileInfo != file) {
|
||||
return;
|
||||
}
|
||||
assert(file == fileInfo);
|
||||
|
||||
fileInfo = file;
|
||||
|
||||
|
|
|
@ -42,19 +42,10 @@ class FileTransferWidget : public QWidget
|
|||
public:
|
||||
explicit FileTransferWidget(QWidget* parent, ToxFile file);
|
||||
virtual ~FileTransferWidget();
|
||||
void autoAcceptTransfer(const QString& path);
|
||||
bool isActive() const;
|
||||
static QString getHumanReadableSize(qint64 size);
|
||||
|
||||
protected slots:
|
||||
void onFileTransferInfo(ToxFile file);
|
||||
void onFileTransferAccepted(ToxFile file);
|
||||
void onFileTransferCancelled(ToxFile file);
|
||||
void onFileTransferPaused(ToxFile file);
|
||||
void onFileTransferResumed(ToxFile file);
|
||||
void onFileTransferFinished(ToxFile file);
|
||||
void fileTransferRemotePausedUnpaused(ToxFile file, bool paused);
|
||||
void fileTransferBrokenUnbroken(ToxFile file, bool broken);
|
||||
void onFileTransferUpdate(ToxFile file);
|
||||
|
||||
protected:
|
||||
void updateWidgetColor(ToxFile const& file);
|
||||
|
|
|
@ -44,8 +44,6 @@
|
|||
|
||||
const QString Core::TOX_EXT = ".tox";
|
||||
|
||||
#define MAX_GROUP_MESSAGE_LEN 1024
|
||||
|
||||
#define ASSERT_CORE_THREAD assert(QThread::currentThread() == coreThread.get())
|
||||
|
||||
namespace {
|
||||
|
@ -715,17 +713,21 @@ void Core::sendGroupMessageWithType(int groupId, const QString& message, Tox_Mes
|
|||
{
|
||||
QMutexLocker ml{&coreLoopLock};
|
||||
|
||||
QStringList cMessages = splitMessage(message, MAX_GROUP_MESSAGE_LEN);
|
||||
int size = message.toUtf8().size();
|
||||
auto maxSize = tox_max_message_length();
|
||||
if (size > maxSize) {
|
||||
qCritical() << "Core::sendMessageWithType called with message of size:" << size
|
||||
<< "when max is:" << maxSize << ". Ignoring.";
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& part : cMessages) {
|
||||
ToxString cMsg(part);
|
||||
Tox_Err_Conference_Send_Message error;
|
||||
bool ok =
|
||||
tox_conference_send_message(tox.get(), groupId, type, cMsg.data(), cMsg.size(), &error);
|
||||
if (!ok || !parseConferenceSendMessageError(error)) {
|
||||
emit groupSentFailed(groupId);
|
||||
return;
|
||||
}
|
||||
ToxString cMsg(message);
|
||||
Tox_Err_Conference_Send_Message error;
|
||||
bool ok =
|
||||
tox_conference_send_message(tox.get(), groupId, type, cMsg.data(), cMsg.size(), &error);
|
||||
if (!ok || !parseConferenceSendMessageError(error)) {
|
||||
emit groupSentFailed(groupId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1434,11 +1436,13 @@ QString Core::getFriendUsername(uint32_t friendnumber) const
|
|||
return sname.getQString();
|
||||
}
|
||||
|
||||
QStringList Core::splitMessage(const QString& message, int maxLen)
|
||||
QStringList Core::splitMessage(const QString& message)
|
||||
{
|
||||
QStringList splittedMsgs;
|
||||
QByteArray ba_message{message.toUtf8()};
|
||||
|
||||
const auto maxLen = tox_max_message_length();
|
||||
|
||||
while (ba_message.size() > maxLen) {
|
||||
int splitPos = ba_message.lastIndexOf('\n', maxLen - 1);
|
||||
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
|
||||
#include "groupid.h"
|
||||
#include "icorefriendmessagesender.h"
|
||||
#include "icoregroupmessagesender.h"
|
||||
#include "icoregroupquery.h"
|
||||
#include "icoreidhandler.h"
|
||||
#include "receiptnum.h"
|
||||
#include "toxfile.h"
|
||||
#include "toxid.h"
|
||||
|
@ -50,7 +53,11 @@ class Core;
|
|||
|
||||
using ToxCorePtr = std::unique_ptr<Core>;
|
||||
|
||||
class Core : public QObject, public ICoreFriendMessageSender
|
||||
class Core : public QObject,
|
||||
public ICoreFriendMessageSender,
|
||||
public ICoreIdHandler,
|
||||
public ICoreGroupMessageSender,
|
||||
public ICoreGroupQuery
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
@ -71,15 +78,15 @@ public:
|
|||
~Core();
|
||||
|
||||
static const QString TOX_EXT;
|
||||
static QStringList splitMessage(const QString& message, int maxLen);
|
||||
static QStringList splitMessage(const QString& message);
|
||||
QString getPeerName(const ToxPk& id) const;
|
||||
QVector<uint32_t> getFriendList() const;
|
||||
GroupId getGroupPersistentId(uint32_t groupNumber) const;
|
||||
uint32_t getGroupNumberPeers(int groupId) const;
|
||||
QString getGroupPeerName(int groupId, int peerId) const;
|
||||
ToxPk getGroupPeerPk(int groupId, int peerId) const;
|
||||
QStringList getGroupPeerNames(int groupId) const;
|
||||
bool getGroupAvEnabled(int groupId) const;
|
||||
GroupId getGroupPersistentId(uint32_t groupNumber) const override;
|
||||
uint32_t getGroupNumberPeers(int groupId) const override;
|
||||
QString getGroupPeerName(int groupId, int peerId) const override;
|
||||
ToxPk getGroupPeerPk(int groupId, int peerId) const override;
|
||||
QStringList getGroupPeerNames(int groupId) const override;
|
||||
bool getGroupAvEnabled(int groupId) const override;
|
||||
ToxPk getFriendPublicKey(uint32_t friendNumber) const;
|
||||
QString getFriendUsername(uint32_t friendNumber) const;
|
||||
|
||||
|
@ -88,11 +95,11 @@ public:
|
|||
uint32_t joinGroupchat(const GroupInvite& inviteInfo);
|
||||
void quitGroupChat(int groupId) const;
|
||||
|
||||
QString getUsername() const;
|
||||
QString getUsername() const override;
|
||||
Status::Status getStatus() const;
|
||||
QString getStatusMessage() const;
|
||||
ToxId getSelfId() const;
|
||||
ToxPk getSelfPublicKey() const;
|
||||
ToxId getSelfId() const override;
|
||||
ToxPk getSelfPublicKey() const override;
|
||||
QPair<QByteArray, QByteArray> getKeypair() const;
|
||||
|
||||
void sendFile(uint32_t friendId, QString filename, QString filePath, long long filesize);
|
||||
|
@ -115,8 +122,8 @@ public slots:
|
|||
void setStatusMessage(const QString& message);
|
||||
|
||||
bool sendMessage(uint32_t friendId, const QString& message, ReceiptNum& receipt) override;
|
||||
void sendGroupMessage(int groupId, const QString& message);
|
||||
void sendGroupAction(int groupId, const QString& message);
|
||||
void sendGroupMessage(int groupId, const QString& message) override;
|
||||
void sendGroupAction(int groupId, const QString& message) override;
|
||||
void changeGroupTitle(int groupId, const QString& title);
|
||||
bool sendAction(uint32_t friendId, const QString& action, ReceiptNum& receipt) override;
|
||||
void sendTyping(uint32_t friendId, bool typing);
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
class ICoreFriendMessageSender
|
||||
{
|
||||
public:
|
||||
virtual ~ICoreFriendMessageSender() = default;
|
||||
virtual bool sendAction(uint32_t friendId, const QString& action, ReceiptNum& receipt) = 0;
|
||||
virtual bool sendMessage(uint32_t friendId, const QString& message, ReceiptNum& receipt) = 0;
|
||||
};
|
||||
|
|
33
src/core/icoregroupmessagesender.h
Normal file
33
src/core/icoregroupmessagesender.h
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
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 ICORE_GROUP_MESSAGE_SENDER_H
|
||||
#define ICORE_GROUP_MESSAGE_SENDER_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
class ICoreGroupMessageSender
|
||||
{
|
||||
public:
|
||||
virtual ~ICoreGroupMessageSender() = default;
|
||||
virtual void sendGroupAction(int groupId, const QString& message) = 0;
|
||||
virtual void sendGroupMessage(int groupId, const QString& message) = 0;
|
||||
};
|
||||
|
||||
#endif /*ICORE_GROUP_MESSAGE_SENDER_H*/
|
44
src/core/icoregroupquery.h
Normal file
44
src/core/icoregroupquery.h
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
Copyright (C) 2013 by Maxim Biro <nurupo.contributions@gmail.com>
|
||||
Copyright © 2014-2018 by The qTox Project Contributors
|
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox.
|
||||
|
||||
This program is free 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 ICORE_GROUP_QUERY_H
|
||||
#define ICORE_GROUP_QUERY_H
|
||||
|
||||
#include "groupid.h"
|
||||
#include "toxpk.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class ICoreGroupQuery
|
||||
{
|
||||
public:
|
||||
virtual ~ICoreGroupQuery() = default;
|
||||
virtual GroupId getGroupPersistentId(uint32_t groupNumber) const = 0;
|
||||
virtual uint32_t getGroupNumberPeers(int groupId) const = 0;
|
||||
virtual QString getGroupPeerName(int groupId, int peerId) const = 0;
|
||||
virtual ToxPk getGroupPeerPk(int groupId, int peerId) const = 0;
|
||||
virtual QStringList getGroupPeerNames(int groupId) const = 0;
|
||||
virtual bool getGroupAvEnabled(int groupId) const = 0;
|
||||
};
|
||||
|
||||
#endif /*ICORE_GROUP_QUERY_H*/
|
37
src/core/icoreidhandler.h
Normal file
37
src/core/icoreidhandler.h
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
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 ICORE_ID_HANDLER_H
|
||||
#define ICORE_ID_HANDLER_H
|
||||
|
||||
#include "toxid.h"
|
||||
#include "toxpk.h"
|
||||
|
||||
class ICoreIdHandler
|
||||
{
|
||||
|
||||
public:
|
||||
virtual ~ICoreIdHandler() = default;
|
||||
virtual ToxId getSelfId() const = 0;
|
||||
virtual ToxPk getSelfPublicKey() const = 0;
|
||||
virtual QString getUsername() const = 0;
|
||||
};
|
||||
|
||||
|
||||
#endif /*ICORE_ID_HANDLER_H*/
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
#include "grouplist.h"
|
||||
#include "src/core/core.h"
|
||||
#include "src/model/group.h"
|
||||
#include <QDebug>
|
||||
#include <QHash>
|
||||
|
@ -31,7 +32,10 @@ Group* GroupList::addGroup(int groupNum, const GroupId& groupId, const QString&
|
|||
if (checker != groupList.end())
|
||||
qWarning() << "addGroup: groupId already taken";
|
||||
|
||||
Group* newGroup = new Group(groupNum, groupId, name, isAvGroupchat, selfName);
|
||||
// TODO: Core instance is bad but grouplist is also an instance so we can
|
||||
// deal with this later
|
||||
auto core = Core::getInstance();
|
||||
Group* newGroup = new Group(groupNum, groupId, name, isAvGroupchat, selfName, *core, *core);
|
||||
groupList[groupId] = newGroup;
|
||||
id2key[groupNum] = groupId;
|
||||
return newGroup;
|
||||
|
|
|
@ -116,7 +116,7 @@ bool AboutFriend::isHistoryExistence()
|
|||
History* const history = Nexus::getProfile()->getHistory();
|
||||
if (history) {
|
||||
const ToxPk pk = f->getPublicKey();
|
||||
return history->isHistoryExistence(pk.toString());
|
||||
return history->historyExists(pk);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
460
src/model/chathistory.cpp
Normal file
460
src/model/chathistory.cpp
Normal file
|
@ -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();
|
||||
}
|
79
src/model/chathistory.h
Normal file
79
src/model/chathistory.h
Normal file
|
@ -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*/
|
136
src/model/chatlogitem.cpp
Normal file
136
src/model/chatlogitem.cpp
Normal 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
75
src/model/chatlogitem.h
Normal 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*/
|
123
src/model/friendmessagedispatcher.cpp
Normal file
123
src/model/friendmessagedispatcher.cpp
Normal file
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
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 "friendmessagedispatcher.h"
|
||||
#include "src/persistence/settings.h"
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Sends message to friend using messageSender
|
||||
* @param[in] messageSender
|
||||
* @param[in] f
|
||||
* @param[in] message
|
||||
* @param[out] receipt
|
||||
*/
|
||||
bool sendMessageToCore(ICoreFriendMessageSender& messageSender, const Friend& f,
|
||||
const Message& message, ReceiptNum& receipt)
|
||||
{
|
||||
uint32_t friendId = f.getId();
|
||||
|
||||
auto sendFn = message.isAction ? std::mem_fn(&ICoreFriendMessageSender::sendAction)
|
||||
: std::mem_fn(&ICoreFriendMessageSender::sendMessage);
|
||||
|
||||
return sendFn(messageSender, friendId, message.content, receipt);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
FriendMessageDispatcher::FriendMessageDispatcher(Friend& f_, MessageProcessor processor_,
|
||||
ICoreFriendMessageSender& messageSender_)
|
||||
: f(f_)
|
||||
, messageSender(messageSender_)
|
||||
, offlineMsgEngine(&f_, &messageSender_)
|
||||
, processor(std::move(processor_))
|
||||
{
|
||||
connect(&f, &Friend::statusChanged, this, &FriendMessageDispatcher::onFriendStatusChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see IMessageSender::sendMessage
|
||||
*/
|
||||
std::pair<DispatchedMessageId, DispatchedMessageId>
|
||||
FriendMessageDispatcher::sendMessage(bool isAction, const QString& content)
|
||||
{
|
||||
const auto firstId = nextMessageId;
|
||||
auto lastId = nextMessageId;
|
||||
for (const auto& message : processor.processOutgoingMessage(isAction, content)) {
|
||||
auto messageId = nextMessageId++;
|
||||
lastId = messageId;
|
||||
auto onOfflineMsgComplete = [this, messageId] { emit this->messageComplete(messageId); };
|
||||
|
||||
ReceiptNum receipt;
|
||||
|
||||
bool messageSent = false;
|
||||
|
||||
if (f.isOnline()) {
|
||||
messageSent = sendMessageToCore(messageSender, f, message, receipt);
|
||||
}
|
||||
|
||||
if (!messageSent) {
|
||||
offlineMsgEngine.addUnsentMessage(message, onOfflineMsgComplete);
|
||||
} else {
|
||||
offlineMsgEngine.addSentMessage(receipt, message, onOfflineMsgComplete);
|
||||
}
|
||||
|
||||
emit this->messageSent(messageId, message);
|
||||
}
|
||||
return std::make_pair(firstId, lastId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles received message from toxcore
|
||||
* @param[in] isAction True if action message
|
||||
* @param[in] content Unprocessed toxcore message
|
||||
*/
|
||||
void FriendMessageDispatcher::onMessageReceived(bool isAction, const QString& content)
|
||||
{
|
||||
emit this->messageReceived(f.getPublicKey(), processor.processIncomingMessage(isAction, content));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles received receipt from toxcore
|
||||
* @param[in] receipt receipt id
|
||||
*/
|
||||
void FriendMessageDispatcher::onReceiptReceived(ReceiptNum receipt)
|
||||
{
|
||||
offlineMsgEngine.onReceiptReceived(receipt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles status change for friend
|
||||
* @note Parameters just to fit slot api
|
||||
*/
|
||||
void FriendMessageDispatcher::onFriendStatusChange(const ToxPk&, Status::Status)
|
||||
{
|
||||
if (f.isOnline()) {
|
||||
offlineMsgEngine.deliverOfflineMsgs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clears all currently outgoing messages
|
||||
*/
|
||||
void FriendMessageDispatcher::clearOutgoingMessages()
|
||||
{
|
||||
offlineMsgEngine.removeAllMessages();
|
||||
}
|
59
src/model/friendmessagedispatcher.h
Normal file
59
src/model/friendmessagedispatcher.h
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
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 FRIEND_MESSAGE_DISPATCHER_H
|
||||
#define FRIEND_MESSAGE_DISPATCHER_H
|
||||
|
||||
#include "src/core/icorefriendmessagesender.h"
|
||||
#include "src/model/friend.h"
|
||||
#include "src/model/imessagedispatcher.h"
|
||||
#include "src/model/message.h"
|
||||
#include "src/persistence/offlinemsgengine.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class FriendMessageDispatcher : public IMessageDispatcher
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FriendMessageDispatcher(Friend& f, MessageProcessor processor,
|
||||
ICoreFriendMessageSender& messageSender);
|
||||
|
||||
std::pair<DispatchedMessageId, DispatchedMessageId> sendMessage(bool isAction,
|
||||
const QString& content) override;
|
||||
void onMessageReceived(bool isAction, const QString& content);
|
||||
void onReceiptReceived(ReceiptNum receipt);
|
||||
void clearOutgoingMessages();
|
||||
private slots:
|
||||
void onFriendStatusChange(const ToxPk& key, Status::Status status);
|
||||
|
||||
private:
|
||||
Friend& f;
|
||||
DispatchedMessageId nextMessageId = DispatchedMessageId(0);
|
||||
|
||||
ICoreFriendMessageSender& messageSender;
|
||||
OfflineMsgEngine offlineMsgEngine;
|
||||
MessageProcessor processor;
|
||||
};
|
||||
|
||||
|
||||
#endif /* IMESSAGE_DISPATCHER_H */
|
|
@ -33,12 +33,14 @@
|
|||
static const int MAX_GROUP_TITLE_LENGTH = 128;
|
||||
|
||||
Group::Group(int groupId, const GroupId persistentGroupId, const QString& name, bool isAvGroupchat,
|
||||
const QString& selfName)
|
||||
const QString& selfName, ICoreGroupQuery& groupQuery, ICoreIdHandler& idHandler)
|
||||
: selfName{selfName}
|
||||
, title{name}
|
||||
, toxGroupNum(groupId)
|
||||
, groupId{persistentGroupId}
|
||||
, avGroupchat{isAvGroupchat}
|
||||
, groupQuery(groupQuery)
|
||||
, idHandler(idHandler)
|
||||
{
|
||||
// in groupchats, we only notify on messages containing your name <-- dumb
|
||||
// sound notifications should be on all messages, but system popup notification
|
||||
|
@ -88,15 +90,14 @@ void Group::regeneratePeerList()
|
|||
// receive the name changed signal a little later, we will emit userJoined before we have their
|
||||
// username, using just their ToxPk, then shortly after emit another peerNameChanged signal.
|
||||
// This can cause double-updated to UI and chatlog, but is unavoidable given the API of toxcore.
|
||||
const Core* core = Core::getInstance();
|
||||
QStringList peers = core->getGroupPeerNames(toxGroupNum);
|
||||
QStringList peers = groupQuery.getGroupPeerNames(toxGroupNum);
|
||||
const auto oldPeerNames = peerDisplayNames;
|
||||
peerDisplayNames.clear();
|
||||
const int nPeers = peers.size();
|
||||
for (int i = 0; i < nPeers; ++i) {
|
||||
const auto pk = core->getGroupPeerPk(toxGroupNum, i);
|
||||
if (pk == core->getSelfPublicKey()) {
|
||||
peerDisplayNames[pk] = core->getUsername();
|
||||
const auto pk = groupQuery.getGroupPeerPk(toxGroupNum, i);
|
||||
if (pk == idHandler.getSelfPublicKey()) {
|
||||
peerDisplayNames[pk] = idHandler.getUsername();
|
||||
} else {
|
||||
peerDisplayNames[pk] = FriendList::decideNickname(pk, peers[i]);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
|
||||
#include "src/core/contactid.h"
|
||||
#include "src/core/groupid.h"
|
||||
#include "src/core/icoregroupquery.h"
|
||||
#include "src/core/icoreidhandler.h"
|
||||
#include "src/core/toxpk.h"
|
||||
|
||||
#include <QMap>
|
||||
|
@ -34,7 +36,8 @@ class Group : public Contact
|
|||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Group(int groupId, const GroupId persistentGroupId, const QString& name, bool isAvGroupchat, const QString& selfName);
|
||||
Group(int groupId, const GroupId persistentGroupId, const QString& name, bool isAvGroupchat,
|
||||
const QString& selfName, ICoreGroupQuery& groupQuery, ICoreIdHandler& idHandler);
|
||||
bool isAvGroupchat() const;
|
||||
uint32_t getId() const override;
|
||||
const GroupId& getPersistentId() const override;
|
||||
|
@ -70,6 +73,8 @@ private:
|
|||
void stopAudioOfDepartedPeers(const ToxPk& peerPk);
|
||||
|
||||
private:
|
||||
ICoreGroupQuery& groupQuery;
|
||||
ICoreIdHandler& idHandler;
|
||||
QString selfName;
|
||||
QString title;
|
||||
QMap<ToxPk, QString> peerDisplayNames;
|
||||
|
|
88
src/model/groupmessagedispatcher.cpp
Normal file
88
src/model/groupmessagedispatcher.cpp
Normal 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/>.
|
||||
*/
|
||||
|
||||
#include "groupmessagedispatcher.h"
|
||||
#include "src/persistence/igroupsettings.h"
|
||||
|
||||
#include <QtCore>
|
||||
|
||||
GroupMessageDispatcher::GroupMessageDispatcher(Group& g_, MessageProcessor processor_,
|
||||
ICoreIdHandler& idHandler_,
|
||||
ICoreGroupMessageSender& messageSender_,
|
||||
const IGroupSettings& groupSettings_)
|
||||
: group(g_)
|
||||
, processor(processor_)
|
||||
, idHandler(idHandler_)
|
||||
, messageSender(messageSender_)
|
||||
, groupSettings(groupSettings_)
|
||||
{
|
||||
processor.enableMentions();
|
||||
}
|
||||
|
||||
std::pair<DispatchedMessageId, DispatchedMessageId>
|
||||
GroupMessageDispatcher::sendMessage(bool isAction, QString const& content)
|
||||
{
|
||||
const auto firstMessageId = nextMessageId;
|
||||
auto lastMessageId = firstMessageId;
|
||||
|
||||
for (auto const& message : processor.processOutgoingMessage(isAction, content)) {
|
||||
auto messageId = nextMessageId++;
|
||||
lastMessageId = messageId;
|
||||
if (group.getPeersCount() != 1) {
|
||||
if (message.isAction) {
|
||||
messageSender.sendGroupAction(group.getId(), message.content);
|
||||
} else {
|
||||
messageSender.sendGroupMessage(group.getId(), message.content);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit both signals since we do not have receipts for groups
|
||||
//
|
||||
// NOTE: We could in theory keep track of our sent message and wait for
|
||||
// toxcore to send it back to us to indicate a completed message, but
|
||||
// this isn't necessarily the design of toxcore and associating the
|
||||
// received message back would be difficult.
|
||||
emit this->messageSent(messageId, message);
|
||||
emit this->messageComplete(messageId);
|
||||
}
|
||||
|
||||
return std::make_pair(firstMessageId, lastMessageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Processes and dispatches received message from toxcore
|
||||
* @param[in] sender
|
||||
* @param[in] isAction True if is action
|
||||
* @param[in] content Message content
|
||||
*/
|
||||
void GroupMessageDispatcher::onMessageReceived(const ToxPk& sender, bool isAction, QString const& content)
|
||||
{
|
||||
bool isSelf = sender == idHandler.getSelfPublicKey();
|
||||
|
||||
if (isSelf) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupSettings.getBlackList().contains(sender.toString())) {
|
||||
qDebug() << "onGroupMessageReceived: Filtered:" << sender.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
emit messageReceived(sender, processor.processIncomingMessage(isAction, content));
|
||||
}
|
58
src/model/groupmessagedispatcher.h
Normal file
58
src/model/groupmessagedispatcher.h
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
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 GROUP_MESSAGE_DISPATCHER_H
|
||||
#define GROUP_MESSAGE_DISPATCHER_H
|
||||
|
||||
#include "src/core/icoregroupmessagesender.h"
|
||||
#include "src/core/icoreidhandler.h"
|
||||
#include "src/model/group.h"
|
||||
#include "src/model/imessagedispatcher.h"
|
||||
#include "src/model/message.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class IGroupSettings;
|
||||
|
||||
class GroupMessageDispatcher : public IMessageDispatcher
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
GroupMessageDispatcher(Group& group, MessageProcessor processor, ICoreIdHandler& idHandler,
|
||||
ICoreGroupMessageSender& messageSender,
|
||||
const IGroupSettings& groupSettings);
|
||||
|
||||
std::pair<DispatchedMessageId, DispatchedMessageId> sendMessage(bool isAction,
|
||||
QString const& content) override;
|
||||
void onMessageReceived(ToxPk const& sender, bool isAction, QString const& content);
|
||||
|
||||
private:
|
||||
Group& group;
|
||||
MessageProcessor processor;
|
||||
ICoreIdHandler& idHandler;
|
||||
ICoreGroupMessageSender& messageSender;
|
||||
const IGroupSettings& groupSettings;
|
||||
DispatchedMessageId nextMessageId{0};
|
||||
};
|
||||
|
||||
|
||||
#endif /* IMESSAGE_DISPATCHER_H */
|
145
src/model/ichatlog.h
Normal file
145
src/model/ichatlog.h
Normal 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*/
|
68
src/model/imessagedispatcher.h
Normal file
68
src/model/imessagedispatcher.h
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
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 IMESSAGE_DISPATCHER_H
|
||||
#define IMESSAGE_DISPATCHER_H
|
||||
|
||||
#include "src/model/friend.h"
|
||||
#include "src/model/message.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
using DispatchedMessageId = NamedType<size_t, struct SentMessageIdTag, Orderable, Incrementable>;
|
||||
Q_DECLARE_METATYPE(DispatchedMessageId);
|
||||
|
||||
class IMessageDispatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
virtual ~IMessageDispatcher() = default;
|
||||
|
||||
/**
|
||||
* @brief Sends message to associated chat
|
||||
* @param[in] isAction True if is action message
|
||||
* @param[in] content Message content
|
||||
* @return Pair of first and last dispatched message IDs
|
||||
*/
|
||||
virtual std::pair<DispatchedMessageId, DispatchedMessageId>
|
||||
sendMessage(bool isAction, const QString& content) = 0;
|
||||
signals:
|
||||
/**
|
||||
* @brief Emitted when a message is received and processed
|
||||
*/
|
||||
void messageReceived(const ToxPk& sender, const Message& message);
|
||||
|
||||
/**
|
||||
* @brief Emitted when a message is processed and sent
|
||||
* @param id message id for completion
|
||||
* @param message sent message
|
||||
*/
|
||||
void messageSent(DispatchedMessageId id, const Message& message);
|
||||
|
||||
/**
|
||||
* @brief Emitted when a receiver report is received from the associated chat
|
||||
* @param id Id of message that is completed
|
||||
*/
|
||||
void messageComplete(DispatchedMessageId id);
|
||||
};
|
||||
|
||||
#endif /* IMESSAGE_DISPATCHER_H */
|
94
src/model/message.cpp
Normal file
94
src/model/message.cpp
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
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 "message.h"
|
||||
#include "friend.h"
|
||||
#include "src/core/core.h"
|
||||
|
||||
void MessageProcessor::SharedParams::onUserNameSet(const QString& username)
|
||||
{
|
||||
QString sanename = username;
|
||||
sanename.remove(QRegularExpression("[\\t\\n\\v\\f\\r\\x0000]"));
|
||||
nameMention = QRegularExpression("\\b" + QRegularExpression::escape(username) + "\\b",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
sanitizedNameMention = QRegularExpression("\\b" + QRegularExpression::escape(sanename) + "\\b",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
|
||||
MessageProcessor::MessageProcessor(const MessageProcessor::SharedParams& sharedParams)
|
||||
: sharedParams(sharedParams)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @brief Converts an outgoing message into one (or many) sanitized Message(s)
|
||||
*/
|
||||
std::vector<Message> MessageProcessor::processOutgoingMessage(bool isAction, QString const& content)
|
||||
{
|
||||
std::vector<Message> ret;
|
||||
|
||||
QStringList splitMsgs = Core::splitMessage(content);
|
||||
ret.reserve(splitMsgs.size());
|
||||
|
||||
QDateTime timestamp = QDateTime::currentDateTime();
|
||||
std::transform(splitMsgs.begin(), splitMsgs.end(), std::back_inserter(ret),
|
||||
[&](const QString& part) {
|
||||
Message message;
|
||||
message.isAction = isAction;
|
||||
message.content = part;
|
||||
message.timestamp = timestamp;
|
||||
return message;
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Converts an incoming message into a sanitized Message
|
||||
*/
|
||||
Message MessageProcessor::processIncomingMessage(bool isAction, QString const& message)
|
||||
{
|
||||
QDateTime timestamp = QDateTime::currentDateTime();
|
||||
auto ret = Message{};
|
||||
ret.isAction = isAction;
|
||||
ret.content = message;
|
||||
ret.timestamp = timestamp;
|
||||
|
||||
if (detectingMentions) {
|
||||
auto nameMention = sharedParams.GetNameMention();
|
||||
auto sanitizedNameMention = sharedParams.GetSanitizedNameMention();
|
||||
|
||||
for (auto const& mention : {nameMention, sanitizedNameMention}) {
|
||||
auto matchIt = mention.globalMatch(ret.content);
|
||||
if (!matchIt.hasNext()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto match = matchIt.next();
|
||||
|
||||
auto pos = static_cast<size_t>(match.capturedStart());
|
||||
auto length = static_cast<size_t>(match.capturedLength());
|
||||
|
||||
ret.metadata.push_back({MessageMetadataType::selfMention, pos, pos + length});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
|
@ -21,13 +21,92 @@
|
|||
#define MESSAGE_H
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
|
||||
#include <vector>
|
||||
|
||||
class Friend;
|
||||
|
||||
// NOTE: This could be extended in the future to handle all text processing (see
|
||||
// ChatMessage::createChatMessage)
|
||||
enum class MessageMetadataType
|
||||
{
|
||||
selfMention,
|
||||
};
|
||||
|
||||
// May need to be extended in the future to have a more varianty type (imagine
|
||||
// if we wanted to add message replies and shoved a reply id in here)
|
||||
struct MessageMetadata
|
||||
{
|
||||
MessageMetadataType type;
|
||||
// Indicates start position within a Message::content
|
||||
size_t start;
|
||||
// Indicates end position within a Message::content
|
||||
size_t end;
|
||||
};
|
||||
|
||||
struct Message
|
||||
{
|
||||
bool isAction;
|
||||
QString content;
|
||||
QDateTime timestamp;
|
||||
std::vector<MessageMetadata> metadata;
|
||||
};
|
||||
|
||||
|
||||
class MessageProcessor
|
||||
{
|
||||
|
||||
public:
|
||||
/**
|
||||
* Parameters needed by all message processors. Used to reduce duplication
|
||||
* of expensive data looked at by all message processors
|
||||
*/
|
||||
class SharedParams
|
||||
{
|
||||
|
||||
public:
|
||||
QRegularExpression GetNameMention() const
|
||||
{
|
||||
return nameMention;
|
||||
}
|
||||
QRegularExpression GetSanitizedNameMention() const
|
||||
{
|
||||
return sanitizedNameMention;
|
||||
}
|
||||
void onUserNameSet(const QString& username);
|
||||
|
||||
private:
|
||||
QRegularExpression nameMention;
|
||||
QRegularExpression sanitizedNameMention;
|
||||
};
|
||||
|
||||
MessageProcessor(const SharedParams& sharedParams);
|
||||
|
||||
std::vector<Message> processOutgoingMessage(bool isAction, QString const& content);
|
||||
|
||||
Message processIncomingMessage(bool isAction, QString const& message);
|
||||
|
||||
/**
|
||||
* @brief Enables mention detection in the processor
|
||||
*/
|
||||
inline void enableMentions()
|
||||
{
|
||||
detectingMentions = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Disables mention detection in the processor
|
||||
*/
|
||||
inline void disableMentions()
|
||||
{
|
||||
detectingMentions = false;
|
||||
};
|
||||
|
||||
private:
|
||||
bool detectingMentions = false;
|
||||
const SharedParams& sharedParams;
|
||||
};
|
||||
|
||||
#endif /*MESSAGE_H*/
|
||||
|
|
419
src/model/sessionchatlog.cpp
Normal file
419
src/model/sessionchatlog.cpp
Normal 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);
|
||||
}
|
88
src/model/sessionchatlog.h
Normal file
88
src/model/sessionchatlog.h
Normal 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*/
|
|
@ -208,6 +208,7 @@ void Nexus::showMainGUI()
|
|||
connect(core, &Core::friendStatusMessageChanged, widget, &Widget::onFriendStatusMessageChanged);
|
||||
connect(core, &Core::friendRequestReceived, widget, &Widget::onFriendRequestReceived);
|
||||
connect(core, &Core::friendMessageReceived, widget, &Widget::onFriendMessageReceived);
|
||||
connect(core, &Core::receiptRecieved, widget, &Widget::onReceiptReceived);
|
||||
connect(core, &Core::groupInviteReceived, widget, &Widget::onGroupInviteReceived);
|
||||
connect(core, &Core::groupMessageReceived, widget, &Widget::onGroupMessageReceived);
|
||||
connect(core, &Core::groupPeerlistChanged, widget, &Widget::onGroupPeerlistChanged);
|
||||
|
|
|
@ -224,9 +224,9 @@ bool History::isValid()
|
|||
* @param friendPk
|
||||
* @return True if has, false otherwise.
|
||||
*/
|
||||
bool History::isHistoryExistence(const QString& friendPk)
|
||||
bool History::historyExists(const ToxPk& friendPk)
|
||||
{
|
||||
return !getChatHistoryDefaultNum(friendPk).isEmpty();
|
||||
return !getMessagesForFriend(friendPk, 0, 1).empty();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -520,76 +520,121 @@ 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.
|
||||
* @param from Start of period to fetch.
|
||||
* @param to End of period to fetch.
|
||||
* @return List of messages.
|
||||
*/
|
||||
QList<History::HistMessage> History::getChatHistoryFromDate(const QString& friendPk,
|
||||
const QDateTime& from, const QDateTime& to)
|
||||
|
||||
size_t History::getNumMessagesForFriend(const ToxPk& friendPk)
|
||||
{
|
||||
if (!isValid()) {
|
||||
return {};
|
||||
}
|
||||
return getChatHistory(friendPk, from, to, 0);
|
||||
return getNumMessagesForFriendBeforeDate(friendPk,
|
||||
// Maximum possible time
|
||||
QDateTime::fromMSecsSinceEpoch(
|
||||
std::numeric_limits<int64_t>::max()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fetches the latest set amount of messages from the database.
|
||||
* @param friendPk Friend public key to fetch.
|
||||
* @return List of messages.
|
||||
*/
|
||||
QList<History::HistMessage> History::getChatHistoryDefaultNum(const QString& friendPk)
|
||||
size_t History::getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const QDateTime& date)
|
||||
{
|
||||
if (!isValid()) {
|
||||
return {};
|
||||
}
|
||||
return getChatHistory(friendPk, QDateTime::fromMSecsSinceEpoch(0), QDateTime::currentDateTime(),
|
||||
NUM_MESSAGES_DEFAULT);
|
||||
}
|
||||
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());
|
||||
|
||||
|
||||
/**
|
||||
* @brief Fetches chat messages counts for each day from the database.
|
||||
* @param friendPk Friend public key to fetch.
|
||||
* @param from Start of period to fetch.
|
||||
* @param to End of period to fetch.
|
||||
* @return List of structs containing days offset and message count for that day.
|
||||
*/
|
||||
QList<History::DateMessages> History::getChatHistoryCounts(const ToxPk& friendPk, const QDate& from,
|
||||
const QDate& to)
|
||||
{
|
||||
if (!isValid()) {
|
||||
return {};
|
||||
}
|
||||
QDateTime fromTime(from);
|
||||
QDateTime toTime(to);
|
||||
|
||||
QList<DateMessages> counts;
|
||||
|
||||
auto rowCallback = [&counts](const QVector<QVariant>& row) {
|
||||
DateMessages app;
|
||||
app.count = row[0].toUInt();
|
||||
app.offsetDays = row[1].toUInt();
|
||||
counts.append(app);
|
||||
size_t numMessages = 0;
|
||||
auto rowCallback = [&numMessages](const QVector<QVariant>& row) {
|
||||
numMessages = row[0].toLongLong();
|
||||
};
|
||||
|
||||
QString queryText =
|
||||
QString("SELECT COUNT(history.id), ((timestamp / 1000 / 60 / 60 / 24) - %4 ) AS day "
|
||||
"FROM history "
|
||||
"JOIN peers chat ON chat_id = chat.id "
|
||||
"WHERE timestamp BETWEEN %1 AND %2 AND chat.public_key='%3'"
|
||||
"GROUP BY day;")
|
||||
.arg(fromTime.toMSecsSinceEpoch())
|
||||
.arg(toTime.toMSecsSinceEpoch())
|
||||
.arg(friendPk.toString())
|
||||
.arg(QDateTime::fromMSecsSinceEpoch(0).daysTo(fromTime));
|
||||
|
||||
db->execNow({queryText, rowCallback});
|
||||
|
||||
return counts;
|
||||
return numMessages;
|
||||
}
|
||||
|
||||
QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk, size_t firstIdx,
|
||||
size_t lastIdx)
|
||||
{
|
||||
QList<HistMessage> 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<QVariant>& 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<ToxFile::FileDirection>(row[11].toLongLong());
|
||||
file.status = static_cast<ToxFile::FileStatus>(row[12].toInt());
|
||||
messages +=
|
||||
{id, isOfflineMessage, timestamp, friend_key, display_name, sender_key, file};
|
||||
}
|
||||
};
|
||||
|
||||
db->execNow({queryText, rowCallback});
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
QList<History::HistMessage> 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<History::HistMessage> ret;
|
||||
auto rowCallback = [&ret](const QVector<QVariant>& 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -682,28 +727,62 @@ QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTi
|
|||
}
|
||||
|
||||
/**
|
||||
* @brief get start date of correspondence
|
||||
* @param friendPk Friend public key
|
||||
* @return start date of correspondence
|
||||
* @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.
|
||||
*/
|
||||
QDateTime History::getStartDateChatHistory(const QString& friendPk)
|
||||
QList<History::DateIdx> History::getNumMessagesForFriendBeforeDateBoundaries(const ToxPk& friendPk,
|
||||
const QDate& from,
|
||||
size_t maxNum)
|
||||
{
|
||||
QDateTime result;
|
||||
auto rowCallback = [&result](const QVector<QVariant>& row) {
|
||||
result = QDateTime::fromMSecsSinceEpoch(row[0].toLongLong());
|
||||
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<DateIdx> dateIdxs;
|
||||
auto rowCallback = [&dateIdxs](const QVector<QVariant>& row) {
|
||||
DateIdx dateIdx;
|
||||
dateIdx.numMessagesIn = row[0].toLongLong();
|
||||
dateIdx.date =
|
||||
QDateTime::fromMSecsSinceEpoch(row[1].toLongLong() * 24 * 60 * 60 * 1000).date();
|
||||
dateIdxs.append(dateIdx);
|
||||
};
|
||||
|
||||
QString queryText =
|
||||
QStringLiteral("SELECT timestamp "
|
||||
"FROM history "
|
||||
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
||||
"JOIN peers chat ON chat_id = chat.id "
|
||||
"WHERE chat.public_key='%1' ORDER BY timestamp ASC LIMIT 1;")
|
||||
.arg(friendPk);
|
||||
db->execNow({queryString, rowCallback});
|
||||
|
||||
db->execNow({queryText, rowCallback});
|
||||
|
||||
return result;
|
||||
return dateIdxs;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -719,74 +798,4 @@ void History::markAsSent(RowId messageId)
|
|||
}
|
||||
|
||||
db->execLater(QString("DELETE FROM faux_offline_pending WHERE id=%1;").arg(messageId.get()));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Fetches chat messages from the database.
|
||||
* @param friendPk Friend publick key to fetch.
|
||||
* @param from Start of period to fetch.
|
||||
* @param to End of period to fetch.
|
||||
* @param numMessages max number of messages to fetch.
|
||||
* @return List of messages.
|
||||
*/
|
||||
QList<History::HistMessage> History::getChatHistory(const QString& friendPk, const QDateTime& from,
|
||||
const QDateTime& to, int numMessages)
|
||||
{
|
||||
QList<HistMessage> messages;
|
||||
|
||||
auto rowCallback = [&messages](const QVector<QVariant>& 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<ToxFile::FileDirection>(row[11].toLongLong());
|
||||
file.status = static_cast<ToxFile::FileStatus>(row[12].toInt());
|
||||
messages +=
|
||||
{id, isOfflineMessage, timestamp, friend_key, display_name, sender_key, file};
|
||||
}
|
||||
};
|
||||
|
||||
// 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 timestamp BETWEEN %1 AND %2 AND chat.public_key='%3'")
|
||||
.arg(from.toMSecsSinceEpoch())
|
||||
.arg(to.toMSecsSinceEpoch())
|
||||
.arg(friendPk);
|
||||
if (numMessages) {
|
||||
queryText =
|
||||
"SELECT * FROM (" + queryText
|
||||
+ QString(" ORDER BY history.id DESC limit %1) AS T1 ORDER BY T1.id ASC;").arg(numMessages);
|
||||
} else {
|
||||
queryText = queryText + ";";
|
||||
}
|
||||
|
||||
db->execNow({queryText, rowCallback});
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
|
@ -143,10 +143,10 @@ public:
|
|||
HistMessageContent content;
|
||||
};
|
||||
|
||||
struct DateMessages
|
||||
struct DateIdx
|
||||
{
|
||||
uint offsetDays;
|
||||
uint count;
|
||||
QDate date;
|
||||
size_t numMessagesIn;
|
||||
};
|
||||
|
||||
public:
|
||||
|
@ -155,7 +155,7 @@ public:
|
|||
|
||||
bool isValid();
|
||||
|
||||
bool isHistoryExistence(const QString& friendPk);
|
||||
bool historyExists(const ToxPk& friendPk);
|
||||
|
||||
void eraseHistory();
|
||||
void removeFriendHistory(const QString& friendPk);
|
||||
|
@ -168,14 +168,14 @@ public:
|
|||
const QString& sender, const QDateTime& time, QString const& dispName);
|
||||
|
||||
void setFileFinished(const QString& fileId, bool success, const QString& filePath, const QByteArray& fileHash);
|
||||
|
||||
QList<HistMessage> getChatHistoryFromDate(const QString& friendPk, const QDateTime& from,
|
||||
const QDateTime& to);
|
||||
QList<HistMessage> getChatHistoryDefaultNum(const QString& friendPk);
|
||||
QList<DateMessages> 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<HistMessage> getMessagesForFriend(const ToxPk& friendPk, size_t firstIdx, size_t lastIdx);
|
||||
QList<HistMessage> getUnsentMessagesForFriend(const ToxPk& friendPk);
|
||||
QDateTime getDateWhereFindPhrase(const QString& friendPk, const QDateTime& from, QString phrase,
|
||||
const ParameterSearch& parameter);
|
||||
QDateTime getStartDateChatHistory(const QString& friendPk);
|
||||
QList<DateIdx> getNumMessagesForFriendBeforeDateBoundaries(const ToxPk& friendPk,
|
||||
const QDate& from, size_t maxNum);
|
||||
|
||||
void markAsSent(RowId messageId);
|
||||
|
||||
|
@ -194,9 +194,6 @@ private slots:
|
|||
void onFileInserted(RowId dbId, QString fileId);
|
||||
|
||||
private:
|
||||
QList<HistMessage> getChatHistory(const QString& friendPk, const QDateTime& from,
|
||||
const QDateTime& to, int numMessages);
|
||||
|
||||
static RawDatabase::Query generateFileFinished(RowId fileId, bool success,
|
||||
const QString& filePath, const QByteArray& fileHash);
|
||||
std::shared_ptr<RawDatabase> db;
|
||||
|
|
35
src/persistence/igroupsettings.h
Normal file
35
src/persistence/igroupsettings.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright © 2014-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 IGROUP_SETTINGS_H
|
||||
#define IGROUP_SETTINGS_H
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
class IGroupSettings
|
||||
{
|
||||
public:
|
||||
virtual ~IGroupSettings() = default;
|
||||
virtual QStringList getBlackList() const = 0;
|
||||
virtual void setBlackList(const QStringList& blist) = 0;
|
||||
virtual bool getGroupAlwaysNotify() const = 0;
|
||||
virtual void setGroupAlwaysNotify(bool newValue) = 0;
|
||||
};
|
||||
|
||||
#endif /*IGROUP_SETTINGS_H*/
|
|
@ -853,8 +853,6 @@ QString Profile::setPassword(const QString& newPassword)
|
|||
"password.");
|
||||
}
|
||||
|
||||
Nexus::getDesktopGUI()->reloadHistory();
|
||||
|
||||
QByteArray avatar = loadAvatarData(core->getSelfId().getPublicKey());
|
||||
saveAvatar(core->getSelfId().getPublicKey(), avatar);
|
||||
|
||||
|
|
|
@ -2331,7 +2331,10 @@ void Settings::setAutoLogin(bool state)
|
|||
void Settings::setEnableGroupChatsColor(bool state)
|
||||
{
|
||||
QMutexLocker locker{&bigLock};
|
||||
nameColors = state;
|
||||
if (state != nameColors) {
|
||||
nameColors = state;
|
||||
emit nameColorsChanged(nameColors);
|
||||
}
|
||||
}
|
||||
|
||||
bool Settings::getEnableGroupChatsColor() const
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include "src/core/toxencrypt.h"
|
||||
#include "src/core/toxfile.h"
|
||||
#include "src/persistence/ifriendsettings.h"
|
||||
#include "src/persistence/igroupsettings.h"
|
||||
#include "src/video/ivideosettings.h"
|
||||
|
||||
#include <QDateTime>
|
||||
|
@ -46,6 +47,7 @@ enum class syncType;
|
|||
class Settings : public QObject,
|
||||
public ICoreSettings,
|
||||
public IFriendSettings,
|
||||
public IGroupSettings,
|
||||
public IAudioSettings,
|
||||
public IVideoSettings
|
||||
{
|
||||
|
@ -199,6 +201,7 @@ signals:
|
|||
|
||||
// GUI
|
||||
void autoLoginChanged(bool enabled);
|
||||
void nameColorsChanged(bool enabled);
|
||||
void separateWindowChanged(bool enabled);
|
||||
void showSystemTrayChanged(bool enabled);
|
||||
bool minimizeOnCloseChanged(bool enabled);
|
||||
|
@ -343,8 +346,8 @@ public:
|
|||
bool getBusySound() const;
|
||||
void setBusySound(bool newValue);
|
||||
|
||||
bool getGroupAlwaysNotify() const;
|
||||
void setGroupAlwaysNotify(bool newValue);
|
||||
bool getGroupAlwaysNotify() const override;
|
||||
void setGroupAlwaysNotify(bool newValue) override;
|
||||
|
||||
QString getInDev() const override;
|
||||
void setInDev(const QString& deviceSpecifier) override;
|
||||
|
@ -476,8 +479,8 @@ public:
|
|||
// Privacy
|
||||
bool getTypingNotification() const;
|
||||
void setTypingNotification(bool enabled);
|
||||
QStringList getBlackList() const;
|
||||
void setBlackList(const QStringList& blist);
|
||||
QStringList getBlackList() const override;
|
||||
void setBlackList(const QStringList& blist) override;
|
||||
|
||||
// State
|
||||
QByteArray getWindowGeometry() const;
|
||||
|
|
|
@ -28,23 +28,69 @@ 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++()
|
||||
{
|
||||
auto& underlying = static_cast<T&>(*this).get();
|
||||
++underlying;
|
||||
return static_cast<T&>(*this);
|
||||
}
|
||||
|
||||
T operator++(int)
|
||||
{
|
||||
auto ret = T(static_cast<T const&>(*this));
|
||||
++(*this);
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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(); }
|
||||
|
@ -64,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_; }
|
||||
|
@ -76,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);
|
||||
|
|
|
@ -102,36 +102,11 @@ namespace
|
|||
|
||||
return cD + res.sprintf("%02ds", seconds);
|
||||
}
|
||||
|
||||
void completeMessage(ChatMessage::Ptr ma, RowId rowId)
|
||||
{
|
||||
auto profile = Nexus::getProfile();
|
||||
if (profile->isHistoryEnabled()) {
|
||||
profile->getHistory()->markAsSent(rowId);
|
||||
}
|
||||
|
||||
// force execution on the gui thread
|
||||
QTimer::singleShot(0, QCoreApplication::instance(), [ma] {
|
||||
ma->markAsSent(QDateTime::currentDateTime());
|
||||
});
|
||||
}
|
||||
|
||||
struct CompleteMessageFunctor
|
||||
{
|
||||
void operator()() const
|
||||
{
|
||||
completeMessage(ma, rowId);
|
||||
}
|
||||
|
||||
ChatMessage::Ptr ma;
|
||||
RowId rowId;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
ChatForm::ChatForm(Friend* chatFriend, History* history)
|
||||
: GenericChatForm(chatFriend)
|
||||
ChatForm::ChatForm(Friend* chatFriend, IChatLog& chatLog, IMessageDispatcher& messageDispatcher)
|
||||
: GenericChatForm(chatFriend, chatLog, messageDispatcher)
|
||||
, f(chatFriend)
|
||||
, history{history}
|
||||
, isTyping{false}
|
||||
, lastCallIsVideo{false}
|
||||
{
|
||||
|
@ -146,8 +121,6 @@ ChatForm::ChatForm(Friend* chatFriend, History* history)
|
|||
statusMessageLabel->setTextFormat(Qt::PlainText);
|
||||
statusMessageLabel->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
|
||||
offlineEngine = new OfflineMsgEngine(f, Core::getInstance());
|
||||
|
||||
typingTimer.setSingleShot(true);
|
||||
|
||||
callDurationTimer = nullptr;
|
||||
|
@ -161,29 +134,18 @@ ChatForm::ChatForm(Friend* chatFriend, History* history)
|
|||
headWidget->addWidget(callDuration, 1, Qt::AlignCenter);
|
||||
callDuration->hide();
|
||||
|
||||
loadHistoryAction = menu.addAction(QString(), this, SLOT(onLoadHistory()));
|
||||
copyStatusAction = statusMessageMenu.addAction(QString(), this, SLOT(onCopyStatusMessage()));
|
||||
|
||||
exportChatAction =
|
||||
menu.addAction(QIcon::fromTheme("document-save"), QString(), this, SLOT(onExportChat()));
|
||||
|
||||
const Core* core = Core::getInstance();
|
||||
const Profile* profile = Nexus::getProfile();
|
||||
const CoreFile* coreFile = core->getCoreFile();
|
||||
connect(coreFile, &CoreFile::fileReceiveRequested, this, &ChatForm::onFileRecvRequest);
|
||||
connect(profile, &Profile::friendAvatarChanged, this, &ChatForm::onAvatarChanged);
|
||||
connect(coreFile, &CoreFile::fileSendStarted, this, &ChatForm::startFileSend);
|
||||
connect(coreFile, &CoreFile::fileTransferFinished, this, &ChatForm::onFileTransferFinished);
|
||||
connect(coreFile, &CoreFile::fileTransferCancelled, this, &ChatForm::onFileTransferCancelled);
|
||||
connect(coreFile, &CoreFile::fileTransferBrokenUnbroken, this, &ChatForm::onFileTransferBrokenUnbroken);
|
||||
connect(coreFile, &CoreFile::fileSendFailed, this, &ChatForm::onFileSendFailed);
|
||||
connect(core, &Core::receiptRecieved, this, &ChatForm::onReceiptReceived);
|
||||
connect(core, &Core::friendMessageReceived, this, &ChatForm::onFriendMessageReceived);
|
||||
connect(coreFile, &CoreFile::fileReceiveRequested, this, &ChatForm::updateFriendActivityForFile);
|
||||
connect(coreFile, &CoreFile::fileSendStarted, this, &ChatForm::updateFriendActivityForFile);
|
||||
connect(core, &Core::friendTypingChanged, this, &ChatForm::onFriendTypingChanged);
|
||||
connect(core, &Core::friendStatusChanged, this, &ChatForm::onFriendStatusChanged);
|
||||
connect(coreFile, &CoreFile::fileNameChanged, this, &ChatForm::onFileNameChanged);
|
||||
|
||||
|
||||
const CoreAV* av = core->getAv();
|
||||
connect(av, &CoreAV::avInvite, this, &ChatForm::onAvInvite);
|
||||
connect(av, &CoreAV::avStart, this, &ChatForm::onAvStart);
|
||||
|
@ -194,7 +156,8 @@ ChatForm::ChatForm(Friend* chatFriend, History* history)
|
|||
connect(headWidget, &ChatFormHeader::micMuteToggle, this, &ChatForm::onMicMuteToggle);
|
||||
connect(headWidget, &ChatFormHeader::volMuteToggle, this, &ChatForm::onVolMuteToggle);
|
||||
|
||||
connect(msgEdit, &ChatTextEdit::enterPressed, this, &ChatForm::onSendTriggered);
|
||||
connect(sendButton, &QPushButton::pressed, this, &ChatForm::updateFriendActivity);
|
||||
connect(msgEdit, &ChatTextEdit::enterPressed, this, &ChatForm::updateFriendActivity);
|
||||
connect(msgEdit, &ChatTextEdit::textChanged, this, &ChatForm::onTextEditChanged);
|
||||
connect(msgEdit, &ChatTextEdit::pasteImage, this, &ChatForm::sendImage);
|
||||
connect(statusMessageLabel, &CroppingLabel::customContextMenuRequested, this,
|
||||
|
@ -218,9 +181,6 @@ ChatForm::ChatForm(Friend* chatFriend, History* history)
|
|||
connect(headWidget, &ChatFormHeader::callRejected, this, &ChatForm::onRejectCallTriggered);
|
||||
|
||||
updateCallButtons();
|
||||
if (Nexus::getProfile()->isHistoryEnabled()) {
|
||||
loadHistoryDefaultNum(true);
|
||||
}
|
||||
|
||||
setAcceptDrops(true);
|
||||
retranslateUi();
|
||||
|
@ -232,7 +192,6 @@ ChatForm::~ChatForm()
|
|||
Translator::unregister(this);
|
||||
delete netcam;
|
||||
netcam = nullptr;
|
||||
delete offlineEngine;
|
||||
}
|
||||
|
||||
void ChatForm::setStatusMessage(const QString& newMessage)
|
||||
|
@ -242,11 +201,22 @@ void ChatForm::setStatusMessage(const QString& newMessage)
|
|||
statusMessageLabel->setToolTip(Qt::convertFromPlainText(newMessage, Qt::WhiteSpaceNormal));
|
||||
}
|
||||
|
||||
void ChatForm::onSendTriggered()
|
||||
void ChatForm::updateFriendActivity()
|
||||
{
|
||||
SendMessageStr(msgEdit->toPlainText());
|
||||
msgEdit->clear();
|
||||
// TODO: Remove Widget::getInstance()
|
||||
Widget::getInstance()->updateFriendActivity(f);
|
||||
}
|
||||
|
||||
void ChatForm::updateFriendActivityForFile(const ToxFile& file)
|
||||
{
|
||||
if (file.friendId != f->getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Remove Widget::getInstance()
|
||||
Widget::getInstance()->updateFriendActivity(f);
|
||||
}
|
||||
|
||||
void ChatForm::onFileNameChanged(const ToxPk& friendPk)
|
||||
{
|
||||
if (friendPk != f->getPublicKey()) {
|
||||
|
@ -311,101 +281,6 @@ void ChatForm::onAttachClicked()
|
|||
}
|
||||
}
|
||||
|
||||
void ChatForm::startFileSend(ToxFile file)
|
||||
{
|
||||
if (file.friendId != f->getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString name;
|
||||
const Core* core = Core::getInstance();
|
||||
ToxPk self = core->getSelfId().getPublicKey();
|
||||
if (previousId != self) {
|
||||
name = core->getUsername();
|
||||
previousId = self;
|
||||
}
|
||||
|
||||
insertChatMessage(
|
||||
ChatMessage::createFileTransferMessage(name, file, true, QDateTime::currentDateTime()));
|
||||
|
||||
if (history && Settings::getInstance().getEnableLogging()) {
|
||||
auto selfPk = Core::getInstance()->getSelfId().toString();
|
||||
auto pk = f->getPublicKey().toString();
|
||||
auto name = Core::getInstance()->getUsername();
|
||||
history->addNewFileMessage(pk, file.resumeFileId, file.fileName, file.filePath,
|
||||
file.filesize, selfPk, QDateTime::currentDateTime(), name);
|
||||
}
|
||||
|
||||
Widget::getInstance()->updateFriendActivity(f);
|
||||
}
|
||||
|
||||
void ChatForm::onFileTransferFinished(ToxFile file)
|
||||
{
|
||||
history->setFileFinished(file.resumeFileId, true, file.filePath, file.hashGenerator->result());
|
||||
}
|
||||
|
||||
void ChatForm::onFileTransferBrokenUnbroken(ToxFile file, bool broken)
|
||||
{
|
||||
if (broken) {
|
||||
history->setFileFinished(file.resumeFileId, false, file.filePath, file.hashGenerator->result());
|
||||
}
|
||||
}
|
||||
|
||||
void ChatForm::onFileTransferCancelled(ToxFile file)
|
||||
{
|
||||
history->setFileFinished(file.resumeFileId, false, file.filePath, file.hashGenerator->result());
|
||||
}
|
||||
|
||||
void ChatForm::onFileRecvRequest(ToxFile file)
|
||||
{
|
||||
if (file.friendId != f->getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Widget::getInstance()->newFriendMessageAlert(f->getPublicKey(),
|
||||
file.fileName +
|
||||
" (" + FileTransferWidget::getHumanReadableSize(file.filesize) + ")",
|
||||
true, true);
|
||||
QString name;
|
||||
ToxPk friendId = f->getPublicKey();
|
||||
if (friendId != previousId) {
|
||||
name = f->getDisplayedName();
|
||||
previousId = friendId;
|
||||
}
|
||||
|
||||
ChatMessage::Ptr msg =
|
||||
ChatMessage::createFileTransferMessage(name, file, false, QDateTime::currentDateTime());
|
||||
|
||||
insertChatMessage(msg);
|
||||
|
||||
if (history && Settings::getInstance().getEnableLogging()) {
|
||||
auto pk = f->getPublicKey().toString();
|
||||
auto name = f->getDisplayedName();
|
||||
history->addNewFileMessage(pk, file.resumeFileId, file.fileName, file.filePath,
|
||||
file.filesize, pk, QDateTime::currentDateTime(), name);
|
||||
}
|
||||
ChatLineContentProxy* proxy = static_cast<ChatLineContentProxy*>(msg->getContent(1));
|
||||
|
||||
assert(proxy->getWidgetType() == ChatLineContentProxy::FileTransferWidgetType);
|
||||
FileTransferWidget* tfWidget = static_cast<FileTransferWidget*>(proxy->getWidget());
|
||||
|
||||
const Settings& settings = Settings::getInstance();
|
||||
QString autoAcceptDir = settings.getAutoAcceptDir(f->getPublicKey());
|
||||
|
||||
if (autoAcceptDir.isEmpty() && settings.getAutoSaveEnabled()) {
|
||||
autoAcceptDir = settings.getGlobalAutoAcceptDir();
|
||||
}
|
||||
|
||||
auto maxAutoAcceptSize = settings.getMaxAutoAcceptSize();
|
||||
bool autoAcceptSizeCheckPassed = maxAutoAcceptSize == 0 || maxAutoAcceptSize >= file.filesize;
|
||||
|
||||
if (!autoAcceptDir.isEmpty() && autoAcceptSizeCheckPassed) {
|
||||
tfWidget->autoAcceptTransfer(autoAcceptDir);
|
||||
}
|
||||
|
||||
Widget::getInstance()->updateFriendActivity(f);
|
||||
}
|
||||
|
||||
void ChatForm::onAvInvite(uint32_t friendId, bool video)
|
||||
{
|
||||
if (friendId != f->getId()) {
|
||||
|
@ -554,95 +429,6 @@ void ChatForm::onVolMuteToggle()
|
|||
updateMuteVolButton();
|
||||
}
|
||||
|
||||
void ChatForm::searchInBegin(const QString& phrase, const ParameterSearch& parameter)
|
||||
{
|
||||
disableSearchText();
|
||||
|
||||
searchPoint = QPoint(1, -1);
|
||||
|
||||
const bool isFirst = (parameter.period == PeriodSearch::WithTheFirst);
|
||||
const bool isAfter = (parameter.period == PeriodSearch::AfterDate);
|
||||
if (isFirst || isAfter) {
|
||||
if (isFirst || (isAfter && parameter.date < getFirstTime().date())) {
|
||||
const QString pk = f->getPublicKey().toString();
|
||||
if ((isFirst || parameter.date >= history->getStartDateChatHistory(pk).date())
|
||||
&& loadHistory(phrase, parameter)) {
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSearchDown(phrase, parameter);
|
||||
} else {
|
||||
if (parameter.period == PeriodSearch::BeforeDate && parameter.date < getFirstTime().date()) {
|
||||
const QString pk = f->getPublicKey().toString();
|
||||
if (parameter.date >= history->getStartDateChatHistory(pk).date()
|
||||
&& loadHistory(phrase, parameter)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSearchUp(phrase, parameter);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatForm::onSearchUp(const QString& phrase, const ParameterSearch& parameter)
|
||||
{
|
||||
if (phrase.isEmpty()) {
|
||||
disableSearchText();
|
||||
}
|
||||
|
||||
QVector<ChatLine::Ptr> lines = chatWidget->getLines();
|
||||
int numLines = lines.size();
|
||||
|
||||
int startLine;
|
||||
|
||||
if (searchAfterLoadHistory) {
|
||||
startLine = 1;
|
||||
searchAfterLoadHistory = false;
|
||||
} else {
|
||||
startLine = numLines - searchPoint.x();
|
||||
}
|
||||
|
||||
if (startLine == 0 && loadHistory(phrase, parameter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool isSearch = searchInText(phrase, parameter, SearchDirection::Up);
|
||||
|
||||
if (!isSearch) {
|
||||
const QString pk = f->getPublicKey().toString();
|
||||
const QDateTime newBaseDate =
|
||||
history->getDateWhereFindPhrase(pk, earliestMessage, phrase, parameter);
|
||||
|
||||
if (!newBaseDate.isValid()) {
|
||||
emit messageNotFoundShow(SearchDirection::Up);
|
||||
return;
|
||||
}
|
||||
|
||||
searchPoint.setX(numLines);
|
||||
searchAfterLoadHistory = true;
|
||||
loadHistoryByDateRange(newBaseDate);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatForm::onSearchDown(const QString& phrase, const ParameterSearch& parameter)
|
||||
{
|
||||
if (!searchInText(phrase, parameter, SearchDirection::Down)) {
|
||||
emit messageNotFoundShow(SearchDirection::Down);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatForm::onFileSendFailed(uint32_t friendId, const QString& fname)
|
||||
{
|
||||
if (friendId != f->getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
addSystemInfoMessage(tr("Failed to send file \"%1\"").arg(fname), ChatMessage::ERROR,
|
||||
QDateTime::currentDateTime());
|
||||
}
|
||||
|
||||
void ChatForm::onFriendStatusChanged(uint32_t friendId, Status::Status status)
|
||||
{
|
||||
// Disable call buttons if friend is offline
|
||||
|
@ -653,8 +439,6 @@ void ChatForm::onFriendStatusChanged(uint32_t friendId, Status::Status status)
|
|||
if (!f->isOnline()) {
|
||||
// Hide the "is typing" message when a friend goes offline
|
||||
setFriendTyping(false);
|
||||
} else {
|
||||
offlineEngine->deliverOfflineMsgs();
|
||||
}
|
||||
|
||||
updateCallButtons();
|
||||
|
@ -682,16 +466,6 @@ void ChatForm::onFriendNameChanged(const QString& name)
|
|||
}
|
||||
}
|
||||
|
||||
void ChatForm::onFriendMessageReceived(quint32 friendId, const QString& message, bool isAction)
|
||||
{
|
||||
if (friendId != f->getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QDateTime timestamp = QDateTime::currentDateTime();
|
||||
addMessage(f->getPublicKey(), message, timestamp, isAction);
|
||||
}
|
||||
|
||||
void ChatForm::onStatusMessage(const QString& message)
|
||||
{
|
||||
if (sender() == f) {
|
||||
|
@ -699,13 +473,6 @@ void ChatForm::onStatusMessage(const QString& message)
|
|||
}
|
||||
}
|
||||
|
||||
void ChatForm::onReceiptReceived(quint32 friendId, ReceiptNum receipt)
|
||||
{
|
||||
if (friendId == f->getId()) {
|
||||
offlineEngine->onReceiptReceived(receipt);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatForm::onAvatarChanged(const ToxPk& friendPk, const QPixmap& pic)
|
||||
{
|
||||
if (friendPk != f->getPublicKey()) {
|
||||
|
@ -751,7 +518,8 @@ void ChatForm::dropEvent(QDropEvent* ev)
|
|||
QString urlString = url.toString();
|
||||
if (url.isValid() && !url.isLocalFile()
|
||||
&& urlString.length() < static_cast<int>(tox_max_message_length())) {
|
||||
SendMessageStr(urlString);
|
||||
messageDispatcher.sendMessage(false, urlString);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -783,190 +551,6 @@ void ChatForm::dropEvent(QDropEvent* ev)
|
|||
void ChatForm::clearChatArea()
|
||||
{
|
||||
GenericChatForm::clearChatArea(/* confirm = */ false, /* inform = */ true);
|
||||
offlineEngine->removeAllMessages();
|
||||
}
|
||||
|
||||
QString getMsgAuthorDispName(const ToxPk& authorPk, const QString& dispName)
|
||||
{
|
||||
QString authorStr;
|
||||
const Core* core = Core::getInstance();
|
||||
bool isSelf = authorPk == core->getSelfId().getPublicKey();
|
||||
|
||||
if (!dispName.isEmpty()) {
|
||||
authorStr = dispName;
|
||||
} else if (isSelf) {
|
||||
authorStr = core->getUsername();
|
||||
} else {
|
||||
authorStr = ChatForm::resolveToxPk(authorPk);
|
||||
}
|
||||
return authorStr;
|
||||
}
|
||||
|
||||
void ChatForm::loadHistoryDefaultNum(bool processUndelivered)
|
||||
{
|
||||
const QString pk = f->getPublicKey().toString();
|
||||
QList<History::HistMessage> msgs = history->getChatHistoryDefaultNum(pk);
|
||||
if (!msgs.isEmpty()) {
|
||||
earliestMessage = msgs.first().timestamp;
|
||||
}
|
||||
handleLoadedMessages(msgs, processUndelivered);
|
||||
}
|
||||
|
||||
void ChatForm::loadHistoryByDateRange(const QDateTime& since, bool processUndelivered)
|
||||
{
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
if (since > now) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!earliestMessage.isNull()) {
|
||||
if (earliestMessage < since) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (earliestMessage < now) {
|
||||
now = earliestMessage;
|
||||
now = now.addMSecs(-1);
|
||||
}
|
||||
}
|
||||
|
||||
QString pk = f->getPublicKey().toString();
|
||||
earliestMessage = since;
|
||||
QList<History::HistMessage> msgs = history->getChatHistoryFromDate(pk, since, now);
|
||||
handleLoadedMessages(msgs, processUndelivered);
|
||||
}
|
||||
|
||||
void ChatForm::handleLoadedMessages(QList<History::HistMessage> newHistMsgs, bool processUndelivered)
|
||||
{
|
||||
ToxPk prevIdBackup = previousId;
|
||||
previousId = ToxPk{};
|
||||
QList<ChatLine::Ptr> chatLines;
|
||||
QDate lastDate(1, 0, 0);
|
||||
for (const auto& histMessage : newHistMsgs) {
|
||||
MessageMetadata const metadata = getMessageMetadata(histMessage);
|
||||
lastDate = addDateLineIfNeeded(chatLines, lastDate, histMessage, metadata);
|
||||
auto msg = chatMessageFromHistMessage(histMessage, metadata);
|
||||
|
||||
if (!msg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processUndelivered) {
|
||||
sendLoadedMessage(msg, metadata);
|
||||
}
|
||||
chatLines.append(msg);
|
||||
previousId = metadata.authorPk;
|
||||
prevMsgDateTime = metadata.msgDateTime;
|
||||
}
|
||||
previousId = prevIdBackup;
|
||||
insertChatlines(chatLines);
|
||||
if (searchAfterLoadHistory && chatLines.isEmpty()) {
|
||||
onContinueSearch();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatForm::insertChatlines(QList<ChatLine::Ptr> chatLines)
|
||||
{
|
||||
QScrollBar* verticalBar = chatWidget->verticalScrollBar();
|
||||
int savedSliderPos = verticalBar->maximum() - verticalBar->value();
|
||||
chatWidget->insertChatlinesOnTop(chatLines);
|
||||
savedSliderPos = verticalBar->maximum() - savedSliderPos;
|
||||
verticalBar->setValue(savedSliderPos);
|
||||
}
|
||||
|
||||
QDate ChatForm::addDateLineIfNeeded(QList<ChatLine::Ptr>& msgs, QDate const& lastDate,
|
||||
History::HistMessage const& newMessage,
|
||||
MessageMetadata const& metadata)
|
||||
{
|
||||
// Show the date every new day
|
||||
QDate newDate = metadata.msgDateTime.date();
|
||||
if (newDate > lastDate) {
|
||||
QString dateText = newDate.toString(Settings::getInstance().getDateFormat());
|
||||
auto msg = ChatMessage::createChatInfoMessage(dateText, ChatMessage::INFO, QDateTime());
|
||||
msgs.append(msg);
|
||||
return newDate;
|
||||
}
|
||||
return lastDate;
|
||||
}
|
||||
|
||||
ChatForm::MessageMetadata ChatForm::getMessageMetadata(History::HistMessage const& histMessage)
|
||||
{
|
||||
const ToxPk authorPk = ToxId(histMessage.sender).getPublicKey();
|
||||
const QDateTime msgDateTime = histMessage.timestamp.toLocalTime();
|
||||
const bool isSelf = Core::getInstance()->getSelfId().getPublicKey() == authorPk;
|
||||
const bool needSending = !histMessage.isSent && isSelf;
|
||||
const bool isAction =
|
||||
histMessage.content.getType() == HistMessageContentType::message
|
||||
&& histMessage.content.asMessage().startsWith(ACTION_PREFIX, Qt::CaseInsensitive);
|
||||
const RowId id = histMessage.id;
|
||||
return {isSelf, needSending, isAction, id, authorPk, msgDateTime};
|
||||
}
|
||||
|
||||
ChatMessage::Ptr ChatForm::chatMessageFromHistMessage(History::HistMessage const& histMessage,
|
||||
MessageMetadata const& metadata)
|
||||
{
|
||||
ToxPk authorPk(ToxId(histMessage.sender).getPublicKey());
|
||||
QString authorStr = getMsgAuthorDispName(authorPk, histMessage.dispName);
|
||||
QDateTime dateTime = metadata.needSending ? QDateTime() : metadata.msgDateTime;
|
||||
|
||||
|
||||
ChatMessage::Ptr msg;
|
||||
|
||||
switch (histMessage.content.getType()) {
|
||||
case HistMessageContentType::message: {
|
||||
ChatMessage::MessageType type = metadata.isAction ? ChatMessage::ACTION : ChatMessage::NORMAL;
|
||||
auto& message = histMessage.content.asMessage();
|
||||
QString messageText = metadata.isAction ? message.mid(ACTION_PREFIX.length()) : message;
|
||||
|
||||
msg = ChatMessage::createChatMessage(authorStr, messageText, type, metadata.isSelf, dateTime);
|
||||
break;
|
||||
}
|
||||
case HistMessageContentType::file: {
|
||||
auto& file = histMessage.content.asFile();
|
||||
bool isMe = file.direction == ToxFile::SENDING;
|
||||
msg = ChatMessage::createFileTransferMessage(authorStr, file, isMe, dateTime);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
qCritical() << "Invalid HistMessageContentType";
|
||||
assert(false);
|
||||
}
|
||||
|
||||
if (!metadata.isAction && needsToHideName(authorPk, metadata.msgDateTime)) {
|
||||
msg->hideSender();
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
void ChatForm::sendLoadedMessage(ChatMessage::Ptr chatMsg, MessageMetadata const& metadata)
|
||||
{
|
||||
if (!metadata.needSending) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReceiptNum receipt;
|
||||
bool messageSent{false};
|
||||
QString stringMsg = chatMsg->toString();
|
||||
if (f->isOnline()) {
|
||||
Core* core = Core::getInstance();
|
||||
uint32_t friendId = f->getId();
|
||||
messageSent = metadata.isAction ? core->sendAction(friendId, stringMsg, receipt)
|
||||
: core->sendMessage(friendId, stringMsg, receipt);
|
||||
if (!messageSent) {
|
||||
qWarning() << "Failed to send loaded message, adding to offline messaging";
|
||||
}
|
||||
}
|
||||
|
||||
auto onCompletion = CompleteMessageFunctor{};
|
||||
onCompletion.ma = chatMsg;
|
||||
onCompletion.rowId = metadata.id;
|
||||
|
||||
auto modelMsg = Message{metadata.isAction, stringMsg, QDateTime::currentDateTime()};
|
||||
if (messageSent) {
|
||||
getOfflineMsgEngine()->addSentMessage(receipt, modelMsg, onCompletion);
|
||||
} else {
|
||||
getOfflineMsgEngine()->addUnsentMessage(modelMsg, onCompletion);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatForm::onScreenshotClicked()
|
||||
|
@ -1012,19 +596,6 @@ void ChatForm::sendImage(const QPixmap& pixmap)
|
|||
}
|
||||
}
|
||||
|
||||
void ChatForm::onLoadHistory()
|
||||
{
|
||||
if (!history) {
|
||||
return;
|
||||
}
|
||||
|
||||
LoadHistoryDialog dlg(f->getPublicKey());
|
||||
if (dlg.exec()) {
|
||||
QDateTime fromTime = dlg.getFromDate();
|
||||
loadHistoryByDateRange(fromTime);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatForm::insertChatMessage(ChatMessage::Ptr msg)
|
||||
{
|
||||
GenericChatForm::insertChatMessage(msg);
|
||||
|
@ -1131,107 +702,9 @@ void ChatForm::hideEvent(QHideEvent* event)
|
|||
GenericChatForm::hideEvent(event);
|
||||
}
|
||||
|
||||
OfflineMsgEngine* ChatForm::getOfflineMsgEngine()
|
||||
{
|
||||
return offlineEngine;
|
||||
}
|
||||
|
||||
void ChatForm::SendMessageStr(QString msg)
|
||||
{
|
||||
if (msg.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool isAction = msg.startsWith(ACTION_PREFIX, Qt::CaseInsensitive);
|
||||
if (isAction) {
|
||||
msg.remove(0, ACTION_PREFIX.length());
|
||||
}
|
||||
|
||||
QStringList splittedMsg = Core::splitMessage(msg, tox_max_message_length());
|
||||
QDateTime timestamp = QDateTime::currentDateTime();
|
||||
|
||||
for (const QString& part : splittedMsg) {
|
||||
QString historyPart = part;
|
||||
if (isAction) {
|
||||
historyPart = ACTION_PREFIX + part;
|
||||
}
|
||||
|
||||
ReceiptNum receipt;
|
||||
bool messageSent{false};
|
||||
if (f->isOnline()) {
|
||||
Core* core = Core::getInstance();
|
||||
uint32_t friendId = f->getId();
|
||||
messageSent = isAction ? core->sendAction(friendId, part, receipt) : core->sendMessage(friendId, part, receipt);
|
||||
if (!messageSent) {
|
||||
qCritical() << "Failed to send message, adding to offline messaging";
|
||||
}
|
||||
}
|
||||
|
||||
ChatMessage::Ptr ma = createSelfMessage(part, timestamp, isAction, false);
|
||||
|
||||
Message modelMsg{isAction, part, timestamp};
|
||||
|
||||
|
||||
if (history && Settings::getInstance().getEnableLogging()) {
|
||||
auto* offMsgEngine = getOfflineMsgEngine();
|
||||
QString selfPk = Core::getInstance()->getSelfId().toString();
|
||||
QString pk = f->getPublicKey().toString();
|
||||
QString name = Core::getInstance()->getUsername();
|
||||
bool const isSent = false; // This forces history to add it to the offline messages table
|
||||
|
||||
// Use functor to avoid having to declare a lambda in a lambda
|
||||
CompleteMessageFunctor onCompletion;
|
||||
onCompletion.ma = ma;
|
||||
|
||||
history->addNewMessage(pk, historyPart, selfPk, timestamp, isSent, name,
|
||||
[messageSent, offMsgEngine, receipt, modelMsg,
|
||||
onCompletion](RowId id) mutable {
|
||||
onCompletion.rowId = id;
|
||||
if (messageSent) {
|
||||
offMsgEngine->addSentMessage(receipt, modelMsg,
|
||||
onCompletion);
|
||||
} else {
|
||||
offMsgEngine->addUnsentMessage(modelMsg, onCompletion);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (messageSent) {
|
||||
offlineEngine->addSentMessage(receipt, modelMsg,
|
||||
[ma] { ma->markAsSent(QDateTime::currentDateTime()); });
|
||||
} else {
|
||||
offlineEngine->addUnsentMessage(modelMsg, [ma] {
|
||||
ma->markAsSent(QDateTime::currentDateTime());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// set last message only when sending it
|
||||
msgEdit->setLastMessage(msg);
|
||||
Widget::getInstance()->updateFriendActivity(f);
|
||||
}
|
||||
}
|
||||
|
||||
bool ChatForm::loadHistory(const QString& phrase, const ParameterSearch& parameter)
|
||||
{
|
||||
const QString pk = f->getPublicKey().toString();
|
||||
const QDateTime newBaseDate =
|
||||
history->getDateWhereFindPhrase(pk, earliestMessage, phrase, parameter);
|
||||
|
||||
if (newBaseDate.isValid() && getFirstTime().isValid() && newBaseDate.date() < getFirstTime().date()) {
|
||||
searchAfterLoadHistory = true;
|
||||
loadHistoryByDateRange(newBaseDate);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ChatForm::retranslateUi()
|
||||
{
|
||||
loadHistoryAction->setText(tr("Load chat history..."));
|
||||
copyStatusAction->setText(tr("Copy"));
|
||||
exportChatAction->setText(tr("Export to file"));
|
||||
|
||||
updateMuteMicButton();
|
||||
updateMuteVolButton();
|
||||
|
@ -1240,38 +713,3 @@ void ChatForm::retranslateUi()
|
|||
netcam->setShowMessages(chatWidget->isVisible());
|
||||
}
|
||||
}
|
||||
|
||||
void ChatForm::onExportChat()
|
||||
{
|
||||
QString pk = f->getPublicKey().toString();
|
||||
QDateTime epochStart = QDateTime::fromMSecsSinceEpoch(0);
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
QList<History::HistMessage> msgs = history->getChatHistoryFromDate(pk, epochStart, now);
|
||||
|
||||
QString path = QFileDialog::getSaveFileName(Q_NULLPTR, tr("Save chat log"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString buffer;
|
||||
for (const auto& it : msgs) {
|
||||
if (it.content.getType() != HistMessageContentType::message) {
|
||||
continue;
|
||||
}
|
||||
QString timestamp = it.timestamp.time().toString("hh:mm:ss");
|
||||
QString datestamp = it.timestamp.date().toString("yyyy-MM-dd");
|
||||
ToxPk authorPk(ToxId(it.sender).getPublicKey());
|
||||
QString author = getMsgAuthorDispName(authorPk, it.dispName);
|
||||
|
||||
buffer = buffer
|
||||
% QString{datestamp % '\t' % timestamp % '\t' % author % '\t'
|
||||
% it.content.asMessage() % '\n'};
|
||||
}
|
||||
file.write(buffer.toUtf8());
|
||||
file.close();
|
||||
}
|
||||
|
|
|
@ -27,8 +27,10 @@
|
|||
|
||||
#include "genericchatform.h"
|
||||
#include "src/core/core.h"
|
||||
#include "src/persistence/history.h"
|
||||
#include "src/model/ichatlog.h"
|
||||
#include "src/model/imessagedispatcher.h"
|
||||
#include "src/model/status.h"
|
||||
#include "src/persistence/history.h"
|
||||
#include "src/widget/tool/screenshotgrabber.h"
|
||||
|
||||
class CallConfirmWidget;
|
||||
|
@ -44,15 +46,11 @@ class ChatForm : public GenericChatForm
|
|||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ChatForm(Friend* chatFriend, History* history);
|
||||
ChatForm(Friend* chatFriend, IChatLog& chatLog, IMessageDispatcher& messageDispatcher);
|
||||
~ChatForm();
|
||||
void setStatusMessage(const QString& newMessage);
|
||||
void loadHistoryByDateRange(const QDateTime& since, bool processUndelivered = false);
|
||||
void loadHistoryDefaultNum(bool processUndelivered = false);
|
||||
|
||||
void dischargeReceipt(int receipt);
|
||||
void setFriendTyping(bool isTyping);
|
||||
OfflineMsgEngine* getOfflineMsgEngine();
|
||||
|
||||
virtual void show(ContentLayout* contentLayout) final override;
|
||||
virtual void reloadTheme() final override;
|
||||
|
@ -69,11 +67,6 @@ signals:
|
|||
void acceptCall(uint32_t friendId);
|
||||
|
||||
public slots:
|
||||
void startFileSend(ToxFile file);
|
||||
void onFileTransferFinished(ToxFile file);
|
||||
void onFileTransferCancelled(ToxFile file);
|
||||
void onFileTransferBrokenUnbroken(ToxFile file, bool broken);
|
||||
void onFileRecvRequest(ToxFile file);
|
||||
void onAvInvite(uint32_t friendId, bool video);
|
||||
void onAvStart(uint32_t friendId, bool video);
|
||||
void onAvEnd(uint32_t friendId, bool error);
|
||||
|
@ -81,13 +74,9 @@ public slots:
|
|||
void onFileNameChanged(const ToxPk& friendPk);
|
||||
void clearChatArea();
|
||||
|
||||
protected slots:
|
||||
void searchInBegin(const QString& phrase, const ParameterSearch& parameter) override;
|
||||
void onSearchUp(const QString& phrase, const ParameterSearch& parameter) override;
|
||||
void onSearchDown(const QString& phrase, const ParameterSearch& parameter) override;
|
||||
|
||||
private slots:
|
||||
void onSendTriggered() override;
|
||||
void updateFriendActivity();
|
||||
void updateFriendActivityForFile(const ToxFile& file);
|
||||
void onAttachClicked() override;
|
||||
void onScreenshotClicked() override;
|
||||
|
||||
|
@ -99,47 +88,16 @@ private slots:
|
|||
void onMicMuteToggle();
|
||||
void onVolMuteToggle();
|
||||
|
||||
void onFileSendFailed(uint32_t friendId, const QString& fname);
|
||||
void onFriendStatusChanged(quint32 friendId, Status::Status status);
|
||||
void onFriendTypingChanged(quint32 friendId, bool isTyping);
|
||||
void onFriendNameChanged(const QString& name);
|
||||
void onFriendMessageReceived(quint32 friendId, const QString& message, bool isAction);
|
||||
void onStatusMessage(const QString& message);
|
||||
void onReceiptReceived(quint32 friendId, ReceiptNum receipt);
|
||||
void onLoadHistory();
|
||||
void onUpdateTime();
|
||||
void sendImage(const QPixmap& pixmap);
|
||||
void doScreenshot();
|
||||
void onCopyStatusMessage();
|
||||
void onExportChat();
|
||||
|
||||
private:
|
||||
struct MessageMetadata
|
||||
{
|
||||
const bool isSelf;
|
||||
const bool needSending;
|
||||
const bool isAction;
|
||||
const RowId id;
|
||||
const ToxPk authorPk;
|
||||
const QDateTime msgDateTime;
|
||||
MessageMetadata(bool isSelf, bool needSending, bool isAction, RowId id, ToxPk authorPk,
|
||||
QDateTime msgDateTime)
|
||||
: isSelf{isSelf}
|
||||
, needSending{needSending}
|
||||
, isAction{isAction}
|
||||
, id{id}
|
||||
, authorPk{authorPk}
|
||||
, msgDateTime{msgDateTime}
|
||||
{}
|
||||
};
|
||||
void handleLoadedMessages(QList<History::HistMessage> newHistMsgs, bool processUndelivered);
|
||||
QDate addDateLineIfNeeded(QList<ChatLine::Ptr>& msgs, QDate const& lastDate,
|
||||
History::HistMessage const& newMessage, MessageMetadata const& metadata);
|
||||
MessageMetadata getMessageMetadata(History::HistMessage const& histMessage);
|
||||
ChatMessage::Ptr chatMessageFromHistMessage(History::HistMessage const& histMessage,
|
||||
MessageMetadata const& metadata);
|
||||
void sendLoadedMessage(ChatMessage::Ptr chatMsg, MessageMetadata const& metadata);
|
||||
void insertChatlines(QList<ChatLine::Ptr> chatLines);
|
||||
void updateMuteMicButton();
|
||||
void updateMuteVolButton();
|
||||
void retranslateUi();
|
||||
|
@ -147,8 +105,6 @@ private:
|
|||
void startCounter();
|
||||
void stopCounter(bool error = false);
|
||||
void updateCallButtons();
|
||||
void SendMessageStr(QString msg);
|
||||
bool loadHistory(const QString& phrase, const ParameterSearch& parameter);
|
||||
|
||||
protected:
|
||||
GenericNetCamView* createNetcam() final override;
|
||||
|
@ -166,13 +122,7 @@ private:
|
|||
QTimer* callDurationTimer;
|
||||
QTimer typingTimer;
|
||||
QElapsedTimer timeElapsed;
|
||||
OfflineMsgEngine* offlineEngine;
|
||||
QAction* loadHistoryAction;
|
||||
QAction* copyStatusAction;
|
||||
QAction* exportChatAction;
|
||||
|
||||
History* history;
|
||||
QHash<uint, FileTransferInstance*> ftransWidgets;
|
||||
bool isTyping;
|
||||
bool lastCallIsVideo;
|
||||
};
|
||||
|
|
|
@ -19,13 +19,15 @@
|
|||
|
||||
#include "genericchatform.h"
|
||||
|
||||
#include "src/chatlog/chatlinecontentproxy.h"
|
||||
#include "src/chatlog/chatlog.h"
|
||||
#include "src/chatlog/content/filetransferwidget.h"
|
||||
#include "src/chatlog/content/timestamp.h"
|
||||
#include "src/core/core.h"
|
||||
#include "src/model/friend.h"
|
||||
#include "src/friendlist.h"
|
||||
#include "src/model/group.h"
|
||||
#include "src/grouplist.h"
|
||||
#include "src/model/friend.h"
|
||||
#include "src/model/group.h"
|
||||
#include "src/persistence/settings.h"
|
||||
#include "src/persistence/smileypack.h"
|
||||
#include "src/video/genericnetcamview.h"
|
||||
|
@ -34,13 +36,15 @@
|
|||
#include "src/widget/contentdialogmanager.h"
|
||||
#include "src/widget/contentlayout.h"
|
||||
#include "src/widget/emoticonswidget.h"
|
||||
#include "src/widget/form/chatform.h"
|
||||
#include "src/widget/form/loadhistorydialog.h"
|
||||
#include "src/widget/maskablepixmapwidget.h"
|
||||
#include "src/widget/searchform.h"
|
||||
#include "src/widget/style.h"
|
||||
#include "src/widget/tool/chattextedit.h"
|
||||
#include "src/widget/tool/flyoutoverlaywidget.h"
|
||||
#include "src/widget/translator.h"
|
||||
#include "src/widget/widget.h"
|
||||
#include "src/widget/searchform.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QFileDialog>
|
||||
|
@ -127,13 +131,129 @@ QPushButton* createButton(const QString& name, T* self, Fun onClickSlot)
|
|||
return btn;
|
||||
}
|
||||
|
||||
ChatMessage::Ptr getChatMessageForIdx(ChatLogIdx idx,
|
||||
const std::map<ChatLogIdx, ChatMessage::Ptr>& messages)
|
||||
{
|
||||
auto existingMessageIt = messages.find(idx);
|
||||
|
||||
if (existingMessageIt == messages.end()) {
|
||||
return ChatMessage::Ptr();
|
||||
}
|
||||
|
||||
return existingMessageIt->second;
|
||||
}
|
||||
|
||||
GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent)
|
||||
bool shouldRenderDate(ChatLogIdx idxToRender, const IChatLog& chatLog)
|
||||
{
|
||||
if (idxToRender == chatLog.getFirstIdx())
|
||||
return true;
|
||||
|
||||
return chatLog.at(idxToRender - 1).getTimestamp().date()
|
||||
!= chatLog.at(idxToRender).getTimestamp().date();
|
||||
}
|
||||
|
||||
ChatMessage::Ptr dateMessageForItem(const ChatLogItem& item)
|
||||
{
|
||||
const auto& s = Settings::getInstance();
|
||||
const auto date = item.getTimestamp().date();
|
||||
auto dateText = date.toString(s.getDateFormat());
|
||||
return ChatMessage::createChatInfoMessage(dateText, ChatMessage::INFO, QDateTime());
|
||||
}
|
||||
|
||||
ChatMessage::Ptr createMessage(const QString& displayName, bool isSelf, bool colorizeNames,
|
||||
const ChatLogMessage& chatLogMessage)
|
||||
{
|
||||
auto messageType = chatLogMessage.message.isAction ? ChatMessage::MessageType::ACTION
|
||||
: ChatMessage::MessageType::NORMAL;
|
||||
|
||||
const bool bSelfMentioned =
|
||||
std::any_of(chatLogMessage.message.metadata.begin(), chatLogMessage.message.metadata.end(),
|
||||
[](const MessageMetadata& metadata) {
|
||||
return metadata.type == MessageMetadataType::selfMention;
|
||||
});
|
||||
|
||||
if (bSelfMentioned) {
|
||||
messageType = ChatMessage::MessageType::ALERT;
|
||||
}
|
||||
|
||||
// Spinner is displayed by passing in an empty date
|
||||
auto timestamp = chatLogMessage.isComplete ? chatLogMessage.message.timestamp : QDateTime();
|
||||
|
||||
return ChatMessage::createChatMessage(displayName, chatLogMessage.message.content, messageType,
|
||||
isSelf, timestamp, colorizeNames);
|
||||
}
|
||||
|
||||
void renderMessage(const QString& displayName, bool isSelf, bool colorizeNames,
|
||||
const ChatLogMessage& chatLogMessage, ChatMessage::Ptr& chatMessage)
|
||||
{
|
||||
|
||||
if (chatMessage) {
|
||||
if (chatLogMessage.isComplete) {
|
||||
chatMessage->markAsSent(chatLogMessage.message.timestamp);
|
||||
}
|
||||
} else {
|
||||
chatMessage = createMessage(displayName, isSelf, colorizeNames, chatLogMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void renderFile(QString displayName, ToxFile file, bool isSelf, QDateTime timestamp,
|
||||
ChatMessage::Ptr& chatMessage)
|
||||
{
|
||||
if (!chatMessage) {
|
||||
chatMessage = ChatMessage::createFileTransferMessage(displayName, file, isSelf, timestamp);
|
||||
} else {
|
||||
auto proxy = static_cast<ChatLineContentProxy*>(chatMessage->getContent(1));
|
||||
assert(proxy->getWidgetType() == ChatLineContentProxy::FileTransferWidgetType);
|
||||
auto ftWidget = static_cast<FileTransferWidget*>(proxy->getWidget());
|
||||
ftWidget->onFileTransferUpdate(file);
|
||||
}
|
||||
}
|
||||
|
||||
void renderItem(const ChatLogItem& item, bool hideName, bool colorizeNames, ChatMessage::Ptr& chatMessage)
|
||||
{
|
||||
const auto& sender = item.getSender();
|
||||
|
||||
const Core* core = Core::getInstance();
|
||||
bool isSelf = sender == core->getSelfId().getPublicKey();
|
||||
|
||||
switch (item.getContentType()) {
|
||||
case ChatLogItem::ContentType::message: {
|
||||
const auto& chatLogMessage = item.getContentAsMessage();
|
||||
|
||||
renderMessage(item.getDisplayName(), isSelf, colorizeNames, chatLogMessage, chatMessage);
|
||||
|
||||
break;
|
||||
}
|
||||
case ChatLogItem::ContentType::fileTransfer: {
|
||||
const auto& file = item.getContentAsFile();
|
||||
renderFile(item.getDisplayName(), file.file, isSelf, item.getTimestamp(), chatMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hideName) {
|
||||
chatMessage->hideSender();
|
||||
}
|
||||
}
|
||||
|
||||
ChatLogIdx firstItemAfterDate(QDate date, const IChatLog& chatLog)
|
||||
{
|
||||
auto idxs = chatLog.getDateIdxs(date, 1);
|
||||
if (idxs.size()) {
|
||||
return idxs[0].idx;
|
||||
} else {
|
||||
return chatLog.getNextIdx();
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
GenericChatForm::GenericChatForm(const Contact* contact, IChatLog& chatLog,
|
||||
IMessageDispatcher& messageDispatcher, QWidget* parent)
|
||||
: QWidget(parent, Qt::Window)
|
||||
, audioInputFlag(false)
|
||||
, audioOutputFlag(false)
|
||||
, searchAfterLoadHistory(false)
|
||||
, chatLog(chatLog)
|
||||
, messageDispatcher(messageDispatcher)
|
||||
{
|
||||
curRow = 0;
|
||||
headWidget = new ChatFormHeader();
|
||||
|
@ -219,8 +339,6 @@ GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent)
|
|||
menu.addActions(chatWidget->actions());
|
||||
menu.addSeparator();
|
||||
|
||||
saveChatAction = menu.addAction(QIcon::fromTheme("document-save"), QString(),
|
||||
this, SLOT(onSaveLogClicked()));
|
||||
clearAction = menu.addAction(QIcon::fromTheme("edit-clear"), QString(),
|
||||
this, SLOT(clearChatArea()),
|
||||
QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_L));
|
||||
|
@ -229,6 +347,10 @@ GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent)
|
|||
copyLinkAction = menu.addAction(QIcon(), QString(), this, SLOT(copyLink()));
|
||||
menu.addSeparator();
|
||||
|
||||
loadHistoryAction = menu.addAction(QIcon(), QString(), this, SLOT(onLoadHistory()));
|
||||
exportChatAction =
|
||||
menu.addAction(QIcon::fromTheme("document-save"), QString(), this, SLOT(onExportChat()));
|
||||
|
||||
connect(chatWidget, &ChatLog::customContextMenuRequested, this,
|
||||
&GenericChatForm::onChatContextMenuRequested);
|
||||
connect(chatWidget, &ChatLog::firstVisibleLineChanged, this, &GenericChatForm::updateShowDateInfo);
|
||||
|
@ -239,7 +361,9 @@ GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent)
|
|||
connect(searchForm, &SearchForm::visibleChanged, this, &GenericChatForm::onSearchTriggered);
|
||||
connect(this, &GenericChatForm::messageNotFoundShow, searchForm, &SearchForm::showMessageNotFound);
|
||||
|
||||
connect(chatWidget, &ChatLog::workerTimeoutFinished, this, &GenericChatForm::onContinueSearch);
|
||||
connect(&chatLog, &IChatLog::itemUpdated, this, &GenericChatForm::renderMessage);
|
||||
|
||||
connect(msgEdit, &ChatTextEdit::enterPressed, this, &GenericChatForm::onSendTriggered);
|
||||
|
||||
reloadTheme();
|
||||
|
||||
|
@ -254,6 +378,11 @@ GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent)
|
|||
// update header on name/title change
|
||||
connect(contact, &Contact::displayedNameChanged, this, &GenericChatForm::setName);
|
||||
|
||||
auto chatLogIdxRange = chatLog.getNextIdx() - chatLog.getFirstIdx();
|
||||
auto firstChatLogIdx = (chatLogIdxRange < 100) ? chatLog.getFirstIdx() : chatLog.getNextIdx() - 100;
|
||||
|
||||
renderMessages(firstChatLogIdx, chatLog.getNextIdx());
|
||||
|
||||
netcam = nullptr;
|
||||
}
|
||||
|
||||
|
@ -373,101 +502,52 @@ void GenericChatForm::onChatContextMenuRequested(QPoint pos)
|
|||
menu.exec(pos);
|
||||
}
|
||||
|
||||
void GenericChatForm::onSendTriggered()
|
||||
{
|
||||
auto msg = msgEdit->toPlainText();
|
||||
|
||||
if (msg.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
msgEdit->setLastMessage(msg);
|
||||
msgEdit->clear();
|
||||
|
||||
bool isAction = msg.startsWith(ChatForm::ACTION_PREFIX, Qt::CaseInsensitive);
|
||||
if (isAction) {
|
||||
msg.remove(0, ChatForm::ACTION_PREFIX.length());
|
||||
}
|
||||
|
||||
messageDispatcher.sendMessage(isAction, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Show, is it needed to hide message author name or not
|
||||
* @param messageAuthor Author of the sent message
|
||||
* @oaran messageTime DateTime of the sent message
|
||||
* @return True if it's needed to hide name, false otherwise
|
||||
*/
|
||||
bool GenericChatForm::needsToHideName(const ToxPk& messageAuthor, const QDateTime& messageTime) const
|
||||
bool GenericChatForm::needsToHideName(ChatLogIdx idx) const
|
||||
{
|
||||
qint64 messagesTimeDiff = prevMsgDateTime.secsTo(messageTime);
|
||||
return messageAuthor == previousId && messagesTimeDiff < chatWidget->repNameAfter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates ChatMessage shared object and inserts it into ChatLog
|
||||
* @param author Author of the message
|
||||
* @param message Message text
|
||||
* @param dt Date and time when message was sent
|
||||
* @param isAction True if this is an action message, false otherwise
|
||||
* @param isSent True if message was received by your friend
|
||||
* @return ChatMessage object
|
||||
*/
|
||||
ChatMessage::Ptr GenericChatForm::createMessage(const ToxPk& author, const QString& message,
|
||||
const QDateTime& dt, bool isAction, bool isSent, bool colorizeName)
|
||||
{
|
||||
const Core* core = Core::getInstance();
|
||||
bool isSelf = author == core->getSelfId().getPublicKey();
|
||||
QString myNickName = core->getUsername().isEmpty() ? author.toString() : core->getUsername();
|
||||
QString authorStr = isSelf ? myNickName : resolveToxPk(author);
|
||||
const auto now = QDateTime::currentDateTime();
|
||||
if (getLatestTime().date() != now.date()) {
|
||||
addSystemDateMessage();
|
||||
// If the previous message is not rendered we should show the name
|
||||
// regardless of other constraints
|
||||
auto itemBefore = messages.find(idx - 1);
|
||||
if (itemBefore == messages.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ChatMessage::Ptr msg;
|
||||
if (isAction) {
|
||||
msg = ChatMessage::createChatMessage(authorStr, message, ChatMessage::ACTION, isSelf, QDateTime(), colorizeName);
|
||||
previousId = ToxPk{};
|
||||
} else {
|
||||
msg = ChatMessage::createChatMessage(authorStr, message, ChatMessage::NORMAL, isSelf, QDateTime(), colorizeName);
|
||||
if (needsToHideName(author, now)) {
|
||||
msg->hideSender();
|
||||
}
|
||||
const auto& prevItem = chatLog.at(idx - 1);
|
||||
const auto& currentItem = chatLog.at(idx);
|
||||
|
||||
previousId = author;
|
||||
prevMsgDateTime = now;
|
||||
// Always show the * in the name field for action messages
|
||||
if (currentItem.getContentType() == ChatLogItem::ContentType::message
|
||||
&& currentItem.getContentAsMessage().message.isAction) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSent) {
|
||||
msg->markAsSent(dt);
|
||||
}
|
||||
|
||||
insertChatMessage(msg);
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Same, as createMessage, but creates message that you will send to someone
|
||||
*/
|
||||
ChatMessage::Ptr GenericChatForm::createSelfMessage(const QString& message, const QDateTime& dt,
|
||||
bool isAction, bool isSent)
|
||||
{
|
||||
ToxPk selfPk = Core::getInstance()->getSelfId().getPublicKey();
|
||||
return createMessage(selfPk, message, dt, isAction, isSent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Inserts message into ChatLog
|
||||
*/
|
||||
void GenericChatForm::addMessage(const ToxPk& author, const QString& message, const QDateTime& dt,
|
||||
bool isAction, bool colorizeName)
|
||||
{
|
||||
createMessage(author, message, dt, isAction, true, colorizeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Inserts int ChatLog message that you have sent
|
||||
*/
|
||||
void GenericChatForm::addSelfMessage(const QString& message, const QDateTime& datetime, bool isAction)
|
||||
{
|
||||
createSelfMessage(message, datetime, isAction, true);
|
||||
}
|
||||
|
||||
void GenericChatForm::addAlertMessage(const ToxPk& author, const QString& msg, const QDateTime& dt, bool colorizeName)
|
||||
{
|
||||
QString authorStr = resolveToxPk(author);
|
||||
bool isSelf = author == Core::getInstance()->getSelfId().getPublicKey();
|
||||
auto chatMsg = ChatMessage::createChatMessage(authorStr, msg, ChatMessage::ALERT, isSelf, dt, colorizeName);
|
||||
const QDateTime newMsgDateTime = QDateTime::currentDateTime();
|
||||
if (needsToHideName(author, newMsgDateTime)) {
|
||||
chatMsg->hideSender();
|
||||
}
|
||||
|
||||
insertChatMessage(chatMsg);
|
||||
previousId = author;
|
||||
prevMsgDateTime = newMsgDateTime;
|
||||
qint64 messagesTimeDiff = prevItem.getTimestamp().secsTo(currentItem.getTimestamp());
|
||||
return currentItem.getSender() == prevItem.getSender()
|
||||
&& messagesTimeDiff < chatWidget->repNameAfter;
|
||||
}
|
||||
|
||||
void GenericChatForm::onEmoteButtonClicked()
|
||||
|
@ -498,37 +578,6 @@ void GenericChatForm::onEmoteInsertRequested(QString str)
|
|||
msgEdit->setFocus(); // refocus so that we can continue typing
|
||||
}
|
||||
|
||||
void GenericChatForm::onSaveLogClicked()
|
||||
{
|
||||
QString path = QFileDialog::getSaveFileName(Q_NULLPTR, tr("Save chat log"));
|
||||
if (path.isEmpty())
|
||||
return;
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
return;
|
||||
|
||||
QString plainText;
|
||||
auto lines = chatWidget->getLines();
|
||||
for (ChatLine::Ptr l : lines) {
|
||||
Timestamp* rightCol = qobject_cast<Timestamp*>(l->getContent(2));
|
||||
|
||||
ChatLineContent* middleCol = l->getContent(1);
|
||||
ChatLineContent* leftCol = l->getContent(0);
|
||||
|
||||
QString nick = leftCol->getText().isNull() ? tr("[System message]") : leftCol->getText();
|
||||
|
||||
QString msg = middleCol->getText();
|
||||
|
||||
QString timestamp = (rightCol == nullptr) ? tr("Not sent") : rightCol->getText();
|
||||
|
||||
plainText += QString{nick % "\t" % timestamp % "\t" % msg % "\n"};
|
||||
}
|
||||
|
||||
file.write(plainText.toUtf8());
|
||||
file.close();
|
||||
}
|
||||
|
||||
void GenericChatForm::onCopyLogClicked()
|
||||
{
|
||||
chatWidget->copySelectedText();
|
||||
|
@ -549,21 +598,22 @@ void GenericChatForm::onChatMessageFontChanged(const QFont& font)
|
|||
+ fontToCss(font, "QTextEdit"));
|
||||
}
|
||||
|
||||
void GenericChatForm::setColorizedNames(bool enable)
|
||||
{
|
||||
colorizeNames = enable;
|
||||
}
|
||||
|
||||
void GenericChatForm::addSystemInfoMessage(const QString& message, ChatMessage::SystemMessageType type,
|
||||
const QDateTime& datetime)
|
||||
{
|
||||
if (getLatestTime().date() != QDate::currentDate()) {
|
||||
addSystemDateMessage();
|
||||
}
|
||||
|
||||
previousId = ToxPk();
|
||||
insertChatMessage(ChatMessage::createChatInfoMessage(message, type, datetime));
|
||||
}
|
||||
|
||||
void GenericChatForm::addSystemDateMessage()
|
||||
void GenericChatForm::addSystemDateMessage(const QDate& date)
|
||||
{
|
||||
const Settings& s = Settings::getInstance();
|
||||
QString dateText = QDate::currentDate().toString(s.getDateFormat());
|
||||
QString dateText = date.toString(s.getDateFormat());
|
||||
|
||||
previousId = ToxPk();
|
||||
insertChatMessage(ChatMessage::createChatInfoMessage(dateText, ChatMessage::INFO, QDateTime()));
|
||||
|
@ -584,258 +634,16 @@ QDateTime GenericChatForm::getTime(const ChatLine::Ptr &chatLine) const
|
|||
return QDateTime();
|
||||
}
|
||||
|
||||
|
||||
void GenericChatForm::disableSearchText()
|
||||
{
|
||||
if (searchPoint != QPoint(1, -1)) {
|
||||
QVector<ChatLine::Ptr> lines = chatWidget->getLines();
|
||||
int numLines = lines.size();
|
||||
int index = numLines - searchPoint.x();
|
||||
if (index >= 0 && numLines > index) {
|
||||
ChatLine::Ptr l = lines[index];
|
||||
if (l->getColumnCount() >= 2) {
|
||||
ChatLineContent* content = l->getContent(1);
|
||||
Text* text = static_cast<Text*>(content);
|
||||
text->deselectText();
|
||||
}
|
||||
}
|
||||
auto msgIt = messages.find(searchPos.logIdx);
|
||||
if (msgIt != messages.end()) {
|
||||
auto text = qobject_cast<Text*>(msgIt->second->getContent(1));
|
||||
text->deselectText();
|
||||
}
|
||||
}
|
||||
|
||||
bool GenericChatForm::searchInText(const QString& phrase, const ParameterSearch& parameter, SearchDirection direction)
|
||||
{
|
||||
bool isSearch = false;
|
||||
|
||||
if (phrase.isEmpty()) {
|
||||
disableSearchText();
|
||||
}
|
||||
|
||||
auto lines = chatWidget->getLines();
|
||||
|
||||
if (lines.isEmpty()) {
|
||||
return isSearch;
|
||||
}
|
||||
|
||||
int numLines = lines.size();
|
||||
|
||||
int startLine = -1;
|
||||
|
||||
if (parameter.period == PeriodSearch::WithTheEnd || parameter.period == PeriodSearch::None) {
|
||||
startLine = numLines - searchPoint.x();
|
||||
} else if (parameter.period == PeriodSearch::WithTheFirst) {
|
||||
startLine = 0;
|
||||
} else if (parameter.period == PeriodSearch::AfterDate) {
|
||||
const auto lambda = [=](const ChatLine::Ptr& item) {
|
||||
const auto d = getTime(item).date();
|
||||
return d.isValid() && parameter.date <= d;
|
||||
};
|
||||
|
||||
const auto find = std::find_if(lines.begin(), lines.end(), lambda);
|
||||
|
||||
if (find != lines.end()) {
|
||||
startLine = static_cast<int>(std::distance(lines.begin(), find));
|
||||
}
|
||||
} else if (parameter.period == PeriodSearch::BeforeDate) {
|
||||
#if QT_VERSION > QT_VERSION_CHECK(5, 6, 0)
|
||||
const auto lambda = [=](const ChatLine::Ptr& item) {
|
||||
const auto d = getTime(item).date();
|
||||
return d.isValid() && parameter.date >= d;
|
||||
};
|
||||
|
||||
const auto find = std::find_if(lines.rbegin(), lines.rend(), lambda);
|
||||
|
||||
if (find != lines.rend()) {
|
||||
startLine = static_cast<int>(std::distance(find, lines.rend())) - 1;
|
||||
}
|
||||
#else
|
||||
for (int i = lines.size() - 1; i >= 0; --i) {
|
||||
auto d = getTime(lines[i]).date();
|
||||
if (d.isValid() && parameter.date >= d) {
|
||||
startLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (startLine < 0 || startLine >= numLines) {
|
||||
return isSearch;
|
||||
}
|
||||
|
||||
const bool searchUp = (direction == SearchDirection::Up);
|
||||
for (int i = startLine; searchUp ? i >= 0 : i < numLines; searchUp ? --i : ++i) {
|
||||
ChatLine::Ptr l = lines[i];
|
||||
|
||||
if (l->getColumnCount() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ChatLineContent* content = l->getContent(1);
|
||||
Text* text = static_cast<Text*>(content);
|
||||
|
||||
if (searchUp && searchPoint.y() == 0) {
|
||||
text->deselectText();
|
||||
searchPoint.setY(-1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
QString txt = content->getText();
|
||||
|
||||
bool find = false;
|
||||
QRegularExpression exp;
|
||||
QRegularExpressionMatch match;
|
||||
|
||||
auto flagIns = QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption;
|
||||
auto flag = QRegularExpression::UseUnicodePropertiesOption;
|
||||
switch (parameter.filter) {
|
||||
case FilterSearch::Register:
|
||||
find = txt.contains(phrase, Qt::CaseSensitive);
|
||||
break;
|
||||
case FilterSearch::WordsOnly:
|
||||
exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flagIns);
|
||||
find = txt.contains(exp);
|
||||
break;
|
||||
case FilterSearch::RegisterAndWordsOnly:
|
||||
exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flag);
|
||||
find = txt.contains(exp);
|
||||
break;
|
||||
case FilterSearch::RegisterAndRegular:
|
||||
exp = QRegularExpression(phrase, flag);
|
||||
find = txt.contains(exp);
|
||||
break;
|
||||
case FilterSearch::Regular:
|
||||
exp = QRegularExpression(phrase, flagIns);
|
||||
find = txt.contains(exp);
|
||||
break;
|
||||
default:
|
||||
find = txt.contains(phrase, Qt::CaseInsensitive);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!find) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto point = indexForSearchInLine(txt, phrase, parameter, direction);
|
||||
if ((point.first == -1 && searchPoint.y() > -1)) {
|
||||
text->deselectText();
|
||||
searchPoint.setY(-1);
|
||||
} else {
|
||||
chatWidget->scrollToLine(l);
|
||||
text->deselectText();
|
||||
|
||||
if (exp.pattern().isEmpty()) {
|
||||
text->selectText(phrase, point);
|
||||
} else {
|
||||
text->selectText(exp, point);
|
||||
}
|
||||
|
||||
searchPoint = QPoint(numLines - i, point.first);
|
||||
isSearch = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isSearch;
|
||||
}
|
||||
|
||||
std::pair<int, int> GenericChatForm::indexForSearchInLine(const QString& txt, const QString& phrase, const ParameterSearch& parameter, SearchDirection direction)
|
||||
{
|
||||
int index = -1;
|
||||
int size = 0;
|
||||
|
||||
QRegularExpression exp;
|
||||
auto flagIns = QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption;
|
||||
auto flag = QRegularExpression::UseUnicodePropertiesOption;
|
||||
if (direction == SearchDirection::Up) {
|
||||
int startIndex = -1;
|
||||
if (searchPoint.y() > -1) {
|
||||
startIndex = searchPoint.y() - 1;
|
||||
}
|
||||
|
||||
switch (parameter.filter) {
|
||||
case FilterSearch::Register:
|
||||
index = txt.lastIndexOf(phrase, startIndex, Qt::CaseSensitive);
|
||||
break;
|
||||
case FilterSearch::WordsOnly:
|
||||
exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flagIns);
|
||||
break;
|
||||
case FilterSearch::RegisterAndWordsOnly:
|
||||
exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flag);
|
||||
break;
|
||||
case FilterSearch::RegisterAndRegular:
|
||||
exp = QRegularExpression(phrase, flag);
|
||||
break;
|
||||
case FilterSearch::Regular:
|
||||
exp = QRegularExpression(phrase, flagIns);
|
||||
break;
|
||||
default:
|
||||
index = txt.lastIndexOf(phrase, startIndex, Qt::CaseInsensitive);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!exp.pattern().isEmpty()) {
|
||||
auto matchIt = exp.globalMatch(txt);
|
||||
|
||||
while (matchIt.hasNext()) {
|
||||
const auto match = matchIt.next();
|
||||
|
||||
int sizeItem = match.capturedLength();
|
||||
int indexItem = match.capturedStart();
|
||||
|
||||
if (startIndex == -1 || indexItem < startIndex) {
|
||||
index = indexItem;
|
||||
size = sizeItem;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
size = phrase.size();
|
||||
}
|
||||
|
||||
} else {
|
||||
int startIndex = 0;
|
||||
if (searchPoint.y() > -1) {
|
||||
startIndex = searchPoint.y() + 1;
|
||||
}
|
||||
|
||||
switch (parameter.filter) {
|
||||
case FilterSearch::Register:
|
||||
index = txt.indexOf(phrase, startIndex, Qt::CaseSensitive);
|
||||
break;
|
||||
case FilterSearch::WordsOnly:
|
||||
exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flagIns);
|
||||
break;
|
||||
case FilterSearch::RegisterAndWordsOnly:
|
||||
exp = QRegularExpression(SearchExtraFunctions::generateFilterWordsOnly(phrase), flag);
|
||||
break;
|
||||
case FilterSearch::RegisterAndRegular:
|
||||
exp = QRegularExpression(phrase, flag);
|
||||
break;
|
||||
case FilterSearch::Regular:
|
||||
exp = QRegularExpression(phrase, flagIns);
|
||||
break;
|
||||
default:
|
||||
index = txt.indexOf(phrase, startIndex, Qt::CaseInsensitive);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!exp.pattern().isEmpty()) {
|
||||
const auto match = exp.match(txt, startIndex);
|
||||
if (match.hasMatch()) {
|
||||
size = match.capturedLength(0);
|
||||
index = match.capturedEnd() - size;
|
||||
}
|
||||
} else {
|
||||
size = phrase.size();
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_pair(index, size);
|
||||
}
|
||||
|
||||
void GenericChatForm::clearChatArea()
|
||||
{
|
||||
clearChatArea(/* confirm = */ true, /* inform = */ true);
|
||||
|
@ -859,7 +667,7 @@ void GenericChatForm::clearChatArea(bool confirm, bool inform)
|
|||
if (inform)
|
||||
addSystemInfoMessage(tr("Cleared"), ChatMessage::INFO, QDateTime::currentDateTime());
|
||||
|
||||
earliestMessage = QDateTime(); // null
|
||||
messages.clear();
|
||||
}
|
||||
|
||||
void GenericChatForm::onSelectAllClicked()
|
||||
|
@ -987,15 +795,177 @@ void GenericChatForm::searchFormShow()
|
|||
}
|
||||
}
|
||||
|
||||
void GenericChatForm::onLoadHistory()
|
||||
{
|
||||
LoadHistoryDialog dlg(&chatLog);
|
||||
if (dlg.exec()) {
|
||||
QDateTime time = dlg.getFromDate();
|
||||
auto idx = firstItemAfterDate(dlg.getFromDate().date(), chatLog);
|
||||
renderMessages(idx, chatLog.getNextIdx());
|
||||
}
|
||||
}
|
||||
|
||||
void GenericChatForm::onExportChat()
|
||||
{
|
||||
QString path = QFileDialog::getSaveFileName(Q_NULLPTR, tr("Save chat log"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString buffer;
|
||||
for (auto i = chatLog.getFirstIdx(); i < chatLog.getNextIdx(); ++i) {
|
||||
const auto& item = chatLog.at(i);
|
||||
if (item.getContentType() != ChatLogItem::ContentType::message) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString timestamp = item.getTimestamp().time().toString("hh:mm:ss");
|
||||
QString datestamp = item.getTimestamp().date().toString("yyyy-MM-dd");
|
||||
QString author = item.getDisplayName();
|
||||
|
||||
buffer = buffer
|
||||
% QString{datestamp % '\t' % timestamp % '\t' % author % '\t'
|
||||
% item.getContentAsMessage().message.content % '\n'};
|
||||
}
|
||||
file.write(buffer.toUtf8());
|
||||
file.close();
|
||||
}
|
||||
|
||||
void GenericChatForm::onSearchTriggered()
|
||||
{
|
||||
if (searchForm->isHidden()) {
|
||||
searchForm->removeSearchPhrase();
|
||||
}
|
||||
disableSearchText();
|
||||
}
|
||||
|
||||
disableSearchText();
|
||||
void GenericChatForm::searchInBegin(const QString& phrase, const ParameterSearch& parameter)
|
||||
{
|
||||
disableSearchText();
|
||||
|
||||
bool bForwardSearch = false;
|
||||
switch (parameter.period) {
|
||||
case PeriodSearch::WithTheFirst: {
|
||||
bForwardSearch = true;
|
||||
searchPos.logIdx = chatLog.getFirstIdx();
|
||||
searchPos.numMatches = 0;
|
||||
break;
|
||||
}
|
||||
case PeriodSearch::WithTheEnd:
|
||||
case PeriodSearch::None: {
|
||||
bForwardSearch = false;
|
||||
searchPos.logIdx = chatLog.getNextIdx();
|
||||
searchPos.numMatches = 0;
|
||||
break;
|
||||
}
|
||||
case PeriodSearch::AfterDate: {
|
||||
bForwardSearch = true;
|
||||
searchPos.logIdx = firstItemAfterDate(parameter.date, chatLog);
|
||||
searchPos.numMatches = 0;
|
||||
break;
|
||||
}
|
||||
case PeriodSearch::BeforeDate: {
|
||||
bForwardSearch = false;
|
||||
searchPos.logIdx = firstItemAfterDate(parameter.date, chatLog);
|
||||
searchPos.numMatches = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bForwardSearch) {
|
||||
onSearchDown(phrase, parameter);
|
||||
} else {
|
||||
searchPoint = QPoint(1, -1);
|
||||
searchAfterLoadHistory = false;
|
||||
onSearchUp(phrase, parameter);
|
||||
}
|
||||
}
|
||||
|
||||
void GenericChatForm::onSearchUp(const QString& phrase, const ParameterSearch& parameter)
|
||||
{
|
||||
auto result = chatLog.searchBackward(searchPos, phrase, parameter);
|
||||
handleSearchResult(result, SearchDirection::Up);
|
||||
}
|
||||
|
||||
void GenericChatForm::onSearchDown(const QString& phrase, const ParameterSearch& parameter)
|
||||
{
|
||||
auto result = chatLog.searchForward(searchPos, phrase, parameter);
|
||||
handleSearchResult(result, SearchDirection::Down);
|
||||
}
|
||||
|
||||
void GenericChatForm::handleSearchResult(SearchResult result, SearchDirection direction)
|
||||
{
|
||||
if (!result.found) {
|
||||
emit messageNotFoundShow(direction);
|
||||
return;
|
||||
}
|
||||
|
||||
disableSearchText();
|
||||
|
||||
searchPos = result.pos;
|
||||
|
||||
auto const firstRenderedIdx = (messages.empty()) ? chatLog.getNextIdx() : messages.begin()->first;
|
||||
|
||||
renderMessages(searchPos.logIdx, firstRenderedIdx, [this, result] {
|
||||
auto msg = messages.at(searchPos.logIdx);
|
||||
chatWidget->scrollToLine(msg);
|
||||
|
||||
auto text = qobject_cast<Text*>(msg->getContent(1));
|
||||
text->selectText(result.exp, std::make_pair(result.start, result.len));
|
||||
});
|
||||
}
|
||||
|
||||
void GenericChatForm::renderMessage(ChatLogIdx idx)
|
||||
{
|
||||
renderMessages(idx, idx + 1);
|
||||
}
|
||||
|
||||
void GenericChatForm::renderMessages(ChatLogIdx begin, ChatLogIdx end,
|
||||
std::function<void(void)> onCompletion)
|
||||
{
|
||||
QList<ChatLine::Ptr> beforeLines;
|
||||
QList<ChatLine::Ptr> afterLines;
|
||||
|
||||
for (auto i = begin; i < end; ++i) {
|
||||
auto chatMessage = getChatMessageForIdx(i, messages);
|
||||
renderItem(chatLog.at(i), needsToHideName(i), colorizeNames, chatMessage);
|
||||
|
||||
if (messages.find(i) == messages.end()) {
|
||||
QList<ChatLine::Ptr>* lines =
|
||||
(messages.empty() || i > messages.rbegin()->first) ? &afterLines : &beforeLines;
|
||||
|
||||
messages.insert({i, chatMessage});
|
||||
|
||||
if (shouldRenderDate(i, chatLog)) {
|
||||
lines->push_back(dateMessageForItem(chatLog.at(i)));
|
||||
}
|
||||
lines->push_back(chatMessage);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& line : afterLines) {
|
||||
chatWidget->insertChatlineAtBottom(line);
|
||||
}
|
||||
|
||||
if (!beforeLines.empty()) {
|
||||
// Rendering upwards is expensive and has async behavior for chatWidget.
|
||||
// Once rendering completes we call our completion callback once and
|
||||
// then disconnect the signal
|
||||
if (onCompletion) {
|
||||
auto connection = std::make_shared<QMetaObject::Connection>();
|
||||
*connection = connect(chatWidget, &ChatLog::workerTimeoutFinished,
|
||||
[onCompletion, connection, this] {
|
||||
onCompletion();
|
||||
disconnect(*connection);
|
||||
});
|
||||
}
|
||||
|
||||
chatWidget->insertChatlinesOnTop(beforeLines);
|
||||
} else if (onCompletion) {
|
||||
onCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1012,31 +982,18 @@ void GenericChatForm::updateShowDateInfo(const ChatLine::Ptr& line)
|
|||
}
|
||||
}
|
||||
|
||||
void GenericChatForm::onContinueSearch()
|
||||
{
|
||||
const QString phrase = searchForm->getSearchPhrase();
|
||||
const ParameterSearch parameter = searchForm->getParameterSearch();
|
||||
if (!phrase.isEmpty() && searchAfterLoadHistory) {
|
||||
if (parameter.period == PeriodSearch::WithTheFirst || parameter.period == PeriodSearch::AfterDate) {
|
||||
searchAfterLoadHistory = false;
|
||||
onSearchDown(phrase, parameter);
|
||||
} else {
|
||||
onSearchUp(phrase, parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GenericChatForm::retranslateUi()
|
||||
{
|
||||
sendButton->setToolTip(tr("Send message"));
|
||||
emoteButton->setToolTip(tr("Smileys"));
|
||||
fileButton->setToolTip(tr("Send file(s)"));
|
||||
screenshotButton->setToolTip(tr("Send a screenshot"));
|
||||
saveChatAction->setText(tr("Save chat log"));
|
||||
clearAction->setText(tr("Clear displayed messages"));
|
||||
quoteAction->setText(tr("Quote selected text"));
|
||||
copyLinkAction->setText(tr("Copy link address"));
|
||||
searchAction->setText(tr("Search in text"));
|
||||
loadHistoryAction->setText(tr("Load chat history..."));
|
||||
exportChatAction->setText(tr("Export to file"));
|
||||
}
|
||||
|
||||
void GenericChatForm::showNetcam()
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
#include "src/chatlog/chatmessage.h"
|
||||
#include "src/core/toxpk.h"
|
||||
#include "src/model/ichatlog.h"
|
||||
#include "src/widget/searchtypes.h"
|
||||
|
||||
#include <QMenu>
|
||||
|
@ -51,6 +52,9 @@ class QSplitter;
|
|||
class QToolButton;
|
||||
class QVBoxLayout;
|
||||
|
||||
class IMessageDispatcher;
|
||||
class Message;
|
||||
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
}
|
||||
|
@ -65,7 +69,8 @@ class GenericChatForm : public QWidget
|
|||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GenericChatForm(const Contact* contact, QWidget* parent = nullptr);
|
||||
GenericChatForm(const Contact* contact, IChatLog& chatLog,
|
||||
IMessageDispatcher& messageDispatcher, QWidget* parent = nullptr);
|
||||
~GenericChatForm() override;
|
||||
|
||||
void setName(const QString& newName);
|
||||
|
@ -75,34 +80,28 @@ public:
|
|||
virtual void show(ContentLayout* contentLayout);
|
||||
virtual void reloadTheme();
|
||||
|
||||
void addMessage(const ToxPk& author, const QString& message, const QDateTime& datetime,
|
||||
bool isAction, bool colorizeName = false);
|
||||
void addSelfMessage(const QString& message, const QDateTime& datetime, bool isAction);
|
||||
void addSystemInfoMessage(const QString& message, ChatMessage::SystemMessageType type,
|
||||
const QDateTime& datetime);
|
||||
void addAlertMessage(const ToxPk& author, const QString& message, const QDateTime& datetime, bool colorizeName = false);
|
||||
static QString resolveToxPk(const ToxPk& pk);
|
||||
QDateTime getLatestTime() const;
|
||||
QDateTime getFirstTime() const;
|
||||
|
||||
signals:
|
||||
void sendMessage(uint32_t, QString);
|
||||
void sendAction(uint32_t, QString);
|
||||
void messageInserted();
|
||||
void messageNotFoundShow(SearchDirection direction);
|
||||
|
||||
public slots:
|
||||
void focusInput();
|
||||
void onChatMessageFontChanged(const QFont& font);
|
||||
void setColorizedNames(bool enable);
|
||||
|
||||
protected slots:
|
||||
void onChatContextMenuRequested(QPoint pos);
|
||||
virtual void onScreenshotClicked() = 0;
|
||||
virtual void onSendTriggered() = 0;
|
||||
void onSendTriggered();
|
||||
virtual void onAttachClicked() = 0;
|
||||
void onEmoteButtonClicked();
|
||||
void onEmoteInsertRequested(QString str);
|
||||
void onSaveLogClicked();
|
||||
void onCopyLogClicked();
|
||||
void clearChatArea();
|
||||
void clearChatArea(bool confirm, bool inform);
|
||||
|
@ -113,26 +112,29 @@ protected slots:
|
|||
void onSplitterMoved(int pos, int index);
|
||||
void quoteSelectedText();
|
||||
void copyLink();
|
||||
void onLoadHistory();
|
||||
void onExportChat();
|
||||
void searchFormShow();
|
||||
void onSearchTriggered();
|
||||
void updateShowDateInfo(const ChatLine::Ptr& line);
|
||||
|
||||
virtual void searchInBegin(const QString& phrase, const ParameterSearch& parameter) = 0;
|
||||
virtual void onSearchUp(const QString& phrase, const ParameterSearch& parameter) = 0;
|
||||
virtual void onSearchDown(const QString& phrase, const ParameterSearch& parameter) = 0;
|
||||
void onContinueSearch();
|
||||
void searchInBegin(const QString& phrase, const ParameterSearch& parameter);
|
||||
void onSearchUp(const QString& phrase, const ParameterSearch& parameter);
|
||||
void onSearchDown(const QString& phrase, const ParameterSearch& parameter);
|
||||
void handleSearchResult(SearchResult result, SearchDirection direction);
|
||||
void renderMessage(ChatLogIdx idx);
|
||||
void renderMessages(ChatLogIdx begin, ChatLogIdx end,
|
||||
std::function<void(void)> onCompletion = std::function<void(void)>());
|
||||
|
||||
private:
|
||||
void retranslateUi();
|
||||
void addSystemDateMessage();
|
||||
void addSystemDateMessage(const QDate& date);
|
||||
QDateTime getTime(const ChatLine::Ptr& chatLine) const;
|
||||
|
||||
protected:
|
||||
ChatMessage::Ptr createMessage(const ToxPk& author, const QString& message,
|
||||
const QDateTime& datetime, bool isAction, bool isSent, bool colorizeName = false);
|
||||
ChatMessage::Ptr createSelfMessage(const QString& message, const QDateTime& datetime,
|
||||
bool isAction, bool isSent);
|
||||
bool needsToHideName(const ToxPk& messageAuthor, const QDateTime& messageTime) const;
|
||||
bool needsToHideName(ChatLogIdx idx) const;
|
||||
void showNetcam();
|
||||
void hideNetcam();
|
||||
virtual GenericNetCamView* createNetcam() = 0;
|
||||
|
@ -152,15 +154,15 @@ protected:
|
|||
bool audioOutputFlag;
|
||||
int curRow;
|
||||
|
||||
QAction* saveChatAction;
|
||||
QAction* clearAction;
|
||||
QAction* quoteAction;
|
||||
QAction* copyLinkAction;
|
||||
QAction* searchAction;
|
||||
QAction* loadHistoryAction;
|
||||
QAction* exportChatAction;
|
||||
|
||||
ToxPk previousId;
|
||||
|
||||
QDateTime prevMsgDateTime;
|
||||
QDateTime earliestMessage;
|
||||
|
||||
QMenu menu;
|
||||
|
@ -185,8 +187,11 @@ protected:
|
|||
GenericNetCamView* netcam;
|
||||
Widget* parent;
|
||||
|
||||
QPoint searchPoint;
|
||||
bool searchAfterLoadHistory;
|
||||
IChatLog& chatLog;
|
||||
IMessageDispatcher& messageDispatcher;
|
||||
SearchPos searchPos;
|
||||
std::map<ChatLogIdx, ChatMessage::Ptr> messages;
|
||||
bool colorizeNames = false;
|
||||
};
|
||||
|
||||
#endif // GENERICCHATFORM_H
|
||||
|
|
|
@ -82,8 +82,8 @@ QString editName(const QString& name)
|
|||
* @brief Timeout = peer stopped sending audio.
|
||||
*/
|
||||
|
||||
GroupChatForm::GroupChatForm(Group* chatGroup)
|
||||
: GenericChatForm (chatGroup)
|
||||
GroupChatForm::GroupChatForm(Group* chatGroup, IChatLog& chatLog, IMessageDispatcher& messageDispatcher)
|
||||
: GenericChatForm(chatGroup, chatLog, messageDispatcher)
|
||||
, group(chatGroup)
|
||||
, inCall(false)
|
||||
{
|
||||
|
@ -118,8 +118,6 @@ GroupChatForm::GroupChatForm(Group* chatGroup)
|
|||
//nameLabel->setMinimumHeight(12);
|
||||
nusersLabel->setMinimumHeight(12);
|
||||
|
||||
connect(sendButton, SIGNAL(clicked()), this, SLOT(onSendTriggered()));
|
||||
connect(msgEdit, SIGNAL(enterPressed()), this, SLOT(onSendTriggered()));
|
||||
connect(msgEdit, &ChatTextEdit::tabPressed, tabber, &TabCompleter::complete);
|
||||
connect(msgEdit, &ChatTextEdit::keyPressed, tabber, &TabCompleter::reset);
|
||||
connect(headWidget, &ChatFormHeader::callTriggered, this, &GroupChatForm::onCallClicked);
|
||||
|
@ -143,31 +141,6 @@ GroupChatForm::~GroupChatForm()
|
|||
Translator::unregister(this);
|
||||
}
|
||||
|
||||
void GroupChatForm::onSendTriggered()
|
||||
{
|
||||
QString msg = msgEdit->toPlainText();
|
||||
if (msg.isEmpty())
|
||||
return;
|
||||
|
||||
msgEdit->setLastMessage(msg);
|
||||
msgEdit->clear();
|
||||
|
||||
if (group->getPeersCount() != 1) {
|
||||
if (msg.startsWith(ChatForm::ACTION_PREFIX, Qt::CaseInsensitive)) {
|
||||
msg.remove(0, ChatForm::ACTION_PREFIX.length());
|
||||
emit sendAction(group->getId(), msg);
|
||||
} else {
|
||||
emit sendMessage(group->getId(), msg);
|
||||
}
|
||||
} else {
|
||||
if (msg.startsWith(ChatForm::ACTION_PREFIX, Qt::CaseInsensitive))
|
||||
addSelfMessage(msg.mid(ChatForm::ACTION_PREFIX.length()), QDateTime::currentDateTime(),
|
||||
true);
|
||||
else
|
||||
addSelfMessage(msg, QDateTime::currentDateTime(), false);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupChatForm::onTitleChanged(const QString& author, const QString& title)
|
||||
{
|
||||
if (author.isEmpty()) {
|
||||
|
@ -179,33 +152,6 @@ void GroupChatForm::onTitleChanged(const QString& author, const QString& title)
|
|||
addSystemInfoMessage(message, ChatMessage::INFO, curTime);
|
||||
}
|
||||
|
||||
void GroupChatForm::searchInBegin(const QString& phrase, const ParameterSearch& parameter)
|
||||
{
|
||||
disableSearchText();
|
||||
|
||||
searchPoint = QPoint(1, -1);
|
||||
|
||||
if (parameter.period == PeriodSearch::WithTheFirst || parameter.period == PeriodSearch::AfterDate) {
|
||||
onSearchDown(phrase, parameter);
|
||||
} else {
|
||||
onSearchUp(phrase, parameter);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupChatForm::onSearchUp(const QString& phrase, const ParameterSearch& parameter)
|
||||
{
|
||||
if (!searchInText(phrase, parameter, SearchDirection::Up)) {
|
||||
emit messageNotFoundShow(SearchDirection::Up);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupChatForm::onSearchDown(const QString& phrase, const ParameterSearch& parameter)
|
||||
{
|
||||
if (!searchInText(phrase, parameter, SearchDirection::Down)) {
|
||||
emit messageNotFoundShow(SearchDirection::Down);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupChatForm::onScreenshotClicked()
|
||||
{
|
||||
// Unsupported
|
||||
|
|
|
@ -32,18 +32,19 @@ class TabCompleter;
|
|||
class FlowLayout;
|
||||
class QTimer;
|
||||
class GroupId;
|
||||
class IMessageDispatcher;
|
||||
class Message;
|
||||
|
||||
class GroupChatForm : public GenericChatForm
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GroupChatForm(Group* chatGroup);
|
||||
explicit GroupChatForm(Group* chatGroup, IChatLog& chatLog, IMessageDispatcher& messageDispatcher);
|
||||
~GroupChatForm();
|
||||
|
||||
void peerAudioPlaying(ToxPk peerPk);
|
||||
|
||||
private slots:
|
||||
void onSendTriggered() override;
|
||||
void onScreenshotClicked() override;
|
||||
void onAttachClicked() override;
|
||||
void onMicMuteToggle();
|
||||
|
@ -53,9 +54,6 @@ private slots:
|
|||
void onUserLeft(const ToxPk& user, const QString& name);
|
||||
void onPeerNameChanged(const ToxPk& peer, const QString& oldName, const QString& newName);
|
||||
void onTitleChanged(const QString& author, const QString& title);
|
||||
void searchInBegin(const QString& phrase, const ParameterSearch& parameter) override;
|
||||
void onSearchUp(const QString& phrase, const ParameterSearch& parameter) override;
|
||||
void onSearchDown(const QString& phrase, const ParameterSearch& parameter) override;
|
||||
void onLabelContextMenuRequested(const QPoint& localPos);
|
||||
|
||||
protected:
|
||||
|
@ -70,7 +68,6 @@ private:
|
|||
void retranslateUi();
|
||||
void updateUserCount(int numPeers);
|
||||
void updateUserNames();
|
||||
void sendJoinLeaveMessages();
|
||||
void leaveGroupCall();
|
||||
|
||||
private:
|
||||
|
|
|
@ -19,17 +19,18 @@
|
|||
|
||||
#include "loadhistorydialog.h"
|
||||
#include "ui_loadhistorydialog.h"
|
||||
#include "src/model/ichatlog.h"
|
||||
#include "src/nexus.h"
|
||||
#include "src/persistence/history.h"
|
||||
#include "src/persistence/profile.h"
|
||||
#include <QCalendarWidget>
|
||||
#include <QDate>
|
||||
#include <QTextCharFormat>
|
||||
#include <QCalendarWidget>
|
||||
|
||||
LoadHistoryDialog::LoadHistoryDialog(const ToxPk& friendPk, QWidget* parent)
|
||||
LoadHistoryDialog::LoadHistoryDialog(const IChatLog* chatLog, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, ui(new Ui::LoadHistoryDialog)
|
||||
, friendPk(friendPk)
|
||||
, chatLog(chatLog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
highlightDates(QDate::currentDate().year(), QDate::currentDate().month());
|
||||
|
@ -76,15 +77,17 @@ void LoadHistoryDialog::highlightDates(int year, int month)
|
|||
History* history = Nexus::getProfile()->getHistory();
|
||||
QDate monthStart(year, month, 1);
|
||||
QDate monthEnd(year, month + 1, 1);
|
||||
QList<History::DateMessages> counts =
|
||||
history->getChatHistoryCounts(this->friendPk, monthStart, monthEnd);
|
||||
|
||||
// Max 31 days in a month
|
||||
auto dateIdxs = chatLog->getDateIdxs(monthStart, 31);
|
||||
|
||||
QTextCharFormat format;
|
||||
format.setFontWeight(QFont::Bold);
|
||||
|
||||
QCalendarWidget* calendar = ui->fromDate;
|
||||
for (History::DateMessages p : counts) {
|
||||
format.setToolTip(tr("%1 messages").arg(p.count));
|
||||
calendar->setDateTextFormat(monthStart.addDays(p.offsetDays), format);
|
||||
for (const auto& item : dateIdxs) {
|
||||
if (item.date < monthEnd) {
|
||||
calendar->setDateTextFormat(item.date, format);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,13 +27,14 @@
|
|||
namespace Ui {
|
||||
class LoadHistoryDialog;
|
||||
}
|
||||
class IChatLog;
|
||||
|
||||
class LoadHistoryDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LoadHistoryDialog(const ToxPk& friendPk, QWidget* parent = nullptr);
|
||||
explicit LoadHistoryDialog(const IChatLog* chatLog, QWidget* parent = nullptr);
|
||||
explicit LoadHistoryDialog(QWidget* parent = nullptr);
|
||||
~LoadHistoryDialog();
|
||||
|
||||
|
@ -46,7 +47,7 @@ public slots:
|
|||
|
||||
private:
|
||||
Ui::LoadHistoryDialog* ui;
|
||||
const ToxPk friendPk;
|
||||
const IChatLog* chatLog;
|
||||
};
|
||||
|
||||
#endif // LOADHISTORYDIALOG_H
|
||||
|
|
|
@ -49,11 +49,13 @@
|
|||
#include "systemtrayicon.h"
|
||||
#include "form/groupchatform.h"
|
||||
#include "src/audio/audio.h"
|
||||
#include "src/chatlog/content/filetransferwidget.h"
|
||||
#include "src/core/core.h"
|
||||
#include "src/core/coreav.h"
|
||||
#include "src/core/corefile.h"
|
||||
#include "src/friendlist.h"
|
||||
#include "src/grouplist.h"
|
||||
#include "src/model/chathistory.h"
|
||||
#include "src/model/chatroom/friendchatroom.h"
|
||||
#include "src/model/chatroom/groupchatroom.h"
|
||||
#include "src/model/friend.h"
|
||||
|
@ -92,6 +94,48 @@ bool toxActivateEventHandler(const QByteArray&)
|
|||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Dangerous way to find out if a path is writable.
|
||||
* @param filepath Path to file which should be deleted.
|
||||
* @return True, if file writeable, false otherwise.
|
||||
*/
|
||||
bool tryRemoveFile(const QString& filepath)
|
||||
{
|
||||
QFile tmp(filepath);
|
||||
bool writable = tmp.open(QIODevice::WriteOnly);
|
||||
tmp.remove();
|
||||
return writable;
|
||||
}
|
||||
|
||||
void acceptFileTransfer(const ToxFile& file, const QString& path)
|
||||
{
|
||||
QString filepath;
|
||||
int number = 0;
|
||||
|
||||
QString suffix = QFileInfo(file.fileName).completeSuffix();
|
||||
QString base = QFileInfo(file.fileName).baseName();
|
||||
|
||||
do {
|
||||
filepath = QString("%1/%2%3.%4")
|
||||
.arg(path, base,
|
||||
number > 0 ? QString(" (%1)").arg(QString::number(number)) : QString(),
|
||||
suffix);
|
||||
++number;
|
||||
} while (QFileInfo(filepath).exists());
|
||||
|
||||
// Do not automatically accept the file-transfer if the path is not writable.
|
||||
// The user can still accept it manually.
|
||||
if (tryRemoveFile(filepath)) {
|
||||
CoreFile* coreFile = Core::getInstance()->getCoreFile();
|
||||
coreFile->acceptFileRecvRequest(file.friendId, file.fileNum, filepath);
|
||||
} else {
|
||||
qWarning() << "Cannot write to " << filepath;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Widget* Widget::instance{nullptr};
|
||||
|
||||
Widget::Widget(IAudioControl& audio, QWidget* parent)
|
||||
|
@ -251,6 +295,7 @@ void Widget::init()
|
|||
|
||||
connect(profile, &Profile::selfAvatarChanged, profileForm, &ProfileForm::onSelfAvatarLoaded);
|
||||
|
||||
connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::onFileReceiveRequested);
|
||||
connect(coreFile, &CoreFile::fileDownloadFinished, filesForm, &FilesForm::onFileDownloadComplete);
|
||||
connect(coreFile, &CoreFile::fileUploadFinished, filesForm, &FilesForm::onFileUploadComplete);
|
||||
connect(ui->addButton, &QPushButton::clicked, this, &Widget::onAddClicked);
|
||||
|
@ -271,6 +316,21 @@ void Widget::init()
|
|||
connect(filterDisplayGroup, &QActionGroup::triggered, this, &Widget::changeDisplayMode);
|
||||
connect(ui->friendList, &QWidget::customContextMenuRequested, this, &Widget::friendListContextMenu);
|
||||
|
||||
connect(coreFile, &CoreFile::fileSendStarted, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferAccepted, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferCancelled, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferFinished, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferPaused, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferInfo, this, &Widget::dispatchFile);
|
||||
connect(coreFile, &CoreFile::fileTransferRemotePausedUnpaused, this, &Widget::dispatchFileWithBool);
|
||||
connect(coreFile, &CoreFile::fileTransferBrokenUnbroken, this, &Widget::dispatchFileWithBool);
|
||||
connect(coreFile, &CoreFile::fileSendFailed, this, &Widget::dispatchFileSendFailed);
|
||||
// NOTE: We intentionally do not connect the fileUploadFinished and fileDownloadFinished signals
|
||||
// because they are duplicates of fileTransferFinished NOTE: We don't hook up the
|
||||
// fileNameChanged signal since it is only emitted before a fileReceiveRequest. We get the
|
||||
// initial request with the sanitized name so there is no work for us to do
|
||||
|
||||
// keyboard shortcuts
|
||||
new QShortcut(Qt::CTRL + Qt::Key_Q, this, SLOT(close()));
|
||||
new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab, this, SLOT(previousContact()));
|
||||
|
@ -904,10 +964,7 @@ void Widget::setUsername(const QString& username)
|
|||
Qt::convertFromPlainText(username, Qt::WhiteSpaceNormal)); // for overlength names
|
||||
}
|
||||
|
||||
QString sanename = username;
|
||||
sanename.remove(QRegExp("[\\t\\n\\v\\f\\r\\x0000]"));
|
||||
nameMention = QRegExp("\\b" + QRegExp::escape(username) + "\\b", Qt::CaseInsensitive);
|
||||
sanitizedNameMention = nameMention;
|
||||
sharedMessageProcessorParams.onUserNameSet(username);
|
||||
}
|
||||
|
||||
void Widget::onStatusMessageChanged(const QString& newStatusMessage)
|
||||
|
@ -924,13 +981,6 @@ void Widget::setStatusMessage(const QString& statusMessage)
|
|||
ui->statusLabel->setToolTip("<p style='white-space:pre'>" + statusMessage.toHtmlEscaped() + "</p>");
|
||||
}
|
||||
|
||||
void Widget::reloadHistory()
|
||||
{
|
||||
for (auto f : FriendList::getAllFriends()) {
|
||||
chatForms[f->getPublicKey()]->loadHistoryDefaultNum(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Plays a sound via the audioNotification AudioSink
|
||||
* @param sound Sound to play
|
||||
|
@ -989,6 +1039,60 @@ void Widget::onStopNotification()
|
|||
audioNotification.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Dispatches file to the appropriate chatlog and accepts the transfer if necessary
|
||||
*/
|
||||
void Widget::dispatchFile(ToxFile file)
|
||||
{
|
||||
const auto& friendId = FriendList::id2Key(file.friendId);
|
||||
Friend* f = FriendList::findFriend(friendId);
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto pk = f->getPublicKey();
|
||||
|
||||
if (file.status == ToxFile::INITIALIZING && file.direction == ToxFile::RECEIVING) {
|
||||
auto sender =
|
||||
(file.direction == ToxFile::SENDING) ? Core::getInstance()->getSelfPublicKey() : pk;
|
||||
|
||||
const Settings& settings = Settings::getInstance();
|
||||
QString autoAcceptDir = settings.getAutoAcceptDir(f->getPublicKey());
|
||||
|
||||
if (autoAcceptDir.isEmpty() && settings.getAutoSaveEnabled()) {
|
||||
autoAcceptDir = settings.getGlobalAutoAcceptDir();
|
||||
}
|
||||
|
||||
auto maxAutoAcceptSize = settings.getMaxAutoAcceptSize();
|
||||
bool autoAcceptSizeCheckPassed = maxAutoAcceptSize == 0 || maxAutoAcceptSize >= file.filesize;
|
||||
|
||||
if (!autoAcceptDir.isEmpty() && autoAcceptSizeCheckPassed) {
|
||||
acceptFileTransfer(file, autoAcceptDir);
|
||||
}
|
||||
}
|
||||
|
||||
const auto senderPk = (file.direction == ToxFile::SENDING) ? core->getSelfPublicKey() : pk;
|
||||
friendChatLogs[pk]->onFileUpdated(senderPk, file);
|
||||
}
|
||||
|
||||
void Widget::dispatchFileWithBool(ToxFile file, bool)
|
||||
{
|
||||
dispatchFile(file);
|
||||
}
|
||||
|
||||
void Widget::dispatchFileSendFailed(uint32_t friendId, const QString& fileName)
|
||||
{
|
||||
const auto& friendPk = FriendList::id2Key(friendId);
|
||||
|
||||
auto chatForm = chatForms.find(friendPk);
|
||||
if (chatForm == chatForms.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
chatForm.value()->addSystemInfoMessage(tr("Failed to send file \"%1\"").arg(fileName),
|
||||
ChatMessage::ERROR, QDateTime::currentDateTime());
|
||||
}
|
||||
|
||||
void Widget::onRejectCall(uint32_t friendId)
|
||||
{
|
||||
CoreAV* const av = core->getAv();
|
||||
|
@ -1006,8 +1110,20 @@ void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk)
|
|||
const auto compact = settings.getCompactLayout();
|
||||
auto widget = new FriendWidget(chatroom, compact);
|
||||
auto history = Nexus::getProfile()->getHistory();
|
||||
auto friendForm = new ChatForm(newfriend, history);
|
||||
|
||||
auto messageProcessor = MessageProcessor(sharedMessageProcessorParams);
|
||||
auto friendMessageDispatcher =
|
||||
std::make_shared<FriendMessageDispatcher>(*newfriend, std::move(messageProcessor), *core);
|
||||
|
||||
// Note: We do not have to connect the message dispatcher signals since
|
||||
// ChatHistory hooks them up in a very specific order
|
||||
auto chatHistory =
|
||||
std::make_shared<ChatHistory>(*newfriend, history, *core, Settings::getInstance(),
|
||||
*friendMessageDispatcher);
|
||||
auto friendForm = new ChatForm(newfriend, *chatHistory, *friendMessageDispatcher);
|
||||
|
||||
friendMessageDispatchers[friendPk] = friendMessageDispatcher;
|
||||
friendChatLogs[friendPk] = chatHistory;
|
||||
friendChatrooms[friendPk] = chatroom;
|
||||
friendWidgets[friendPk] = widget;
|
||||
chatForms[friendPk] = friendForm;
|
||||
|
@ -1021,6 +1137,20 @@ void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk)
|
|||
contactListWidget->addFriendWidget(widget, Status::Status::Offline,
|
||||
settings.getFriendCircleID(friendPk));
|
||||
|
||||
|
||||
auto notifyReceivedCallback = [this, friendPk](const ToxPk& author, const Message& message) {
|
||||
auto isTargeted = std::any_of(message.metadata.begin(), message.metadata.end(),
|
||||
[](MessageMetadata metadata) {
|
||||
return metadata.type == MessageMetadataType::selfMention;
|
||||
});
|
||||
newFriendMessageAlert(friendPk, message.content);
|
||||
};
|
||||
|
||||
auto notifyReceivedConnection =
|
||||
connect(friendMessageDispatcher.get(), &IMessageDispatcher::messageReceived,
|
||||
notifyReceivedCallback);
|
||||
|
||||
friendAlertConnections.insert(friendPk, notifyReceivedConnection);
|
||||
connect(newfriend, &Friend::aliasChanged, this, &Widget::onFriendAliasChanged);
|
||||
connect(newfriend, &Friend::displayedNameChanged, this, &Widget::onFriendDisplayedNameChanged);
|
||||
|
||||
|
@ -1228,19 +1358,18 @@ void Widget::onFriendMessageReceived(uint32_t friendnumber, const QString& messa
|
|||
return;
|
||||
}
|
||||
|
||||
QDateTime timestamp = QDateTime::currentDateTime();
|
||||
Profile* profile = Nexus::getProfile();
|
||||
if (profile->isHistoryEnabled()) {
|
||||
QString publicKey = f->getPublicKey().toString();
|
||||
QString name = f->getDisplayedName();
|
||||
QString text = message;
|
||||
if (isAction) {
|
||||
text = ChatForm::ACTION_PREFIX + text;
|
||||
}
|
||||
profile->getHistory()->addNewMessage(publicKey, text, publicKey, timestamp, true, name);
|
||||
friendMessageDispatchers[f->getPublicKey()]->onMessageReceived(isAction, message);
|
||||
}
|
||||
|
||||
void Widget::onReceiptReceived(int friendId, ReceiptNum receipt)
|
||||
{
|
||||
const auto& friendKey = FriendList::id2Key(friendId);
|
||||
Friend* f = FriendList::findFriend(friendKey);
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
newFriendMessageAlert(friendId, message);
|
||||
friendMessageDispatchers[f->getPublicKey()]->onReceiptReceived(receipt);
|
||||
}
|
||||
|
||||
void Widget::addFriendDialog(const Friend* frnd, ContentDialog* dialog)
|
||||
|
@ -1526,6 +1655,15 @@ void Widget::onFriendRequestReceived(const ToxPk& friendPk, const QString& messa
|
|||
}
|
||||
}
|
||||
|
||||
void Widget::onFileReceiveRequested(const ToxFile& file)
|
||||
{
|
||||
const ToxPk& friendPk = FriendList::id2Key(file.friendId);
|
||||
newFriendMessageAlert(friendPk,
|
||||
file.fileName + " ("
|
||||
+ FileTransferWidget::getHumanReadableSize(file.filesize) + ")",
|
||||
true, true);
|
||||
}
|
||||
|
||||
void Widget::updateFriendActivity(const Friend* frnd)
|
||||
{
|
||||
const ToxPk& pk = frnd->getPublicKey();
|
||||
|
@ -1560,6 +1698,8 @@ void Widget::removeFriend(Friend* f, bool fake)
|
|||
onAddClicked();
|
||||
}
|
||||
|
||||
friendAlertConnections.remove(friendPk);
|
||||
|
||||
contactListWidget->removeFriendWidget(widget);
|
||||
|
||||
ContentDialog* lastDialog = ContentDialogManager::getInstance()->getFriendDialog(friendPk);
|
||||
|
@ -1790,26 +1930,8 @@ void Widget::onGroupMessageReceived(int groupnumber, int peernumber, const QStri
|
|||
assert(g);
|
||||
|
||||
ToxPk author = core->getGroupPeerPk(groupnumber, peernumber);
|
||||
bool isSelf = author == core->getSelfId().getPublicKey();
|
||||
|
||||
if (settings.getBlackList().contains(author.toString())) {
|
||||
qDebug() << "onGroupMessageReceived: Filtered:" << author.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto mention = !core->getUsername().isEmpty()
|
||||
&& (message.contains(nameMention) || message.contains(sanitizedNameMention));
|
||||
const auto targeted = !isSelf && mention;
|
||||
const auto date = QDateTime::currentDateTime();
|
||||
auto form = groupChatForms[groupId].data();
|
||||
|
||||
if (targeted && !isAction) {
|
||||
form->addAlertMessage(author, message, date, true);
|
||||
} else {
|
||||
form->addMessage(author, message, date, isAction, true);
|
||||
}
|
||||
|
||||
newGroupMessageAlert(groupId, author, message, targeted || settings.getGroupAlwaysNotify());
|
||||
groupMessageDispatchers[groupId]->onMessageReceived(author, isAction, message);
|
||||
}
|
||||
|
||||
void Widget::onGroupPeerlistChanged(uint32_t groupnumber)
|
||||
|
@ -1902,6 +2024,8 @@ void Widget::removeGroup(Group* g, bool fake)
|
|||
onAddClicked();
|
||||
}
|
||||
|
||||
groupAlertConnections.remove(groupId);
|
||||
|
||||
contactListWidget->reDraw();
|
||||
}
|
||||
|
||||
|
@ -1928,7 +2052,37 @@ Group* Widget::createGroup(uint32_t groupnumber, const GroupId& groupId)
|
|||
|
||||
const auto compact = settings.getCompactLayout();
|
||||
auto widget = new GroupWidget(chatroom, compact);
|
||||
auto form = new GroupChatForm(newgroup);
|
||||
auto messageProcessor = MessageProcessor(sharedMessageProcessorParams);
|
||||
auto messageDispatcher =
|
||||
std::make_shared<GroupMessageDispatcher>(*newgroup, std::move(messageProcessor), *core,
|
||||
*core, Settings::getInstance());
|
||||
auto groupChatLog = std::make_shared<SessionChatLog>(*core);
|
||||
|
||||
connect(messageDispatcher.get(), &IMessageDispatcher::messageReceived, groupChatLog.get(),
|
||||
&SessionChatLog::onMessageReceived);
|
||||
connect(messageDispatcher.get(), &IMessageDispatcher::messageSent, groupChatLog.get(),
|
||||
&SessionChatLog::onMessageSent);
|
||||
connect(messageDispatcher.get(), &IMessageDispatcher::messageComplete, groupChatLog.get(),
|
||||
&SessionChatLog::onMessageComplete);
|
||||
|
||||
auto notifyReceivedCallback = [this, groupId](const ToxPk& author, const Message& message) {
|
||||
auto isTargeted = std::any_of(message.metadata.begin(), message.metadata.end(),
|
||||
[](MessageMetadata metadata) {
|
||||
return metadata.type == MessageMetadataType::selfMention;
|
||||
});
|
||||
newGroupMessageAlert(groupId, author, message.content,
|
||||
isTargeted || settings.getGroupAlwaysNotify());
|
||||
};
|
||||
|
||||
auto notifyReceivedConnection =
|
||||
connect(messageDispatcher.get(), &IMessageDispatcher::messageReceived, notifyReceivedCallback);
|
||||
groupAlertConnections.insert(groupId, notifyReceivedConnection);
|
||||
|
||||
auto form = new GroupChatForm(newgroup, *groupChatLog, *messageDispatcher);
|
||||
connect(&settings, &Settings::nameColorsChanged, form, &GenericChatForm::setColorizedNames);
|
||||
form->setColorizedNames(settings.getEnableGroupChatsColor());
|
||||
groupMessageDispatchers[groupId] = messageDispatcher;
|
||||
groupChatLogs[groupId] = groupChatLog;
|
||||
groupWidgets[groupId] = widget;
|
||||
groupChatrooms[groupId] = chatroom;
|
||||
groupChatForms[groupId] = QSharedPointer<GroupChatForm>(form);
|
||||
|
@ -1947,8 +2101,6 @@ Group* Widget::createGroup(uint32_t groupnumber, const GroupId& groupId)
|
|||
connect(widget, &GroupWidget::removeGroup, this, widgetRemoveGroup);
|
||||
connect(widget, &GroupWidget::middleMouseClicked, this, [=]() { removeGroup(groupId); });
|
||||
connect(widget, &GroupWidget::chatroomWidgetClicked, form, &ChatForm::focusInput);
|
||||
connect(form, &GroupChatForm::sendMessage, core, &Core::sendGroupMessage);
|
||||
connect(form, &GroupChatForm::sendAction, core, &Core::sendGroupAction);
|
||||
connect(newgroup, &Group::titleChangedByUser, this, &Widget::titleChangedByUser);
|
||||
connect(this, &Widget::changeGroupTitle, core, &Core::changeGroupTitle);
|
||||
connect(core, &Core::usernameSet, newgroup, &Group::setSelfName);
|
||||
|
@ -2220,7 +2372,7 @@ void Widget::clearAllReceipts()
|
|||
{
|
||||
QList<Friend*> frnds = FriendList::getAllFriends();
|
||||
for (Friend* f : frnds) {
|
||||
chatForms[f->getPublicKey()]->getOfflineMsgEngine()->removeAllMessages();
|
||||
friendMessageDispatchers[f->getPublicKey()]->clearOutgoingMessages();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,8 @@
|
|||
#include "src/core/toxfile.h"
|
||||
#include "src/core/toxid.h"
|
||||
#include "src/core/toxpk.h"
|
||||
#include "src/model/friendmessagedispatcher.h"
|
||||
#include "src/model/groupmessagedispatcher.h"
|
||||
#if DESKTOP_NOTIFICATIONS
|
||||
#include "src/platform/desktop_notifications/desktopnotify.h"
|
||||
#endif
|
||||
|
@ -79,6 +81,8 @@ class SystemTrayIcon;
|
|||
class VideoSurface;
|
||||
class UpdateCheck;
|
||||
class Settings;
|
||||
class IChatLog;
|
||||
class ChatHistory;
|
||||
|
||||
class Widget final : public QMainWindow
|
||||
{
|
||||
|
@ -135,7 +139,6 @@ public:
|
|||
static void confirmExecutableOpen(const QFileInfo& file);
|
||||
|
||||
void clearAllReceipts();
|
||||
void reloadHistory();
|
||||
|
||||
void reloadTheme();
|
||||
static inline QIcon prepareIcon(QString path, int w = 0, int h = 0);
|
||||
|
@ -167,7 +170,9 @@ public slots:
|
|||
void onFriendUsernameChanged(int friendId, const QString& username);
|
||||
void onFriendAliasChanged(const ToxPk& friendId, const QString& alias);
|
||||
void onFriendMessageReceived(uint32_t friendnumber, const QString& message, bool isAction);
|
||||
void onReceiptReceived(int friendId, ReceiptNum receipt);
|
||||
void onFriendRequestReceived(const ToxPk& friendPk, const QString& message);
|
||||
void onFileReceiveRequested(const ToxFile& file);
|
||||
void updateFriendActivity(const Friend* frnd);
|
||||
void onEmptyGroupCreated(uint32_t groupnumber, const GroupId& groupId, const QString& title);
|
||||
void onGroupJoined(int groupNum, const GroupId& groupId);
|
||||
|
@ -230,6 +235,9 @@ private slots:
|
|||
void incomingNotification(uint32_t friendId);
|
||||
void onRejectCall(uint32_t friendId);
|
||||
void onStopNotification();
|
||||
void dispatchFile(ToxFile file);
|
||||
void dispatchFileWithBool(ToxFile file, bool);
|
||||
void dispatchFileSendFailed(uint32_t friendId, const QString& fileName);
|
||||
|
||||
private:
|
||||
// QMainWindow overrides
|
||||
|
@ -305,7 +313,6 @@ private:
|
|||
bool notify(QObject* receiver, QEvent* event);
|
||||
bool autoAwayActive = false;
|
||||
QTimer* timer;
|
||||
QRegExp nameMention, sanitizedNameMention;
|
||||
bool eventFlag;
|
||||
bool eventIcon;
|
||||
bool wasMaximized = false;
|
||||
|
@ -319,14 +326,32 @@ private:
|
|||
Settings& settings;
|
||||
|
||||
QMap<ToxPk, FriendWidget*> friendWidgets;
|
||||
// Shared pointer because qmap copies stuff all over the place
|
||||
QMap<ToxPk, std::shared_ptr<FriendMessageDispatcher>> friendMessageDispatchers;
|
||||
// Stop gap method of linking our friend messages back to a group id.
|
||||
// Eventual goal is to have a notification manager that works on
|
||||
// Messages hooked up to message dispatchers but we aren't there
|
||||
// yet
|
||||
QMap<ToxPk, QMetaObject::Connection> friendAlertConnections;
|
||||
QMap<ToxPk, std::shared_ptr<ChatHistory>> friendChatLogs;
|
||||
QMap<ToxPk, std::shared_ptr<FriendChatroom>> friendChatrooms;
|
||||
QMap<ToxPk, ChatForm*> chatForms;
|
||||
|
||||
QMap<GroupId, GroupWidget*> groupWidgets;
|
||||
QMap<GroupId, std::shared_ptr<GroupMessageDispatcher>> groupMessageDispatchers;
|
||||
|
||||
// Stop gap method of linking our group messages back to a group id.
|
||||
// Eventual goal is to have a notification manager that works on
|
||||
// Messages hooked up to message dispatchers but we aren't there
|
||||
// yet
|
||||
QMap<GroupId, QMetaObject::Connection> groupAlertConnections;
|
||||
QMap<GroupId, std::shared_ptr<IChatLog>> groupChatLogs;
|
||||
QMap<GroupId, std::shared_ptr<GroupChatroom>> groupChatrooms;
|
||||
QMap<GroupId, QSharedPointer<GroupChatForm>> groupChatForms;
|
||||
Core* core = nullptr;
|
||||
|
||||
|
||||
MessageProcessor::SharedParams sharedMessageProcessorParams;
|
||||
#if DESKTOP_NOTIFICATIONS
|
||||
DesktopNotify notifier;
|
||||
#endif
|
||||
|
|
212
test/model/friendmessagedispatcher_test.cpp
Normal file
212
test/model/friendmessagedispatcher_test.cpp
Normal file
|
@ -0,0 +1,212 @@
|
|||
#include "src/core/icorefriendmessagesender.h"
|
||||
#include "src/model/friend.h"
|
||||
#include "src/model/friendmessagedispatcher.h"
|
||||
#include "src/model/message.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QtTest/QtTest>
|
||||
|
||||
#include <deque>
|
||||
|
||||
|
||||
class MockFriendMessageSender : public ICoreFriendMessageSender
|
||||
{
|
||||
public:
|
||||
bool sendAction(uint32_t friendId, const QString& action, ReceiptNum& receipt) override
|
||||
{
|
||||
if (canSend) {
|
||||
numSentActions++;
|
||||
receipt = receiptNum;
|
||||
receiptNum.get() += 1;
|
||||
}
|
||||
return canSend;
|
||||
}
|
||||
|
||||
bool sendMessage(uint32_t friendId, const QString& message, ReceiptNum& receipt) override
|
||||
{
|
||||
if (canSend) {
|
||||
numSentMessages++;
|
||||
receipt = receiptNum;
|
||||
receiptNum.get() += 1;
|
||||
}
|
||||
return canSend;
|
||||
}
|
||||
|
||||
bool canSend = true;
|
||||
ReceiptNum receiptNum{0};
|
||||
size_t numSentActions = 0;
|
||||
size_t numSentMessages = 0;
|
||||
};
|
||||
class TestFriendMessageDispatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TestFriendMessageDispatcher();
|
||||
|
||||
private slots:
|
||||
void init();
|
||||
void testSignals();
|
||||
void testMessageSending();
|
||||
void testOfflineMessages();
|
||||
void testFailedMessage();
|
||||
|
||||
void onMessageSent(DispatchedMessageId id, Message message)
|
||||
{
|
||||
auto it = outgoingMessages.find(id);
|
||||
QVERIFY(it == outgoingMessages.end());
|
||||
outgoingMessages.emplace(id, std::move(message));
|
||||
}
|
||||
|
||||
void onMessageComplete(DispatchedMessageId id)
|
||||
{
|
||||
auto it = outgoingMessages.find(id);
|
||||
QVERIFY(it != outgoingMessages.end());
|
||||
outgoingMessages.erase(it);
|
||||
}
|
||||
|
||||
void onMessageReceived(const ToxPk& sender, Message message)
|
||||
{
|
||||
receivedMessages.push_back(std::move(message));
|
||||
}
|
||||
|
||||
private:
|
||||
// All unique_ptrs to make construction/init() easier to manage
|
||||
std::unique_ptr<Friend> f;
|
||||
std::unique_ptr<MockFriendMessageSender> messageSender;
|
||||
std::unique_ptr<MessageProcessor::SharedParams> sharedProcessorParams;
|
||||
std::unique_ptr<MessageProcessor> messageProcessor;
|
||||
std::unique_ptr<FriendMessageDispatcher> friendMessageDispatcher;
|
||||
std::map<DispatchedMessageId, Message> outgoingMessages;
|
||||
std::deque<Message> receivedMessages;
|
||||
};
|
||||
|
||||
TestFriendMessageDispatcher::TestFriendMessageDispatcher() {}
|
||||
|
||||
/**
|
||||
* @brief Test initialization. Resets all member variables for a fresh test state
|
||||
*/
|
||||
void TestFriendMessageDispatcher::init()
|
||||
{
|
||||
f = std::unique_ptr<Friend>(new Friend(0, ToxPk()));
|
||||
f->setStatus(Status::Status::Online);
|
||||
messageSender = std::unique_ptr<MockFriendMessageSender>(new MockFriendMessageSender());
|
||||
sharedProcessorParams =
|
||||
std::unique_ptr<MessageProcessor::SharedParams>(new MessageProcessor::SharedParams());
|
||||
messageProcessor = std::unique_ptr<MessageProcessor>(new MessageProcessor(*sharedProcessorParams));
|
||||
friendMessageDispatcher = std::unique_ptr<FriendMessageDispatcher>(
|
||||
new FriendMessageDispatcher(*f, *messageProcessor, *messageSender));
|
||||
|
||||
connect(friendMessageDispatcher.get(), &FriendMessageDispatcher::messageSent, this,
|
||||
&TestFriendMessageDispatcher::onMessageSent);
|
||||
connect(friendMessageDispatcher.get(), &FriendMessageDispatcher::messageComplete, this,
|
||||
&TestFriendMessageDispatcher::onMessageComplete);
|
||||
connect(friendMessageDispatcher.get(), &FriendMessageDispatcher::messageReceived, this,
|
||||
&TestFriendMessageDispatcher::onMessageReceived);
|
||||
|
||||
outgoingMessages = std::map<DispatchedMessageId, Message>();
|
||||
receivedMessages = std::deque<Message>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests that the signals emitted by the dispatcher are all emitted at the correct times
|
||||
*/
|
||||
void TestFriendMessageDispatcher::testSignals()
|
||||
{
|
||||
auto startReceiptNum = messageSender->receiptNum;
|
||||
auto sentIds = friendMessageDispatcher->sendMessage(false, "test");
|
||||
auto endReceiptNum = messageSender->receiptNum;
|
||||
|
||||
// We should have received some message ids in our callbacks
|
||||
QVERIFY(sentIds.first == sentIds.second);
|
||||
QVERIFY(outgoingMessages.find(sentIds.first) != outgoingMessages.end());
|
||||
QVERIFY(startReceiptNum.get() != endReceiptNum.get());
|
||||
QVERIFY(outgoingMessages.size() == 1);
|
||||
|
||||
QVERIFY(outgoingMessages.begin()->second.isAction == false);
|
||||
QVERIFY(outgoingMessages.begin()->second.content == "test");
|
||||
|
||||
for (auto i = startReceiptNum; i < endReceiptNum; ++i.get()) {
|
||||
friendMessageDispatcher->onReceiptReceived(i);
|
||||
}
|
||||
|
||||
// If our completion ids were hooked up right this should be empty
|
||||
QVERIFY(outgoingMessages.empty());
|
||||
|
||||
// If signals are emitted correctly we should have one message in our received message buffer
|
||||
QVERIFY(receivedMessages.empty());
|
||||
friendMessageDispatcher->onMessageReceived(false, "test2");
|
||||
|
||||
QVERIFY(!receivedMessages.empty());
|
||||
QVERIFY(receivedMessages.front().isAction == false);
|
||||
QVERIFY(receivedMessages.front().content == "test2");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests that sent messages actually go through to core
|
||||
*/
|
||||
void TestFriendMessageDispatcher::testMessageSending()
|
||||
{
|
||||
friendMessageDispatcher->sendMessage(false, "Test");
|
||||
|
||||
QVERIFY(messageSender->numSentMessages == 1);
|
||||
QVERIFY(messageSender->numSentActions == 0);
|
||||
|
||||
friendMessageDispatcher->sendMessage(true, "Test");
|
||||
|
||||
QVERIFY(messageSender->numSentMessages == 1);
|
||||
QVERIFY(messageSender->numSentActions == 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests that messages dispatched while a friend is offline are sent later
|
||||
*/
|
||||
void TestFriendMessageDispatcher::testOfflineMessages()
|
||||
{
|
||||
f->setStatus(Status::Status::Offline);
|
||||
auto firstReceipt = messageSender->receiptNum;
|
||||
|
||||
friendMessageDispatcher->sendMessage(false, "test");
|
||||
friendMessageDispatcher->sendMessage(false, "test2");
|
||||
friendMessageDispatcher->sendMessage(true, "test3");
|
||||
|
||||
QVERIFY(messageSender->numSentActions == 0);
|
||||
QVERIFY(messageSender->numSentMessages == 0);
|
||||
QVERIFY(outgoingMessages.size() == 3);
|
||||
|
||||
f->setStatus(Status::Status::Online);
|
||||
|
||||
QVERIFY(messageSender->numSentActions == 1);
|
||||
QVERIFY(messageSender->numSentMessages == 2);
|
||||
QVERIFY(outgoingMessages.size() == 3);
|
||||
|
||||
auto lastReceipt = messageSender->receiptNum;
|
||||
for (auto i = firstReceipt; i < lastReceipt; ++i.get()) {
|
||||
friendMessageDispatcher->onReceiptReceived(i);
|
||||
}
|
||||
|
||||
QVERIFY(messageSender->numSentActions == 1);
|
||||
QVERIFY(messageSender->numSentMessages == 2);
|
||||
QVERIFY(outgoingMessages.size() == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests that messages that failed to send due to toxcore are resent later
|
||||
*/
|
||||
void TestFriendMessageDispatcher::testFailedMessage()
|
||||
{
|
||||
messageSender->canSend = false;
|
||||
|
||||
friendMessageDispatcher->sendMessage(false, "test");
|
||||
|
||||
QVERIFY(messageSender->numSentMessages == 0);
|
||||
|
||||
messageSender->canSend = true;
|
||||
f->setStatus(Status::Status::Offline);
|
||||
f->setStatus(Status::Status::Online);
|
||||
|
||||
QVERIFY(messageSender->numSentMessages == 1);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TestFriendMessageDispatcher)
|
||||
#include "friendmessagedispatcher_test.moc"
|
300
test/model/groupmessagedispatcher_test.cpp
Normal file
300
test/model/groupmessagedispatcher_test.cpp
Normal file
|
@ -0,0 +1,300 @@
|
|||
#include "src/core/icoregroupmessagesender.h"
|
||||
#include "src/model/group.h"
|
||||
#include "src/model/groupmessagedispatcher.h"
|
||||
#include "src/model/message.h"
|
||||
#include "src/persistence/settings.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QtTest/QtTest>
|
||||
|
||||
#include <deque>
|
||||
|
||||
|
||||
class MockGroupMessageSender : public ICoreGroupMessageSender
|
||||
{
|
||||
public:
|
||||
void sendGroupAction(int groupId, const QString& action) override
|
||||
{
|
||||
numSentActions++;
|
||||
}
|
||||
|
||||
void sendGroupMessage(int groupId, const QString& message) override
|
||||
{
|
||||
numSentMessages++;
|
||||
}
|
||||
|
||||
size_t numSentActions = 0;
|
||||
size_t numSentMessages = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock 1 peer at group number 0
|
||||
*/
|
||||
class MockGroupQuery : public ICoreGroupQuery
|
||||
{
|
||||
public:
|
||||
GroupId getGroupPersistentId(uint32_t groupNumber) const override
|
||||
{
|
||||
return GroupId(0);
|
||||
}
|
||||
|
||||
uint32_t getGroupNumberPeers(int groupId) const override
|
||||
{
|
||||
if (emptyGroup) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
QString getGroupPeerName(int groupId, int peerId) const override
|
||||
{
|
||||
return QString("peer") + peerId;
|
||||
}
|
||||
|
||||
ToxPk getGroupPeerPk(int groupId, int peerId) const override
|
||||
{
|
||||
uint8_t id[TOX_PUBLIC_KEY_SIZE] = {static_cast<uint8_t>(peerId)};
|
||||
return ToxPk(id);
|
||||
}
|
||||
|
||||
QStringList getGroupPeerNames(int groupId) const override
|
||||
{
|
||||
if (emptyGroup) {
|
||||
return QStringList({QString("me")});
|
||||
}
|
||||
return QStringList({QString("me"), QString("other")});
|
||||
}
|
||||
|
||||
bool getGroupAvEnabled(int groupId) const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void setAsEmptyGroup()
|
||||
{
|
||||
emptyGroup = true;
|
||||
}
|
||||
|
||||
void setAsFunctionalGroup()
|
||||
{
|
||||
emptyGroup = false;
|
||||
}
|
||||
|
||||
private:
|
||||
bool emptyGroup = false;
|
||||
};
|
||||
|
||||
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] = {0};
|
||||
return ToxPk(id);
|
||||
}
|
||||
|
||||
QString getUsername() const override
|
||||
{
|
||||
return "me";
|
||||
}
|
||||
};
|
||||
|
||||
class MockGroupSettings : public IGroupSettings
|
||||
{
|
||||
public:
|
||||
QStringList getBlackList() const override
|
||||
{
|
||||
return blacklist;
|
||||
}
|
||||
|
||||
void setBlackList(const QStringList& blist) override
|
||||
{
|
||||
blacklist = blist;
|
||||
}
|
||||
|
||||
bool getGroupAlwaysNotify() const override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void setGroupAlwaysNotify(bool newValue) override {}
|
||||
|
||||
private:
|
||||
QStringList blacklist;
|
||||
};
|
||||
|
||||
class TestGroupMessageDispatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TestGroupMessageDispatcher();
|
||||
|
||||
private slots:
|
||||
void init();
|
||||
void testSignals();
|
||||
void testMessageSending();
|
||||
void testEmptyGroup();
|
||||
void testSelfReceive();
|
||||
void testBlacklist();
|
||||
|
||||
void onMessageSent(DispatchedMessageId id, Message message)
|
||||
{
|
||||
auto it = outgoingMessages.find(id);
|
||||
QVERIFY(it == outgoingMessages.end());
|
||||
outgoingMessages.emplace(id);
|
||||
sentMessages.push_back(std::move(message));
|
||||
}
|
||||
|
||||
void onMessageComplete(DispatchedMessageId id)
|
||||
{
|
||||
auto it = outgoingMessages.find(id);
|
||||
QVERIFY(it != outgoingMessages.end());
|
||||
outgoingMessages.erase(it);
|
||||
}
|
||||
|
||||
void onMessageReceived(const ToxPk& sender, Message message)
|
||||
{
|
||||
receivedMessages.push_back(std::move(message));
|
||||
}
|
||||
|
||||
private:
|
||||
// All unique_ptrs to make construction/init() easier to manage
|
||||
std::unique_ptr<MockGroupSettings> groupSettings;
|
||||
std::unique_ptr<MockGroupQuery> groupQuery;
|
||||
std::unique_ptr<MockCoreIdHandler> coreIdHandler;
|
||||
std::unique_ptr<Group> g;
|
||||
std::unique_ptr<MockGroupMessageSender> messageSender;
|
||||
std::unique_ptr<MessageProcessor::SharedParams> sharedProcessorParams;
|
||||
std::unique_ptr<MessageProcessor> messageProcessor;
|
||||
std::unique_ptr<GroupMessageDispatcher> groupMessageDispatcher;
|
||||
std::set<DispatchedMessageId> outgoingMessages;
|
||||
std::deque<Message> sentMessages;
|
||||
std::deque<Message> receivedMessages;
|
||||
};
|
||||
|
||||
TestGroupMessageDispatcher::TestGroupMessageDispatcher() {}
|
||||
|
||||
/**
|
||||
* @brief Test initialization. Resets all members to initial state
|
||||
*/
|
||||
void TestGroupMessageDispatcher::init()
|
||||
{
|
||||
groupSettings = std::unique_ptr<MockGroupSettings>(new MockGroupSettings());
|
||||
groupQuery = std::unique_ptr<MockGroupQuery>(new MockGroupQuery());
|
||||
coreIdHandler = std::unique_ptr<MockCoreIdHandler>(new MockCoreIdHandler());
|
||||
g = std::unique_ptr<Group>(
|
||||
new Group(0, GroupId(0), "TestGroup", false, "me", *groupQuery, *coreIdHandler));
|
||||
messageSender = std::unique_ptr<MockGroupMessageSender>(new MockGroupMessageSender());
|
||||
sharedProcessorParams =
|
||||
std::unique_ptr<MessageProcessor::SharedParams>(new MessageProcessor::SharedParams());
|
||||
messageProcessor = std::unique_ptr<MessageProcessor>(new MessageProcessor(*sharedProcessorParams));
|
||||
groupMessageDispatcher = std::unique_ptr<GroupMessageDispatcher>(
|
||||
new GroupMessageDispatcher(*g, *messageProcessor, *coreIdHandler, *messageSender,
|
||||
*groupSettings));
|
||||
|
||||
connect(groupMessageDispatcher.get(), &GroupMessageDispatcher::messageSent, this,
|
||||
&TestGroupMessageDispatcher::onMessageSent);
|
||||
connect(groupMessageDispatcher.get(), &GroupMessageDispatcher::messageComplete, this,
|
||||
&TestGroupMessageDispatcher::onMessageComplete);
|
||||
connect(groupMessageDispatcher.get(), &GroupMessageDispatcher::messageReceived, this,
|
||||
&TestGroupMessageDispatcher::onMessageReceived);
|
||||
|
||||
outgoingMessages = std::set<DispatchedMessageId>();
|
||||
sentMessages = std::deque<Message>();
|
||||
receivedMessages = std::deque<Message>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests that the signals emitted by the dispatcher are all emitted at the correct times
|
||||
*/
|
||||
void TestGroupMessageDispatcher::testSignals()
|
||||
{
|
||||
groupMessageDispatcher->sendMessage(false, "test");
|
||||
|
||||
// For groups we pair our sent and completed signals since we have no receiver reports
|
||||
QVERIFY(outgoingMessages.size() == 0);
|
||||
QVERIFY(!sentMessages.empty());
|
||||
QVERIFY(sentMessages.front().isAction == false);
|
||||
QVERIFY(sentMessages.front().content == "test");
|
||||
|
||||
// If signals are emitted correctly we should have one message in our received message buffer
|
||||
QVERIFY(receivedMessages.empty());
|
||||
groupMessageDispatcher->onMessageReceived(ToxPk(), false, "test2");
|
||||
|
||||
QVERIFY(!receivedMessages.empty());
|
||||
QVERIFY(receivedMessages.front().isAction == false);
|
||||
QVERIFY(receivedMessages.front().content == "test2");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests that sent messages actually go through to core
|
||||
*/
|
||||
void TestGroupMessageDispatcher::testMessageSending()
|
||||
{
|
||||
groupMessageDispatcher->sendMessage(false, "Test");
|
||||
|
||||
QVERIFY(messageSender->numSentMessages == 1);
|
||||
QVERIFY(messageSender->numSentActions == 0);
|
||||
|
||||
groupMessageDispatcher->sendMessage(true, "Test");
|
||||
|
||||
QVERIFY(messageSender->numSentMessages == 1);
|
||||
QVERIFY(messageSender->numSentActions == 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests that if we are the only member in a group we do _not_ send messages to core. Toxcore
|
||||
* isn't too happy if we send messages and we're the only one in the group
|
||||
*/
|
||||
void TestGroupMessageDispatcher::testEmptyGroup()
|
||||
{
|
||||
groupQuery->setAsEmptyGroup();
|
||||
g->regeneratePeerList();
|
||||
|
||||
groupMessageDispatcher->sendMessage(false, "Test");
|
||||
groupMessageDispatcher->sendMessage(true, "Test");
|
||||
|
||||
QVERIFY(messageSender->numSentMessages == 0);
|
||||
QVERIFY(messageSender->numSentActions == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests that we do not emit any signals if we receive a message from ourself. Toxcore will send us back messages we sent
|
||||
*/
|
||||
void TestGroupMessageDispatcher::testSelfReceive()
|
||||
{
|
||||
uint8_t selfId[TOX_PUBLIC_KEY_SIZE] = {0};
|
||||
groupMessageDispatcher->onMessageReceived(ToxPk(selfId), false, "Test");
|
||||
QVERIFY(receivedMessages.size() == 0);
|
||||
|
||||
uint8_t id[TOX_PUBLIC_KEY_SIZE] = {1};
|
||||
groupMessageDispatcher->onMessageReceived(ToxPk(id), false, "Test");
|
||||
QVERIFY(receivedMessages.size() == 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests that messages from blacklisted peers do not get propogated from the dispatcher
|
||||
*/
|
||||
void TestGroupMessageDispatcher::testBlacklist()
|
||||
{
|
||||
uint8_t id[TOX_PUBLIC_KEY_SIZE] = {1};
|
||||
auto otherPk = ToxPk(id);
|
||||
groupMessageDispatcher->onMessageReceived(otherPk, false, "Test");
|
||||
QVERIFY(receivedMessages.size() == 1);
|
||||
|
||||
groupSettings->setBlackList({otherPk.toString()});
|
||||
groupMessageDispatcher->onMessageReceived(otherPk, false, "Test");
|
||||
QVERIFY(receivedMessages.size() == 1);
|
||||
}
|
||||
|
||||
// Cannot be guiless due to a settings instance in GroupMessageDispatcher
|
||||
QTEST_GUILESS_MAIN(TestGroupMessageDispatcher)
|
||||
#include "groupmessagedispatcher_test.moc"
|
113
test/model/messageprocessor_test.cpp
Normal file
113
test/model/messageprocessor_test.cpp
Normal file
|
@ -0,0 +1,113 @@
|
|||
#include "src/model/message.h"
|
||||
|
||||
#include <tox/tox.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QtTest/QtTest>
|
||||
|
||||
namespace {
|
||||
bool messageHasSelfMention(const Message& message)
|
||||
{
|
||||
return std::any_of(message.metadata.begin(), message.metadata.end(), [](MessageMetadata meta) {
|
||||
return meta.type == MessageMetadataType::selfMention;
|
||||
});
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class TestMessageProcessor : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TestMessageProcessor(){};
|
||||
|
||||
private slots:
|
||||
void testSelfMention();
|
||||
void testOutgoingMessage();
|
||||
void testIncomingMessage();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @brief Tests detection of username
|
||||
*/
|
||||
void TestMessageProcessor::testSelfMention()
|
||||
{
|
||||
MessageProcessor::SharedParams sharedParams;
|
||||
sharedParams.onUserNameSet("MyUserName");
|
||||
|
||||
auto messageProcessor = MessageProcessor(sharedParams);
|
||||
messageProcessor.enableMentions();
|
||||
|
||||
// Using my name should match
|
||||
auto processedMessage = messageProcessor.processIncomingMessage(false, "MyUserName hi");
|
||||
QVERIFY(messageHasSelfMention(processedMessage));
|
||||
|
||||
// Action messages should match too
|
||||
processedMessage = messageProcessor.processIncomingMessage(true, "MyUserName hi");
|
||||
QVERIFY(messageHasSelfMention(processedMessage));
|
||||
|
||||
// Too much text shouldn't match
|
||||
processedMessage = messageProcessor.processIncomingMessage(false, "MyUserName2");
|
||||
QVERIFY(!messageHasSelfMention(processedMessage));
|
||||
|
||||
// Unless it's a colon
|
||||
processedMessage = messageProcessor.processIncomingMessage(false, "MyUserName: test");
|
||||
QVERIFY(messageHasSelfMention(processedMessage));
|
||||
|
||||
// Too little text shouldn't match
|
||||
processedMessage = messageProcessor.processIncomingMessage(false, "MyUser");
|
||||
QVERIFY(!messageHasSelfMention(processedMessage));
|
||||
|
||||
// The regex should be case insensitive
|
||||
processedMessage = messageProcessor.processIncomingMessage(false, "myusername hi");
|
||||
QVERIFY(messageHasSelfMention(processedMessage));
|
||||
|
||||
// New user name changes should be detected
|
||||
sharedParams.onUserNameSet("NewUserName");
|
||||
processedMessage = messageProcessor.processIncomingMessage(false, "NewUserName: hi");
|
||||
QVERIFY(messageHasSelfMention(processedMessage));
|
||||
|
||||
// Special characters should be removed
|
||||
sharedParams.onUserNameSet("New\nUserName");
|
||||
processedMessage = messageProcessor.processIncomingMessage(false, "NewUserName: hi");
|
||||
QVERIFY(messageHasSelfMention(processedMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests behavior of the processor for outgoing messages
|
||||
*/
|
||||
void TestMessageProcessor::testOutgoingMessage()
|
||||
{
|
||||
auto sharedParams = MessageProcessor::SharedParams();
|
||||
auto messageProcessor = MessageProcessor(sharedParams);
|
||||
|
||||
QString testStr;
|
||||
|
||||
for (size_t i = 0; i < tox_max_message_length() + 50; ++i) {
|
||||
testStr += "a";
|
||||
}
|
||||
|
||||
auto messages = messageProcessor.processOutgoingMessage(false, testStr);
|
||||
|
||||
// The message processor should split our messages
|
||||
QVERIFY(messages.size() == 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests behavior of the processor for incoming messages
|
||||
*/
|
||||
void TestMessageProcessor::testIncomingMessage()
|
||||
{
|
||||
// Nothing too special happening on the incoming side if we aren't looking for self mentions
|
||||
auto sharedParams = MessageProcessor::SharedParams();
|
||||
auto messageProcessor = MessageProcessor(sharedParams);
|
||||
auto message = messageProcessor.processIncomingMessage(false, "test");
|
||||
|
||||
QVERIFY(message.isAction == false);
|
||||
QVERIFY(message.content == "test");
|
||||
QVERIFY(message.timestamp.isValid());
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TestMessageProcessor)
|
||||
#include "messageprocessor_test.moc"
|
117
test/model/sessionchatlog_test.cpp
Normal file
117
test/model/sessionchatlog_test.cpp
Normal 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"
|
Loading…
Reference in New Issue
Block a user