mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
refactor(chatform): Remove message handling logic from gui path
Functional changes * Offline messages are still sent when the chat log is cleared * Spinner now does not wait for history to be complete, just a receipt from our friend * Export chat and load chat history are now available in group chats * Merged save chat log and export chat log * Note that we lost the info messages in the process NonFunctional Changes * FileTransferWidget slots only called for correct file * Settings::getEnableGroupChatsColor now embedded in GenericChatForm::colorizeNames * Settings::setEnableGroupChatscolor now emits signal connected to GenericChatForm::setColorizedNames to keep state in sync * Chatlog history not reloaded on setPassword() * I am pretty sure this had no purpose * Removed a lot of responsibility from ChatForm * History moved to ChatHistory implementation of IChatLog * OfflineMsgEngine moved to FriendMessageDispatcher * Export chat and load chat history moved to GenericChatLog * Backed by IChatLog so can be used generically * Message processing moved to FriendMessageDispatcher * The action of sending files to coreFile is still handled by ChatForm, but displaying of the sent messages is done through IChatLog -> GenericChatForm * Search moved to ChatHistory/SessionChatLog * All insertion of chat log elements should be handled by GenericChatForm now * Removed overlapping responsibilities from GroupChatForm * Search and message sending goes through ichatlog/messagedispatcher too * Lots of search functionality pushed down into IChatLog * Some of the file logic was moved into Widget. This is mostly to avoid scope increase of this PR even further. * History APIs removed that were no longer used
This commit is contained in:
parent
e607e6ecb4
commit
ed514d7166
|
@ -88,7 +88,7 @@ ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QSt
|
||||||
authorFont.setBold(true);
|
authorFont.setBold(true);
|
||||||
|
|
||||||
QColor color = Style::getColor(Style::MainText);
|
QColor color = Style::getColor(Style::MainText);
|
||||||
if (colorizeName && Settings::getInstance().getEnableGroupChatsColor()) {
|
if (colorizeName) {
|
||||||
QByteArray hash = QCryptographicHash::hash((sender.toUtf8()), QCryptographicHash::Sha256);
|
QByteArray hash = QCryptographicHash::hash((sender.toUtf8()), QCryptographicHash::Sha256);
|
||||||
quint8 *data = (quint8*)hash.data();
|
quint8 *data = (quint8*)hash.data();
|
||||||
|
|
||||||
|
|
|
@ -88,20 +88,6 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file)
|
||||||
|
|
||||||
CoreFile* coreFile = Core::getInstance()->getCoreFile();
|
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->leftButton, &QPushButton::clicked, this, &FileTransferWidget::onLeftButtonClicked);
|
||||||
connect(ui->rightButton, &QPushButton::clicked, this, &FileTransferWidget::onRightButtonClicked);
|
connect(ui->rightButton, &QPushButton::clicked, this, &FileTransferWidget::onRightButtonClicked);
|
||||||
connect(ui->previewButton, &QPushButton::clicked, this,
|
connect(ui->previewButton, &QPushButton::clicked, this,
|
||||||
|
@ -133,30 +119,9 @@ bool FileTransferWidget::tryRemoveFile(const QString& filepath)
|
||||||
return writable;
|
return writable;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileTransferWidget::autoAcceptTransfer(const QString& path)
|
void FileTransferWidget::onFileTransferUpdate(ToxFile file)
|
||||||
{
|
{
|
||||||
QString filepath;
|
updateWidget(file);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileTransferWidget::isActive() const
|
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)
|
QString FileTransferWidget::getHumanReadableSize(qint64 size)
|
||||||
{
|
{
|
||||||
static const char* suffix[] = {"B", "kiB", "MiB", "GiB", "TiB"};
|
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)
|
void FileTransferWidget::updateWidget(ToxFile const& file)
|
||||||
{
|
{
|
||||||
if (fileInfo != file) {
|
assert(file == fileInfo);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInfo = file;
|
fileInfo = file;
|
||||||
|
|
||||||
|
|
|
@ -42,19 +42,10 @@ class FileTransferWidget : public QWidget
|
||||||
public:
|
public:
|
||||||
explicit FileTransferWidget(QWidget* parent, ToxFile file);
|
explicit FileTransferWidget(QWidget* parent, ToxFile file);
|
||||||
virtual ~FileTransferWidget();
|
virtual ~FileTransferWidget();
|
||||||
void autoAcceptTransfer(const QString& path);
|
|
||||||
bool isActive() const;
|
bool isActive() const;
|
||||||
static QString getHumanReadableSize(qint64 size);
|
static QString getHumanReadableSize(qint64 size);
|
||||||
|
|
||||||
protected slots:
|
void onFileTransferUpdate(ToxFile file);
|
||||||
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);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void updateWidgetColor(ToxFile const& file);
|
void updateWidgetColor(ToxFile const& file);
|
||||||
|
|
|
@ -116,7 +116,7 @@ bool AboutFriend::isHistoryExistence()
|
||||||
History* const history = Nexus::getProfile()->getHistory();
|
History* const history = Nexus::getProfile()->getHistory();
|
||||||
if (history) {
|
if (history) {
|
||||||
const ToxPk pk = f->getPublicKey();
|
const ToxPk pk = f->getPublicKey();
|
||||||
return history->isHistoryExistence(pk.toString());
|
return history->historyExists(pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -208,6 +208,7 @@ void Nexus::showMainGUI()
|
||||||
connect(core, &Core::friendStatusMessageChanged, widget, &Widget::onFriendStatusMessageChanged);
|
connect(core, &Core::friendStatusMessageChanged, widget, &Widget::onFriendStatusMessageChanged);
|
||||||
connect(core, &Core::friendRequestReceived, widget, &Widget::onFriendRequestReceived);
|
connect(core, &Core::friendRequestReceived, widget, &Widget::onFriendRequestReceived);
|
||||||
connect(core, &Core::friendMessageReceived, widget, &Widget::onFriendMessageReceived);
|
connect(core, &Core::friendMessageReceived, widget, &Widget::onFriendMessageReceived);
|
||||||
|
connect(core, &Core::receiptRecieved, widget, &Widget::onReceiptReceived);
|
||||||
connect(core, &Core::groupInviteReceived, widget, &Widget::onGroupInviteReceived);
|
connect(core, &Core::groupInviteReceived, widget, &Widget::onGroupInviteReceived);
|
||||||
connect(core, &Core::groupMessageReceived, widget, &Widget::onGroupMessageReceived);
|
connect(core, &Core::groupMessageReceived, widget, &Widget::onGroupMessageReceived);
|
||||||
connect(core, &Core::groupPeerlistChanged, widget, &Widget::onGroupPeerlistChanged);
|
connect(core, &Core::groupPeerlistChanged, widget, &Widget::onGroupPeerlistChanged);
|
||||||
|
|
|
@ -224,9 +224,9 @@ bool History::isValid()
|
||||||
* @param friendPk
|
* @param friendPk
|
||||||
* @return True if has, false otherwise.
|
* @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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -521,78 +521,6 @@ void History::setFileFinished(const QString& fileId, bool success, const QString
|
||||||
fileInfos.remove(fileId);
|
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)
|
|
||||||
{
|
|
||||||
if (!isValid()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return getChatHistory(friendPk, from, to, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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)
|
|
||||||
{
|
|
||||||
if (!isValid()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return getChatHistory(friendPk, QDateTime::fromMSecsSinceEpoch(0), QDateTime::currentDateTime(),
|
|
||||||
NUM_MESSAGES_DEFAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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);
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t History::getNumMessagesForFriend(const ToxPk& friendPk)
|
size_t History::getNumMessagesForFriend(const ToxPk& friendPk)
|
||||||
{
|
{
|
||||||
return getNumMessagesForFriendBeforeDate(friendPk,
|
return getNumMessagesForFriendBeforeDate(friendPk,
|
||||||
|
@ -857,31 +785,6 @@ QList<History::DateIdx> History::getNumMessagesForFriendBeforeDateBoundaries(con
|
||||||
return dateIdxs;
|
return dateIdxs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief get start date of correspondence
|
|
||||||
* @param friendPk Friend public key
|
|
||||||
* @return start date of correspondence
|
|
||||||
*/
|
|
||||||
QDateTime History::getStartDateChatHistory(const QString& friendPk)
|
|
||||||
{
|
|
||||||
QDateTime result;
|
|
||||||
auto rowCallback = [&result](const QVector<QVariant>& row) {
|
|
||||||
result = QDateTime::fromMSecsSinceEpoch(row[0].toLongLong());
|
|
||||||
};
|
|
||||||
|
|
||||||
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({queryText, rowCallback});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Marks a message as sent.
|
* @brief Marks a message as sent.
|
||||||
* Removing message from the faux-offline pending messages list.
|
* Removing message from the faux-offline pending messages list.
|
||||||
|
@ -896,73 +799,3 @@ void History::markAsSent(RowId messageId)
|
||||||
|
|
||||||
db->execLater(QString("DELETE FROM faux_offline_pending WHERE id=%1;").arg(messageId.get()));
|
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,12 +143,6 @@ public:
|
||||||
HistMessageContent content;
|
HistMessageContent content;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DateMessages
|
|
||||||
{
|
|
||||||
uint offsetDays;
|
|
||||||
uint count;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct DateIdx
|
struct DateIdx
|
||||||
{
|
{
|
||||||
QDate date;
|
QDate date;
|
||||||
|
@ -161,7 +155,7 @@ public:
|
||||||
|
|
||||||
bool isValid();
|
bool isValid();
|
||||||
|
|
||||||
bool isHistoryExistence(const QString& friendPk);
|
bool historyExists(const ToxPk& friendPk);
|
||||||
|
|
||||||
void eraseHistory();
|
void eraseHistory();
|
||||||
void removeFriendHistory(const QString& friendPk);
|
void removeFriendHistory(const QString& friendPk);
|
||||||
|
@ -174,11 +168,6 @@ public:
|
||||||
const QString& sender, const QDateTime& time, QString const& dispName);
|
const QString& sender, const QDateTime& time, QString const& dispName);
|
||||||
|
|
||||||
void setFileFinished(const QString& fileId, bool success, const QString& filePath, const QByteArray& fileHash);
|
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 getNumMessagesForFriend(const ToxPk& friendPk);
|
||||||
size_t getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const QDateTime& date);
|
size_t getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const QDateTime& date);
|
||||||
QList<HistMessage> getMessagesForFriend(const ToxPk& friendPk, size_t firstIdx, size_t lastIdx);
|
QList<HistMessage> getMessagesForFriend(const ToxPk& friendPk, size_t firstIdx, size_t lastIdx);
|
||||||
|
@ -187,7 +176,6 @@ public:
|
||||||
const ParameterSearch& parameter);
|
const ParameterSearch& parameter);
|
||||||
QList<DateIdx> getNumMessagesForFriendBeforeDateBoundaries(const ToxPk& friendPk,
|
QList<DateIdx> getNumMessagesForFriendBeforeDateBoundaries(const ToxPk& friendPk,
|
||||||
const QDate& from, size_t maxNum);
|
const QDate& from, size_t maxNum);
|
||||||
QDateTime getStartDateChatHistory(const QString& friendPk);
|
|
||||||
|
|
||||||
void markAsSent(RowId messageId);
|
void markAsSent(RowId messageId);
|
||||||
|
|
||||||
|
@ -206,9 +194,6 @@ private slots:
|
||||||
void onFileInserted(RowId dbId, QString fileId);
|
void onFileInserted(RowId dbId, QString fileId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QList<HistMessage> getChatHistory(const QString& friendPk, const QDateTime& from,
|
|
||||||
const QDateTime& to, int numMessages);
|
|
||||||
|
|
||||||
static RawDatabase::Query generateFileFinished(RowId fileId, bool success,
|
static RawDatabase::Query generateFileFinished(RowId fileId, bool success,
|
||||||
const QString& filePath, const QByteArray& fileHash);
|
const QString& filePath, const QByteArray& fileHash);
|
||||||
std::shared_ptr<RawDatabase> db;
|
std::shared_ptr<RawDatabase> db;
|
||||||
|
|
|
@ -853,8 +853,6 @@ QString Profile::setPassword(const QString& newPassword)
|
||||||
"password.");
|
"password.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Nexus::getDesktopGUI()->reloadHistory();
|
|
||||||
|
|
||||||
QByteArray avatar = loadAvatarData(core->getSelfId().getPublicKey());
|
QByteArray avatar = loadAvatarData(core->getSelfId().getPublicKey());
|
||||||
saveAvatar(core->getSelfId().getPublicKey(), avatar);
|
saveAvatar(core->getSelfId().getPublicKey(), avatar);
|
||||||
|
|
||||||
|
|
|
@ -2331,7 +2331,10 @@ void Settings::setAutoLogin(bool state)
|
||||||
void Settings::setEnableGroupChatsColor(bool state)
|
void Settings::setEnableGroupChatsColor(bool state)
|
||||||
{
|
{
|
||||||
QMutexLocker locker{&bigLock};
|
QMutexLocker locker{&bigLock};
|
||||||
nameColors = state;
|
if (state != nameColors) {
|
||||||
|
nameColors = state;
|
||||||
|
emit nameColorsChanged(nameColors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::getEnableGroupChatsColor() const
|
bool Settings::getEnableGroupChatsColor() const
|
||||||
|
|
|
@ -201,6 +201,7 @@ signals:
|
||||||
|
|
||||||
// GUI
|
// GUI
|
||||||
void autoLoginChanged(bool enabled);
|
void autoLoginChanged(bool enabled);
|
||||||
|
void nameColorsChanged(bool enabled);
|
||||||
void separateWindowChanged(bool enabled);
|
void separateWindowChanged(bool enabled);
|
||||||
void showSystemTrayChanged(bool enabled);
|
void showSystemTrayChanged(bool enabled);
|
||||||
bool minimizeOnCloseChanged(bool enabled);
|
bool minimizeOnCloseChanged(bool enabled);
|
||||||
|
|
|
@ -102,36 +102,11 @@ namespace
|
||||||
|
|
||||||
return cD + res.sprintf("%02ds", seconds);
|
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
|
} // namespace
|
||||||
|
|
||||||
ChatForm::ChatForm(Friend* chatFriend, History* history)
|
ChatForm::ChatForm(Friend* chatFriend, IChatLog& chatLog, IMessageDispatcher& messageDispatcher)
|
||||||
: GenericChatForm(chatFriend)
|
: GenericChatForm(chatFriend, chatLog, messageDispatcher)
|
||||||
, f(chatFriend)
|
, f(chatFriend)
|
||||||
, history{history}
|
|
||||||
, isTyping{false}
|
, isTyping{false}
|
||||||
, lastCallIsVideo{false}
|
, lastCallIsVideo{false}
|
||||||
{
|
{
|
||||||
|
@ -146,8 +121,6 @@ ChatForm::ChatForm(Friend* chatFriend, History* history)
|
||||||
statusMessageLabel->setTextFormat(Qt::PlainText);
|
statusMessageLabel->setTextFormat(Qt::PlainText);
|
||||||
statusMessageLabel->setContextMenuPolicy(Qt::CustomContextMenu);
|
statusMessageLabel->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
|
||||||
offlineEngine = new OfflineMsgEngine(f, Core::getInstance());
|
|
||||||
|
|
||||||
typingTimer.setSingleShot(true);
|
typingTimer.setSingleShot(true);
|
||||||
|
|
||||||
callDurationTimer = nullptr;
|
callDurationTimer = nullptr;
|
||||||
|
@ -161,29 +134,18 @@ ChatForm::ChatForm(Friend* chatFriend, History* history)
|
||||||
headWidget->addWidget(callDuration, 1, Qt::AlignCenter);
|
headWidget->addWidget(callDuration, 1, Qt::AlignCenter);
|
||||||
callDuration->hide();
|
callDuration->hide();
|
||||||
|
|
||||||
loadHistoryAction = menu.addAction(QString(), this, SLOT(onLoadHistory()));
|
|
||||||
copyStatusAction = statusMessageMenu.addAction(QString(), this, SLOT(onCopyStatusMessage()));
|
copyStatusAction = statusMessageMenu.addAction(QString(), this, SLOT(onCopyStatusMessage()));
|
||||||
|
|
||||||
exportChatAction =
|
|
||||||
menu.addAction(QIcon::fromTheme("document-save"), QString(), this, SLOT(onExportChat()));
|
|
||||||
|
|
||||||
const Core* core = Core::getInstance();
|
const Core* core = Core::getInstance();
|
||||||
const Profile* profile = Nexus::getProfile();
|
const Profile* profile = Nexus::getProfile();
|
||||||
const CoreFile* coreFile = core->getCoreFile();
|
const CoreFile* coreFile = core->getCoreFile();
|
||||||
connect(coreFile, &CoreFile::fileReceiveRequested, this, &ChatForm::onFileRecvRequest);
|
|
||||||
connect(profile, &Profile::friendAvatarChanged, this, &ChatForm::onAvatarChanged);
|
connect(profile, &Profile::friendAvatarChanged, this, &ChatForm::onAvatarChanged);
|
||||||
connect(coreFile, &CoreFile::fileSendStarted, this, &ChatForm::startFileSend);
|
connect(coreFile, &CoreFile::fileReceiveRequested, this, &ChatForm::updateFriendActivityForFile);
|
||||||
connect(coreFile, &CoreFile::fileTransferFinished, this, &ChatForm::onFileTransferFinished);
|
connect(coreFile, &CoreFile::fileSendStarted, this, &ChatForm::updateFriendActivityForFile);
|
||||||
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(core, &Core::friendTypingChanged, this, &ChatForm::onFriendTypingChanged);
|
connect(core, &Core::friendTypingChanged, this, &ChatForm::onFriendTypingChanged);
|
||||||
connect(core, &Core::friendStatusChanged, this, &ChatForm::onFriendStatusChanged);
|
connect(core, &Core::friendStatusChanged, this, &ChatForm::onFriendStatusChanged);
|
||||||
connect(coreFile, &CoreFile::fileNameChanged, this, &ChatForm::onFileNameChanged);
|
connect(coreFile, &CoreFile::fileNameChanged, this, &ChatForm::onFileNameChanged);
|
||||||
|
|
||||||
|
|
||||||
const CoreAV* av = core->getAv();
|
const CoreAV* av = core->getAv();
|
||||||
connect(av, &CoreAV::avInvite, this, &ChatForm::onAvInvite);
|
connect(av, &CoreAV::avInvite, this, &ChatForm::onAvInvite);
|
||||||
connect(av, &CoreAV::avStart, this, &ChatForm::onAvStart);
|
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::micMuteToggle, this, &ChatForm::onMicMuteToggle);
|
||||||
connect(headWidget, &ChatFormHeader::volMuteToggle, this, &ChatForm::onVolMuteToggle);
|
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::textChanged, this, &ChatForm::onTextEditChanged);
|
||||||
connect(msgEdit, &ChatTextEdit::pasteImage, this, &ChatForm::sendImage);
|
connect(msgEdit, &ChatTextEdit::pasteImage, this, &ChatForm::sendImage);
|
||||||
connect(statusMessageLabel, &CroppingLabel::customContextMenuRequested, this,
|
connect(statusMessageLabel, &CroppingLabel::customContextMenuRequested, this,
|
||||||
|
@ -218,9 +181,6 @@ ChatForm::ChatForm(Friend* chatFriend, History* history)
|
||||||
connect(headWidget, &ChatFormHeader::callRejected, this, &ChatForm::onRejectCallTriggered);
|
connect(headWidget, &ChatFormHeader::callRejected, this, &ChatForm::onRejectCallTriggered);
|
||||||
|
|
||||||
updateCallButtons();
|
updateCallButtons();
|
||||||
if (Nexus::getProfile()->isHistoryEnabled()) {
|
|
||||||
loadHistoryDefaultNum(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setAcceptDrops(true);
|
setAcceptDrops(true);
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
|
@ -232,7 +192,6 @@ ChatForm::~ChatForm()
|
||||||
Translator::unregister(this);
|
Translator::unregister(this);
|
||||||
delete netcam;
|
delete netcam;
|
||||||
netcam = nullptr;
|
netcam = nullptr;
|
||||||
delete offlineEngine;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatForm::setStatusMessage(const QString& newMessage)
|
void ChatForm::setStatusMessage(const QString& newMessage)
|
||||||
|
@ -242,11 +201,22 @@ void ChatForm::setStatusMessage(const QString& newMessage)
|
||||||
statusMessageLabel->setToolTip(Qt::convertFromPlainText(newMessage, Qt::WhiteSpaceNormal));
|
statusMessageLabel->setToolTip(Qt::convertFromPlainText(newMessage, Qt::WhiteSpaceNormal));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatForm::onSendTriggered()
|
void ChatForm::updateFriendActivity()
|
||||||
{
|
{
|
||||||
SendMessageStr(msgEdit->toPlainText());
|
// TODO: Remove Widget::getInstance()
|
||||||
msgEdit->clear();
|
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)
|
void ChatForm::onFileNameChanged(const ToxPk& friendPk)
|
||||||
{
|
{
|
||||||
if (friendPk != f->getPublicKey()) {
|
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)
|
void ChatForm::onAvInvite(uint32_t friendId, bool video)
|
||||||
{
|
{
|
||||||
if (friendId != f->getId()) {
|
if (friendId != f->getId()) {
|
||||||
|
@ -554,95 +429,6 @@ void ChatForm::onVolMuteToggle()
|
||||||
updateMuteVolButton();
|
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)
|
void ChatForm::onFriendStatusChanged(uint32_t friendId, Status::Status status)
|
||||||
{
|
{
|
||||||
// Disable call buttons if friend is offline
|
// Disable call buttons if friend is offline
|
||||||
|
@ -653,8 +439,6 @@ void ChatForm::onFriendStatusChanged(uint32_t friendId, Status::Status status)
|
||||||
if (!f->isOnline()) {
|
if (!f->isOnline()) {
|
||||||
// Hide the "is typing" message when a friend goes offline
|
// Hide the "is typing" message when a friend goes offline
|
||||||
setFriendTyping(false);
|
setFriendTyping(false);
|
||||||
} else {
|
|
||||||
offlineEngine->deliverOfflineMsgs();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCallButtons();
|
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)
|
void ChatForm::onStatusMessage(const QString& message)
|
||||||
{
|
{
|
||||||
if (sender() == f) {
|
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)
|
void ChatForm::onAvatarChanged(const ToxPk& friendPk, const QPixmap& pic)
|
||||||
{
|
{
|
||||||
if (friendPk != f->getPublicKey()) {
|
if (friendPk != f->getPublicKey()) {
|
||||||
|
@ -751,7 +518,8 @@ void ChatForm::dropEvent(QDropEvent* ev)
|
||||||
QString urlString = url.toString();
|
QString urlString = url.toString();
|
||||||
if (url.isValid() && !url.isLocalFile()
|
if (url.isValid() && !url.isLocalFile()
|
||||||
&& urlString.length() < static_cast<int>(tox_max_message_length())) {
|
&& urlString.length() < static_cast<int>(tox_max_message_length())) {
|
||||||
SendMessageStr(urlString);
|
messageDispatcher.sendMessage(false, urlString);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -783,190 +551,6 @@ void ChatForm::dropEvent(QDropEvent* ev)
|
||||||
void ChatForm::clearChatArea()
|
void ChatForm::clearChatArea()
|
||||||
{
|
{
|
||||||
GenericChatForm::clearChatArea(/* confirm = */ false, /* inform = */ true);
|
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()
|
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)
|
void ChatForm::insertChatMessage(ChatMessage::Ptr msg)
|
||||||
{
|
{
|
||||||
GenericChatForm::insertChatMessage(msg);
|
GenericChatForm::insertChatMessage(msg);
|
||||||
|
@ -1131,107 +702,9 @@ void ChatForm::hideEvent(QHideEvent* event)
|
||||||
GenericChatForm::hideEvent(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()
|
void ChatForm::retranslateUi()
|
||||||
{
|
{
|
||||||
loadHistoryAction->setText(tr("Load chat history..."));
|
|
||||||
copyStatusAction->setText(tr("Copy"));
|
copyStatusAction->setText(tr("Copy"));
|
||||||
exportChatAction->setText(tr("Export to file"));
|
|
||||||
|
|
||||||
updateMuteMicButton();
|
updateMuteMicButton();
|
||||||
updateMuteVolButton();
|
updateMuteVolButton();
|
||||||
|
@ -1240,38 +713,3 @@ void ChatForm::retranslateUi()
|
||||||
netcam->setShowMessages(chatWidget->isVisible());
|
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 "genericchatform.h"
|
||||||
#include "src/core/core.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/model/status.h"
|
||||||
|
#include "src/persistence/history.h"
|
||||||
#include "src/widget/tool/screenshotgrabber.h"
|
#include "src/widget/tool/screenshotgrabber.h"
|
||||||
|
|
||||||
class CallConfirmWidget;
|
class CallConfirmWidget;
|
||||||
|
@ -44,15 +46,11 @@ class ChatForm : public GenericChatForm
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
ChatForm(Friend* chatFriend, History* history);
|
ChatForm(Friend* chatFriend, IChatLog& chatLog, IMessageDispatcher& messageDispatcher);
|
||||||
~ChatForm();
|
~ChatForm();
|
||||||
void setStatusMessage(const QString& newMessage);
|
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);
|
void setFriendTyping(bool isTyping);
|
||||||
OfflineMsgEngine* getOfflineMsgEngine();
|
|
||||||
|
|
||||||
virtual void show(ContentLayout* contentLayout) final override;
|
virtual void show(ContentLayout* contentLayout) final override;
|
||||||
virtual void reloadTheme() final override;
|
virtual void reloadTheme() final override;
|
||||||
|
@ -69,11 +67,6 @@ signals:
|
||||||
void acceptCall(uint32_t friendId);
|
void acceptCall(uint32_t friendId);
|
||||||
|
|
||||||
public slots:
|
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 onAvInvite(uint32_t friendId, bool video);
|
||||||
void onAvStart(uint32_t friendId, bool video);
|
void onAvStart(uint32_t friendId, bool video);
|
||||||
void onAvEnd(uint32_t friendId, bool error);
|
void onAvEnd(uint32_t friendId, bool error);
|
||||||
|
@ -81,13 +74,9 @@ public slots:
|
||||||
void onFileNameChanged(const ToxPk& friendPk);
|
void onFileNameChanged(const ToxPk& friendPk);
|
||||||
void clearChatArea();
|
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:
|
private slots:
|
||||||
void onSendTriggered() override;
|
void updateFriendActivity();
|
||||||
|
void updateFriendActivityForFile(const ToxFile& file);
|
||||||
void onAttachClicked() override;
|
void onAttachClicked() override;
|
||||||
void onScreenshotClicked() override;
|
void onScreenshotClicked() override;
|
||||||
|
|
||||||
|
@ -99,47 +88,16 @@ private slots:
|
||||||
void onMicMuteToggle();
|
void onMicMuteToggle();
|
||||||
void onVolMuteToggle();
|
void onVolMuteToggle();
|
||||||
|
|
||||||
void onFileSendFailed(uint32_t friendId, const QString& fname);
|
|
||||||
void onFriendStatusChanged(quint32 friendId, Status::Status status);
|
void onFriendStatusChanged(quint32 friendId, Status::Status status);
|
||||||
void onFriendTypingChanged(quint32 friendId, bool isTyping);
|
void onFriendTypingChanged(quint32 friendId, bool isTyping);
|
||||||
void onFriendNameChanged(const QString& name);
|
void onFriendNameChanged(const QString& name);
|
||||||
void onFriendMessageReceived(quint32 friendId, const QString& message, bool isAction);
|
|
||||||
void onStatusMessage(const QString& message);
|
void onStatusMessage(const QString& message);
|
||||||
void onReceiptReceived(quint32 friendId, ReceiptNum receipt);
|
|
||||||
void onLoadHistory();
|
|
||||||
void onUpdateTime();
|
void onUpdateTime();
|
||||||
void sendImage(const QPixmap& pixmap);
|
void sendImage(const QPixmap& pixmap);
|
||||||
void doScreenshot();
|
void doScreenshot();
|
||||||
void onCopyStatusMessage();
|
void onCopyStatusMessage();
|
||||||
void onExportChat();
|
|
||||||
|
|
||||||
private:
|
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 updateMuteMicButton();
|
||||||
void updateMuteVolButton();
|
void updateMuteVolButton();
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
|
@ -147,8 +105,6 @@ private:
|
||||||
void startCounter();
|
void startCounter();
|
||||||
void stopCounter(bool error = false);
|
void stopCounter(bool error = false);
|
||||||
void updateCallButtons();
|
void updateCallButtons();
|
||||||
void SendMessageStr(QString msg);
|
|
||||||
bool loadHistory(const QString& phrase, const ParameterSearch& parameter);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
GenericNetCamView* createNetcam() final override;
|
GenericNetCamView* createNetcam() final override;
|
||||||
|
@ -166,13 +122,7 @@ private:
|
||||||
QTimer* callDurationTimer;
|
QTimer* callDurationTimer;
|
||||||
QTimer typingTimer;
|
QTimer typingTimer;
|
||||||
QElapsedTimer timeElapsed;
|
QElapsedTimer timeElapsed;
|
||||||
OfflineMsgEngine* offlineEngine;
|
|
||||||
QAction* loadHistoryAction;
|
|
||||||
QAction* copyStatusAction;
|
QAction* copyStatusAction;
|
||||||
QAction* exportChatAction;
|
|
||||||
|
|
||||||
History* history;
|
|
||||||
QHash<uint, FileTransferInstance*> ftransWidgets;
|
|
||||||
bool isTyping;
|
bool isTyping;
|
||||||
bool lastCallIsVideo;
|
bool lastCallIsVideo;
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,13 +19,15 @@
|
||||||
|
|
||||||
#include "genericchatform.h"
|
#include "genericchatform.h"
|
||||||
|
|
||||||
|
#include "src/chatlog/chatlinecontentproxy.h"
|
||||||
#include "src/chatlog/chatlog.h"
|
#include "src/chatlog/chatlog.h"
|
||||||
|
#include "src/chatlog/content/filetransferwidget.h"
|
||||||
#include "src/chatlog/content/timestamp.h"
|
#include "src/chatlog/content/timestamp.h"
|
||||||
#include "src/core/core.h"
|
#include "src/core/core.h"
|
||||||
#include "src/model/friend.h"
|
|
||||||
#include "src/friendlist.h"
|
#include "src/friendlist.h"
|
||||||
#include "src/model/group.h"
|
|
||||||
#include "src/grouplist.h"
|
#include "src/grouplist.h"
|
||||||
|
#include "src/model/friend.h"
|
||||||
|
#include "src/model/group.h"
|
||||||
#include "src/persistence/settings.h"
|
#include "src/persistence/settings.h"
|
||||||
#include "src/persistence/smileypack.h"
|
#include "src/persistence/smileypack.h"
|
||||||
#include "src/video/genericnetcamview.h"
|
#include "src/video/genericnetcamview.h"
|
||||||
|
@ -34,13 +36,15 @@
|
||||||
#include "src/widget/contentdialogmanager.h"
|
#include "src/widget/contentdialogmanager.h"
|
||||||
#include "src/widget/contentlayout.h"
|
#include "src/widget/contentlayout.h"
|
||||||
#include "src/widget/emoticonswidget.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/maskablepixmapwidget.h"
|
||||||
|
#include "src/widget/searchform.h"
|
||||||
#include "src/widget/style.h"
|
#include "src/widget/style.h"
|
||||||
#include "src/widget/tool/chattextedit.h"
|
#include "src/widget/tool/chattextedit.h"
|
||||||
#include "src/widget/tool/flyoutoverlaywidget.h"
|
#include "src/widget/tool/flyoutoverlaywidget.h"
|
||||||
#include "src/widget/translator.h"
|
#include "src/widget/translator.h"
|
||||||
#include "src/widget/widget.h"
|
#include "src/widget/widget.h"
|
||||||
#include "src/widget/searchform.h"
|
|
||||||
|
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
|
@ -127,13 +131,129 @@ QPushButton* createButton(const QString& name, T* self, Fun onClickSlot)
|
||||||
return btn;
|
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)
|
: QWidget(parent, Qt::Window)
|
||||||
, audioInputFlag(false)
|
, audioInputFlag(false)
|
||||||
, audioOutputFlag(false)
|
, audioOutputFlag(false)
|
||||||
, searchAfterLoadHistory(false)
|
, chatLog(chatLog)
|
||||||
|
, messageDispatcher(messageDispatcher)
|
||||||
{
|
{
|
||||||
curRow = 0;
|
curRow = 0;
|
||||||
headWidget = new ChatFormHeader();
|
headWidget = new ChatFormHeader();
|
||||||
|
@ -219,8 +339,6 @@ GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent)
|
||||||
menu.addActions(chatWidget->actions());
|
menu.addActions(chatWidget->actions());
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
saveChatAction = menu.addAction(QIcon::fromTheme("document-save"), QString(),
|
|
||||||
this, SLOT(onSaveLogClicked()));
|
|
||||||
clearAction = menu.addAction(QIcon::fromTheme("edit-clear"), QString(),
|
clearAction = menu.addAction(QIcon::fromTheme("edit-clear"), QString(),
|
||||||
this, SLOT(clearChatArea()),
|
this, SLOT(clearChatArea()),
|
||||||
QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_L));
|
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()));
|
copyLinkAction = menu.addAction(QIcon(), QString(), this, SLOT(copyLink()));
|
||||||
menu.addSeparator();
|
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,
|
connect(chatWidget, &ChatLog::customContextMenuRequested, this,
|
||||||
&GenericChatForm::onChatContextMenuRequested);
|
&GenericChatForm::onChatContextMenuRequested);
|
||||||
connect(chatWidget, &ChatLog::firstVisibleLineChanged, this, &GenericChatForm::updateShowDateInfo);
|
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(searchForm, &SearchForm::visibleChanged, this, &GenericChatForm::onSearchTriggered);
|
||||||
connect(this, &GenericChatForm::messageNotFoundShow, searchForm, &SearchForm::showMessageNotFound);
|
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();
|
reloadTheme();
|
||||||
|
|
||||||
|
@ -254,6 +378,11 @@ GenericChatForm::GenericChatForm(const Contact* contact, QWidget* parent)
|
||||||
// update header on name/title change
|
// update header on name/title change
|
||||||
connect(contact, &Contact::displayedNameChanged, this, &GenericChatForm::setName);
|
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;
|
netcam = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,101 +502,52 @@ void GenericChatForm::onChatContextMenuRequested(QPoint pos)
|
||||||
menu.exec(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
|
* @brief Show, is it needed to hide message author name or not
|
||||||
* @param messageAuthor Author of the sent message
|
* @param messageAuthor Author of the sent message
|
||||||
* @oaran messageTime DateTime of the sent message
|
* @oaran messageTime DateTime of the sent message
|
||||||
* @return True if it's needed to hide name, false otherwise
|
* @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);
|
// If the previous message is not rendered we should show the name
|
||||||
return messageAuthor == previousId && messagesTimeDiff < chatWidget->repNameAfter;
|
// regardless of other constraints
|
||||||
}
|
auto itemBefore = messages.find(idx - 1);
|
||||||
|
if (itemBefore == messages.end()) {
|
||||||
/**
|
return false;
|
||||||
* @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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatMessage::Ptr msg;
|
const auto& prevItem = chatLog.at(idx - 1);
|
||||||
if (isAction) {
|
const auto& currentItem = chatLog.at(idx);
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
previousId = author;
|
// Always show the * in the name field for action messages
|
||||||
prevMsgDateTime = now;
|
if (currentItem.getContentType() == ChatLogItem::ContentType::message
|
||||||
|
&& currentItem.getContentAsMessage().message.isAction) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSent) {
|
qint64 messagesTimeDiff = prevItem.getTimestamp().secsTo(currentItem.getTimestamp());
|
||||||
msg->markAsSent(dt);
|
return currentItem.getSender() == prevItem.getSender()
|
||||||
}
|
&& messagesTimeDiff < chatWidget->repNameAfter;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenericChatForm::onEmoteButtonClicked()
|
void GenericChatForm::onEmoteButtonClicked()
|
||||||
|
@ -498,37 +578,6 @@ void GenericChatForm::onEmoteInsertRequested(QString str)
|
||||||
msgEdit->setFocus(); // refocus so that we can continue typing
|
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()
|
void GenericChatForm::onCopyLogClicked()
|
||||||
{
|
{
|
||||||
chatWidget->copySelectedText();
|
chatWidget->copySelectedText();
|
||||||
|
@ -549,21 +598,22 @@ void GenericChatForm::onChatMessageFontChanged(const QFont& font)
|
||||||
+ fontToCss(font, "QTextEdit"));
|
+ fontToCss(font, "QTextEdit"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GenericChatForm::setColorizedNames(bool enable)
|
||||||
|
{
|
||||||
|
colorizeNames = enable;
|
||||||
|
}
|
||||||
|
|
||||||
void GenericChatForm::addSystemInfoMessage(const QString& message, ChatMessage::SystemMessageType type,
|
void GenericChatForm::addSystemInfoMessage(const QString& message, ChatMessage::SystemMessageType type,
|
||||||
const QDateTime& datetime)
|
const QDateTime& datetime)
|
||||||
{
|
{
|
||||||
if (getLatestTime().date() != QDate::currentDate()) {
|
|
||||||
addSystemDateMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
previousId = ToxPk();
|
previousId = ToxPk();
|
||||||
insertChatMessage(ChatMessage::createChatInfoMessage(message, type, datetime));
|
insertChatMessage(ChatMessage::createChatInfoMessage(message, type, datetime));
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenericChatForm::addSystemDateMessage()
|
void GenericChatForm::addSystemDateMessage(const QDate& date)
|
||||||
{
|
{
|
||||||
const Settings& s = Settings::getInstance();
|
const Settings& s = Settings::getInstance();
|
||||||
QString dateText = QDate::currentDate().toString(s.getDateFormat());
|
QString dateText = date.toString(s.getDateFormat());
|
||||||
|
|
||||||
previousId = ToxPk();
|
previousId = ToxPk();
|
||||||
insertChatMessage(ChatMessage::createChatInfoMessage(dateText, ChatMessage::INFO, QDateTime()));
|
insertChatMessage(ChatMessage::createChatInfoMessage(dateText, ChatMessage::INFO, QDateTime()));
|
||||||
|
@ -584,258 +634,16 @@ QDateTime GenericChatForm::getTime(const ChatLine::Ptr &chatLine) const
|
||||||
return QDateTime();
|
return QDateTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void GenericChatForm::disableSearchText()
|
void GenericChatForm::disableSearchText()
|
||||||
{
|
{
|
||||||
if (searchPoint != QPoint(1, -1)) {
|
auto msgIt = messages.find(searchPos.logIdx);
|
||||||
QVector<ChatLine::Ptr> lines = chatWidget->getLines();
|
if (msgIt != messages.end()) {
|
||||||
int numLines = lines.size();
|
auto text = qobject_cast<Text*>(msgIt->second->getContent(1));
|
||||||
int index = numLines - searchPoint.x();
|
text->deselectText();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
void GenericChatForm::clearChatArea()
|
||||||
{
|
{
|
||||||
clearChatArea(/* confirm = */ true, /* inform = */ true);
|
clearChatArea(/* confirm = */ true, /* inform = */ true);
|
||||||
|
@ -859,7 +667,7 @@ void GenericChatForm::clearChatArea(bool confirm, bool inform)
|
||||||
if (inform)
|
if (inform)
|
||||||
addSystemInfoMessage(tr("Cleared"), ChatMessage::INFO, QDateTime::currentDateTime());
|
addSystemInfoMessage(tr("Cleared"), ChatMessage::INFO, QDateTime::currentDateTime());
|
||||||
|
|
||||||
earliestMessage = QDateTime(); // null
|
messages.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenericChatForm::onSelectAllClicked()
|
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()
|
void GenericChatForm::onSearchTriggered()
|
||||||
{
|
{
|
||||||
if (searchForm->isHidden()) {
|
if (searchForm->isHidden()) {
|
||||||
searchForm->removeSearchPhrase();
|
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 {
|
} else {
|
||||||
searchPoint = QPoint(1, -1);
|
onSearchUp(phrase, parameter);
|
||||||
searchAfterLoadHistory = false;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
void GenericChatForm::retranslateUi()
|
||||||
{
|
{
|
||||||
sendButton->setToolTip(tr("Send message"));
|
sendButton->setToolTip(tr("Send message"));
|
||||||
emoteButton->setToolTip(tr("Smileys"));
|
emoteButton->setToolTip(tr("Smileys"));
|
||||||
fileButton->setToolTip(tr("Send file(s)"));
|
fileButton->setToolTip(tr("Send file(s)"));
|
||||||
screenshotButton->setToolTip(tr("Send a screenshot"));
|
screenshotButton->setToolTip(tr("Send a screenshot"));
|
||||||
saveChatAction->setText(tr("Save chat log"));
|
|
||||||
clearAction->setText(tr("Clear displayed messages"));
|
clearAction->setText(tr("Clear displayed messages"));
|
||||||
quoteAction->setText(tr("Quote selected text"));
|
quoteAction->setText(tr("Quote selected text"));
|
||||||
copyLinkAction->setText(tr("Copy link address"));
|
copyLinkAction->setText(tr("Copy link address"));
|
||||||
searchAction->setText(tr("Search in text"));
|
searchAction->setText(tr("Search in text"));
|
||||||
|
loadHistoryAction->setText(tr("Load chat history..."));
|
||||||
|
exportChatAction->setText(tr("Export to file"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void GenericChatForm::showNetcam()
|
void GenericChatForm::showNetcam()
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
|
|
||||||
#include "src/chatlog/chatmessage.h"
|
#include "src/chatlog/chatmessage.h"
|
||||||
#include "src/core/toxpk.h"
|
#include "src/core/toxpk.h"
|
||||||
|
#include "src/model/ichatlog.h"
|
||||||
#include "src/widget/searchtypes.h"
|
#include "src/widget/searchtypes.h"
|
||||||
|
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
@ -51,6 +52,9 @@ class QSplitter;
|
||||||
class QToolButton;
|
class QToolButton;
|
||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
|
|
||||||
|
class IMessageDispatcher;
|
||||||
|
class Message;
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class MainWindow;
|
class MainWindow;
|
||||||
}
|
}
|
||||||
|
@ -65,7 +69,8 @@ class GenericChatForm : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit GenericChatForm(const Contact* contact, QWidget* parent = nullptr);
|
GenericChatForm(const Contact* contact, IChatLog& chatLog,
|
||||||
|
IMessageDispatcher& messageDispatcher, QWidget* parent = nullptr);
|
||||||
~GenericChatForm() override;
|
~GenericChatForm() override;
|
||||||
|
|
||||||
void setName(const QString& newName);
|
void setName(const QString& newName);
|
||||||
|
@ -75,34 +80,28 @@ public:
|
||||||
virtual void show(ContentLayout* contentLayout);
|
virtual void show(ContentLayout* contentLayout);
|
||||||
virtual void reloadTheme();
|
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,
|
void addSystemInfoMessage(const QString& message, ChatMessage::SystemMessageType type,
|
||||||
const QDateTime& datetime);
|
const QDateTime& datetime);
|
||||||
void addAlertMessage(const ToxPk& author, const QString& message, const QDateTime& datetime, bool colorizeName = false);
|
|
||||||
static QString resolveToxPk(const ToxPk& pk);
|
static QString resolveToxPk(const ToxPk& pk);
|
||||||
QDateTime getLatestTime() const;
|
QDateTime getLatestTime() const;
|
||||||
QDateTime getFirstTime() const;
|
QDateTime getFirstTime() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void sendMessage(uint32_t, QString);
|
|
||||||
void sendAction(uint32_t, QString);
|
|
||||||
void messageInserted();
|
void messageInserted();
|
||||||
void messageNotFoundShow(SearchDirection direction);
|
void messageNotFoundShow(SearchDirection direction);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void focusInput();
|
void focusInput();
|
||||||
void onChatMessageFontChanged(const QFont& font);
|
void onChatMessageFontChanged(const QFont& font);
|
||||||
|
void setColorizedNames(bool enable);
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
void onChatContextMenuRequested(QPoint pos);
|
void onChatContextMenuRequested(QPoint pos);
|
||||||
virtual void onScreenshotClicked() = 0;
|
virtual void onScreenshotClicked() = 0;
|
||||||
virtual void onSendTriggered() = 0;
|
void onSendTriggered();
|
||||||
virtual void onAttachClicked() = 0;
|
virtual void onAttachClicked() = 0;
|
||||||
void onEmoteButtonClicked();
|
void onEmoteButtonClicked();
|
||||||
void onEmoteInsertRequested(QString str);
|
void onEmoteInsertRequested(QString str);
|
||||||
void onSaveLogClicked();
|
|
||||||
void onCopyLogClicked();
|
void onCopyLogClicked();
|
||||||
void clearChatArea();
|
void clearChatArea();
|
||||||
void clearChatArea(bool confirm, bool inform);
|
void clearChatArea(bool confirm, bool inform);
|
||||||
|
@ -113,26 +112,29 @@ protected slots:
|
||||||
void onSplitterMoved(int pos, int index);
|
void onSplitterMoved(int pos, int index);
|
||||||
void quoteSelectedText();
|
void quoteSelectedText();
|
||||||
void copyLink();
|
void copyLink();
|
||||||
|
void onLoadHistory();
|
||||||
|
void onExportChat();
|
||||||
void searchFormShow();
|
void searchFormShow();
|
||||||
void onSearchTriggered();
|
void onSearchTriggered();
|
||||||
void updateShowDateInfo(const ChatLine::Ptr& line);
|
void updateShowDateInfo(const ChatLine::Ptr& line);
|
||||||
|
|
||||||
virtual void searchInBegin(const QString& phrase, const ParameterSearch& parameter) = 0;
|
void searchInBegin(const QString& phrase, const ParameterSearch& parameter);
|
||||||
virtual void onSearchUp(const QString& phrase, const ParameterSearch& parameter) = 0;
|
void onSearchUp(const QString& phrase, const ParameterSearch& parameter);
|
||||||
virtual void onSearchDown(const QString& phrase, const ParameterSearch& parameter) = 0;
|
void onSearchDown(const QString& phrase, const ParameterSearch& parameter);
|
||||||
void onContinueSearch();
|
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:
|
private:
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
void addSystemDateMessage();
|
void addSystemDateMessage(const QDate& date);
|
||||||
QDateTime getTime(const ChatLine::Ptr& chatLine) const;
|
QDateTime getTime(const ChatLine::Ptr& chatLine) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
ChatMessage::Ptr createMessage(const ToxPk& author, const QString& message,
|
ChatMessage::Ptr createMessage(const ToxPk& author, const QString& message,
|
||||||
const QDateTime& datetime, bool isAction, bool isSent, bool colorizeName = false);
|
const QDateTime& datetime, bool isAction, bool isSent, bool colorizeName = false);
|
||||||
ChatMessage::Ptr createSelfMessage(const QString& message, const QDateTime& datetime,
|
bool needsToHideName(ChatLogIdx idx) const;
|
||||||
bool isAction, bool isSent);
|
|
||||||
bool needsToHideName(const ToxPk& messageAuthor, const QDateTime& messageTime) const;
|
|
||||||
void showNetcam();
|
void showNetcam();
|
||||||
void hideNetcam();
|
void hideNetcam();
|
||||||
virtual GenericNetCamView* createNetcam() = 0;
|
virtual GenericNetCamView* createNetcam() = 0;
|
||||||
|
@ -152,15 +154,15 @@ protected:
|
||||||
bool audioOutputFlag;
|
bool audioOutputFlag;
|
||||||
int curRow;
|
int curRow;
|
||||||
|
|
||||||
QAction* saveChatAction;
|
|
||||||
QAction* clearAction;
|
QAction* clearAction;
|
||||||
QAction* quoteAction;
|
QAction* quoteAction;
|
||||||
QAction* copyLinkAction;
|
QAction* copyLinkAction;
|
||||||
QAction* searchAction;
|
QAction* searchAction;
|
||||||
|
QAction* loadHistoryAction;
|
||||||
|
QAction* exportChatAction;
|
||||||
|
|
||||||
ToxPk previousId;
|
ToxPk previousId;
|
||||||
|
|
||||||
QDateTime prevMsgDateTime;
|
|
||||||
QDateTime earliestMessage;
|
QDateTime earliestMessage;
|
||||||
|
|
||||||
QMenu menu;
|
QMenu menu;
|
||||||
|
@ -185,8 +187,11 @@ protected:
|
||||||
GenericNetCamView* netcam;
|
GenericNetCamView* netcam;
|
||||||
Widget* parent;
|
Widget* parent;
|
||||||
|
|
||||||
QPoint searchPoint;
|
IChatLog& chatLog;
|
||||||
bool searchAfterLoadHistory;
|
IMessageDispatcher& messageDispatcher;
|
||||||
|
SearchPos searchPos;
|
||||||
|
std::map<ChatLogIdx, ChatMessage::Ptr> messages;
|
||||||
|
bool colorizeNames = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // GENERICCHATFORM_H
|
#endif // GENERICCHATFORM_H
|
||||||
|
|
|
@ -82,8 +82,8 @@ QString editName(const QString& name)
|
||||||
* @brief Timeout = peer stopped sending audio.
|
* @brief Timeout = peer stopped sending audio.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
GroupChatForm::GroupChatForm(Group* chatGroup)
|
GroupChatForm::GroupChatForm(Group* chatGroup, IChatLog& chatLog, IMessageDispatcher& messageDispatcher)
|
||||||
: GenericChatForm (chatGroup)
|
: GenericChatForm(chatGroup, chatLog, messageDispatcher)
|
||||||
, group(chatGroup)
|
, group(chatGroup)
|
||||||
, inCall(false)
|
, inCall(false)
|
||||||
{
|
{
|
||||||
|
@ -118,8 +118,6 @@ GroupChatForm::GroupChatForm(Group* chatGroup)
|
||||||
//nameLabel->setMinimumHeight(12);
|
//nameLabel->setMinimumHeight(12);
|
||||||
nusersLabel->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::tabPressed, tabber, &TabCompleter::complete);
|
||||||
connect(msgEdit, &ChatTextEdit::keyPressed, tabber, &TabCompleter::reset);
|
connect(msgEdit, &ChatTextEdit::keyPressed, tabber, &TabCompleter::reset);
|
||||||
connect(headWidget, &ChatFormHeader::callTriggered, this, &GroupChatForm::onCallClicked);
|
connect(headWidget, &ChatFormHeader::callTriggered, this, &GroupChatForm::onCallClicked);
|
||||||
|
@ -143,31 +141,6 @@ GroupChatForm::~GroupChatForm()
|
||||||
Translator::unregister(this);
|
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)
|
void GroupChatForm::onTitleChanged(const QString& author, const QString& title)
|
||||||
{
|
{
|
||||||
if (author.isEmpty()) {
|
if (author.isEmpty()) {
|
||||||
|
@ -179,33 +152,6 @@ void GroupChatForm::onTitleChanged(const QString& author, const QString& title)
|
||||||
addSystemInfoMessage(message, ChatMessage::INFO, curTime);
|
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()
|
void GroupChatForm::onScreenshotClicked()
|
||||||
{
|
{
|
||||||
// Unsupported
|
// Unsupported
|
||||||
|
|
|
@ -32,18 +32,19 @@ class TabCompleter;
|
||||||
class FlowLayout;
|
class FlowLayout;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
class GroupId;
|
class GroupId;
|
||||||
|
class IMessageDispatcher;
|
||||||
|
class Message;
|
||||||
|
|
||||||
class GroupChatForm : public GenericChatForm
|
class GroupChatForm : public GenericChatForm
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit GroupChatForm(Group* chatGroup);
|
explicit GroupChatForm(Group* chatGroup, IChatLog& chatLog, IMessageDispatcher& messageDispatcher);
|
||||||
~GroupChatForm();
|
~GroupChatForm();
|
||||||
|
|
||||||
void peerAudioPlaying(ToxPk peerPk);
|
void peerAudioPlaying(ToxPk peerPk);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onSendTriggered() override;
|
|
||||||
void onScreenshotClicked() override;
|
void onScreenshotClicked() override;
|
||||||
void onAttachClicked() override;
|
void onAttachClicked() override;
|
||||||
void onMicMuteToggle();
|
void onMicMuteToggle();
|
||||||
|
@ -53,9 +54,6 @@ private slots:
|
||||||
void onUserLeft(const ToxPk& user, const QString& name);
|
void onUserLeft(const ToxPk& user, const QString& name);
|
||||||
void onPeerNameChanged(const ToxPk& peer, const QString& oldName, const QString& newName);
|
void onPeerNameChanged(const ToxPk& peer, const QString& oldName, const QString& newName);
|
||||||
void onTitleChanged(const QString& author, const QString& title);
|
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);
|
void onLabelContextMenuRequested(const QPoint& localPos);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -70,7 +68,6 @@ private:
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
void updateUserCount(int numPeers);
|
void updateUserCount(int numPeers);
|
||||||
void updateUserNames();
|
void updateUserNames();
|
||||||
void sendJoinLeaveMessages();
|
|
||||||
void leaveGroupCall();
|
void leaveGroupCall();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -19,17 +19,18 @@
|
||||||
|
|
||||||
#include "loadhistorydialog.h"
|
#include "loadhistorydialog.h"
|
||||||
#include "ui_loadhistorydialog.h"
|
#include "ui_loadhistorydialog.h"
|
||||||
|
#include "src/model/ichatlog.h"
|
||||||
#include "src/nexus.h"
|
#include "src/nexus.h"
|
||||||
#include "src/persistence/history.h"
|
#include "src/persistence/history.h"
|
||||||
#include "src/persistence/profile.h"
|
#include "src/persistence/profile.h"
|
||||||
|
#include <QCalendarWidget>
|
||||||
#include <QDate>
|
#include <QDate>
|
||||||
#include <QTextCharFormat>
|
#include <QTextCharFormat>
|
||||||
#include <QCalendarWidget>
|
|
||||||
|
|
||||||
LoadHistoryDialog::LoadHistoryDialog(const ToxPk& friendPk, QWidget* parent)
|
LoadHistoryDialog::LoadHistoryDialog(const IChatLog* chatLog, QWidget* parent)
|
||||||
: QDialog(parent)
|
: QDialog(parent)
|
||||||
, ui(new Ui::LoadHistoryDialog)
|
, ui(new Ui::LoadHistoryDialog)
|
||||||
, friendPk(friendPk)
|
, chatLog(chatLog)
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
highlightDates(QDate::currentDate().year(), QDate::currentDate().month());
|
highlightDates(QDate::currentDate().year(), QDate::currentDate().month());
|
||||||
|
@ -76,15 +77,17 @@ void LoadHistoryDialog::highlightDates(int year, int month)
|
||||||
History* history = Nexus::getProfile()->getHistory();
|
History* history = Nexus::getProfile()->getHistory();
|
||||||
QDate monthStart(year, month, 1);
|
QDate monthStart(year, month, 1);
|
||||||
QDate monthEnd(year, month + 1, 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;
|
QTextCharFormat format;
|
||||||
format.setFontWeight(QFont::Bold);
|
format.setFontWeight(QFont::Bold);
|
||||||
|
|
||||||
QCalendarWidget* calendar = ui->fromDate;
|
QCalendarWidget* calendar = ui->fromDate;
|
||||||
for (History::DateMessages p : counts) {
|
for (const auto& item : dateIdxs) {
|
||||||
format.setToolTip(tr("%1 messages").arg(p.count));
|
if (item.date < monthEnd) {
|
||||||
calendar->setDateTextFormat(monthStart.addDays(p.offsetDays), format);
|
calendar->setDateTextFormat(item.date, format);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,13 +27,14 @@
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class LoadHistoryDialog;
|
class LoadHistoryDialog;
|
||||||
}
|
}
|
||||||
|
class IChatLog;
|
||||||
|
|
||||||
class LoadHistoryDialog : public QDialog
|
class LoadHistoryDialog : public QDialog
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit LoadHistoryDialog(const ToxPk& friendPk, QWidget* parent = nullptr);
|
explicit LoadHistoryDialog(const IChatLog* chatLog, QWidget* parent = nullptr);
|
||||||
explicit LoadHistoryDialog(QWidget* parent = nullptr);
|
explicit LoadHistoryDialog(QWidget* parent = nullptr);
|
||||||
~LoadHistoryDialog();
|
~LoadHistoryDialog();
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ public slots:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::LoadHistoryDialog* ui;
|
Ui::LoadHistoryDialog* ui;
|
||||||
const ToxPk friendPk;
|
const IChatLog* chatLog;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // LOADHISTORYDIALOG_H
|
#endif // LOADHISTORYDIALOG_H
|
||||||
|
|
|
@ -49,11 +49,13 @@
|
||||||
#include "systemtrayicon.h"
|
#include "systemtrayicon.h"
|
||||||
#include "form/groupchatform.h"
|
#include "form/groupchatform.h"
|
||||||
#include "src/audio/audio.h"
|
#include "src/audio/audio.h"
|
||||||
|
#include "src/chatlog/content/filetransferwidget.h"
|
||||||
#include "src/core/core.h"
|
#include "src/core/core.h"
|
||||||
#include "src/core/coreav.h"
|
#include "src/core/coreav.h"
|
||||||
#include "src/core/corefile.h"
|
#include "src/core/corefile.h"
|
||||||
#include "src/friendlist.h"
|
#include "src/friendlist.h"
|
||||||
#include "src/grouplist.h"
|
#include "src/grouplist.h"
|
||||||
|
#include "src/model/chathistory.h"
|
||||||
#include "src/model/chatroom/friendchatroom.h"
|
#include "src/model/chatroom/friendchatroom.h"
|
||||||
#include "src/model/chatroom/groupchatroom.h"
|
#include "src/model/chatroom/groupchatroom.h"
|
||||||
#include "src/model/friend.h"
|
#include "src/model/friend.h"
|
||||||
|
@ -92,6 +94,48 @@ bool toxActivateEventHandler(const QByteArray&)
|
||||||
return true;
|
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::instance{nullptr};
|
||||||
|
|
||||||
Widget::Widget(IAudioControl& audio, QWidget* parent)
|
Widget::Widget(IAudioControl& audio, QWidget* parent)
|
||||||
|
@ -251,6 +295,7 @@ void Widget::init()
|
||||||
|
|
||||||
connect(profile, &Profile::selfAvatarChanged, profileForm, &ProfileForm::onSelfAvatarLoaded);
|
connect(profile, &Profile::selfAvatarChanged, profileForm, &ProfileForm::onSelfAvatarLoaded);
|
||||||
|
|
||||||
|
connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::onFileReceiveRequested);
|
||||||
connect(coreFile, &CoreFile::fileDownloadFinished, filesForm, &FilesForm::onFileDownloadComplete);
|
connect(coreFile, &CoreFile::fileDownloadFinished, filesForm, &FilesForm::onFileDownloadComplete);
|
||||||
connect(coreFile, &CoreFile::fileUploadFinished, filesForm, &FilesForm::onFileUploadComplete);
|
connect(coreFile, &CoreFile::fileUploadFinished, filesForm, &FilesForm::onFileUploadComplete);
|
||||||
connect(ui->addButton, &QPushButton::clicked, this, &Widget::onAddClicked);
|
connect(ui->addButton, &QPushButton::clicked, this, &Widget::onAddClicked);
|
||||||
|
@ -271,6 +316,21 @@ void Widget::init()
|
||||||
connect(filterDisplayGroup, &QActionGroup::triggered, this, &Widget::changeDisplayMode);
|
connect(filterDisplayGroup, &QActionGroup::triggered, this, &Widget::changeDisplayMode);
|
||||||
connect(ui->friendList, &QWidget::customContextMenuRequested, this, &Widget::friendListContextMenu);
|
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
|
// keyboard shortcuts
|
||||||
new QShortcut(Qt::CTRL + Qt::Key_Q, this, SLOT(close()));
|
new QShortcut(Qt::CTRL + Qt::Key_Q, this, SLOT(close()));
|
||||||
new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab, this, SLOT(previousContact()));
|
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
|
Qt::convertFromPlainText(username, Qt::WhiteSpaceNormal)); // for overlength names
|
||||||
}
|
}
|
||||||
|
|
||||||
QString sanename = username;
|
sharedMessageProcessorParams.onUserNameSet(username);
|
||||||
sanename.remove(QRegExp("[\\t\\n\\v\\f\\r\\x0000]"));
|
|
||||||
nameMention = QRegExp("\\b" + QRegExp::escape(username) + "\\b", Qt::CaseInsensitive);
|
|
||||||
sanitizedNameMention = nameMention;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Widget::onStatusMessageChanged(const QString& newStatusMessage)
|
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>");
|
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
|
* @brief Plays a sound via the audioNotification AudioSink
|
||||||
* @param sound Sound to play
|
* @param sound Sound to play
|
||||||
|
@ -989,6 +1039,60 @@ void Widget::onStopNotification()
|
||||||
audioNotification.reset();
|
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)
|
void Widget::onRejectCall(uint32_t friendId)
|
||||||
{
|
{
|
||||||
CoreAV* const av = core->getAv();
|
CoreAV* const av = core->getAv();
|
||||||
|
@ -1006,8 +1110,20 @@ void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk)
|
||||||
const auto compact = settings.getCompactLayout();
|
const auto compact = settings.getCompactLayout();
|
||||||
auto widget = new FriendWidget(chatroom, compact);
|
auto widget = new FriendWidget(chatroom, compact);
|
||||||
auto history = Nexus::getProfile()->getHistory();
|
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;
|
friendChatrooms[friendPk] = chatroom;
|
||||||
friendWidgets[friendPk] = widget;
|
friendWidgets[friendPk] = widget;
|
||||||
chatForms[friendPk] = friendForm;
|
chatForms[friendPk] = friendForm;
|
||||||
|
@ -1021,6 +1137,20 @@ void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk)
|
||||||
contactListWidget->addFriendWidget(widget, Status::Status::Offline,
|
contactListWidget->addFriendWidget(widget, Status::Status::Offline,
|
||||||
settings.getFriendCircleID(friendPk));
|
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::aliasChanged, this, &Widget::onFriendAliasChanged);
|
||||||
connect(newfriend, &Friend::displayedNameChanged, this, &Widget::onFriendDisplayedNameChanged);
|
connect(newfriend, &Friend::displayedNameChanged, this, &Widget::onFriendDisplayedNameChanged);
|
||||||
|
|
||||||
|
@ -1228,19 +1358,18 @@ void Widget::onFriendMessageReceived(uint32_t friendnumber, const QString& messa
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDateTime timestamp = QDateTime::currentDateTime();
|
friendMessageDispatchers[f->getPublicKey()]->onMessageReceived(isAction, message);
|
||||||
Profile* profile = Nexus::getProfile();
|
}
|
||||||
if (profile->isHistoryEnabled()) {
|
|
||||||
QString publicKey = f->getPublicKey().toString();
|
void Widget::onReceiptReceived(int friendId, ReceiptNum receipt)
|
||||||
QString name = f->getDisplayedName();
|
{
|
||||||
QString text = message;
|
const auto& friendKey = FriendList::id2Key(friendId);
|
||||||
if (isAction) {
|
Friend* f = FriendList::findFriend(friendKey);
|
||||||
text = ChatForm::ACTION_PREFIX + text;
|
if (!f) {
|
||||||
}
|
return;
|
||||||
profile->getHistory()->addNewMessage(publicKey, text, publicKey, timestamp, true, name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newFriendMessageAlert(friendId, message);
|
friendMessageDispatchers[f->getPublicKey()]->onReceiptReceived(receipt);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Widget::addFriendDialog(const Friend* frnd, ContentDialog* dialog)
|
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)
|
void Widget::updateFriendActivity(const Friend* frnd)
|
||||||
{
|
{
|
||||||
const ToxPk& pk = frnd->getPublicKey();
|
const ToxPk& pk = frnd->getPublicKey();
|
||||||
|
@ -1560,6 +1698,8 @@ void Widget::removeFriend(Friend* f, bool fake)
|
||||||
onAddClicked();
|
onAddClicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
friendAlertConnections.remove(friendPk);
|
||||||
|
|
||||||
contactListWidget->removeFriendWidget(widget);
|
contactListWidget->removeFriendWidget(widget);
|
||||||
|
|
||||||
ContentDialog* lastDialog = ContentDialogManager::getInstance()->getFriendDialog(friendPk);
|
ContentDialog* lastDialog = ContentDialogManager::getInstance()->getFriendDialog(friendPk);
|
||||||
|
@ -1790,26 +1930,8 @@ void Widget::onGroupMessageReceived(int groupnumber, int peernumber, const QStri
|
||||||
assert(g);
|
assert(g);
|
||||||
|
|
||||||
ToxPk author = core->getGroupPeerPk(groupnumber, peernumber);
|
ToxPk author = core->getGroupPeerPk(groupnumber, peernumber);
|
||||||
bool isSelf = author == core->getSelfId().getPublicKey();
|
|
||||||
|
|
||||||
if (settings.getBlackList().contains(author.toString())) {
|
groupMessageDispatchers[groupId]->onMessageReceived(author, isAction, message);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Widget::onGroupPeerlistChanged(uint32_t groupnumber)
|
void Widget::onGroupPeerlistChanged(uint32_t groupnumber)
|
||||||
|
@ -1902,6 +2024,8 @@ void Widget::removeGroup(Group* g, bool fake)
|
||||||
onAddClicked();
|
onAddClicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupAlertConnections.remove(groupId);
|
||||||
|
|
||||||
contactListWidget->reDraw();
|
contactListWidget->reDraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1928,7 +2052,37 @@ Group* Widget::createGroup(uint32_t groupnumber, const GroupId& groupId)
|
||||||
|
|
||||||
const auto compact = settings.getCompactLayout();
|
const auto compact = settings.getCompactLayout();
|
||||||
auto widget = new GroupWidget(chatroom, compact);
|
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;
|
groupWidgets[groupId] = widget;
|
||||||
groupChatrooms[groupId] = chatroom;
|
groupChatrooms[groupId] = chatroom;
|
||||||
groupChatForms[groupId] = QSharedPointer<GroupChatForm>(form);
|
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::removeGroup, this, widgetRemoveGroup);
|
||||||
connect(widget, &GroupWidget::middleMouseClicked, this, [=]() { removeGroup(groupId); });
|
connect(widget, &GroupWidget::middleMouseClicked, this, [=]() { removeGroup(groupId); });
|
||||||
connect(widget, &GroupWidget::chatroomWidgetClicked, form, &ChatForm::focusInput);
|
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(newgroup, &Group::titleChangedByUser, this, &Widget::titleChangedByUser);
|
||||||
connect(this, &Widget::changeGroupTitle, core, &Core::changeGroupTitle);
|
connect(this, &Widget::changeGroupTitle, core, &Core::changeGroupTitle);
|
||||||
connect(core, &Core::usernameSet, newgroup, &Group::setSelfName);
|
connect(core, &Core::usernameSet, newgroup, &Group::setSelfName);
|
||||||
|
@ -2220,7 +2372,7 @@ void Widget::clearAllReceipts()
|
||||||
{
|
{
|
||||||
QList<Friend*> frnds = FriendList::getAllFriends();
|
QList<Friend*> frnds = FriendList::getAllFriends();
|
||||||
for (Friend* f : frnds) {
|
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/toxfile.h"
|
||||||
#include "src/core/toxid.h"
|
#include "src/core/toxid.h"
|
||||||
#include "src/core/toxpk.h"
|
#include "src/core/toxpk.h"
|
||||||
|
#include "src/model/friendmessagedispatcher.h"
|
||||||
|
#include "src/model/groupmessagedispatcher.h"
|
||||||
#if DESKTOP_NOTIFICATIONS
|
#if DESKTOP_NOTIFICATIONS
|
||||||
#include "src/platform/desktop_notifications/desktopnotify.h"
|
#include "src/platform/desktop_notifications/desktopnotify.h"
|
||||||
#endif
|
#endif
|
||||||
|
@ -79,6 +81,8 @@ class SystemTrayIcon;
|
||||||
class VideoSurface;
|
class VideoSurface;
|
||||||
class UpdateCheck;
|
class UpdateCheck;
|
||||||
class Settings;
|
class Settings;
|
||||||
|
class IChatLog;
|
||||||
|
class ChatHistory;
|
||||||
|
|
||||||
class Widget final : public QMainWindow
|
class Widget final : public QMainWindow
|
||||||
{
|
{
|
||||||
|
@ -135,7 +139,6 @@ public:
|
||||||
static void confirmExecutableOpen(const QFileInfo& file);
|
static void confirmExecutableOpen(const QFileInfo& file);
|
||||||
|
|
||||||
void clearAllReceipts();
|
void clearAllReceipts();
|
||||||
void reloadHistory();
|
|
||||||
|
|
||||||
void reloadTheme();
|
void reloadTheme();
|
||||||
static inline QIcon prepareIcon(QString path, int w = 0, int h = 0);
|
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 onFriendUsernameChanged(int friendId, const QString& username);
|
||||||
void onFriendAliasChanged(const ToxPk& friendId, const QString& alias);
|
void onFriendAliasChanged(const ToxPk& friendId, const QString& alias);
|
||||||
void onFriendMessageReceived(uint32_t friendnumber, const QString& message, bool isAction);
|
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 onFriendRequestReceived(const ToxPk& friendPk, const QString& message);
|
||||||
|
void onFileReceiveRequested(const ToxFile& file);
|
||||||
void updateFriendActivity(const Friend* frnd);
|
void updateFriendActivity(const Friend* frnd);
|
||||||
void onEmptyGroupCreated(uint32_t groupnumber, const GroupId& groupId, const QString& title);
|
void onEmptyGroupCreated(uint32_t groupnumber, const GroupId& groupId, const QString& title);
|
||||||
void onGroupJoined(int groupNum, const GroupId& groupId);
|
void onGroupJoined(int groupNum, const GroupId& groupId);
|
||||||
|
@ -230,6 +235,9 @@ private slots:
|
||||||
void incomingNotification(uint32_t friendId);
|
void incomingNotification(uint32_t friendId);
|
||||||
void onRejectCall(uint32_t friendId);
|
void onRejectCall(uint32_t friendId);
|
||||||
void onStopNotification();
|
void onStopNotification();
|
||||||
|
void dispatchFile(ToxFile file);
|
||||||
|
void dispatchFileWithBool(ToxFile file, bool);
|
||||||
|
void dispatchFileSendFailed(uint32_t friendId, const QString& fileName);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// QMainWindow overrides
|
// QMainWindow overrides
|
||||||
|
@ -305,7 +313,6 @@ private:
|
||||||
bool notify(QObject* receiver, QEvent* event);
|
bool notify(QObject* receiver, QEvent* event);
|
||||||
bool autoAwayActive = false;
|
bool autoAwayActive = false;
|
||||||
QTimer* timer;
|
QTimer* timer;
|
||||||
QRegExp nameMention, sanitizedNameMention;
|
|
||||||
bool eventFlag;
|
bool eventFlag;
|
||||||
bool eventIcon;
|
bool eventIcon;
|
||||||
bool wasMaximized = false;
|
bool wasMaximized = false;
|
||||||
|
@ -319,14 +326,32 @@ private:
|
||||||
Settings& settings;
|
Settings& settings;
|
||||||
|
|
||||||
QMap<ToxPk, FriendWidget*> friendWidgets;
|
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, std::shared_ptr<FriendChatroom>> friendChatrooms;
|
||||||
QMap<ToxPk, ChatForm*> chatForms;
|
QMap<ToxPk, ChatForm*> chatForms;
|
||||||
|
|
||||||
QMap<GroupId, GroupWidget*> groupWidgets;
|
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, std::shared_ptr<GroupChatroom>> groupChatrooms;
|
||||||
QMap<GroupId, QSharedPointer<GroupChatForm>> groupChatForms;
|
QMap<GroupId, QSharedPointer<GroupChatForm>> groupChatForms;
|
||||||
Core* core = nullptr;
|
Core* core = nullptr;
|
||||||
|
|
||||||
|
|
||||||
|
MessageProcessor::SharedParams sharedMessageProcessorParams;
|
||||||
#if DESKTOP_NOTIFICATIONS
|
#if DESKTOP_NOTIFICATIONS
|
||||||
DesktopNotify notifier;
|
DesktopNotify notifier;
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Reference in New Issue
Block a user