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:
commit
cbf2a1801f
|
@ -249,6 +249,8 @@ set(${PROJECT_NAME}_SOURCES
|
||||||
src/chatlog/documentcache.h
|
src/chatlog/documentcache.h
|
||||||
src/chatlog/pixmapcache.cpp
|
src/chatlog/pixmapcache.cpp
|
||||||
src/chatlog/pixmapcache.h
|
src/chatlog/pixmapcache.h
|
||||||
|
src/chatlog/toxfileprogress.cpp
|
||||||
|
src/chatlog/toxfileprogress.h
|
||||||
src/chatlog/textformatter.cpp
|
src/chatlog/textformatter.cpp
|
||||||
src/chatlog/textformatter.h
|
src/chatlog/textformatter.h
|
||||||
src/core/coreav.cpp
|
src/core/coreav.cpp
|
||||||
|
|
|
@ -39,8 +39,10 @@
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QVariantAnimation>
|
#include <QVariantAnimation>
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
|
|
||||||
// The leftButton is used to accept, pause, or resume a file transfer, as well as to open a
|
// The leftButton is used to accept, pause, or resume a file transfer, as well as to open a
|
||||||
// received file.
|
// received file.
|
||||||
// The rightButton is used to cancel a file transfer, or to open the directory a file was
|
// 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)
|
: QWidget(parent)
|
||||||
, ui(new Ui::FileTransferWidget)
|
, ui(new Ui::FileTransferWidget)
|
||||||
, fileInfo(file)
|
, fileInfo(file)
|
||||||
, lastTick(QTime::currentTime())
|
|
||||||
, backgroundColor(Style::getColor(Style::LightGrey))
|
, backgroundColor(Style::getColor(Style::LightGrey))
|
||||||
, buttonColor(Style::getColor(Style::Yellow))
|
, buttonColor(Style::getColor(Style::Yellow))
|
||||||
, buttonBackgroundColor(Style::getColor(Style::White))
|
, buttonBackgroundColor(Style::getColor(Style::White))
|
||||||
|
@ -84,8 +85,6 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file)
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
|
||||||
setBackgroundColor(Style::getColor(Style::LightGrey), false);
|
|
||||||
|
|
||||||
connect(Core::getInstance(), &Core::fileTransferInfo, this,
|
connect(Core::getInstance(), &Core::fileTransferInfo, this,
|
||||||
&FileTransferWidget::onFileTransferInfo);
|
&FileTransferWidget::onFileTransferInfo);
|
||||||
connect(Core::getInstance(), &Core::fileTransferAccepted, this,
|
connect(Core::getInstance(), &Core::fileTransferAccepted, this,
|
||||||
|
@ -105,15 +104,9 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file)
|
||||||
connect(ui->previewButton, &QPushButton::clicked, this,
|
connect(ui->previewButton, &QPushButton::clicked, this,
|
||||||
&FileTransferWidget::onPreviewButtonClicked);
|
&FileTransferWidget::onPreviewButtonClicked);
|
||||||
|
|
||||||
setupButtons();
|
// Set lastStatus to anything but the file's current value, this forces an update
|
||||||
|
lastStatus = file.status == ToxFile::FINISHED ? ToxFile::INITIALIZING : ToxFile::FINISHED;
|
||||||
// preview
|
updateWidget(file);
|
||||||
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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
setFixedHeight(64);
|
setFixedHeight(64);
|
||||||
}
|
}
|
||||||
|
@ -155,10 +148,11 @@ void FileTransferWidget::autoAcceptTransfer(const QString& path)
|
||||||
|
|
||||||
// Do not automatically accept the file-transfer if the path is not writable.
|
// Do not automatically accept the file-transfer if the path is not writable.
|
||||||
// The user can still accept it manually.
|
// The user can still accept it manually.
|
||||||
if (tryRemoveFile(filepath))
|
if (tryRemoveFile(filepath)) {
|
||||||
Core::getInstance()->acceptFileRecvRequest(fileInfo.friendId, fileInfo.fileNum, filepath);
|
Core::getInstance()->acceptFileRecvRequest(fileInfo.friendId, fileInfo.fileNum, filepath);
|
||||||
else
|
} else {
|
||||||
qWarning() << "Cannot write to " << filepath;
|
qWarning() << "Cannot write to " << filepath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileTransferWidget::isActive() const
|
bool FileTransferWidget::isActive() const
|
||||||
|
@ -168,8 +162,9 @@ bool FileTransferWidget::isActive() const
|
||||||
|
|
||||||
void FileTransferWidget::acceptTransfer(const QString& filepath)
|
void FileTransferWidget::acceptTransfer(const QString& filepath)
|
||||||
{
|
{
|
||||||
if (filepath.isEmpty())
|
if (filepath.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// test if writable
|
// test if writable
|
||||||
if (!tryRemoveFile(filepath)) {
|
if (!tryRemoveFile(filepath)) {
|
||||||
|
@ -267,169 +262,49 @@ void FileTransferWidget::paintEvent(QPaintEvent*)
|
||||||
|
|
||||||
void FileTransferWidget::onFileTransferInfo(ToxFile file)
|
void FileTransferWidget::onFileTransferInfo(ToxFile file)
|
||||||
{
|
{
|
||||||
QTime now = QTime::currentTime();
|
updateWidget(file);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileTransferWidget::onFileTransferAccepted(ToxFile file)
|
void FileTransferWidget::onFileTransferAccepted(ToxFile file)
|
||||||
{
|
{
|
||||||
if (fileInfo != file)
|
updateWidget(file);
|
||||||
return;
|
|
||||||
|
|
||||||
fileInfo = file;
|
|
||||||
|
|
||||||
setBackgroundColor(Style::getColor(Style::LightGrey), false);
|
|
||||||
|
|
||||||
setupButtons();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileTransferWidget::onFileTransferCancelled(ToxFile file)
|
void FileTransferWidget::onFileTransferCancelled(ToxFile file)
|
||||||
{
|
{
|
||||||
if (fileInfo != file)
|
updateWidget(file);
|
||||||
return;
|
|
||||||
|
|
||||||
fileInfo = file;
|
|
||||||
active = false;
|
|
||||||
|
|
||||||
setBackgroundColor(Style::getColor(Style::Red), true);
|
|
||||||
|
|
||||||
setupButtons();
|
|
||||||
hideWidgets();
|
|
||||||
|
|
||||||
disconnect(Core::getInstance(), nullptr, this, nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileTransferWidget::onFileTransferPaused(ToxFile file)
|
void FileTransferWidget::onFileTransferPaused(ToxFile file)
|
||||||
{
|
{
|
||||||
if (fileInfo != file)
|
updateWidget(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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileTransferWidget::onFileTransferResumed(ToxFile file)
|
void FileTransferWidget::onFileTransferResumed(ToxFile file)
|
||||||
{
|
{
|
||||||
if (fileInfo != file)
|
updateWidget(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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileTransferWidget::onFileTransferFinished(ToxFile file)
|
void FileTransferWidget::onFileTransferFinished(ToxFile file)
|
||||||
{
|
{
|
||||||
if (fileInfo != file)
|
updateWidget(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileTransferWidget::fileTransferRemotePausedUnpaused(ToxFile file, bool paused)
|
void FileTransferWidget::fileTransferRemotePausedUnpaused(ToxFile file, bool paused)
|
||||||
{
|
{
|
||||||
if (paused)
|
if (paused) {
|
||||||
onFileTransferPaused(file);
|
onFileTransferPaused(file);
|
||||||
else
|
} else {
|
||||||
onFileTransferResumed(file);
|
onFileTransferResumed(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileTransferWidget::fileTransferBrokenUnbroken(ToxFile file, bool broken)
|
void FileTransferWidget::fileTransferBrokenUnbroken(ToxFile file, bool broken)
|
||||||
{
|
{
|
||||||
// TODO: Handle broken transfer differently once we have resuming code
|
// TODO: Handle broken transfer differently once we have resuming code
|
||||||
if (broken)
|
if (broken) {
|
||||||
onFileTransferCancelled(file);
|
onFileTransferCancelled(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString FileTransferWidget::getHumanReadableSize(qint64 size)
|
QString FileTransferWidget::getHumanReadableSize(qint64 size)
|
||||||
|
@ -437,24 +312,173 @@ QString FileTransferWidget::getHumanReadableSize(qint64 size)
|
||||||
static const char* suffix[] = {"B", "kiB", "MiB", "GiB", "TiB"};
|
static const char* suffix[] = {"B", "kiB", "MiB", "GiB", "TiB"};
|
||||||
int exp = 0;
|
int exp = 0;
|
||||||
|
|
||||||
if (size > 0)
|
if (size > 0) {
|
||||||
exp = std::min((int)(log(size) / log(1024)), (int)(sizeof(suffix) / sizeof(suffix[0]) - 1));
|
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]);
|
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();
|
if (lastStatus == file.status) {
|
||||||
ui->rightButton->hide();
|
return;
|
||||||
ui->progressBar->hide();
|
}
|
||||||
ui->progressLabel->hide();
|
|
||||||
ui->etaLabel->hide();
|
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::setupButtons()
|
void FileTransferWidget::updateWidgetText(ToxFile const& file)
|
||||||
{
|
{
|
||||||
switch (fileInfo.status) {
|
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::updateSignals(ToxFile const& file)
|
||||||
|
{
|
||||||
|
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:
|
case ToxFile::TRANSMITTING:
|
||||||
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/pause.svg")));
|
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/pause.svg")));
|
||||||
ui->leftButton->setObjectName("pause");
|
ui->leftButton->setObjectName("pause");
|
||||||
|
@ -479,13 +503,12 @@ void FileTransferWidget::setupButtons()
|
||||||
setButtonColor(Style::getColor(Style::LightGrey));
|
setButtonColor(Style::getColor(Style::LightGrey));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ToxFile::STOPPED:
|
case ToxFile::INITIALIZING:
|
||||||
case ToxFile::BROKEN:
|
|
||||||
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/no.svg")));
|
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/no.svg")));
|
||||||
ui->rightButton->setObjectName("cancel");
|
ui->rightButton->setObjectName("cancel");
|
||||||
ui->rightButton->setToolTip(tr("Cancel transfer"));
|
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->setIcon(QIcon(Style::getImagePath("fileTransferInstance/pause.svg")));
|
||||||
ui->leftButton->setObjectName("pause");
|
ui->leftButton->setObjectName("pause");
|
||||||
ui->leftButton->setToolTip(tr("Pause transfer"));
|
ui->leftButton->setToolTip(tr("Pause transfer"));
|
||||||
|
@ -495,27 +518,48 @@ void FileTransferWidget::setupButtons()
|
||||||
ui->leftButton->setToolTip(tr("Accept transfer"));
|
ui->leftButton->setToolTip(tr("Accept transfer"));
|
||||||
}
|
}
|
||||||
break;
|
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)
|
void FileTransferWidget::handleButton(QPushButton* btn)
|
||||||
{
|
{
|
||||||
if (fileInfo.direction == ToxFile::SENDING) {
|
if (fileInfo.direction == ToxFile::SENDING) {
|
||||||
if (btn->objectName() == "cancel")
|
if (btn->objectName() == "cancel") {
|
||||||
Core::getInstance()->cancelFileSend(fileInfo.friendId, fileInfo.fileNum);
|
Core::getInstance()->cancelFileSend(fileInfo.friendId, fileInfo.fileNum);
|
||||||
else if (btn->objectName() == "pause")
|
} else if (btn->objectName() == "pause") {
|
||||||
Core::getInstance()->pauseResumeFileSend(fileInfo.friendId, fileInfo.fileNum);
|
Core::getInstance()->pauseResumeFileSend(fileInfo.friendId, fileInfo.fileNum);
|
||||||
else if (btn->objectName() == "resume")
|
} else if (btn->objectName() == "resume") {
|
||||||
Core::getInstance()->pauseResumeFileSend(fileInfo.friendId, fileInfo.fileNum);
|
Core::getInstance()->pauseResumeFileSend(fileInfo.friendId, fileInfo.fileNum);
|
||||||
|
}
|
||||||
} else // receiving or paused
|
} else // receiving or paused
|
||||||
{
|
{
|
||||||
if (btn->objectName() == "cancel")
|
if (btn->objectName() == "cancel") {
|
||||||
Core::getInstance()->cancelFileRecv(fileInfo.friendId, fileInfo.fileNum);
|
Core::getInstance()->cancelFileRecv(fileInfo.friendId, fileInfo.fileNum);
|
||||||
else if (btn->objectName() == "pause")
|
} else if (btn->objectName() == "pause") {
|
||||||
Core::getInstance()->pauseResumeFileRecv(fileInfo.friendId, fileInfo.fileNum);
|
Core::getInstance()->pauseResumeFileRecv(fileInfo.friendId, fileInfo.fileNum);
|
||||||
else if (btn->objectName() == "resume")
|
} else if (btn->objectName() == "resume") {
|
||||||
Core::getInstance()->pauseResumeFileRecv(fileInfo.friendId, fileInfo.fileNum);
|
Core::getInstance()->pauseResumeFileRecv(fileInfo.friendId, fileInfo.fileNum);
|
||||||
else if (btn->objectName() == "accept") {
|
} else if (btn->objectName() == "accept") {
|
||||||
QString path =
|
QString path =
|
||||||
QFileDialog::getSaveFileName(Q_NULLPTR,
|
QFileDialog::getSaveFileName(Q_NULLPTR,
|
||||||
tr("Save a file", "Title of the file saving dialog"),
|
tr("Save a file", "Title of the file saving dialog"),
|
||||||
|
@ -544,12 +588,12 @@ void FileTransferWidget::showPreview(const QString& filename)
|
||||||
|
|
||||||
QFile imageFile(filename);
|
QFile imageFile(filename);
|
||||||
if (!imageFile.open(QIODevice::ReadOnly)) {
|
if (!imageFile.open(QIODevice::ReadOnly)) {
|
||||||
qCritical() << "Failed to open file for preview";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QByteArray imageFileData = imageFile.readAll();
|
const QByteArray imageFileData = imageFile.readAll();
|
||||||
QImage image = QImage::fromData(imageFileData);
|
QImage image = QImage::fromData(imageFileData);
|
||||||
const int exifOrientation = getExifOrientation(imageFileData.constData(), imageFileData.size());
|
const int exifOrientation =
|
||||||
|
getExifOrientation(imageFileData.constData(), imageFileData.size());
|
||||||
if (exifOrientation) {
|
if (exifOrientation) {
|
||||||
applyTransformation(exifOrientation, image);
|
applyTransformation(exifOrientation, image);
|
||||||
}
|
}
|
||||||
|
@ -559,16 +603,16 @@ void FileTransferWidget::showPreview(const QString& filename)
|
||||||
ui->previewButton->setIcon(QIcon(iconPixmap));
|
ui->previewButton->setIcon(QIcon(iconPixmap));
|
||||||
ui->previewButton->setIconSize(iconPixmap.size());
|
ui->previewButton->setIconSize(iconPixmap.size());
|
||||||
ui->previewButton->show();
|
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 QRect desktopSize = QApplication::desktop()->screenGeometry();
|
||||||
const int maxPreviewWidth{desktopSize.width() / 2};
|
const int maxPreviewWidth{desktopSize.width() / 2};
|
||||||
const int maxPreviewHeight{desktopSize.height() /2};
|
const int maxPreviewHeight{desktopSize.height() / 2};
|
||||||
const QImage previewImage = [&image, maxPreviewWidth, maxPreviewHeight]() {
|
const QImage previewImage = [&image, maxPreviewWidth, maxPreviewHeight]() {
|
||||||
if (image.width() > maxPreviewWidth || image.height() > maxPreviewHeight) {
|
if (image.width() > maxPreviewWidth || image.height() > maxPreviewHeight) {
|
||||||
return image.scaled(maxPreviewWidth, maxPreviewHeight,
|
return image.scaled(maxPreviewWidth, maxPreviewHeight, Qt::KeepAspectRatio,
|
||||||
Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
Qt::SmoothTransformation);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
@ -578,8 +622,7 @@ void FileTransferWidget::showPreview(const QString& filename)
|
||||||
buffer.open(QIODevice::WriteOnly);
|
buffer.open(QIODevice::WriteOnly);
|
||||||
previewImage.save(&buffer, "PNG");
|
previewImage.save(&buffer, "PNG");
|
||||||
buffer.close();
|
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;
|
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) {
|
if (source.width() < targetSize || source.height() < targetSize) {
|
||||||
result = source;
|
result = source;
|
||||||
} else {
|
} 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
|
// Then, image has to be cropped (if needed) so it will not overflow rectangle
|
||||||
// Only one dimension will be bigger after Qt::KeepAspectRatioByExpanding
|
// 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);
|
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);
|
return result.copy(0, (result.height() - targetSize) / 2, targetSize, targetSize);
|
||||||
|
}
|
||||||
|
|
||||||
// Picture was rectangle in the first place, no cropping
|
// Picture was rectangle in the first place, no cropping
|
||||||
return result;
|
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);
|
ExifData* exifData = exif_data_new_from_data(reinterpret_cast<const unsigned char*>(data), size);
|
||||||
|
|
||||||
if (!exifData)
|
if (!exifData) {
|
||||||
return 0;
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int orientation = 0;
|
int orientation = 0;
|
||||||
const ExifByteOrder byteOrder = exif_data_get_byte_order(exifData);
|
const ExifByteOrder byteOrder = exif_data_get_byte_order(exifData);
|
||||||
|
@ -641,12 +687,11 @@ int FileTransferWidget::getExifOrientation(const char* data, const int size)
|
||||||
void FileTransferWidget::applyTransformation(const int orientation, QImage& image)
|
void FileTransferWidget::applyTransformation(const int orientation, QImage& image)
|
||||||
{
|
{
|
||||||
QTransform exifTransform;
|
QTransform exifTransform;
|
||||||
switch(static_cast<ExifOrientation>(orientation))
|
switch (static_cast<ExifOrientation>(orientation)) {
|
||||||
{
|
|
||||||
case ExifOrientation::TopLeft:
|
case ExifOrientation::TopLeft:
|
||||||
break;
|
break;
|
||||||
case ExifOrientation::TopRight:
|
case ExifOrientation::TopRight:
|
||||||
image = image.mirrored(1,0);
|
image = image.mirrored(1, 0);
|
||||||
break;
|
break;
|
||||||
case ExifOrientation::BottomRight:
|
case ExifOrientation::BottomRight:
|
||||||
exifTransform.rotate(180);
|
exifTransform.rotate(180);
|
||||||
|
@ -673,3 +718,35 @@ void FileTransferWidget::applyTransformation(const int orientation, QImage& imag
|
||||||
}
|
}
|
||||||
image = image.transformed(exifTransform);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "src/chatlog/chatlinecontent.h"
|
#include "src/chatlog/chatlinecontent.h"
|
||||||
|
#include "src/chatlog/toxfileprogress.h"
|
||||||
#include "src/core/toxfile.h"
|
#include "src/core/toxfile.h"
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,8 +57,12 @@ protected slots:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QString getHumanReadableSize(qint64 size);
|
QString getHumanReadableSize(qint64 size);
|
||||||
void hideWidgets();
|
void updateWidgetColor(ToxFile const& file);
|
||||||
void setupButtons();
|
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 handleButton(QPushButton* btn);
|
||||||
void showPreview(const QString& filename);
|
void showPreview(const QString& filename);
|
||||||
void acceptTransfer(const QString& filepath);
|
void acceptTransfer(const QString& filepath);
|
||||||
|
@ -79,28 +84,29 @@ private:
|
||||||
static void applyTransformation(const int oritentation, QImage& image);
|
static void applyTransformation(const int oritentation, QImage& image);
|
||||||
static bool tryRemoveFile(const QString &filepath);
|
static bool tryRemoveFile(const QString &filepath);
|
||||||
|
|
||||||
|
void updateWidget(ToxFile const& file);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::FileTransferWidget* ui;
|
Ui::FileTransferWidget* ui;
|
||||||
|
ToxFileProgress fileProgress;
|
||||||
ToxFile fileInfo;
|
ToxFile fileInfo;
|
||||||
QTime lastTick;
|
|
||||||
quint64 lastBytesSent = 0;
|
|
||||||
QVariantAnimation* backgroundColorAnimation = nullptr;
|
QVariantAnimation* backgroundColorAnimation = nullptr;
|
||||||
QVariantAnimation* buttonColorAnimation = nullptr;
|
QVariantAnimation* buttonColorAnimation = nullptr;
|
||||||
QColor backgroundColor;
|
QColor backgroundColor;
|
||||||
QColor buttonColor;
|
QColor buttonColor;
|
||||||
QColor buttonBackgroundColor;
|
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;
|
bool active;
|
||||||
enum class ExifOrientation {
|
ToxFile::FileStatus lastStatus = ToxFile::INITIALIZING;
|
||||||
|
|
||||||
|
enum class ExifOrientation
|
||||||
|
{
|
||||||
/* do not change values, this is exif spec
|
/* do not change values, this is exif spec
|
||||||
*
|
*
|
||||||
* name corresponds to where the 0 row and 0 column is in form row-column
|
* 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
|
* i.e. entry 5 here means that the 0'th row corresponds to the left side of the scene and
|
||||||
* to the top of the captured scene. This means that the image needs to be mirrored and rotated to be displayed.
|
* 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,
|
TopLeft = 1,
|
||||||
TopRight = 2,
|
TopRight = 2,
|
||||||
|
|
93
src/chatlog/toxfileprogress.cpp
Normal file
93
src/chatlog/toxfileprogress.cpp
Normal 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;
|
||||||
|
}
|
53
src/chatlog/toxfileprogress.h
Normal file
53
src/chatlog/toxfileprogress.h
Normal 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
|
|
@ -108,7 +108,6 @@ void CoreFile::sendAvatarFile(Core* core, uint32_t friendId, const QByteArray& d
|
||||||
|
|
||||||
ToxFile file{fileNum, friendId, "", "", ToxFile::SENDING};
|
ToxFile file{fileNum, friendId, "", "", ToxFile::SENDING};
|
||||||
file.filesize = filesize;
|
file.filesize = filesize;
|
||||||
file.fileName = QByteArray((char*)avatarHash, TOX_HASH_LENGTH);
|
|
||||||
file.fileKind = TOX_FILE_KIND_AVATAR;
|
file.fileKind = TOX_FILE_KIND_AVATAR;
|
||||||
file.avatarData = data;
|
file.avatarData = data;
|
||||||
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
|
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)
|
long long filesize)
|
||||||
{
|
{
|
||||||
QMutexLocker mlocker(&fileSendMutex);
|
QMutexLocker mlocker(&fileSendMutex);
|
||||||
|
ToxString fileName(filename);
|
||||||
QByteArray fileName = filename.toUtf8();
|
|
||||||
TOX_ERR_FILE_SEND sendErr;
|
TOX_ERR_FILE_SEND sendErr;
|
||||||
uint32_t fileNum = tox_file_send(core->tox.get(), friendId, TOX_FILE_KIND_DATA, filesize,
|
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) {
|
if (sendErr != TOX_ERR_FILE_SEND_OK) {
|
||||||
qWarning() << "sendFile: Can't create the Tox file sender (" << sendErr << ")";
|
qWarning() << "sendFile: Can't create the Tox file sender (" << sendErr << ")";
|
||||||
emit core->fileSendFailed(friendId, filename);
|
emit core->fileSendFailed(friendId, fileName.getQString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
qDebug() << QString("sendFile: Created file sender %1 with friend %2").arg(fileNum).arg(friendId);
|
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.filesize = filesize;
|
||||||
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
|
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
|
||||||
tox_file_get_file_id(core->tox.get(), friendId, fileNum, (uint8_t*)file.resumeFileId.data(),
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
file->status = ToxFile::STOPPED;
|
file->status = ToxFile::CANCELED;
|
||||||
emit core->fileTransferCancelled(*file);
|
emit core->fileTransferCancelled(*file);
|
||||||
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
|
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
|
||||||
removeFile(friendId, fileId);
|
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");
|
qWarning("cancelFileRecv: No such file in queue");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
file->status = ToxFile::STOPPED;
|
file->status = ToxFile::CANCELED;
|
||||||
emit core->fileTransferCancelled(*file);
|
emit core->fileTransferCancelled(*file);
|
||||||
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
|
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
|
||||||
removeFile(friendId, fileId);
|
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");
|
qWarning("rejectFileRecvRequest: No such file in queue");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
file->status = ToxFile::STOPPED;
|
file->status = ToxFile::CANCELED;
|
||||||
emit core->fileTransferCancelled(*file);
|
emit core->fileTransferCancelled(*file);
|
||||||
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
|
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
|
||||||
removeFile(friendId, fileId);
|
removeFile(friendId, fileId);
|
||||||
|
@ -379,6 +377,7 @@ void CoreFile::onFileControlCallback(Tox*, uint32_t friendId, uint32_t fileId,
|
||||||
if (control == TOX_FILE_CONTROL_CANCEL) {
|
if (control == TOX_FILE_CONTROL_CANCEL) {
|
||||||
if (file->fileKind != TOX_FILE_KIND_AVATAR)
|
if (file->fileKind != TOX_FILE_KIND_AVATAR)
|
||||||
qDebug() << "File tranfer" << friendId << ":" << fileId << "cancelled by friend";
|
qDebug() << "File tranfer" << friendId << ":" << fileId << "cancelled by friend";
|
||||||
|
file->status = ToxFile::CANCELED;
|
||||||
emit static_cast<Core*>(core)->fileTransferCancelled(*file);
|
emit static_cast<Core*>(core)->fileTransferCancelled(*file);
|
||||||
removeFile(friendId, fileId);
|
removeFile(friendId, fileId);
|
||||||
} else if (control == TOX_FILE_CONTROL_PAUSE) {
|
} 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 we reached EOF, ack and cleanup the transfer
|
||||||
if (!length) {
|
if (!length) {
|
||||||
|
file->status = ToxFile::FINISHED;
|
||||||
if (file->fileKind != TOX_FILE_KIND_AVATAR) {
|
if (file->fileKind != TOX_FILE_KIND_AVATAR) {
|
||||||
emit static_cast<Core*>(core)->fileTransferFinished(*file);
|
emit static_cast<Core*>(core)->fileTransferFinished(*file);
|
||||||
emit static_cast<Core*>(core)->fileUploadFinished(file->filePath);
|
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);
|
nread = file->file->read((char*)data.get(), length);
|
||||||
if (nread <= 0) {
|
if (nread <= 0) {
|
||||||
qWarning("onFileDataCallback: Failed to read from file");
|
qWarning("onFileDataCallback: Failed to read from file");
|
||||||
|
file->status = ToxFile::CANCELED;
|
||||||
emit static_cast<Core*>(core)->fileTransferCancelled(*file);
|
emit static_cast<Core*>(core)->fileTransferCancelled(*file);
|
||||||
tox_file_send_chunk(tox, friendId, fileId, pos, nullptr, 0, nullptr);
|
tox_file_send_chunk(tox, friendId, fileId, pos, nullptr, 0, nullptr);
|
||||||
removeFile(friendId, fileId);
|
removeFile(friendId, fileId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
file->bytesSent += length;
|
file->bytesSent += length;
|
||||||
|
file->hashGenerator->addData((const char*)data.get(), length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tox_file_send_chunk(tox, friendId, fileId, pos, data.get(), nread, nullptr)) {
|
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) {
|
if (file->bytesSent != position) {
|
||||||
qWarning("onFileRecvChunkCallback: Received a chunk out-of-order, aborting transfer");
|
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);
|
emit core->fileTransferCancelled(*file);
|
||||||
|
}
|
||||||
tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_CANCEL, nullptr);
|
tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_CANCEL, nullptr);
|
||||||
removeFile(friendId, fileId);
|
removeFile(friendId, fileId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!length) {
|
if (!length) {
|
||||||
|
file->status = ToxFile::FINISHED;
|
||||||
if (file->fileKind == TOX_FILE_KIND_AVATAR) {
|
if (file->fileKind == TOX_FILE_KIND_AVATAR) {
|
||||||
QPixmap pic;
|
QPixmap pic;
|
||||||
pic.loadFromData(file->avatarData);
|
pic.loadFromData(file->avatarData);
|
||||||
|
@ -486,6 +491,7 @@ void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fil
|
||||||
else
|
else
|
||||||
file->file->write((char*)data, length);
|
file->file->write((char*)data, length);
|
||||||
file->bytesSent += length;
|
file->bytesSent += length;
|
||||||
|
file->hashGenerator->addData((const char*)data, length);
|
||||||
|
|
||||||
if (file->fileKind != TOX_FILE_KIND_AVATAR)
|
if (file->fileKind != TOX_FILE_KIND_AVATAR)
|
||||||
emit static_cast<Core*>(core)->fileTransferInfo(*file);
|
emit static_cast<Core*>(core)->fileTransferInfo(*file);
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
/**
|
/**
|
||||||
* @brief ToxFile constructor
|
* @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)
|
FileDirection Direction)
|
||||||
: fileKind{TOX_FILE_KIND_DATA}
|
: fileKind{TOX_FILE_KIND_DATA}
|
||||||
, fileNum(fileNum)
|
, fileNum(fileNum)
|
||||||
|
@ -28,10 +28,9 @@ ToxFile::ToxFile(uint32_t fileNum, uint32_t friendId, QByteArray filename, QStri
|
||||||
, file{new QFile(filePath)}
|
, file{new QFile(filePath)}
|
||||||
, bytesSent{0}
|
, bytesSent{0}
|
||||||
, filesize{0}
|
, filesize{0}
|
||||||
, status{STOPPED}
|
, status{INITIALIZING}
|
||||||
, direction{Direction}
|
, direction{Direction}
|
||||||
{
|
{}
|
||||||
}
|
|
||||||
|
|
||||||
bool ToxFile::operator==(const ToxFile& other) const
|
bool ToxFile::operator==(const ToxFile& other) const
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,32 +3,36 @@
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
|
||||||
class QFile;
|
class QFile;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
|
|
||||||
struct ToxFile
|
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
|
enum FileStatus
|
||||||
{
|
{
|
||||||
STOPPED,
|
INITIALIZING = 0,
|
||||||
PAUSED,
|
PAUSED = 1,
|
||||||
TRANSMITTING,
|
TRANSMITTING = 2,
|
||||||
BROKEN
|
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
|
enum FileDirection : bool
|
||||||
{
|
{
|
||||||
SENDING,
|
SENDING = 0,
|
||||||
RECEIVING
|
RECEIVING = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
ToxFile() = default;
|
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);
|
FileDirection Direction);
|
||||||
~ToxFile()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool operator==(const ToxFile& other) const;
|
bool operator==(const ToxFile& other) const;
|
||||||
bool operator!=(const ToxFile& other) const;
|
bool operator!=(const ToxFile& other) const;
|
||||||
|
@ -39,7 +43,7 @@ struct ToxFile
|
||||||
uint8_t fileKind;
|
uint8_t fileKind;
|
||||||
uint32_t fileNum;
|
uint32_t fileNum;
|
||||||
uint32_t friendId;
|
uint32_t friendId;
|
||||||
QByteArray fileName;
|
QString fileName;
|
||||||
QString filePath;
|
QString filePath;
|
||||||
std::shared_ptr<QFile> file;
|
std::shared_ptr<QFile> file;
|
||||||
quint64 bytesSent;
|
quint64 bytesSent;
|
||||||
|
@ -48,6 +52,7 @@ struct ToxFile
|
||||||
FileDirection direction;
|
FileDirection direction;
|
||||||
QByteArray avatarData;
|
QByteArray avatarData;
|
||||||
QByteArray resumeFileId;
|
QByteArray resumeFileId;
|
||||||
|
std::shared_ptr<QCryptographicHash> hashGenerator = std::make_shared<QCryptographicHash>(QCryptographicHash::Sha256);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // CORESTRUCTS_H
|
#endif // CORESTRUCTS_H
|
||||||
|
|
|
@ -34,7 +34,15 @@
|
||||||
* Caches mappings to speed up message saving.
|
* 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.
|
* @brief Prepares the database to work with the history.
|
||||||
|
@ -48,14 +56,44 @@ History::History(std::shared_ptr<RawDatabase> db)
|
||||||
return;
|
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(
|
db->execLater(
|
||||||
"CREATE TABLE IF NOT EXISTS peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL "
|
"CREATE TABLE IF NOT EXISTS peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL "
|
||||||
"UNIQUE);"
|
"UNIQUE);"
|
||||||
"CREATE TABLE IF NOT EXISTS aliases (id INTEGER PRIMARY KEY, owner INTEGER,"
|
"CREATE TABLE IF NOT EXISTS aliases (id INTEGER PRIMARY KEY, owner INTEGER,"
|
||||||
"display_name BLOB NOT NULL, UNIQUE(owner, display_name));"
|
"display_name BLOB NOT NULL, UNIQUE(owner, display_name));"
|
||||||
"CREATE TABLE IF NOT EXISTS history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, "
|
"CREATE TABLE IF NOT EXISTS history "
|
||||||
"chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, "
|
"(id INTEGER PRIMARY KEY,"
|
||||||
"message BLOB NOT NULL);"
|
"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);");
|
"CREATE TABLE IF NOT EXISTS faux_offline_pending (id INTEGER PRIMARY KEY);");
|
||||||
|
|
||||||
// Cache our current peers
|
// Cache our current peers
|
||||||
|
@ -108,6 +146,7 @@ void History::eraseHistory()
|
||||||
"DELETE FROM history;"
|
"DELETE FROM history;"
|
||||||
"DELETE FROM aliases;"
|
"DELETE FROM aliases;"
|
||||||
"DELETE FROM peers;"
|
"DELETE FROM peers;"
|
||||||
|
"DELETE FROM file_transfers;"
|
||||||
"VACUUM;");
|
"VACUUM;");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,6 +175,7 @@ void History::removeFriendHistory(const QString& friendPk)
|
||||||
"DELETE FROM history WHERE chat_id=%1; "
|
"DELETE FROM history WHERE chat_id=%1; "
|
||||||
"DELETE FROM aliases WHERE owner=%1; "
|
"DELETE FROM aliases WHERE owner=%1; "
|
||||||
"DELETE FROM peers WHERE id=%1; "
|
"DELETE FROM peers WHERE id=%1; "
|
||||||
|
"DELETE FROM file_transfers WHERE chat_id=%1;"
|
||||||
"VACUUM;")
|
"VACUUM;")
|
||||||
.arg(id);
|
.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
|
// Get the db id of the peer we're chatting with
|
||||||
int64_t peerId;
|
int64_t peerId;
|
||||||
if (peers.contains(friendPk)) {
|
if (peers.contains(friendPk)) {
|
||||||
peerId = peers[friendPk];
|
peerId = (peers)[friendPk];
|
||||||
} else {
|
} else {
|
||||||
if (peers.isEmpty()) {
|
if (peers.isEmpty()) {
|
||||||
peerId = 0;
|
peerId = 0;
|
||||||
|
@ -174,7 +214,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
|
||||||
peerId = *std::max_element(peers.begin(), peers.end()) + 1;
|
peerId = *std::max_element(peers.begin(), peers.end()) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
peers[friendPk] = peerId;
|
(peers)[friendPk] = peerId;
|
||||||
queries += RawDatabase::Query(("INSERT INTO peers (id, public_key) "
|
queries += RawDatabase::Query(("INSERT INTO peers (id, public_key) "
|
||||||
"VALUES (%1, '"
|
"VALUES (%1, '"
|
||||||
+ friendPk + "');")
|
+ friendPk + "');")
|
||||||
|
@ -184,7 +224,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
|
||||||
// Get the db id of the sender of the message
|
// Get the db id of the sender of the message
|
||||||
int64_t senderId;
|
int64_t senderId;
|
||||||
if (peers.contains(sender)) {
|
if (peers.contains(sender)) {
|
||||||
senderId = peers[sender];
|
senderId = (peers)[sender];
|
||||||
} else {
|
} else {
|
||||||
if (peers.isEmpty()) {
|
if (peers.isEmpty()) {
|
||||||
senderId = 0;
|
senderId = 0;
|
||||||
|
@ -192,7 +232,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
|
||||||
senderId = *std::max_element(peers.begin(), peers.end()) + 1;
|
senderId = *std::max_element(peers.begin(), peers.end()) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
peers[sender] = senderId;
|
(peers)[sender] = senderId;
|
||||||
queries += RawDatabase::Query{("INSERT INTO peers (id, public_key) "
|
queries += RawDatabase::Query{("INSERT INTO peers (id, public_key) "
|
||||||
"VALUES (%1, '"
|
"VALUES (%1, '"
|
||||||
+ sender + "');")
|
+ sender + "');")
|
||||||
|
@ -228,6 +268,121 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
|
||||||
return queries;
|
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.
|
* @brief Saves a chat message in the database.
|
||||||
* @param friendPk Friend publick key to save.
|
* @param friendPk Friend publick key to save.
|
||||||
|
@ -254,6 +409,21 @@ void History::addNewMessage(const QString& friendPk, const QString& message, con
|
||||||
insertIdCallback));
|
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.
|
* @brief Fetches chat messages from the database.
|
||||||
* @param friendPk Friend publick key to fetch.
|
* @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.
|
* @param to End of period to fetch.
|
||||||
* @return List of messages.
|
* @return List of messages.
|
||||||
*/
|
*/
|
||||||
QList<History::HistMessage> History::getChatHistoryFromDate(const QString& friendPk, const QDateTime& from,
|
QList<History::HistMessage> History::getChatHistoryFromDate(const QString& friendPk,
|
||||||
const QDateTime& to)
|
const QDateTime& from, const QDateTime& to)
|
||||||
{
|
{
|
||||||
if (!isValid()) {
|
if (!isValid()) {
|
||||||
return {};
|
return {};
|
||||||
|
@ -280,7 +450,8 @@ QList<History::HistMessage> History::getChatHistoryDefaultNum(const QString& fri
|
||||||
if (!isValid()) {
|
if (!isValid()) {
|
||||||
return {};
|
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
|
* @param parameter for search
|
||||||
* @return date of the message where the phrase was found
|
* @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;
|
QDateTime result;
|
||||||
auto rowCallback = [&result](const QVector<QVariant>& row) {
|
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);
|
message = QStringLiteral("message LIKE '%%1%'").arg(phrase);
|
||||||
break;
|
break;
|
||||||
case FilterSearch::WordsOnly:
|
case FilterSearch::WordsOnly:
|
||||||
message = QStringLiteral("message REGEXP '%1'").arg(SearchExtraFunctions::generateFilterWordsOnly(phrase).toLower());
|
message = QStringLiteral("message REGEXP '%1'")
|
||||||
|
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase).toLower());
|
||||||
break;
|
break;
|
||||||
case FilterSearch::RegisterAndWordsOnly:
|
case FilterSearch::RegisterAndWordsOnly:
|
||||||
message = QStringLiteral("REGEXPSENSITIVE(message, '%1')").arg(SearchExtraFunctions::generateFilterWordsOnly(phrase));
|
message = QStringLiteral("REGEXPSENSITIVE(message, '%1')")
|
||||||
|
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase));
|
||||||
break;
|
break;
|
||||||
case FilterSearch::Regular:
|
case FilterSearch::Regular:
|
||||||
message = QStringLiteral("message REGEXP '%1'").arg(phrase);
|
message = QStringLiteral("message REGEXP '%1'").arg(phrase);
|
||||||
|
@ -376,24 +550,27 @@ QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTi
|
||||||
period = QStringLiteral("ORDER BY timestamp ASC LIMIT 1;");
|
period = QStringLiteral("ORDER BY timestamp ASC LIMIT 1;");
|
||||||
break;
|
break;
|
||||||
case PeriodSearch::AfterDate:
|
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;
|
break;
|
||||||
case PeriodSearch::BeforeDate:
|
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;
|
break;
|
||||||
default:
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString queryText =
|
QString queryText =
|
||||||
QStringLiteral("SELECT timestamp "
|
QStringLiteral("SELECT timestamp "
|
||||||
"FROM history "
|
"FROM history "
|
||||||
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
||||||
"JOIN peers chat ON chat_id = chat.id "
|
"JOIN peers chat ON chat_id = chat.id "
|
||||||
"WHERE chat.public_key='%1' "
|
"WHERE chat.public_key='%1' "
|
||||||
"AND %2 "
|
"AND %2 "
|
||||||
"%3")
|
"%3")
|
||||||
.arg(friendPk)
|
.arg(friendPk)
|
||||||
.arg(message)
|
.arg(message)
|
||||||
.arg(period);
|
.arg(period);
|
||||||
|
@ -408,7 +585,7 @@ QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTi
|
||||||
* @param friendPk Friend public key
|
* @param friendPk Friend public key
|
||||||
* @return start date of correspondence
|
* @return start date of correspondence
|
||||||
*/
|
*/
|
||||||
QDateTime History::getStartDateChatHistory(const QString &friendPk)
|
QDateTime History::getStartDateChatHistory(const QString& friendPk)
|
||||||
{
|
{
|
||||||
QDateTime result;
|
QDateTime result;
|
||||||
auto rowCallback = [&result](const QVector<QVariant>& row) {
|
auto rowCallback = [&result](const QVector<QVariant>& row) {
|
||||||
|
@ -416,11 +593,11 @@ QDateTime History::getStartDateChatHistory(const QString &friendPk)
|
||||||
};
|
};
|
||||||
|
|
||||||
QString queryText =
|
QString queryText =
|
||||||
QStringLiteral("SELECT timestamp "
|
QStringLiteral("SELECT timestamp "
|
||||||
"FROM history "
|
"FROM history "
|
||||||
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
||||||
"JOIN peers chat ON chat_id = chat.id "
|
"JOIN peers chat ON chat_id = chat.id "
|
||||||
"WHERE chat.public_key='%1' ORDER BY timestamp ASC LIMIT 1;")
|
"WHERE chat.public_key='%1' ORDER BY timestamp ASC LIMIT 1;")
|
||||||
.arg(friendPk);
|
.arg(friendPk);
|
||||||
|
|
||||||
db->execNow({queryText, rowCallback});
|
db->execNow({queryText, rowCallback});
|
||||||
|
@ -460,31 +637,50 @@ QList<History::HistMessage> History::getChatHistory(const QString& friendPk, con
|
||||||
auto rowCallback = [&messages](const QVector<QVariant>& row) {
|
auto rowCallback = [&messages](const QVector<QVariant>& row) {
|
||||||
// dispName and message could have null bytes, QString::fromUtf8
|
// dispName and message could have null bytes, QString::fromUtf8
|
||||||
// truncates on null bytes so we strip them
|
// truncates on null bytes so we strip them
|
||||||
messages += {row[0].toLongLong(),
|
auto id = row[0].toLongLong();
|
||||||
row[1].isNull(),
|
auto isOfflineMessage = row[1].isNull();
|
||||||
QDateTime::fromMSecsSinceEpoch(row[2].toLongLong()),
|
auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong());
|
||||||
row[3].toString(),
|
auto friend_key = row[3].toString();
|
||||||
QString::fromUtf8(row[4].toByteArray().replace('\0', "")),
|
auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', ""));
|
||||||
row[5].toString(),
|
auto sender_key = row[5].toString();
|
||||||
QString::fromUtf8(row[6].toByteArray().replace('\0', ""))};
|
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!
|
// Don't forget to update the rowCallback if you change the selected columns!
|
||||||
QString queryText =
|
QString queryText =
|
||||||
QString("SELECT history.id, faux_offline_pending.id, timestamp, "
|
QString("SELECT history.id, faux_offline_pending.id, timestamp, "
|
||||||
"chat.public_key, aliases.display_name, sender.public_key, "
|
"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 "
|
"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 aliases ON sender_alias = aliases.id "
|
||||||
"JOIN peers sender ON aliases.owner = sender.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'")
|
"WHERE timestamp BETWEEN %1 AND %2 AND chat.public_key='%3'")
|
||||||
.arg(from.toMSecsSinceEpoch())
|
.arg(from.toMSecsSinceEpoch())
|
||||||
.arg(to.toMSecsSinceEpoch())
|
.arg(to.toMSecsSinceEpoch())
|
||||||
.arg(friendPk);
|
.arg(friendPk);
|
||||||
if (numMessages) {
|
if (numMessages) {
|
||||||
queryText = "SELECT * FROM (" + queryText +
|
queryText =
|
||||||
QString(" ORDER BY history.id DESC limit %1) AS T1 ORDER BY T1.id ASC;").arg(numMessages);
|
"SELECT * FROM (" + queryText
|
||||||
|
+ QString(" ORDER BY history.id DESC limit %1) AS T1 ORDER BY T1.id ASC;").arg(numMessages);
|
||||||
} else {
|
} else {
|
||||||
queryText = queryText + ";";
|
queryText = queryText + ";";
|
||||||
}
|
}
|
||||||
|
@ -493,3 +689,44 @@ QList<History::HistMessage> History::getChatHistory(const QString& friendPk, con
|
||||||
|
|
||||||
return messages;
|
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 << ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -22,11 +22,14 @@
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
#include <QPointer>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <tox/toxencryptsave.h>
|
#include <tox/toxencryptsave.h>
|
||||||
|
|
||||||
|
#include "src/core/toxfile.h"
|
||||||
#include "src/core/toxpk.h"
|
#include "src/core/toxpk.h"
|
||||||
#include "src/persistence/db/rawdatabase.h"
|
#include "src/persistence/db/rawdatabase.h"
|
||||||
#include "src/widget/searchtypes.h"
|
#include "src/widget/searchtypes.h"
|
||||||
|
@ -34,8 +37,77 @@
|
||||||
class Profile;
|
class Profile;
|
||||||
class HistoryKeeper;
|
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:
|
public:
|
||||||
struct HistMessage
|
struct HistMessage
|
||||||
{
|
{
|
||||||
|
@ -43,21 +115,32 @@ public:
|
||||||
QString sender, QString message)
|
QString sender, QString message)
|
||||||
: chat{chat}
|
: chat{chat}
|
||||||
, sender{sender}
|
, sender{sender}
|
||||||
, message{message}
|
|
||||||
, dispName{dispName}
|
, dispName{dispName}
|
||||||
, timestamp{timestamp}
|
, timestamp{timestamp}
|
||||||
, id{id}
|
, id{id}
|
||||||
, isSent{isSent}
|
, 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 chat;
|
||||||
QString sender;
|
QString sender;
|
||||||
QString message;
|
|
||||||
QString dispName;
|
QString dispName;
|
||||||
QDateTime timestamp;
|
QDateTime timestamp;
|
||||||
qint64 id;
|
qint64 id;
|
||||||
bool isSent;
|
bool isSent;
|
||||||
|
HistMessageContent content;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DateMessages
|
struct DateMessages
|
||||||
|
@ -80,6 +163,12 @@ public:
|
||||||
const QDateTime& time, bool isSent, QString dispName,
|
const QDateTime& time, bool isSent, QString dispName,
|
||||||
const std::function<void(int64_t)>& insertIdCallback = {});
|
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,
|
QList<HistMessage> getChatHistoryFromDate(const QString& friendPk, const QDateTime& from,
|
||||||
const QDateTime& to);
|
const QDateTime& to);
|
||||||
QList<HistMessage> getChatHistoryDefaultNum(const QString& friendPk);
|
QList<HistMessage> getChatHistoryDefaultNum(const QString& friendPk);
|
||||||
|
@ -96,11 +185,37 @@ protected:
|
||||||
const QString& sender, const QDateTime& time, bool isSent,
|
const QString& sender, const QDateTime& time, bool isSent,
|
||||||
QString dispName, std::function<void(int64_t)> insertIdCallback = {});
|
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:
|
private:
|
||||||
QList<HistMessage> getChatHistory(const QString& friendPk, const QDateTime& from,
|
QList<HistMessage> getChatHistory(const QString& friendPk, const QDateTime& from,
|
||||||
const QDateTime& to, int numMessages);
|
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;
|
std::shared_ptr<RawDatabase> db;
|
||||||
|
|
||||||
|
|
||||||
QHash<QString, int64_t> peers;
|
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
|
#endif // HISTORY_H
|
||||||
|
|
|
@ -748,7 +748,7 @@ QStringList Profile::remove()
|
||||||
qWarning() << "Could not remove file " << dbPath;
|
qWarning() << "Could not remove file " << dbPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
history.release();
|
history.reset();
|
||||||
database.reset();
|
database.reset();
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|
|
@ -109,7 +109,7 @@ private:
|
||||||
QString name;
|
QString name;
|
||||||
std::unique_ptr<ToxEncrypt> passkey = nullptr;
|
std::unique_ptr<ToxEncrypt> passkey = nullptr;
|
||||||
std::shared_ptr<RawDatabase> database;
|
std::shared_ptr<RawDatabase> database;
|
||||||
std::unique_ptr<History> history;
|
std::shared_ptr<History> history;
|
||||||
bool isRemoved;
|
bool isRemoved;
|
||||||
bool encrypted = false;
|
bool encrypted = false;
|
||||||
static QStringList profiles;
|
static QStringList profiles;
|
||||||
|
|
|
@ -161,6 +161,9 @@ ChatForm::ChatForm(Friend* chatFriend, History* history)
|
||||||
connect(core, &Core::fileReceiveRequested, this, &ChatForm::onFileRecvRequest);
|
connect(core, &Core::fileReceiveRequested, this, &ChatForm::onFileRecvRequest);
|
||||||
connect(profile, &Profile::friendAvatarChanged, this, &ChatForm::onAvatarChanged);
|
connect(profile, &Profile::friendAvatarChanged, this, &ChatForm::onAvatarChanged);
|
||||||
connect(core, &Core::fileSendStarted, this, &ChatForm::startFileSend);
|
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::fileSendFailed, this, &ChatForm::onFileSendFailed);
|
||||||
connect(core, &Core::receiptRecieved, this, &ChatForm::onReceiptReceived);
|
connect(core, &Core::receiptRecieved, this, &ChatForm::onReceiptReceived);
|
||||||
connect(core, &Core::friendMessageReceived, this, &ChatForm::onFriendMessageReceived);
|
connect(core, &Core::friendMessageReceived, this, &ChatForm::onFriendMessageReceived);
|
||||||
|
@ -312,9 +315,35 @@ void ChatForm::startFileSend(ToxFile file)
|
||||||
|
|
||||||
insertChatMessage(
|
insertChatMessage(
|
||||||
ChatMessage::createFileTransferMessage(name, file, true, QDateTime::currentDateTime()));
|
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);
|
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)
|
void ChatForm::onFileRecvRequest(ToxFile file)
|
||||||
{
|
{
|
||||||
if (file.friendId != f->getId()) {
|
if (file.friendId != f->getId()) {
|
||||||
|
@ -331,9 +360,17 @@ void ChatForm::onFileRecvRequest(ToxFile file)
|
||||||
|
|
||||||
ChatMessage::Ptr msg =
|
ChatMessage::Ptr msg =
|
||||||
ChatMessage::createFileTransferMessage(name, file, false, QDateTime::currentDateTime());
|
ChatMessage::createFileTransferMessage(name, file, false, QDateTime::currentDateTime());
|
||||||
|
|
||||||
insertChatMessage(msg);
|
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));
|
ChatLineContentProxy* proxy = static_cast<ChatLineContentProxy*>(msg->getContent(1));
|
||||||
|
|
||||||
assert(proxy->getWidgetType() == ChatLineContentProxy::FileTransferWidgetType);
|
assert(proxy->getWidgetType() == ChatLineContentProxy::FileTransferWidgetType);
|
||||||
FileTransferWidget* tfWidget = static_cast<FileTransferWidget*>(proxy->getWidget());
|
FileTransferWidget* tfWidget = static_cast<FileTransferWidget*>(proxy->getWidget());
|
||||||
|
|
||||||
|
@ -794,6 +831,11 @@ void ChatForm::handleLoadedMessages(QList<History::HistMessage> newHistMsgs, boo
|
||||||
MessageMetadata const metadata = getMessageMetadata(histMessage);
|
MessageMetadata const metadata = getMessageMetadata(histMessage);
|
||||||
lastDate = addDateLineIfNeeded(chatLines, lastDate, histMessage, metadata);
|
lastDate = addDateLineIfNeeded(chatLines, lastDate, histMessage, metadata);
|
||||||
auto msg = chatMessageFromHistMessage(histMessage, metadata);
|
auto msg = chatMessageFromHistMessage(histMessage, metadata);
|
||||||
|
|
||||||
|
if (!msg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (processUndelivered) {
|
if (processUndelivered) {
|
||||||
sendLoadedMessage(msg, metadata);
|
sendLoadedMessage(msg, metadata);
|
||||||
}
|
}
|
||||||
|
@ -838,7 +880,9 @@ ChatForm::MessageMetadata ChatForm::getMessageMetadata(History::HistMessage cons
|
||||||
const QDateTime msgDateTime = histMessage.timestamp.toLocalTime();
|
const QDateTime msgDateTime = histMessage.timestamp.toLocalTime();
|
||||||
const bool isSelf = Core::getInstance()->getSelfId().getPublicKey() == authorPk;
|
const bool isSelf = Core::getInstance()->getSelfId().getPublicKey() == authorPk;
|
||||||
const bool needSending = !histMessage.isSent && isSelf;
|
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;
|
const qint64 id = histMessage.id;
|
||||||
return {isSelf, needSending, isAction, id, authorPk, msgDateTime};
|
return {isSelf, needSending, isAction, id, authorPk, msgDateTime};
|
||||||
}
|
}
|
||||||
|
@ -848,11 +892,31 @@ ChatMessage::Ptr ChatForm::chatMessageFromHistMessage(History::HistMessage const
|
||||||
{
|
{
|
||||||
ToxPk authorPk(ToxId(histMessage.sender).getPublicKey());
|
ToxPk authorPk(ToxId(histMessage.sender).getPublicKey());
|
||||||
QString authorStr = getMsgAuthorDispName(authorPk, histMessage.dispName);
|
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;
|
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)) {
|
if (!metadata.isAction && needsToHideName(authorPk, metadata.msgDateTime)) {
|
||||||
msg->hideSender();
|
msg->hideSender();
|
||||||
}
|
}
|
||||||
|
@ -1135,13 +1199,17 @@ void ChatForm::onExportChat()
|
||||||
|
|
||||||
QString buffer;
|
QString buffer;
|
||||||
for (const auto& it : msgs) {
|
for (const auto& it : msgs) {
|
||||||
|
if (it.content.getType() != HistMessageContentType::message) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
QString timestamp = it.timestamp.time().toString("hh:mm:ss");
|
QString timestamp = it.timestamp.time().toString("hh:mm:ss");
|
||||||
QString datestamp = it.timestamp.date().toString("yyyy-MM-dd");
|
QString datestamp = it.timestamp.date().toString("yyyy-MM-dd");
|
||||||
ToxPk authorPk(ToxId(it.sender).getPublicKey());
|
ToxPk authorPk(ToxId(it.sender).getPublicKey());
|
||||||
QString author = getMsgAuthorDispName(authorPk, it.dispName);
|
QString author = getMsgAuthorDispName(authorPk, it.dispName);
|
||||||
|
|
||||||
buffer = buffer
|
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.write(buffer.toUtf8());
|
||||||
file.close();
|
file.close();
|
||||||
|
|
|
@ -68,11 +68,14 @@ signals:
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void startFileSend(ToxFile file);
|
void startFileSend(ToxFile file);
|
||||||
|
void onFileTransferFinished(ToxFile file);
|
||||||
|
void onFileTransferCancelled(ToxFile file);
|
||||||
|
void onFileTransferBrokenUnbroken(ToxFile file, bool broken);
|
||||||
void onFileRecvRequest(ToxFile file);
|
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);
|
||||||
void onAvatarChanged(const ToxPk &friendPk, const QPixmap& pic);
|
void onAvatarChanged(const ToxPk& friendPk, const QPixmap& pic);
|
||||||
void onFileNameChanged(const ToxPk& friendPk);
|
void onFileNameChanged(const ToxPk& friendPk);
|
||||||
void clearChatArea();
|
void clearChatArea();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user