1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00

Merge pull request #5354

Anthony Bilinski (2):
      feat(db): add file hash to file history
      refactor(files): change ToxFile's fileName to QString

Mick Sayson (5):
      refactor(files): Refactor FileTransferWidget
      feat(db): Support schema version upgrades
      feat(db): Database support for file history
      feat(db): Hookup file history to the rest of the system
      feat(db): File transfer history review comments
This commit is contained in:
Anthony Bilinski 2018-12-14 08:34:53 -08:00
commit cbf2a1801f
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
14 changed files with 936 additions and 272 deletions

View File

@ -249,6 +249,8 @@ set(${PROJECT_NAME}_SOURCES
src/chatlog/documentcache.h
src/chatlog/pixmapcache.cpp
src/chatlog/pixmapcache.h
src/chatlog/toxfileprogress.cpp
src/chatlog/toxfileprogress.h
src/chatlog/textformatter.cpp
src/chatlog/textformatter.h
src/core/coreav.cpp

View File

@ -39,8 +39,10 @@
#include <QPainter>
#include <QVariantAnimation>
#include <cassert>
#include <math.h>
// The leftButton is used to accept, pause, or resume a file transfer, as well as to open a
// received file.
// The rightButton is used to cancel a file transfer, or to open the directory a file was
@ -50,7 +52,6 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file)
: QWidget(parent)
, ui(new Ui::FileTransferWidget)
, fileInfo(file)
, lastTick(QTime::currentTime())
, backgroundColor(Style::getColor(Style::LightGrey))
, buttonColor(Style::getColor(Style::Yellow))
, buttonBackgroundColor(Style::getColor(Style::White))
@ -84,8 +85,6 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file)
update();
});
setBackgroundColor(Style::getColor(Style::LightGrey), false);
connect(Core::getInstance(), &Core::fileTransferInfo, this,
&FileTransferWidget::onFileTransferInfo);
connect(Core::getInstance(), &Core::fileTransferAccepted, this,
@ -105,15 +104,9 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file)
connect(ui->previewButton, &QPushButton::clicked, this,
&FileTransferWidget::onPreviewButtonClicked);
setupButtons();
// preview
if (fileInfo.direction == ToxFile::SENDING) {
showPreview(fileInfo.filePath);
ui->progressLabel->setText(tr("Waiting to send...", "file transfer widget"));
} else {
ui->progressLabel->setText(tr("Accept to receive this file", "file transfer widget"));
}
// Set lastStatus to anything but the file's current value, this forces an update
lastStatus = file.status == ToxFile::FINISHED ? ToxFile::INITIALIZING : ToxFile::FINISHED;
updateWidget(file);
setFixedHeight(64);
}
@ -155,11 +148,12 @@ void FileTransferWidget::autoAcceptTransfer(const QString& path)
// Do not automatically accept the file-transfer if the path is not writable.
// The user can still accept it manually.
if (tryRemoveFile(filepath))
if (tryRemoveFile(filepath)) {
Core::getInstance()->acceptFileRecvRequest(fileInfo.friendId, fileInfo.fileNum, filepath);
else
} else {
qWarning() << "Cannot write to " << filepath;
}
}
bool FileTransferWidget::isActive() const
{
@ -168,8 +162,9 @@ bool FileTransferWidget::isActive() const
void FileTransferWidget::acceptTransfer(const QString& filepath)
{
if (filepath.isEmpty())
if (filepath.isEmpty()) {
return;
}
// test if writable
if (!tryRemoveFile(filepath)) {
@ -267,194 +262,223 @@ void FileTransferWidget::paintEvent(QPaintEvent*)
void FileTransferWidget::onFileTransferInfo(ToxFile file)
{
QTime now = QTime::currentTime();
qint64 dt = lastTick.msecsTo(now); // ms
if (fileInfo != file || dt < 1000)
return;
fileInfo = file;
if (fileInfo.status == ToxFile::TRANSMITTING) {
// update progress
qreal progress = static_cast<qreal>(file.bytesSent) / static_cast<qreal>(file.filesize);
ui->progressBar->setValue(static_cast<int>(progress * 100.0));
// ETA, speed
qreal deltaSecs = dt / 1000.0;
// (can't use ::abs or ::max on unsigned types substraction, they'd just overflow)
quint64 deltaBytes = file.bytesSent > lastBytesSent ? file.bytesSent - lastBytesSent
: lastBytesSent - file.bytesSent;
qreal bytesPerSec = static_cast<int>(static_cast<qreal>(deltaBytes) / deltaSecs);
// calculate mean
meanIndex = meanIndex % TRANSFER_ROLLING_AVG_COUNT;
meanData[meanIndex++] = bytesPerSec;
qreal meanBytesPerSec = 0.0;
for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i)
meanBytesPerSec += meanData[i];
meanBytesPerSec /= static_cast<qreal>(TRANSFER_ROLLING_AVG_COUNT);
// update UI
if (meanBytesPerSec > 0) {
// ETA
QTime toGo = QTime(0, 0).addSecs((file.filesize - file.bytesSent) / meanBytesPerSec);
QString format = toGo.hour() > 0 ? "hh:mm:ss" : "mm:ss";
ui->etaLabel->setText(toGo.toString(format));
} else {
ui->etaLabel->setText("");
}
ui->progressLabel->setText(getHumanReadableSize(meanBytesPerSec) + "/s");
lastBytesSent = file.bytesSent;
}
lastTick = now;
// trigger repaint
update();
updateWidget(file);
}
void FileTransferWidget::onFileTransferAccepted(ToxFile file)
{
if (fileInfo != file)
return;
fileInfo = file;
setBackgroundColor(Style::getColor(Style::LightGrey), false);
setupButtons();
updateWidget(file);
}
void FileTransferWidget::onFileTransferCancelled(ToxFile file)
{
if (fileInfo != file)
return;
fileInfo = file;
active = false;
setBackgroundColor(Style::getColor(Style::Red), true);
setupButtons();
hideWidgets();
disconnect(Core::getInstance(), nullptr, this, nullptr);
updateWidget(file);
}
void FileTransferWidget::onFileTransferPaused(ToxFile file)
{
if (fileInfo != file)
return;
fileInfo = file;
ui->etaLabel->setText("");
ui->progressLabel->setText(tr("Paused", "file transfer widget"));
// reset mean
meanIndex = 0;
for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i)
meanData[i] = 0.0;
setBackgroundColor(Style::getColor(Style::LightGrey), false);
setupButtons();
updateWidget(file);
}
void FileTransferWidget::onFileTransferResumed(ToxFile file)
{
if (fileInfo != file)
return;
fileInfo = file;
ui->etaLabel->setText("");
ui->progressLabel->setText(tr("Resuming...", "file transfer widget"));
// reset mean
meanIndex = 0;
for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i)
meanData[i] = 0.0;
setBackgroundColor(Style::getColor(Style::LightGrey), false);
setupButtons();
updateWidget(file);
}
void FileTransferWidget::onFileTransferFinished(ToxFile file)
{
if (fileInfo != file)
return;
fileInfo = file;
active = false;
setBackgroundColor(Style::getColor(Style::Green), true);
setupButtons();
hideWidgets();
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/yes.svg")));
ui->leftButton->setObjectName("ok");
ui->leftButton->setToolTip(tr("Open file"));
ui->leftButton->show();
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/dir.svg")));
ui->rightButton->setObjectName("dir");
ui->rightButton->setToolTip(tr("Open file directory"));
ui->rightButton->show();
// preview
if (fileInfo.direction == ToxFile::RECEIVING)
showPreview(fileInfo.filePath);
disconnect(Core::getInstance(), nullptr, this, nullptr);
updateWidget(file);
}
void FileTransferWidget::fileTransferRemotePausedUnpaused(ToxFile file, bool paused)
{
if (paused)
if (paused) {
onFileTransferPaused(file);
else
} else {
onFileTransferResumed(file);
}
}
void FileTransferWidget::fileTransferBrokenUnbroken(ToxFile file, bool broken)
{
// TODO: Handle broken transfer differently once we have resuming code
if (broken)
if (broken) {
onFileTransferCancelled(file);
}
}
QString FileTransferWidget::getHumanReadableSize(qint64 size)
{
static const char* suffix[] = {"B", "kiB", "MiB", "GiB", "TiB"};
int exp = 0;
if (size > 0)
if (size > 0) {
exp = std::min((int)(log(size) / log(1024)), (int)(sizeof(suffix) / sizeof(suffix[0]) - 1));
}
return QString().setNum(size / pow(1024, exp), 'f', exp > 1 ? 2 : 0).append(suffix[exp]);
}
void FileTransferWidget::hideWidgets()
void FileTransferWidget::updateWidgetColor(ToxFile const& file)
{
ui->leftButton->hide();
ui->rightButton->hide();
if (lastStatus == file.status) {
return;
}
switch (file.status) {
case ToxFile::INITIALIZING:
case ToxFile::PAUSED:
case ToxFile::TRANSMITTING:
setBackgroundColor(Style::getColor(Style::LightGrey), false);
break;
case ToxFile::BROKEN:
case ToxFile::CANCELED:
setBackgroundColor(Style::getColor(Style::Red), true);
break;
case ToxFile::FINISHED:
setBackgroundColor(Style::getColor(Style::Green), true);
break;
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::updateWidgetText(ToxFile const& file)
{
if (lastStatus == file.status) {
return;
}
switch (file.status) {
case ToxFile::INITIALIZING:
if (file.direction == ToxFile::SENDING) {
ui->progressLabel->setText(tr("Waiting to send...", "file transfer widget"));
} else {
ui->progressLabel->setText(tr("Accept to receive this file", "file transfer widget"));
}
break;
case ToxFile::PAUSED:
ui->etaLabel->setText("");
ui->progressLabel->setText(tr("Paused", "file transfer widget"));
break;
case ToxFile::TRANSMITTING:
ui->etaLabel->setText("");
ui->progressLabel->setText(tr("Resuming...", "file transfer widget"));
break;
case ToxFile::BROKEN:
case ToxFile::CANCELED:
break;
case ToxFile::FINISHED:
break;
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::updatePreview(ToxFile const& file)
{
if (lastStatus == file.status) {
return;
}
switch (file.status) {
case ToxFile::INITIALIZING:
case ToxFile::PAUSED:
case ToxFile::TRANSMITTING:
case ToxFile::BROKEN:
case ToxFile::CANCELED:
if (file.direction == ToxFile::SENDING) {
showPreview(file.filePath);
}
break;
case ToxFile::FINISHED:
showPreview(file.filePath);
break;
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::updateFileProgress(ToxFile const& file)
{
switch (file.status) {
case ToxFile::INITIALIZING:
break;
case ToxFile::PAUSED:
fileProgress.resetSpeed();
break;
case ToxFile::TRANSMITTING: {
if (!fileProgress.needsUpdate()) {
break;
}
fileProgress.addSample(file);
auto speed = fileProgress.getSpeed();
auto progress = fileProgress.getProgress();
auto remainingTime = fileProgress.getTimeLeftSeconds();
ui->progressBar->setValue(static_cast<int>(progress * 100.0));
// update UI
if (speed > 0) {
// ETA
QTime toGo = QTime(0, 0).addSecs(remainingTime);
QString format = toGo.hour() > 0 ? "hh:mm:ss" : "mm:ss";
ui->etaLabel->setText(toGo.toString(format));
} else {
ui->etaLabel->setText("");
}
ui->progressLabel->setText(getHumanReadableSize(speed) + "/s");
break;
}
case ToxFile::BROKEN:
case ToxFile::CANCELED:
case ToxFile::FINISHED: {
ui->progressBar->hide();
ui->progressLabel->hide();
ui->etaLabel->hide();
break;
}
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::setupButtons()
void FileTransferWidget::updateSignals(ToxFile const& file)
{
switch (fileInfo.status) {
if (lastStatus == file.status) {
return;
}
switch (file.status) {
case ToxFile::CANCELED:
case ToxFile::BROKEN:
case ToxFile::FINISHED:
active = false;
disconnect(Core::getInstance(), nullptr, this, nullptr);
break;
case ToxFile::INITIALIZING:
case ToxFile::PAUSED:
case ToxFile::TRANSMITTING:
break;
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::setupButtons(ToxFile const& file)
{
if (lastStatus == file.status) {
return;
}
switch (file.status) {
case ToxFile::TRANSMITTING:
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/pause.svg")));
ui->leftButton->setObjectName("pause");
@ -479,13 +503,12 @@ void FileTransferWidget::setupButtons()
setButtonColor(Style::getColor(Style::LightGrey));
break;
case ToxFile::STOPPED:
case ToxFile::BROKEN:
case ToxFile::INITIALIZING:
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/no.svg")));
ui->rightButton->setObjectName("cancel");
ui->rightButton->setToolTip(tr("Cancel transfer"));
if (fileInfo.direction == ToxFile::SENDING) {
if (file.direction == ToxFile::SENDING) {
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/pause.svg")));
ui->leftButton->setObjectName("pause");
ui->leftButton->setToolTip(tr("Pause transfer"));
@ -495,27 +518,48 @@ void FileTransferWidget::setupButtons()
ui->leftButton->setToolTip(tr("Accept transfer"));
}
break;
case ToxFile::CANCELED:
case ToxFile::BROKEN:
ui->leftButton->hide();
ui->rightButton->hide();
break;
case ToxFile::FINISHED:
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/yes.svg")));
ui->leftButton->setObjectName("ok");
ui->leftButton->setToolTip(tr("Open file"));
ui->leftButton->show();
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/dir.svg")));
ui->rightButton->setObjectName("dir");
ui->rightButton->setToolTip(tr("Open file directory"));
ui->rightButton->show();
break;
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::handleButton(QPushButton* btn)
{
if (fileInfo.direction == ToxFile::SENDING) {
if (btn->objectName() == "cancel")
if (btn->objectName() == "cancel") {
Core::getInstance()->cancelFileSend(fileInfo.friendId, fileInfo.fileNum);
else if (btn->objectName() == "pause")
} else if (btn->objectName() == "pause") {
Core::getInstance()->pauseResumeFileSend(fileInfo.friendId, fileInfo.fileNum);
else if (btn->objectName() == "resume")
} else if (btn->objectName() == "resume") {
Core::getInstance()->pauseResumeFileSend(fileInfo.friendId, fileInfo.fileNum);
}
} else // receiving or paused
{
if (btn->objectName() == "cancel")
if (btn->objectName() == "cancel") {
Core::getInstance()->cancelFileRecv(fileInfo.friendId, fileInfo.fileNum);
else if (btn->objectName() == "pause")
} else if (btn->objectName() == "pause") {
Core::getInstance()->pauseResumeFileRecv(fileInfo.friendId, fileInfo.fileNum);
else if (btn->objectName() == "resume")
} else if (btn->objectName() == "resume") {
Core::getInstance()->pauseResumeFileRecv(fileInfo.friendId, fileInfo.fileNum);
else if (btn->objectName() == "accept") {
} else if (btn->objectName() == "accept") {
QString path =
QFileDialog::getSaveFileName(Q_NULLPTR,
tr("Save a file", "Title of the file saving dialog"),
@ -544,12 +588,12 @@ void FileTransferWidget::showPreview(const QString& filename)
QFile imageFile(filename);
if (!imageFile.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open file for preview";
return;
}
const QByteArray imageFileData = imageFile.readAll();
QImage image = QImage::fromData(imageFileData);
const int exifOrientation = getExifOrientation(imageFileData.constData(), imageFileData.size());
const int exifOrientation =
getExifOrientation(imageFileData.constData(), imageFileData.size());
if (exifOrientation) {
applyTransformation(exifOrientation, image);
}
@ -559,16 +603,16 @@ void FileTransferWidget::showPreview(const QString& filename)
ui->previewButton->setIcon(QIcon(iconPixmap));
ui->previewButton->setIconSize(iconPixmap.size());
ui->previewButton->show();
// Show mouseover preview, but make sure it's not larger than 50% of the screen width/height
// Show mouseover preview, but make sure it's not larger than 50% of the screen
// width/height
const QRect desktopSize = QApplication::desktop()->screenGeometry();
const int maxPreviewWidth{desktopSize.width() / 2};
const int maxPreviewHeight{desktopSize.height() / 2};
const QImage previewImage = [&image, maxPreviewWidth, maxPreviewHeight]() {
if (image.width() > maxPreviewWidth || image.height() > maxPreviewHeight) {
return image.scaled(maxPreviewWidth, maxPreviewHeight,
Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
else {
return image.scaled(maxPreviewWidth, maxPreviewHeight, Qt::KeepAspectRatio,
Qt::SmoothTransformation);
} else {
return image;
}
}();
@ -578,8 +622,7 @@ void FileTransferWidget::showPreview(const QString& filename)
buffer.open(QIODevice::WriteOnly);
previewImage.save(&buffer, "PNG");
buffer.close();
ui->previewButton->setToolTip("<img src=data:image/png;base64," + imageData.toBase64()
+ "/>");
ui->previewButton->setToolTip("<img src=data:image/png;base64," + imageData.toBase64() + "/>");
}
}
@ -602,7 +645,8 @@ QPixmap FileTransferWidget::scaleCropIntoSquare(const QPixmap& source, const int
{
QPixmap result;
// Make sure smaller-than-icon images (at least one dimension is smaller) will not be upscaled
// Make sure smaller-than-icon images (at least one dimension is smaller) will not be
// upscaled
if (source.width() < targetSize || source.height() < targetSize) {
result = source;
} else {
@ -612,10 +656,11 @@ QPixmap FileTransferWidget::scaleCropIntoSquare(const QPixmap& source, const int
// Then, image has to be cropped (if needed) so it will not overflow rectangle
// Only one dimension will be bigger after Qt::KeepAspectRatioByExpanding
if (result.width() > targetSize)
if (result.width() > targetSize) {
return result.copy((result.width() - targetSize) / 2, 0, targetSize, targetSize);
else if (result.height() > targetSize)
} else if (result.height() > targetSize) {
return result.copy(0, (result.height() - targetSize) / 2, targetSize, targetSize);
}
// Picture was rectangle in the first place, no cropping
return result;
@ -625,8 +670,9 @@ int FileTransferWidget::getExifOrientation(const char* data, const int size)
{
ExifData* exifData = exif_data_new_from_data(reinterpret_cast<const unsigned char*>(data), size);
if (!exifData)
if (!exifData) {
return 0;
}
int orientation = 0;
const ExifByteOrder byteOrder = exif_data_get_byte_order(exifData);
@ -641,8 +687,7 @@ int FileTransferWidget::getExifOrientation(const char* data, const int size)
void FileTransferWidget::applyTransformation(const int orientation, QImage& image)
{
QTransform exifTransform;
switch(static_cast<ExifOrientation>(orientation))
{
switch (static_cast<ExifOrientation>(orientation)) {
case ExifOrientation::TopLeft:
break;
case ExifOrientation::TopRight:
@ -673,3 +718,35 @@ void FileTransferWidget::applyTransformation(const int orientation, QImage& imag
}
image = image.transformed(exifTransform);
}
void FileTransferWidget::updateWidget(ToxFile const& file)
{
if (fileInfo != file) {
return;
}
fileInfo = file;
// If we repainted on every packet our gui would be *very* slow
bool bTransmitNeedsUpdate = fileProgress.needsUpdate();
updatePreview(file);
updateFileProgress(file);
updateWidgetText(file);
updateWidgetColor(file);
setupButtons(file);
updateSignals(file);
lastStatus = file.status;
// trigger repaint
switch (file.status) {
case ToxFile::TRANSMITTING:
if (!bTransmitNeedsUpdate) {
break;
}
// fallthrough
default:
update();
}
}

View File

@ -24,6 +24,7 @@
#include <QWidget>
#include "src/chatlog/chatlinecontent.h"
#include "src/chatlog/toxfileprogress.h"
#include "src/core/toxfile.h"
@ -56,8 +57,12 @@ protected slots:
protected:
QString getHumanReadableSize(qint64 size);
void hideWidgets();
void setupButtons();
void updateWidgetColor(ToxFile const& file);
void updateWidgetText(ToxFile const& file);
void updateFileProgress(ToxFile const& file);
void updateSignals(ToxFile const& file);
void updatePreview(ToxFile const& file);
void setupButtons(ToxFile const& file);
void handleButton(QPushButton* btn);
void showPreview(const QString& filename);
void acceptTransfer(const QString& filepath);
@ -79,28 +84,29 @@ private:
static void applyTransformation(const int oritentation, QImage& image);
static bool tryRemoveFile(const QString &filepath);
void updateWidget(ToxFile const& file);
private:
Ui::FileTransferWidget* ui;
ToxFileProgress fileProgress;
ToxFile fileInfo;
QTime lastTick;
quint64 lastBytesSent = 0;
QVariantAnimation* backgroundColorAnimation = nullptr;
QVariantAnimation* buttonColorAnimation = nullptr;
QColor backgroundColor;
QColor buttonColor;
QColor buttonBackgroundColor;
static const uint8_t TRANSFER_ROLLING_AVG_COUNT = 4;
uint8_t meanIndex = 0;
qreal meanData[TRANSFER_ROLLING_AVG_COUNT] = {0.0};
bool active;
enum class ExifOrientation {
ToxFile::FileStatus lastStatus = ToxFile::INITIALIZING;
enum class ExifOrientation
{
/* do not change values, this is exif spec
*
* name corresponds to where the 0 row and 0 column is in form row-column
* i.e. entry 5 here means that the 0'th row corresponds to the left side of the scene and the 0'th column corresponds
* to the top of the captured scene. This means that the image needs to be mirrored and rotated to be displayed.
* i.e. entry 5 here means that the 0'th row corresponds to the left side of the scene and
* the 0'th column corresponds to the top of the captured scene. This means that the image
* needs to be mirrored and rotated to be displayed.
*/
TopLeft = 1,
TopRight = 2,

View File

@ -0,0 +1,93 @@
/*
Copyright © 2018 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include "toxfileprogress.h"
#include "src/core/toxfile.h"
bool ToxFileProgress::needsUpdate() const
{
QTime now = QTime::currentTime();
qint64 dt = lastTick.msecsTo(now); // ms
if (dt < 1000) {
return false;
}
return true;
}
void ToxFileProgress::addSample(ToxFile const& file)
{
QTime now = QTime::currentTime();
qint64 dt = lastTick.msecsTo(now); // ms
if (dt < 1000) {
return;
}
// ETA, speed
qreal deltaSecs = dt / 1000.0;
// (can't use ::abs or ::max on unsigned types substraction, they'd just overflow)
quint64 deltaBytes = file.bytesSent > lastBytesSent ? file.bytesSent - lastBytesSent
: lastBytesSent - file.bytesSent;
qreal bytesPerSec = static_cast<int>(static_cast<qreal>(deltaBytes) / deltaSecs);
// Update member variables
meanIndex = meanIndex % TRANSFER_ROLLING_AVG_COUNT;
meanData[meanIndex++] = bytesPerSec;
double meanBytesPerSec = 0.0;
for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i) {
meanBytesPerSec += meanData[i];
}
meanBytesPerSec /= static_cast<qreal>(TRANSFER_ROLLING_AVG_COUNT);
lastTick = now;
progress = static_cast<double>(file.bytesSent) / static_cast<double>(file.filesize);
speedBytesPerSecond = meanBytesPerSec;
timeLeftSeconds = (file.filesize - file.bytesSent) / getSpeed();
lastBytesSent = file.bytesSent;
}
void ToxFileProgress::resetSpeed()
{
meanIndex = 0;
for (auto& item : meanData) {
item = 0;
}
}
double ToxFileProgress::getProgress() const
{
return progress;
}
double ToxFileProgress::getSpeed() const
{
return speedBytesPerSecond;
}
double ToxFileProgress::getTimeLeftSeconds() const
{
return timeLeftSeconds;
}

View File

@ -0,0 +1,53 @@
/*
Copyright © 2018 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TOXFILEPROGRESS_H
#define TOXFILEPROGRESS_H
#include <QTime>
struct ToxFile;
class ToxFileProgress
{
public:
bool needsUpdate() const;
void addSample(ToxFile const& file);
void resetSpeed();
double getProgress() const;
double getSpeed() const;
double getTimeLeftSeconds() const;
private:
uint64_t lastBytesSent = 0;
static const uint8_t TRANSFER_ROLLING_AVG_COUNT = 4;
uint8_t meanIndex = 0;
double meanData[TRANSFER_ROLLING_AVG_COUNT] = {0.0};
QTime lastTick = QTime::currentTime();
double speedBytesPerSecond;
double timeLeftSeconds;
double progress;
};
#endif // TOXFILEPROGRESS_H

View File

@ -108,7 +108,6 @@ void CoreFile::sendAvatarFile(Core* core, uint32_t friendId, const QByteArray& d
ToxFile file{fileNum, friendId, "", "", ToxFile::SENDING};
file.filesize = filesize;
file.fileName = QByteArray((char*)avatarHash, TOX_HASH_LENGTH);
file.fileKind = TOX_FILE_KIND_AVATAR;
file.avatarData = data;
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
@ -121,19 +120,18 @@ void CoreFile::sendFile(Core* core, uint32_t friendId, QString filename, QString
long long filesize)
{
QMutexLocker mlocker(&fileSendMutex);
QByteArray fileName = filename.toUtf8();
ToxString fileName(filename);
TOX_ERR_FILE_SEND sendErr;
uint32_t fileNum = tox_file_send(core->tox.get(), friendId, TOX_FILE_KIND_DATA, filesize,
nullptr, (uint8_t*)fileName.data(), fileName.size(), &sendErr);
nullptr, fileName.data(), fileName.size(), &sendErr);
if (sendErr != TOX_ERR_FILE_SEND_OK) {
qWarning() << "sendFile: Can't create the Tox file sender (" << sendErr << ")";
emit core->fileSendFailed(friendId, filename);
emit core->fileSendFailed(friendId, fileName.getQString());
return;
}
qDebug() << QString("sendFile: Created file sender %1 with friend %2").arg(fileNum).arg(friendId);
ToxFile file{fileNum, friendId, fileName, filePath, ToxFile::SENDING};
ToxFile file{fileNum, friendId, fileName.getQString(), filePath, ToxFile::SENDING};
file.filesize = filesize;
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
tox_file_get_file_id(core->tox.get(), friendId, fileNum, (uint8_t*)file.resumeFileId.data(),
@ -199,7 +197,7 @@ void CoreFile::cancelFileSend(Core* core, uint32_t friendId, uint32_t fileId)
return;
}
file->status = ToxFile::STOPPED;
file->status = ToxFile::CANCELED;
emit core->fileTransferCancelled(*file);
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
removeFile(friendId, fileId);
@ -212,7 +210,7 @@ void CoreFile::cancelFileRecv(Core* core, uint32_t friendId, uint32_t fileId)
qWarning("cancelFileRecv: No such file in queue");
return;
}
file->status = ToxFile::STOPPED;
file->status = ToxFile::CANCELED;
emit core->fileTransferCancelled(*file);
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
removeFile(friendId, fileId);
@ -225,7 +223,7 @@ void CoreFile::rejectFileRecvRequest(Core* core, uint32_t friendId, uint32_t fil
qWarning("rejectFileRecvRequest: No such file in queue");
return;
}
file->status = ToxFile::STOPPED;
file->status = ToxFile::CANCELED;
emit core->fileTransferCancelled(*file);
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
removeFile(friendId, fileId);
@ -379,6 +377,7 @@ void CoreFile::onFileControlCallback(Tox*, uint32_t friendId, uint32_t fileId,
if (control == TOX_FILE_CONTROL_CANCEL) {
if (file->fileKind != TOX_FILE_KIND_AVATAR)
qDebug() << "File tranfer" << friendId << ":" << fileId << "cancelled by friend";
file->status = ToxFile::CANCELED;
emit static_cast<Core*>(core)->fileTransferCancelled(*file);
removeFile(friendId, fileId);
} else if (control == TOX_FILE_CONTROL_PAUSE) {
@ -409,6 +408,7 @@ void CoreFile::onFileDataCallback(Tox* tox, uint32_t friendId, uint32_t fileId,
// If we reached EOF, ack and cleanup the transfer
if (!length) {
file->status = ToxFile::FINISHED;
if (file->fileKind != TOX_FILE_KIND_AVATAR) {
emit static_cast<Core*>(core)->fileTransferFinished(*file);
emit static_cast<Core*>(core)->fileUploadFinished(file->filePath);
@ -429,12 +429,14 @@ void CoreFile::onFileDataCallback(Tox* tox, uint32_t friendId, uint32_t fileId,
nread = file->file->read((char*)data.get(), length);
if (nread <= 0) {
qWarning("onFileDataCallback: Failed to read from file");
file->status = ToxFile::CANCELED;
emit static_cast<Core*>(core)->fileTransferCancelled(*file);
tox_file_send_chunk(tox, friendId, fileId, pos, nullptr, 0, nullptr);
removeFile(friendId, fileId);
return;
}
file->bytesSent += length;
file->hashGenerator->addData((const char*)data.get(), length);
}
if (!tox_file_send_chunk(tox, friendId, fileId, pos, data.get(), nread, nullptr)) {
@ -458,14 +460,17 @@ void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fil
if (file->bytesSent != position) {
qWarning("onFileRecvChunkCallback: Received a chunk out-of-order, aborting transfer");
if (file->fileKind != TOX_FILE_KIND_AVATAR)
if (file->fileKind != TOX_FILE_KIND_AVATAR) {
file->status = ToxFile::CANCELED;
emit core->fileTransferCancelled(*file);
}
tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_CANCEL, nullptr);
removeFile(friendId, fileId);
return;
}
if (!length) {
file->status = ToxFile::FINISHED;
if (file->fileKind == TOX_FILE_KIND_AVATAR) {
QPixmap pic;
pic.loadFromData(file->avatarData);
@ -486,6 +491,7 @@ void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fil
else
file->file->write((char*)data, length);
file->bytesSent += length;
file->hashGenerator->addData((const char*)data, length);
if (file->fileKind != TOX_FILE_KIND_AVATAR)
emit static_cast<Core*>(core)->fileTransferInfo(*file);

View File

@ -18,7 +18,7 @@
/**
* @brief ToxFile constructor
*/
ToxFile::ToxFile(uint32_t fileNum, uint32_t friendId, QByteArray filename, QString filePath,
ToxFile::ToxFile(uint32_t fileNum, uint32_t friendId, QString filename, QString filePath,
FileDirection Direction)
: fileKind{TOX_FILE_KIND_DATA}
, fileNum(fileNum)
@ -28,10 +28,9 @@ ToxFile::ToxFile(uint32_t fileNum, uint32_t friendId, QByteArray filename, QStri
, file{new QFile(filePath)}
, bytesSent{0}
, filesize{0}
, status{STOPPED}
, status{INITIALIZING}
, direction{Direction}
{
}
{}
bool ToxFile::operator==(const ToxFile& other) const
{

View File

@ -3,32 +3,36 @@
#include <QString>
#include <memory>
#include <QCryptographicHash>
class QFile;
class QTimer;
struct ToxFile
{
// Note do not change values, these are directly inserted into the DB in their
// current form, changing order would mess up database state!
enum FileStatus
{
STOPPED,
PAUSED,
TRANSMITTING,
BROKEN
INITIALIZING = 0,
PAUSED = 1,
TRANSMITTING = 2,
BROKEN = 3,
CANCELED = 4,
FINISHED = 5,
};
// Note do not change values, these are directly inserted into the DB in their
// current form (can add fields though as db representation is an int)
enum FileDirection : bool
{
SENDING,
RECEIVING
SENDING = 0,
RECEIVING = 1,
};
ToxFile() = default;
ToxFile(uint32_t FileNum, uint32_t FriendId, QByteArray FileName, QString filePath,
ToxFile(uint32_t FileNum, uint32_t FriendId, QString FileName, QString filePath,
FileDirection Direction);
~ToxFile()
{
}
bool operator==(const ToxFile& other) const;
bool operator!=(const ToxFile& other) const;
@ -39,7 +43,7 @@ struct ToxFile
uint8_t fileKind;
uint32_t fileNum;
uint32_t friendId;
QByteArray fileName;
QString fileName;
QString filePath;
std::shared_ptr<QFile> file;
quint64 bytesSent;
@ -48,6 +52,7 @@ struct ToxFile
FileDirection direction;
QByteArray avatarData;
QByteArray resumeFileId;
std::shared_ptr<QCryptographicHash> hashGenerator = std::make_shared<QCryptographicHash>(QCryptographicHash::Sha256);
};
#endif // CORESTRUCTS_H

View File

@ -34,7 +34,15 @@
* Caches mappings to speed up message saving.
*/
static constexpr int NUM_MESSAGES_DEFAULT = 100; // arbitrary number of messages loaded when not loading by date
static constexpr int NUM_MESSAGES_DEFAULT =
100; // arbitrary number of messages loaded when not loading by date
static constexpr int SCHEMA_VERSION = 1;
FileDbInsertionData::FileDbInsertionData()
{
static int id = qRegisterMetaType<FileDbInsertionData>();
(void)id;
}
/**
* @brief Prepares the database to work with the history.
@ -48,14 +56,44 @@ History::History(std::shared_ptr<RawDatabase> db)
return;
}
dbSchemaUpgrade();
// dbSchemaUpgrade may have put us in an invalid state
if (!isValid()) {
return;
}
connect(this, &History::fileInsertionReady, this, &History::onFileInsertionReady);
connect(this, &History::fileInserted, this, &History::onFileInserted);
db->execLater(
"CREATE TABLE IF NOT EXISTS peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL "
"UNIQUE);"
"CREATE TABLE IF NOT EXISTS aliases (id INTEGER PRIMARY KEY, owner INTEGER,"
"display_name BLOB NOT NULL, UNIQUE(owner, display_name));"
"CREATE TABLE IF NOT EXISTS history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, "
"chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, "
"message BLOB NOT NULL);"
"CREATE TABLE IF NOT EXISTS history "
"(id INTEGER PRIMARY KEY,"
"timestamp INTEGER NOT NULL,"
"chat_id INTEGER NOT NULL,"
"sender_alias INTEGER NOT NULL,"
// even though technically a message can be null for file transfer, we've opted
// to just insert an empty string when there's no content, this moderately simplifies
// implementation as currently our database doesn't have support for optional fields.
// We would either have to insert "?" or "null" based on if message exists and then
// ensure that our blob vector always has the right number of fields. Better to just
// leave this as NOT NULL for now.
"message BLOB NOT NULL,"
"file_id INTEGER);"
"CREATE TABLE IF NOT EXISTS file_transfers "
"(id INTEGER PRIMARY KEY,"
"chat_id INTEGER NOT NULL,"
"file_restart_id BLOB NOT NULL,"
"file_name BLOB NOT NULL, "
"file_path BLOB NOT NULL,"
"file_hash BLOB NOT NULL,"
"file_size INTEGER NOT NULL,"
"direction INTEGER NOT NULL,"
"file_state INTEGER NOT NULL);"
"CREATE TABLE IF NOT EXISTS faux_offline_pending (id INTEGER PRIMARY KEY);");
// Cache our current peers
@ -108,6 +146,7 @@ void History::eraseHistory()
"DELETE FROM history;"
"DELETE FROM aliases;"
"DELETE FROM peers;"
"DELETE FROM file_transfers;"
"VACUUM;");
}
@ -136,6 +175,7 @@ void History::removeFriendHistory(const QString& friendPk)
"DELETE FROM history WHERE chat_id=%1; "
"DELETE FROM aliases WHERE owner=%1; "
"DELETE FROM peers WHERE id=%1; "
"DELETE FROM file_transfers WHERE chat_id=%1;"
"VACUUM;")
.arg(id);
@ -166,7 +206,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
// Get the db id of the peer we're chatting with
int64_t peerId;
if (peers.contains(friendPk)) {
peerId = peers[friendPk];
peerId = (peers)[friendPk];
} else {
if (peers.isEmpty()) {
peerId = 0;
@ -174,7 +214,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
peerId = *std::max_element(peers.begin(), peers.end()) + 1;
}
peers[friendPk] = peerId;
(peers)[friendPk] = peerId;
queries += RawDatabase::Query(("INSERT INTO peers (id, public_key) "
"VALUES (%1, '"
+ friendPk + "');")
@ -184,7 +224,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
// Get the db id of the sender of the message
int64_t senderId;
if (peers.contains(sender)) {
senderId = peers[sender];
senderId = (peers)[sender];
} else {
if (peers.isEmpty()) {
senderId = 0;
@ -192,7 +232,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
senderId = *std::max_element(peers.begin(), peers.end()) + 1;
}
peers[sender] = senderId;
(peers)[sender] = senderId;
queries += RawDatabase::Query{("INSERT INTO peers (id, public_key) "
"VALUES (%1, '"
+ sender + "');")
@ -228,6 +268,121 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
return queries;
}
void History::onFileInsertionReady(FileDbInsertionData data)
{
QVector<RawDatabase::Query> queries;
std::weak_ptr<History> weakThis = shared_from_this();
// peerId is guaranteed to be inserted since we just used it in addNewMessage
auto peerId = peers[data.friendPk];
// Copy to pass into labmda for later
auto fileId = data.fileId;
queries +=
RawDatabase::Query(QStringLiteral(
"INSERT INTO file_transfers (chat_id, file_restart_id, "
"file_path, file_name, file_hash, file_size, direction, file_state) "
"VALUES (%1, ?, ?, ?, ?, %2, %3, %4);")
.arg(peerId)
.arg(data.size)
.arg(static_cast<int>(data.direction))
.arg(ToxFile::CANCELED),
{data.fileId.toUtf8(), data.filePath.toUtf8(), data.fileName.toUtf8(), QByteArray()},
[weakThis, fileId](int64_t id) {
auto pThis = weakThis.lock();
if (pThis) {
emit pThis->fileInserted(id, fileId);
}
});
queries += RawDatabase::Query(QStringLiteral("UPDATE history "
"SET file_id = (last_insert_rowid()) "
"WHERE id = %1")
.arg(data.historyId));
db->execLater(queries);
}
void History::onFileInserted(int64_t dbId, QString fileId)
{
auto& fileInfo = fileInfos[fileId];
if (fileInfo.finished) {
db->execLater(
generateFileFinished(dbId, fileInfo.success, fileInfo.filePath, fileInfo.fileHash));
fileInfos.remove(fileId);
} else {
fileInfo.finished = false;
fileInfo.fileId = dbId;
}
}
RawDatabase::Query History::generateFileFinished(int64_t id, bool success, const QString& filePath,
const QByteArray& fileHash)
{
auto file_state = success ? ToxFile::FINISHED : ToxFile::CANCELED;
if (filePath.length()) {
return RawDatabase::Query(QStringLiteral("UPDATE file_transfers "
"SET file_state = %1, file_path = ?, file_hash = ?"
"WHERE id = %2")
.arg(file_state)
.arg(id),
{filePath.toUtf8(), fileHash});
} else {
return RawDatabase::Query(QStringLiteral("UPDATE file_transfers "
"SET finished = %1 "
"WHERE id = %2")
.arg(file_state)
.arg(id));
}
}
void History::addNewFileMessage(const QString& friendPk, const QString& fileId,
const QString& fileName, const QString& filePath, int64_t size,
const QString& sender, const QDateTime& time, QString const& dispName)
{
// This is an incredibly far from an optimal way of implementing this,
// but given the frequency that people are going to be initiating a file
// transfer we can probably live with it.
// Since both inserting an alias for a user and inserting a file transfer
// will generate new ids, there is no good way to inject both new ids into the
// history query without refactoring our RawDatabase::Query and processor loops.
// What we will do instead is chain callbacks to try to get reasonable behavior.
// We can call the generateNewMessageQueries() fn to insert a message with an empty
// message in it, and get the id with the callbck. Once we have the id we can ammend
// the data to have our newly inserted file_id as well
ToxFile::FileDirection direction;
if (sender == friendPk) {
direction = ToxFile::RECEIVING;
} else {
direction = ToxFile::SENDING;
}
std::weak_ptr<History> weakThis = shared_from_this();
FileDbInsertionData insertionData;
insertionData.friendPk = friendPk;
insertionData.fileId = fileId;
insertionData.fileName = fileName;
insertionData.filePath = filePath;
insertionData.size = size;
insertionData.direction = direction;
auto insertFileTransferFn = [weakThis, insertionData](int64_t messageId) {
auto insertionDataRw = std::move(insertionData);
insertionDataRw.historyId = messageId;
auto thisPtr = weakThis.lock();
if (thisPtr)
emit thisPtr->fileInsertionReady(std::move(insertionDataRw));
};
addNewMessage(friendPk, "", sender, time, true, dispName, insertFileTransferFn);
}
/**
* @brief Saves a chat message in the database.
* @param friendPk Friend publick key to save.
@ -254,6 +409,21 @@ void History::addNewMessage(const QString& friendPk, const QString& message, con
insertIdCallback));
}
void History::setFileFinished(const QString& fileId, bool success, const QString& filePath,
const QByteArray& fileHash)
{
auto& fileInfo = fileInfos[fileId];
if (fileInfo.fileId == -1) {
fileInfo.finished = true;
fileInfo.success = success;
fileInfo.filePath = filePath;
fileInfo.fileHash = fileHash;
} else {
db->execLater(generateFileFinished(fileInfo.fileId, success, filePath, fileHash));
}
fileInfos.remove(fileId);
}
/**
* @brief Fetches chat messages from the database.
* @param friendPk Friend publick key to fetch.
@ -261,8 +431,8 @@ void History::addNewMessage(const QString& friendPk, const QString& message, con
* @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)
QList<History::HistMessage> History::getChatHistoryFromDate(const QString& friendPk,
const QDateTime& from, const QDateTime& to)
{
if (!isValid()) {
return {};
@ -280,7 +450,8 @@ QList<History::HistMessage> History::getChatHistoryDefaultNum(const QString& fri
if (!isValid()) {
return {};
}
return getChatHistory(friendPk, QDateTime::fromMSecsSinceEpoch(0), QDateTime::currentDateTime(), NUM_MESSAGES_DEFAULT);
return getChatHistory(friendPk, QDateTime::fromMSecsSinceEpoch(0), QDateTime::currentDateTime(),
NUM_MESSAGES_DEFAULT);
}
@ -333,7 +504,8 @@ QList<History::DateMessages> History::getChatHistoryCounts(const ToxPk& friendPk
* @param parameter for search
* @return date of the message where the phrase was found
*/
QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTime& from, QString phrase, const ParameterSearch& parameter)
QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTime& from,
QString phrase, const ParameterSearch& parameter)
{
QDateTime result;
auto rowCallback = [&result](const QVector<QVariant>& row) {
@ -349,10 +521,12 @@ QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTi
message = QStringLiteral("message LIKE '%%1%'").arg(phrase);
break;
case FilterSearch::WordsOnly:
message = QStringLiteral("message REGEXP '%1'").arg(SearchExtraFunctions::generateFilterWordsOnly(phrase).toLower());
message = QStringLiteral("message REGEXP '%1'")
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase).toLower());
break;
case FilterSearch::RegisterAndWordsOnly:
message = QStringLiteral("REGEXPSENSITIVE(message, '%1')").arg(SearchExtraFunctions::generateFilterWordsOnly(phrase));
message = QStringLiteral("REGEXPSENSITIVE(message, '%1')")
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase));
break;
case FilterSearch::Regular:
message = QStringLiteral("message REGEXP '%1'").arg(phrase);
@ -376,13 +550,16 @@ QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTi
period = QStringLiteral("ORDER BY timestamp ASC LIMIT 1;");
break;
case PeriodSearch::AfterDate:
period = QStringLiteral("AND timestamp > '%1' ORDER BY timestamp ASC LIMIT 1;").arg(date.toMSecsSinceEpoch());
period = QStringLiteral("AND timestamp > '%1' ORDER BY timestamp ASC LIMIT 1;")
.arg(date.toMSecsSinceEpoch());
break;
case PeriodSearch::BeforeDate:
period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;").arg(date.toMSecsSinceEpoch());
period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;")
.arg(date.toMSecsSinceEpoch());
break;
default:
period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;").arg(date.toMSecsSinceEpoch());
period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;")
.arg(date.toMSecsSinceEpoch());
break;
}
@ -460,31 +637,50 @@ QList<History::HistMessage> History::getChatHistory(const QString& friendPk, con
auto rowCallback = [&messages](const QVector<QVariant>& row) {
// dispName and message could have null bytes, QString::fromUtf8
// truncates on null bytes so we strip them
messages += {row[0].toLongLong(),
row[1].isNull(),
QDateTime::fromMSecsSinceEpoch(row[2].toLongLong()),
row[3].toString(),
QString::fromUtf8(row[4].toByteArray().replace('\0', "")),
row[5].toString(),
QString::fromUtf8(row[6].toByteArray().replace('\0', ""))};
auto id = 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 FROM history "
"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 chat_id = chat.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);
queryText =
"SELECT * FROM (" + queryText
+ QString(" ORDER BY history.id DESC limit %1) AS T1 ORDER BY T1.id ASC;").arg(numMessages);
} else {
queryText = queryText + ";";
}
@ -493,3 +689,44 @@ QList<History::HistMessage> History::getChatHistory(const QString& friendPk, con
return messages;
}
/**
* @brief Upgrade the db schema
* @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION
* variable and add another case to the switch statement below. Make sure to fall through on each case.
*/
void History::dbSchemaUpgrade()
{
int64_t databaseSchemaVersion;
db->execNow(RawDatabase::Query("PRAGMA user_version", [&](const QVector<QVariant>& row) {
databaseSchemaVersion = row[0].toLongLong();
}));
if (databaseSchemaVersion > SCHEMA_VERSION) {
qWarning() << "Database version is newer than we currently support. Please upgrade qTox";
// We don't know what future versions have done, we have to disable db access until we re-upgrade
db.reset();
return;
} else if (databaseSchemaVersion == SCHEMA_VERSION) {
// No work to do
return;
}
// Make sure to handle the un-created case as well in the following upgrade code
switch (databaseSchemaVersion) {
case 0:
// This will generate a warning on new profiles, but we have no easy way to chain execs. I
// don't want to block the rest of the program on db creation so I guess we can just live with the warning for now
db->execLater(RawDatabase::Query("ALTER TABLE history ADD file_id INTEGER;"));
// fallthrough
// case 1:
// do 1 -> 2 upgrade
// //fallthrough
// etc.
default:
db->execLater(
RawDatabase::Query(QStringLiteral("PRAGMA user_version = %1;").arg(SCHEMA_VERSION)));
qDebug() << "Database upgrade finished (databaseSchemaVersion " << databaseSchemaVersion
<< " -> " << SCHEMA_VERSION << ")";
}
}

View File

@ -22,11 +22,14 @@
#include <QDateTime>
#include <QHash>
#include <QPointer>
#include <QVector>
#include <cassert>
#include <cstdint>
#include <tox/toxencryptsave.h>
#include "src/core/toxfile.h"
#include "src/core/toxpk.h"
#include "src/persistence/db/rawdatabase.h"
#include "src/widget/searchtypes.h"
@ -34,8 +37,77 @@
class Profile;
class HistoryKeeper;
class History
enum class HistMessageContentType
{
message,
file
};
class HistMessageContent
{
public:
HistMessageContent(QString message)
: data(std::make_shared<QString>(std::move(message)))
, type(HistMessageContentType::message)
{}
HistMessageContent(ToxFile file)
: data(std::make_shared<ToxFile>(std::move(file)))
, type(HistMessageContentType::file)
{}
HistMessageContentType getType() const
{
return type;
}
QString& asMessage()
{
assert(type == HistMessageContentType::message);
return *static_cast<QString*>(data.get());
}
ToxFile& asFile()
{
assert(type == HistMessageContentType::file);
return *static_cast<ToxFile*>(data.get());
}
const QString& asMessage() const
{
assert(type == HistMessageContentType::message);
return *static_cast<QString*>(data.get());
}
const ToxFile& asFile() const
{
assert(type == HistMessageContentType::file);
return *static_cast<ToxFile*>(data.get());
}
private:
// Not really shared but shared_ptr has support for shared_ptr<void>
std::shared_ptr<void> data;
HistMessageContentType type;
};
struct FileDbInsertionData
{
FileDbInsertionData();
int64_t historyId;
QString friendPk;
QString fileId;
QString fileName;
QString filePath;
int64_t size;
int direction;
};
Q_DECLARE_METATYPE(FileDbInsertionData);
class History : public QObject, public std::enable_shared_from_this<History>
{
Q_OBJECT
public:
struct HistMessage
{
@ -43,21 +115,32 @@ public:
QString sender, QString message)
: chat{chat}
, sender{sender}
, message{message}
, dispName{dispName}
, timestamp{timestamp}
, id{id}
, isSent{isSent}
{
}
, content(std::move(message))
{}
HistMessage(qint64 id, bool isSent, QDateTime timestamp, QString chat, QString dispName,
QString sender, ToxFile file)
: chat{chat}
, sender{sender}
, dispName{dispName}
, timestamp{timestamp}
, id{id}
, isSent{isSent}
, content(std::move(file))
{}
QString chat;
QString sender;
QString message;
QString dispName;
QDateTime timestamp;
qint64 id;
bool isSent;
HistMessageContent content;
};
struct DateMessages
@ -80,6 +163,12 @@ public:
const QDateTime& time, bool isSent, QString dispName,
const std::function<void(int64_t)>& insertIdCallback = {});
void addNewFileMessage(const QString& friendPk, const QString& fileId,
const QString& fileName, const QString& filePath, int64_t size,
const QString& sender, const QDateTime& time, QString const& dispName);
void setFileFinished(const QString& fileId, bool success, const QString& filePath, const QByteArray& fileHash);
QList<HistMessage> getChatHistoryFromDate(const QString& friendPk, const QDateTime& from,
const QDateTime& to);
QList<HistMessage> getChatHistoryDefaultNum(const QString& friendPk);
@ -96,11 +185,37 @@ protected:
const QString& sender, const QDateTime& time, bool isSent,
QString dispName, std::function<void(int64_t)> insertIdCallback = {});
signals:
void fileInsertionReady(FileDbInsertionData data);
void fileInserted(int64_t dbId, QString fileId);
private slots:
void onFileInsertionReady(FileDbInsertionData data);
void onFileInserted(int64_t dbId, QString fileId);
private:
QList<HistMessage> getChatHistory(const QString& friendPk, const QDateTime& from,
const QDateTime& to, int numMessages);
static RawDatabase::Query generateFileFinished(int64_t fileId, bool success,
const QString& filePath, const QByteArray& fileHash);
void dbSchemaUpgrade();
std::shared_ptr<RawDatabase> db;
QHash<QString, int64_t> peers;
struct FileInfo
{
bool finished = false;
bool success = false;
QString filePath;
QByteArray fileHash;
int64_t fileId = -1;
};
// This needs to be a shared pointer to avoid callback lifetime issues
QHash<QString, FileInfo> fileInfos;
};
#endif // HISTORY_H

View File

@ -748,7 +748,7 @@ QStringList Profile::remove()
qWarning() << "Could not remove file " << dbPath;
}
history.release();
history.reset();
database.reset();
return ret;

View File

@ -109,7 +109,7 @@ private:
QString name;
std::unique_ptr<ToxEncrypt> passkey = nullptr;
std::shared_ptr<RawDatabase> database;
std::unique_ptr<History> history;
std::shared_ptr<History> history;
bool isRemoved;
bool encrypted = false;
static QStringList profiles;

View File

@ -161,6 +161,9 @@ ChatForm::ChatForm(Friend* chatFriend, History* history)
connect(core, &Core::fileReceiveRequested, this, &ChatForm::onFileRecvRequest);
connect(profile, &Profile::friendAvatarChanged, this, &ChatForm::onAvatarChanged);
connect(core, &Core::fileSendStarted, this, &ChatForm::startFileSend);
connect(core, &Core::fileTransferFinished, this, &ChatForm::onFileTransferFinished);
connect(core, &Core::fileTransferCancelled, this, &ChatForm::onFileTransferCancelled);
connect(core, &Core::fileTransferBrokenUnbroken, this, &ChatForm::onFileTransferBrokenUnbroken);
connect(core, &Core::fileSendFailed, this, &ChatForm::onFileSendFailed);
connect(core, &Core::receiptRecieved, this, &ChatForm::onReceiptReceived);
connect(core, &Core::friendMessageReceived, this, &ChatForm::onFriendMessageReceived);
@ -312,9 +315,35 @@ void ChatForm::startFileSend(ToxFile file)
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()) {
@ -331,9 +360,17 @@ void ChatForm::onFileRecvRequest(ToxFile file)
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());
@ -794,6 +831,11 @@ void ChatForm::handleLoadedMessages(QList<History::HistMessage> newHistMsgs, boo
MessageMetadata const metadata = getMessageMetadata(histMessage);
lastDate = addDateLineIfNeeded(chatLines, lastDate, histMessage, metadata);
auto msg = chatMessageFromHistMessage(histMessage, metadata);
if (!msg) {
continue;
}
if (processUndelivered) {
sendLoadedMessage(msg, metadata);
}
@ -838,7 +880,9 @@ ChatForm::MessageMetadata ChatForm::getMessageMetadata(History::HistMessage cons
const QDateTime msgDateTime = histMessage.timestamp.toLocalTime();
const bool isSelf = Core::getInstance()->getSelfId().getPublicKey() == authorPk;
const bool needSending = !histMessage.isSent && isSelf;
const bool isAction = histMessage.message.startsWith(ACTION_PREFIX, Qt::CaseInsensitive);
const bool isAction =
histMessage.content.getType() == HistMessageContentType::message
&& histMessage.content.asMessage().startsWith(ACTION_PREFIX, Qt::CaseInsensitive);
const qint64 id = histMessage.id;
return {isSelf, needSending, isAction, id, authorPk, msgDateTime};
}
@ -848,11 +892,31 @@ ChatMessage::Ptr ChatForm::chatMessageFromHistMessage(History::HistMessage const
{
ToxPk authorPk(ToxId(histMessage.sender).getPublicKey());
QString authorStr = getMsgAuthorDispName(authorPk, histMessage.dispName);
QString messageText =
metadata.isAction ? histMessage.message.mid(ACTION_PREFIX.length()) : histMessage.message;
ChatMessage::MessageType type = metadata.isAction ? ChatMessage::ACTION : ChatMessage::NORMAL;
QDateTime dateTime = metadata.needSending ? QDateTime() : metadata.msgDateTime;
auto msg = ChatMessage::createChatMessage(authorStr, messageText, type, metadata.isSelf, dateTime);
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();
}
@ -1135,13 +1199,17 @@ void ChatForm::onExportChat()
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.message % '\n'};
% QString{datestamp % '\t' % timestamp % '\t' % author % '\t'
% it.content.asMessage() % '\n'};
}
file.write(buffer.toUtf8());
file.close();

View File

@ -68,6 +68,9 @@ signals:
public slots:
void startFileSend(ToxFile file);
void onFileTransferFinished(ToxFile file);
void onFileTransferCancelled(ToxFile file);
void onFileTransferBrokenUnbroken(ToxFile file, bool broken);
void onFileRecvRequest(ToxFile file);
void onAvInvite(uint32_t friendId, bool video);
void onAvStart(uint32_t friendId, bool video);