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/pixmapcache.cpp
|
||||
src/chatlog/pixmapcache.h
|
||||
src/chatlog/toxfileprogress.cpp
|
||||
src/chatlog/toxfileprogress.h
|
||||
src/chatlog/textformatter.cpp
|
||||
src/chatlog/textformatter.h
|
||||
src/core/coreav.cpp
|
||||
|
|
|
@ -39,8 +39,10 @@
|
|||
#include <QPainter>
|
||||
#include <QVariantAnimation>
|
||||
|
||||
#include <cassert>
|
||||
#include <math.h>
|
||||
|
||||
|
||||
// The leftButton is used to accept, pause, or resume a file transfer, as well as to open a
|
||||
// received file.
|
||||
// The rightButton is used to cancel a file transfer, or to open the directory a file was
|
||||
|
@ -50,7 +52,6 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file)
|
|||
: QWidget(parent)
|
||||
, ui(new Ui::FileTransferWidget)
|
||||
, fileInfo(file)
|
||||
, lastTick(QTime::currentTime())
|
||||
, backgroundColor(Style::getColor(Style::LightGrey))
|
||||
, buttonColor(Style::getColor(Style::Yellow))
|
||||
, buttonBackgroundColor(Style::getColor(Style::White))
|
||||
|
@ -84,8 +85,6 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file)
|
|||
update();
|
||||
});
|
||||
|
||||
setBackgroundColor(Style::getColor(Style::LightGrey), false);
|
||||
|
||||
connect(Core::getInstance(), &Core::fileTransferInfo, this,
|
||||
&FileTransferWidget::onFileTransferInfo);
|
||||
connect(Core::getInstance(), &Core::fileTransferAccepted, this,
|
||||
|
@ -105,15 +104,9 @@ FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file)
|
|||
connect(ui->previewButton, &QPushButton::clicked, this,
|
||||
&FileTransferWidget::onPreviewButtonClicked);
|
||||
|
||||
setupButtons();
|
||||
|
||||
// preview
|
||||
if (fileInfo.direction == ToxFile::SENDING) {
|
||||
showPreview(fileInfo.filePath);
|
||||
ui->progressLabel->setText(tr("Waiting to send...", "file transfer widget"));
|
||||
} else {
|
||||
ui->progressLabel->setText(tr("Accept to receive this file", "file transfer widget"));
|
||||
}
|
||||
// Set lastStatus to anything but the file's current value, this forces an update
|
||||
lastStatus = file.status == ToxFile::FINISHED ? ToxFile::INITIALIZING : ToxFile::FINISHED;
|
||||
updateWidget(file);
|
||||
|
||||
setFixedHeight(64);
|
||||
}
|
||||
|
@ -155,11 +148,12 @@ void FileTransferWidget::autoAcceptTransfer(const QString& path)
|
|||
|
||||
// Do not automatically accept the file-transfer if the path is not writable.
|
||||
// The user can still accept it manually.
|
||||
if (tryRemoveFile(filepath))
|
||||
if (tryRemoveFile(filepath)) {
|
||||
Core::getInstance()->acceptFileRecvRequest(fileInfo.friendId, fileInfo.fileNum, filepath);
|
||||
else
|
||||
} else {
|
||||
qWarning() << "Cannot write to " << filepath;
|
||||
}
|
||||
}
|
||||
|
||||
bool FileTransferWidget::isActive() const
|
||||
{
|
||||
|
@ -168,8 +162,9 @@ bool FileTransferWidget::isActive() const
|
|||
|
||||
void FileTransferWidget::acceptTransfer(const QString& filepath)
|
||||
{
|
||||
if (filepath.isEmpty())
|
||||
if (filepath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// test if writable
|
||||
if (!tryRemoveFile(filepath)) {
|
||||
|
@ -267,194 +262,223 @@ void FileTransferWidget::paintEvent(QPaintEvent*)
|
|||
|
||||
void FileTransferWidget::onFileTransferInfo(ToxFile file)
|
||||
{
|
||||
QTime now = QTime::currentTime();
|
||||
qint64 dt = lastTick.msecsTo(now); // ms
|
||||
|
||||
if (fileInfo != file || dt < 1000)
|
||||
return;
|
||||
|
||||
fileInfo = file;
|
||||
|
||||
if (fileInfo.status == ToxFile::TRANSMITTING) {
|
||||
// update progress
|
||||
qreal progress = static_cast<qreal>(file.bytesSent) / static_cast<qreal>(file.filesize);
|
||||
ui->progressBar->setValue(static_cast<int>(progress * 100.0));
|
||||
|
||||
// ETA, speed
|
||||
qreal deltaSecs = dt / 1000.0;
|
||||
|
||||
// (can't use ::abs or ::max on unsigned types substraction, they'd just overflow)
|
||||
quint64 deltaBytes = file.bytesSent > lastBytesSent ? file.bytesSent - lastBytesSent
|
||||
: lastBytesSent - file.bytesSent;
|
||||
qreal bytesPerSec = static_cast<int>(static_cast<qreal>(deltaBytes) / deltaSecs);
|
||||
|
||||
// calculate mean
|
||||
meanIndex = meanIndex % TRANSFER_ROLLING_AVG_COUNT;
|
||||
meanData[meanIndex++] = bytesPerSec;
|
||||
|
||||
qreal meanBytesPerSec = 0.0;
|
||||
for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i)
|
||||
meanBytesPerSec += meanData[i];
|
||||
|
||||
meanBytesPerSec /= static_cast<qreal>(TRANSFER_ROLLING_AVG_COUNT);
|
||||
|
||||
// update UI
|
||||
if (meanBytesPerSec > 0) {
|
||||
// ETA
|
||||
QTime toGo = QTime(0, 0).addSecs((file.filesize - file.bytesSent) / meanBytesPerSec);
|
||||
QString format = toGo.hour() > 0 ? "hh:mm:ss" : "mm:ss";
|
||||
ui->etaLabel->setText(toGo.toString(format));
|
||||
} else {
|
||||
ui->etaLabel->setText("");
|
||||
}
|
||||
|
||||
ui->progressLabel->setText(getHumanReadableSize(meanBytesPerSec) + "/s");
|
||||
|
||||
lastBytesSent = file.bytesSent;
|
||||
}
|
||||
|
||||
lastTick = now;
|
||||
|
||||
// trigger repaint
|
||||
update();
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::onFileTransferAccepted(ToxFile file)
|
||||
{
|
||||
if (fileInfo != file)
|
||||
return;
|
||||
|
||||
fileInfo = file;
|
||||
|
||||
setBackgroundColor(Style::getColor(Style::LightGrey), false);
|
||||
|
||||
setupButtons();
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::onFileTransferCancelled(ToxFile file)
|
||||
{
|
||||
if (fileInfo != file)
|
||||
return;
|
||||
|
||||
fileInfo = file;
|
||||
active = false;
|
||||
|
||||
setBackgroundColor(Style::getColor(Style::Red), true);
|
||||
|
||||
setupButtons();
|
||||
hideWidgets();
|
||||
|
||||
disconnect(Core::getInstance(), nullptr, this, nullptr);
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::onFileTransferPaused(ToxFile file)
|
||||
{
|
||||
if (fileInfo != file)
|
||||
return;
|
||||
|
||||
fileInfo = file;
|
||||
|
||||
ui->etaLabel->setText("");
|
||||
ui->progressLabel->setText(tr("Paused", "file transfer widget"));
|
||||
|
||||
// reset mean
|
||||
meanIndex = 0;
|
||||
for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i)
|
||||
meanData[i] = 0.0;
|
||||
|
||||
setBackgroundColor(Style::getColor(Style::LightGrey), false);
|
||||
|
||||
setupButtons();
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::onFileTransferResumed(ToxFile file)
|
||||
{
|
||||
if (fileInfo != file)
|
||||
return;
|
||||
|
||||
fileInfo = file;
|
||||
|
||||
ui->etaLabel->setText("");
|
||||
ui->progressLabel->setText(tr("Resuming...", "file transfer widget"));
|
||||
|
||||
// reset mean
|
||||
meanIndex = 0;
|
||||
for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i)
|
||||
meanData[i] = 0.0;
|
||||
|
||||
setBackgroundColor(Style::getColor(Style::LightGrey), false);
|
||||
|
||||
setupButtons();
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::onFileTransferFinished(ToxFile file)
|
||||
{
|
||||
if (fileInfo != file)
|
||||
return;
|
||||
|
||||
fileInfo = file;
|
||||
active = false;
|
||||
|
||||
setBackgroundColor(Style::getColor(Style::Green), true);
|
||||
|
||||
setupButtons();
|
||||
hideWidgets();
|
||||
|
||||
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/yes.svg")));
|
||||
ui->leftButton->setObjectName("ok");
|
||||
ui->leftButton->setToolTip(tr("Open file"));
|
||||
ui->leftButton->show();
|
||||
|
||||
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/dir.svg")));
|
||||
ui->rightButton->setObjectName("dir");
|
||||
ui->rightButton->setToolTip(tr("Open file directory"));
|
||||
ui->rightButton->show();
|
||||
|
||||
// preview
|
||||
if (fileInfo.direction == ToxFile::RECEIVING)
|
||||
showPreview(fileInfo.filePath);
|
||||
|
||||
disconnect(Core::getInstance(), nullptr, this, nullptr);
|
||||
updateWidget(file);
|
||||
}
|
||||
|
||||
void FileTransferWidget::fileTransferRemotePausedUnpaused(ToxFile file, bool paused)
|
||||
{
|
||||
if (paused)
|
||||
if (paused) {
|
||||
onFileTransferPaused(file);
|
||||
else
|
||||
} else {
|
||||
onFileTransferResumed(file);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferWidget::fileTransferBrokenUnbroken(ToxFile file, bool broken)
|
||||
{
|
||||
// TODO: Handle broken transfer differently once we have resuming code
|
||||
if (broken)
|
||||
if (broken) {
|
||||
onFileTransferCancelled(file);
|
||||
}
|
||||
}
|
||||
|
||||
QString FileTransferWidget::getHumanReadableSize(qint64 size)
|
||||
{
|
||||
static const char* suffix[] = {"B", "kiB", "MiB", "GiB", "TiB"};
|
||||
int exp = 0;
|
||||
|
||||
if (size > 0)
|
||||
if (size > 0) {
|
||||
exp = std::min((int)(log(size) / log(1024)), (int)(sizeof(suffix) / sizeof(suffix[0]) - 1));
|
||||
}
|
||||
|
||||
return QString().setNum(size / pow(1024, exp), 'f', exp > 1 ? 2 : 0).append(suffix[exp]);
|
||||
}
|
||||
|
||||
void FileTransferWidget::hideWidgets()
|
||||
void FileTransferWidget::updateWidgetColor(ToxFile const& file)
|
||||
{
|
||||
ui->leftButton->hide();
|
||||
ui->rightButton->hide();
|
||||
if (lastStatus == file.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (file.status) {
|
||||
case ToxFile::INITIALIZING:
|
||||
case ToxFile::PAUSED:
|
||||
case ToxFile::TRANSMITTING:
|
||||
setBackgroundColor(Style::getColor(Style::LightGrey), false);
|
||||
break;
|
||||
case ToxFile::BROKEN:
|
||||
case ToxFile::CANCELED:
|
||||
setBackgroundColor(Style::getColor(Style::Red), true);
|
||||
break;
|
||||
case ToxFile::FINISHED:
|
||||
setBackgroundColor(Style::getColor(Style::Green), true);
|
||||
break;
|
||||
default:
|
||||
qCritical() << "Invalid file status";
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferWidget::updateWidgetText(ToxFile const& file)
|
||||
{
|
||||
if (lastStatus == file.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (file.status) {
|
||||
case ToxFile::INITIALIZING:
|
||||
if (file.direction == ToxFile::SENDING) {
|
||||
ui->progressLabel->setText(tr("Waiting to send...", "file transfer widget"));
|
||||
} else {
|
||||
ui->progressLabel->setText(tr("Accept to receive this file", "file transfer widget"));
|
||||
}
|
||||
break;
|
||||
case ToxFile::PAUSED:
|
||||
ui->etaLabel->setText("");
|
||||
ui->progressLabel->setText(tr("Paused", "file transfer widget"));
|
||||
break;
|
||||
case ToxFile::TRANSMITTING:
|
||||
ui->etaLabel->setText("");
|
||||
ui->progressLabel->setText(tr("Resuming...", "file transfer widget"));
|
||||
break;
|
||||
case ToxFile::BROKEN:
|
||||
case ToxFile::CANCELED:
|
||||
break;
|
||||
case ToxFile::FINISHED:
|
||||
break;
|
||||
default:
|
||||
qCritical() << "Invalid file status";
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferWidget::updatePreview(ToxFile const& file)
|
||||
{
|
||||
if (lastStatus == file.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (file.status) {
|
||||
case ToxFile::INITIALIZING:
|
||||
case ToxFile::PAUSED:
|
||||
case ToxFile::TRANSMITTING:
|
||||
case ToxFile::BROKEN:
|
||||
case ToxFile::CANCELED:
|
||||
if (file.direction == ToxFile::SENDING) {
|
||||
showPreview(file.filePath);
|
||||
}
|
||||
break;
|
||||
case ToxFile::FINISHED:
|
||||
showPreview(file.filePath);
|
||||
break;
|
||||
default:
|
||||
qCritical() << "Invalid file status";
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferWidget::updateFileProgress(ToxFile const& file)
|
||||
{
|
||||
switch (file.status) {
|
||||
case ToxFile::INITIALIZING:
|
||||
break;
|
||||
case ToxFile::PAUSED:
|
||||
fileProgress.resetSpeed();
|
||||
break;
|
||||
case ToxFile::TRANSMITTING: {
|
||||
if (!fileProgress.needsUpdate()) {
|
||||
break;
|
||||
}
|
||||
|
||||
fileProgress.addSample(file);
|
||||
auto speed = fileProgress.getSpeed();
|
||||
auto progress = fileProgress.getProgress();
|
||||
auto remainingTime = fileProgress.getTimeLeftSeconds();
|
||||
|
||||
ui->progressBar->setValue(static_cast<int>(progress * 100.0));
|
||||
|
||||
// update UI
|
||||
if (speed > 0) {
|
||||
// ETA
|
||||
QTime toGo = QTime(0, 0).addSecs(remainingTime);
|
||||
QString format = toGo.hour() > 0 ? "hh:mm:ss" : "mm:ss";
|
||||
ui->etaLabel->setText(toGo.toString(format));
|
||||
} else {
|
||||
ui->etaLabel->setText("");
|
||||
}
|
||||
|
||||
ui->progressLabel->setText(getHumanReadableSize(speed) + "/s");
|
||||
break;
|
||||
}
|
||||
case ToxFile::BROKEN:
|
||||
case ToxFile::CANCELED:
|
||||
case ToxFile::FINISHED: {
|
||||
ui->progressBar->hide();
|
||||
ui->progressLabel->hide();
|
||||
ui->etaLabel->hide();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
qCritical() << "Invalid file status";
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferWidget::setupButtons()
|
||||
void FileTransferWidget::updateSignals(ToxFile const& file)
|
||||
{
|
||||
switch (fileInfo.status) {
|
||||
if (lastStatus == file.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (file.status) {
|
||||
case ToxFile::CANCELED:
|
||||
case ToxFile::BROKEN:
|
||||
case ToxFile::FINISHED:
|
||||
active = false;
|
||||
disconnect(Core::getInstance(), nullptr, this, nullptr);
|
||||
break;
|
||||
case ToxFile::INITIALIZING:
|
||||
case ToxFile::PAUSED:
|
||||
case ToxFile::TRANSMITTING:
|
||||
break;
|
||||
default:
|
||||
qCritical() << "Invalid file status";
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferWidget::setupButtons(ToxFile const& file)
|
||||
{
|
||||
if (lastStatus == file.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (file.status) {
|
||||
case ToxFile::TRANSMITTING:
|
||||
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/pause.svg")));
|
||||
ui->leftButton->setObjectName("pause");
|
||||
|
@ -479,13 +503,12 @@ void FileTransferWidget::setupButtons()
|
|||
setButtonColor(Style::getColor(Style::LightGrey));
|
||||
break;
|
||||
|
||||
case ToxFile::STOPPED:
|
||||
case ToxFile::BROKEN:
|
||||
case ToxFile::INITIALIZING:
|
||||
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/no.svg")));
|
||||
ui->rightButton->setObjectName("cancel");
|
||||
ui->rightButton->setToolTip(tr("Cancel transfer"));
|
||||
|
||||
if (fileInfo.direction == ToxFile::SENDING) {
|
||||
if (file.direction == ToxFile::SENDING) {
|
||||
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/pause.svg")));
|
||||
ui->leftButton->setObjectName("pause");
|
||||
ui->leftButton->setToolTip(tr("Pause transfer"));
|
||||
|
@ -495,27 +518,48 @@ void FileTransferWidget::setupButtons()
|
|||
ui->leftButton->setToolTip(tr("Accept transfer"));
|
||||
}
|
||||
break;
|
||||
case ToxFile::CANCELED:
|
||||
case ToxFile::BROKEN:
|
||||
ui->leftButton->hide();
|
||||
ui->rightButton->hide();
|
||||
break;
|
||||
case ToxFile::FINISHED:
|
||||
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/yes.svg")));
|
||||
ui->leftButton->setObjectName("ok");
|
||||
ui->leftButton->setToolTip(tr("Open file"));
|
||||
ui->leftButton->show();
|
||||
|
||||
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/dir.svg")));
|
||||
ui->rightButton->setObjectName("dir");
|
||||
ui->rightButton->setToolTip(tr("Open file directory"));
|
||||
ui->rightButton->show();
|
||||
|
||||
break;
|
||||
default:
|
||||
qCritical() << "Invalid file status";
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferWidget::handleButton(QPushButton* btn)
|
||||
{
|
||||
if (fileInfo.direction == ToxFile::SENDING) {
|
||||
if (btn->objectName() == "cancel")
|
||||
if (btn->objectName() == "cancel") {
|
||||
Core::getInstance()->cancelFileSend(fileInfo.friendId, fileInfo.fileNum);
|
||||
else if (btn->objectName() == "pause")
|
||||
} else if (btn->objectName() == "pause") {
|
||||
Core::getInstance()->pauseResumeFileSend(fileInfo.friendId, fileInfo.fileNum);
|
||||
else if (btn->objectName() == "resume")
|
||||
} else if (btn->objectName() == "resume") {
|
||||
Core::getInstance()->pauseResumeFileSend(fileInfo.friendId, fileInfo.fileNum);
|
||||
}
|
||||
} else // receiving or paused
|
||||
{
|
||||
if (btn->objectName() == "cancel")
|
||||
if (btn->objectName() == "cancel") {
|
||||
Core::getInstance()->cancelFileRecv(fileInfo.friendId, fileInfo.fileNum);
|
||||
else if (btn->objectName() == "pause")
|
||||
} else if (btn->objectName() == "pause") {
|
||||
Core::getInstance()->pauseResumeFileRecv(fileInfo.friendId, fileInfo.fileNum);
|
||||
else if (btn->objectName() == "resume")
|
||||
} else if (btn->objectName() == "resume") {
|
||||
Core::getInstance()->pauseResumeFileRecv(fileInfo.friendId, fileInfo.fileNum);
|
||||
else if (btn->objectName() == "accept") {
|
||||
} else if (btn->objectName() == "accept") {
|
||||
QString path =
|
||||
QFileDialog::getSaveFileName(Q_NULLPTR,
|
||||
tr("Save a file", "Title of the file saving dialog"),
|
||||
|
@ -544,12 +588,12 @@ void FileTransferWidget::showPreview(const QString& filename)
|
|||
|
||||
QFile imageFile(filename);
|
||||
if (!imageFile.open(QIODevice::ReadOnly)) {
|
||||
qCritical() << "Failed to open file for preview";
|
||||
return;
|
||||
}
|
||||
const QByteArray imageFileData = imageFile.readAll();
|
||||
QImage image = QImage::fromData(imageFileData);
|
||||
const int exifOrientation = getExifOrientation(imageFileData.constData(), imageFileData.size());
|
||||
const int exifOrientation =
|
||||
getExifOrientation(imageFileData.constData(), imageFileData.size());
|
||||
if (exifOrientation) {
|
||||
applyTransformation(exifOrientation, image);
|
||||
}
|
||||
|
@ -559,16 +603,16 @@ void FileTransferWidget::showPreview(const QString& filename)
|
|||
ui->previewButton->setIcon(QIcon(iconPixmap));
|
||||
ui->previewButton->setIconSize(iconPixmap.size());
|
||||
ui->previewButton->show();
|
||||
// Show mouseover preview, but make sure it's not larger than 50% of the screen width/height
|
||||
// Show mouseover preview, but make sure it's not larger than 50% of the screen
|
||||
// width/height
|
||||
const QRect desktopSize = QApplication::desktop()->screenGeometry();
|
||||
const int maxPreviewWidth{desktopSize.width() / 2};
|
||||
const int maxPreviewHeight{desktopSize.height() / 2};
|
||||
const QImage previewImage = [&image, maxPreviewWidth, maxPreviewHeight]() {
|
||||
if (image.width() > maxPreviewWidth || image.height() > maxPreviewHeight) {
|
||||
return image.scaled(maxPreviewWidth, maxPreviewHeight,
|
||||
Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
else {
|
||||
return image.scaled(maxPreviewWidth, maxPreviewHeight, Qt::KeepAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
} else {
|
||||
return image;
|
||||
}
|
||||
}();
|
||||
|
@ -578,8 +622,7 @@ void FileTransferWidget::showPreview(const QString& filename)
|
|||
buffer.open(QIODevice::WriteOnly);
|
||||
previewImage.save(&buffer, "PNG");
|
||||
buffer.close();
|
||||
ui->previewButton->setToolTip("<img src=data:image/png;base64," + imageData.toBase64()
|
||||
+ "/>");
|
||||
ui->previewButton->setToolTip("<img src=data:image/png;base64," + imageData.toBase64() + "/>");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -602,7 +645,8 @@ QPixmap FileTransferWidget::scaleCropIntoSquare(const QPixmap& source, const int
|
|||
{
|
||||
QPixmap result;
|
||||
|
||||
// Make sure smaller-than-icon images (at least one dimension is smaller) will not be upscaled
|
||||
// Make sure smaller-than-icon images (at least one dimension is smaller) will not be
|
||||
// upscaled
|
||||
if (source.width() < targetSize || source.height() < targetSize) {
|
||||
result = source;
|
||||
} else {
|
||||
|
@ -612,10 +656,11 @@ QPixmap FileTransferWidget::scaleCropIntoSquare(const QPixmap& source, const int
|
|||
|
||||
// Then, image has to be cropped (if needed) so it will not overflow rectangle
|
||||
// Only one dimension will be bigger after Qt::KeepAspectRatioByExpanding
|
||||
if (result.width() > targetSize)
|
||||
if (result.width() > targetSize) {
|
||||
return result.copy((result.width() - targetSize) / 2, 0, targetSize, targetSize);
|
||||
else if (result.height() > targetSize)
|
||||
} else if (result.height() > targetSize) {
|
||||
return result.copy(0, (result.height() - targetSize) / 2, targetSize, targetSize);
|
||||
}
|
||||
|
||||
// Picture was rectangle in the first place, no cropping
|
||||
return result;
|
||||
|
@ -625,8 +670,9 @@ int FileTransferWidget::getExifOrientation(const char* data, const int size)
|
|||
{
|
||||
ExifData* exifData = exif_data_new_from_data(reinterpret_cast<const unsigned char*>(data), size);
|
||||
|
||||
if (!exifData)
|
||||
if (!exifData) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int orientation = 0;
|
||||
const ExifByteOrder byteOrder = exif_data_get_byte_order(exifData);
|
||||
|
@ -641,8 +687,7 @@ int FileTransferWidget::getExifOrientation(const char* data, const int size)
|
|||
void FileTransferWidget::applyTransformation(const int orientation, QImage& image)
|
||||
{
|
||||
QTransform exifTransform;
|
||||
switch(static_cast<ExifOrientation>(orientation))
|
||||
{
|
||||
switch (static_cast<ExifOrientation>(orientation)) {
|
||||
case ExifOrientation::TopLeft:
|
||||
break;
|
||||
case ExifOrientation::TopRight:
|
||||
|
@ -673,3 +718,35 @@ void FileTransferWidget::applyTransformation(const int orientation, QImage& imag
|
|||
}
|
||||
image = image.transformed(exifTransform);
|
||||
}
|
||||
|
||||
void FileTransferWidget::updateWidget(ToxFile const& file)
|
||||
{
|
||||
if (fileInfo != file) {
|
||||
return;
|
||||
}
|
||||
|
||||
fileInfo = file;
|
||||
|
||||
// If we repainted on every packet our gui would be *very* slow
|
||||
bool bTransmitNeedsUpdate = fileProgress.needsUpdate();
|
||||
|
||||
updatePreview(file);
|
||||
updateFileProgress(file);
|
||||
updateWidgetText(file);
|
||||
updateWidgetColor(file);
|
||||
setupButtons(file);
|
||||
updateSignals(file);
|
||||
|
||||
lastStatus = file.status;
|
||||
|
||||
// trigger repaint
|
||||
switch (file.status) {
|
||||
case ToxFile::TRANSMITTING:
|
||||
if (!bTransmitNeedsUpdate) {
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
default:
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#include <QWidget>
|
||||
|
||||
#include "src/chatlog/chatlinecontent.h"
|
||||
#include "src/chatlog/toxfileprogress.h"
|
||||
#include "src/core/toxfile.h"
|
||||
|
||||
|
||||
|
@ -56,8 +57,12 @@ protected slots:
|
|||
|
||||
protected:
|
||||
QString getHumanReadableSize(qint64 size);
|
||||
void hideWidgets();
|
||||
void setupButtons();
|
||||
void updateWidgetColor(ToxFile const& file);
|
||||
void updateWidgetText(ToxFile const& file);
|
||||
void updateFileProgress(ToxFile const& file);
|
||||
void updateSignals(ToxFile const& file);
|
||||
void updatePreview(ToxFile const& file);
|
||||
void setupButtons(ToxFile const& file);
|
||||
void handleButton(QPushButton* btn);
|
||||
void showPreview(const QString& filename);
|
||||
void acceptTransfer(const QString& filepath);
|
||||
|
@ -79,28 +84,29 @@ private:
|
|||
static void applyTransformation(const int oritentation, QImage& image);
|
||||
static bool tryRemoveFile(const QString &filepath);
|
||||
|
||||
void updateWidget(ToxFile const& file);
|
||||
|
||||
private:
|
||||
Ui::FileTransferWidget* ui;
|
||||
ToxFileProgress fileProgress;
|
||||
ToxFile fileInfo;
|
||||
QTime lastTick;
|
||||
quint64 lastBytesSent = 0;
|
||||
QVariantAnimation* backgroundColorAnimation = nullptr;
|
||||
QVariantAnimation* buttonColorAnimation = nullptr;
|
||||
QColor backgroundColor;
|
||||
QColor buttonColor;
|
||||
QColor buttonBackgroundColor;
|
||||
|
||||
static const uint8_t TRANSFER_ROLLING_AVG_COUNT = 4;
|
||||
uint8_t meanIndex = 0;
|
||||
qreal meanData[TRANSFER_ROLLING_AVG_COUNT] = {0.0};
|
||||
|
||||
bool active;
|
||||
enum class ExifOrientation {
|
||||
ToxFile::FileStatus lastStatus = ToxFile::INITIALIZING;
|
||||
|
||||
enum class ExifOrientation
|
||||
{
|
||||
/* do not change values, this is exif spec
|
||||
*
|
||||
* name corresponds to where the 0 row and 0 column is in form row-column
|
||||
* i.e. entry 5 here means that the 0'th row corresponds to the left side of the scene and the 0'th column corresponds
|
||||
* to the top of the captured scene. This means that the image needs to be mirrored and rotated to be displayed.
|
||||
* i.e. entry 5 here means that the 0'th row corresponds to the left side of the scene and
|
||||
* the 0'th column corresponds to the top of the captured scene. This means that the image
|
||||
* needs to be mirrored and rotated to be displayed.
|
||||
*/
|
||||
TopLeft = 1,
|
||||
TopRight = 2,
|
||||
|
|
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};
|
||||
file.filesize = filesize;
|
||||
file.fileName = QByteArray((char*)avatarHash, TOX_HASH_LENGTH);
|
||||
file.fileKind = TOX_FILE_KIND_AVATAR;
|
||||
file.avatarData = data;
|
||||
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
|
||||
|
@ -121,19 +120,18 @@ void CoreFile::sendFile(Core* core, uint32_t friendId, QString filename, QString
|
|||
long long filesize)
|
||||
{
|
||||
QMutexLocker mlocker(&fileSendMutex);
|
||||
|
||||
QByteArray fileName = filename.toUtf8();
|
||||
ToxString fileName(filename);
|
||||
TOX_ERR_FILE_SEND sendErr;
|
||||
uint32_t fileNum = tox_file_send(core->tox.get(), friendId, TOX_FILE_KIND_DATA, filesize,
|
||||
nullptr, (uint8_t*)fileName.data(), fileName.size(), &sendErr);
|
||||
nullptr, fileName.data(), fileName.size(), &sendErr);
|
||||
if (sendErr != TOX_ERR_FILE_SEND_OK) {
|
||||
qWarning() << "sendFile: Can't create the Tox file sender (" << sendErr << ")";
|
||||
emit core->fileSendFailed(friendId, filename);
|
||||
emit core->fileSendFailed(friendId, fileName.getQString());
|
||||
return;
|
||||
}
|
||||
qDebug() << QString("sendFile: Created file sender %1 with friend %2").arg(fileNum).arg(friendId);
|
||||
|
||||
ToxFile file{fileNum, friendId, fileName, filePath, ToxFile::SENDING};
|
||||
ToxFile file{fileNum, friendId, fileName.getQString(), filePath, ToxFile::SENDING};
|
||||
file.filesize = filesize;
|
||||
file.resumeFileId.resize(TOX_FILE_ID_LENGTH);
|
||||
tox_file_get_file_id(core->tox.get(), friendId, fileNum, (uint8_t*)file.resumeFileId.data(),
|
||||
|
@ -199,7 +197,7 @@ void CoreFile::cancelFileSend(Core* core, uint32_t friendId, uint32_t fileId)
|
|||
return;
|
||||
}
|
||||
|
||||
file->status = ToxFile::STOPPED;
|
||||
file->status = ToxFile::CANCELED;
|
||||
emit core->fileTransferCancelled(*file);
|
||||
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
|
||||
removeFile(friendId, fileId);
|
||||
|
@ -212,7 +210,7 @@ void CoreFile::cancelFileRecv(Core* core, uint32_t friendId, uint32_t fileId)
|
|||
qWarning("cancelFileRecv: No such file in queue");
|
||||
return;
|
||||
}
|
||||
file->status = ToxFile::STOPPED;
|
||||
file->status = ToxFile::CANCELED;
|
||||
emit core->fileTransferCancelled(*file);
|
||||
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
|
||||
removeFile(friendId, fileId);
|
||||
|
@ -225,7 +223,7 @@ void CoreFile::rejectFileRecvRequest(Core* core, uint32_t friendId, uint32_t fil
|
|||
qWarning("rejectFileRecvRequest: No such file in queue");
|
||||
return;
|
||||
}
|
||||
file->status = ToxFile::STOPPED;
|
||||
file->status = ToxFile::CANCELED;
|
||||
emit core->fileTransferCancelled(*file);
|
||||
tox_file_control(core->tox.get(), file->friendId, file->fileNum, TOX_FILE_CONTROL_CANCEL, nullptr);
|
||||
removeFile(friendId, fileId);
|
||||
|
@ -379,6 +377,7 @@ void CoreFile::onFileControlCallback(Tox*, uint32_t friendId, uint32_t fileId,
|
|||
if (control == TOX_FILE_CONTROL_CANCEL) {
|
||||
if (file->fileKind != TOX_FILE_KIND_AVATAR)
|
||||
qDebug() << "File tranfer" << friendId << ":" << fileId << "cancelled by friend";
|
||||
file->status = ToxFile::CANCELED;
|
||||
emit static_cast<Core*>(core)->fileTransferCancelled(*file);
|
||||
removeFile(friendId, fileId);
|
||||
} else if (control == TOX_FILE_CONTROL_PAUSE) {
|
||||
|
@ -409,6 +408,7 @@ void CoreFile::onFileDataCallback(Tox* tox, uint32_t friendId, uint32_t fileId,
|
|||
|
||||
// If we reached EOF, ack and cleanup the transfer
|
||||
if (!length) {
|
||||
file->status = ToxFile::FINISHED;
|
||||
if (file->fileKind != TOX_FILE_KIND_AVATAR) {
|
||||
emit static_cast<Core*>(core)->fileTransferFinished(*file);
|
||||
emit static_cast<Core*>(core)->fileUploadFinished(file->filePath);
|
||||
|
@ -429,12 +429,14 @@ void CoreFile::onFileDataCallback(Tox* tox, uint32_t friendId, uint32_t fileId,
|
|||
nread = file->file->read((char*)data.get(), length);
|
||||
if (nread <= 0) {
|
||||
qWarning("onFileDataCallback: Failed to read from file");
|
||||
file->status = ToxFile::CANCELED;
|
||||
emit static_cast<Core*>(core)->fileTransferCancelled(*file);
|
||||
tox_file_send_chunk(tox, friendId, fileId, pos, nullptr, 0, nullptr);
|
||||
removeFile(friendId, fileId);
|
||||
return;
|
||||
}
|
||||
file->bytesSent += length;
|
||||
file->hashGenerator->addData((const char*)data.get(), length);
|
||||
}
|
||||
|
||||
if (!tox_file_send_chunk(tox, friendId, fileId, pos, data.get(), nread, nullptr)) {
|
||||
|
@ -458,14 +460,17 @@ void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fil
|
|||
|
||||
if (file->bytesSent != position) {
|
||||
qWarning("onFileRecvChunkCallback: Received a chunk out-of-order, aborting transfer");
|
||||
if (file->fileKind != TOX_FILE_KIND_AVATAR)
|
||||
if (file->fileKind != TOX_FILE_KIND_AVATAR) {
|
||||
file->status = ToxFile::CANCELED;
|
||||
emit core->fileTransferCancelled(*file);
|
||||
}
|
||||
tox_file_control(tox, friendId, fileId, TOX_FILE_CONTROL_CANCEL, nullptr);
|
||||
removeFile(friendId, fileId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!length) {
|
||||
file->status = ToxFile::FINISHED;
|
||||
if (file->fileKind == TOX_FILE_KIND_AVATAR) {
|
||||
QPixmap pic;
|
||||
pic.loadFromData(file->avatarData);
|
||||
|
@ -486,6 +491,7 @@ void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fil
|
|||
else
|
||||
file->file->write((char*)data, length);
|
||||
file->bytesSent += length;
|
||||
file->hashGenerator->addData((const char*)data, length);
|
||||
|
||||
if (file->fileKind != TOX_FILE_KIND_AVATAR)
|
||||
emit static_cast<Core*>(core)->fileTransferInfo(*file);
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
/**
|
||||
* @brief ToxFile constructor
|
||||
*/
|
||||
ToxFile::ToxFile(uint32_t fileNum, uint32_t friendId, QByteArray filename, QString filePath,
|
||||
ToxFile::ToxFile(uint32_t fileNum, uint32_t friendId, QString filename, QString filePath,
|
||||
FileDirection Direction)
|
||||
: fileKind{TOX_FILE_KIND_DATA}
|
||||
, fileNum(fileNum)
|
||||
|
@ -28,10 +28,9 @@ ToxFile::ToxFile(uint32_t fileNum, uint32_t friendId, QByteArray filename, QStri
|
|||
, file{new QFile(filePath)}
|
||||
, bytesSent{0}
|
||||
, filesize{0}
|
||||
, status{STOPPED}
|
||||
, status{INITIALIZING}
|
||||
, direction{Direction}
|
||||
{
|
||||
}
|
||||
{}
|
||||
|
||||
bool ToxFile::operator==(const ToxFile& other) const
|
||||
{
|
||||
|
|
|
@ -3,32 +3,36 @@
|
|||
|
||||
#include <QString>
|
||||
#include <memory>
|
||||
#include <QCryptographicHash>
|
||||
|
||||
class QFile;
|
||||
class QTimer;
|
||||
|
||||
struct ToxFile
|
||||
{
|
||||
// Note do not change values, these are directly inserted into the DB in their
|
||||
// current form, changing order would mess up database state!
|
||||
enum FileStatus
|
||||
{
|
||||
STOPPED,
|
||||
PAUSED,
|
||||
TRANSMITTING,
|
||||
BROKEN
|
||||
INITIALIZING = 0,
|
||||
PAUSED = 1,
|
||||
TRANSMITTING = 2,
|
||||
BROKEN = 3,
|
||||
CANCELED = 4,
|
||||
FINISHED = 5,
|
||||
};
|
||||
|
||||
// Note do not change values, these are directly inserted into the DB in their
|
||||
// current form (can add fields though as db representation is an int)
|
||||
enum FileDirection : bool
|
||||
{
|
||||
SENDING,
|
||||
RECEIVING
|
||||
SENDING = 0,
|
||||
RECEIVING = 1,
|
||||
};
|
||||
|
||||
ToxFile() = default;
|
||||
ToxFile(uint32_t FileNum, uint32_t FriendId, QByteArray FileName, QString filePath,
|
||||
ToxFile(uint32_t FileNum, uint32_t FriendId, QString FileName, QString filePath,
|
||||
FileDirection Direction);
|
||||
~ToxFile()
|
||||
{
|
||||
}
|
||||
|
||||
bool operator==(const ToxFile& other) const;
|
||||
bool operator!=(const ToxFile& other) const;
|
||||
|
@ -39,7 +43,7 @@ struct ToxFile
|
|||
uint8_t fileKind;
|
||||
uint32_t fileNum;
|
||||
uint32_t friendId;
|
||||
QByteArray fileName;
|
||||
QString fileName;
|
||||
QString filePath;
|
||||
std::shared_ptr<QFile> file;
|
||||
quint64 bytesSent;
|
||||
|
@ -48,6 +52,7 @@ struct ToxFile
|
|||
FileDirection direction;
|
||||
QByteArray avatarData;
|
||||
QByteArray resumeFileId;
|
||||
std::shared_ptr<QCryptographicHash> hashGenerator = std::make_shared<QCryptographicHash>(QCryptographicHash::Sha256);
|
||||
};
|
||||
|
||||
#endif // CORESTRUCTS_H
|
||||
|
|
|
@ -34,7 +34,15 @@
|
|||
* Caches mappings to speed up message saving.
|
||||
*/
|
||||
|
||||
static constexpr int NUM_MESSAGES_DEFAULT = 100; // arbitrary number of messages loaded when not loading by date
|
||||
static constexpr int NUM_MESSAGES_DEFAULT =
|
||||
100; // arbitrary number of messages loaded when not loading by date
|
||||
static constexpr int SCHEMA_VERSION = 1;
|
||||
|
||||
FileDbInsertionData::FileDbInsertionData()
|
||||
{
|
||||
static int id = qRegisterMetaType<FileDbInsertionData>();
|
||||
(void)id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Prepares the database to work with the history.
|
||||
|
@ -48,14 +56,44 @@ History::History(std::shared_ptr<RawDatabase> db)
|
|||
return;
|
||||
}
|
||||
|
||||
dbSchemaUpgrade();
|
||||
|
||||
// dbSchemaUpgrade may have put us in an invalid state
|
||||
if (!isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
connect(this, &History::fileInsertionReady, this, &History::onFileInsertionReady);
|
||||
connect(this, &History::fileInserted, this, &History::onFileInserted);
|
||||
|
||||
db->execLater(
|
||||
"CREATE TABLE IF NOT EXISTS peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL "
|
||||
"UNIQUE);"
|
||||
"CREATE TABLE IF NOT EXISTS aliases (id INTEGER PRIMARY KEY, owner INTEGER,"
|
||||
"display_name BLOB NOT NULL, UNIQUE(owner, display_name));"
|
||||
"CREATE TABLE IF NOT EXISTS history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, "
|
||||
"chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, "
|
||||
"message BLOB NOT NULL);"
|
||||
"CREATE TABLE IF NOT EXISTS history "
|
||||
"(id INTEGER PRIMARY KEY,"
|
||||
"timestamp INTEGER NOT NULL,"
|
||||
"chat_id INTEGER NOT NULL,"
|
||||
"sender_alias INTEGER NOT NULL,"
|
||||
// even though technically a message can be null for file transfer, we've opted
|
||||
// to just insert an empty string when there's no content, this moderately simplifies
|
||||
// implementation as currently our database doesn't have support for optional fields.
|
||||
// We would either have to insert "?" or "null" based on if message exists and then
|
||||
// ensure that our blob vector always has the right number of fields. Better to just
|
||||
// leave this as NOT NULL for now.
|
||||
"message BLOB NOT NULL,"
|
||||
"file_id INTEGER);"
|
||||
"CREATE TABLE IF NOT EXISTS file_transfers "
|
||||
"(id INTEGER PRIMARY KEY,"
|
||||
"chat_id INTEGER NOT NULL,"
|
||||
"file_restart_id BLOB NOT NULL,"
|
||||
"file_name BLOB NOT NULL, "
|
||||
"file_path BLOB NOT NULL,"
|
||||
"file_hash BLOB NOT NULL,"
|
||||
"file_size INTEGER NOT NULL,"
|
||||
"direction INTEGER NOT NULL,"
|
||||
"file_state INTEGER NOT NULL);"
|
||||
"CREATE TABLE IF NOT EXISTS faux_offline_pending (id INTEGER PRIMARY KEY);");
|
||||
|
||||
// Cache our current peers
|
||||
|
@ -108,6 +146,7 @@ void History::eraseHistory()
|
|||
"DELETE FROM history;"
|
||||
"DELETE FROM aliases;"
|
||||
"DELETE FROM peers;"
|
||||
"DELETE FROM file_transfers;"
|
||||
"VACUUM;");
|
||||
}
|
||||
|
||||
|
@ -136,6 +175,7 @@ void History::removeFriendHistory(const QString& friendPk)
|
|||
"DELETE FROM history WHERE chat_id=%1; "
|
||||
"DELETE FROM aliases WHERE owner=%1; "
|
||||
"DELETE FROM peers WHERE id=%1; "
|
||||
"DELETE FROM file_transfers WHERE chat_id=%1;"
|
||||
"VACUUM;")
|
||||
.arg(id);
|
||||
|
||||
|
@ -166,7 +206,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
|
|||
// Get the db id of the peer we're chatting with
|
||||
int64_t peerId;
|
||||
if (peers.contains(friendPk)) {
|
||||
peerId = peers[friendPk];
|
||||
peerId = (peers)[friendPk];
|
||||
} else {
|
||||
if (peers.isEmpty()) {
|
||||
peerId = 0;
|
||||
|
@ -174,7 +214,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
|
|||
peerId = *std::max_element(peers.begin(), peers.end()) + 1;
|
||||
}
|
||||
|
||||
peers[friendPk] = peerId;
|
||||
(peers)[friendPk] = peerId;
|
||||
queries += RawDatabase::Query(("INSERT INTO peers (id, public_key) "
|
||||
"VALUES (%1, '"
|
||||
+ friendPk + "');")
|
||||
|
@ -184,7 +224,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
|
|||
// Get the db id of the sender of the message
|
||||
int64_t senderId;
|
||||
if (peers.contains(sender)) {
|
||||
senderId = peers[sender];
|
||||
senderId = (peers)[sender];
|
||||
} else {
|
||||
if (peers.isEmpty()) {
|
||||
senderId = 0;
|
||||
|
@ -192,7 +232,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
|
|||
senderId = *std::max_element(peers.begin(), peers.end()) + 1;
|
||||
}
|
||||
|
||||
peers[sender] = senderId;
|
||||
(peers)[sender] = senderId;
|
||||
queries += RawDatabase::Query{("INSERT INTO peers (id, public_key) "
|
||||
"VALUES (%1, '"
|
||||
+ sender + "');")
|
||||
|
@ -228,6 +268,121 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
|
|||
return queries;
|
||||
}
|
||||
|
||||
void History::onFileInsertionReady(FileDbInsertionData data)
|
||||
{
|
||||
|
||||
QVector<RawDatabase::Query> queries;
|
||||
std::weak_ptr<History> weakThis = shared_from_this();
|
||||
|
||||
// peerId is guaranteed to be inserted since we just used it in addNewMessage
|
||||
auto peerId = peers[data.friendPk];
|
||||
// Copy to pass into labmda for later
|
||||
auto fileId = data.fileId;
|
||||
queries +=
|
||||
RawDatabase::Query(QStringLiteral(
|
||||
"INSERT INTO file_transfers (chat_id, file_restart_id, "
|
||||
"file_path, file_name, file_hash, file_size, direction, file_state) "
|
||||
"VALUES (%1, ?, ?, ?, ?, %2, %3, %4);")
|
||||
.arg(peerId)
|
||||
.arg(data.size)
|
||||
.arg(static_cast<int>(data.direction))
|
||||
.arg(ToxFile::CANCELED),
|
||||
{data.fileId.toUtf8(), data.filePath.toUtf8(), data.fileName.toUtf8(), QByteArray()},
|
||||
[weakThis, fileId](int64_t id) {
|
||||
auto pThis = weakThis.lock();
|
||||
if (pThis) {
|
||||
emit pThis->fileInserted(id, fileId);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
queries += RawDatabase::Query(QStringLiteral("UPDATE history "
|
||||
"SET file_id = (last_insert_rowid()) "
|
||||
"WHERE id = %1")
|
||||
.arg(data.historyId));
|
||||
|
||||
db->execLater(queries);
|
||||
}
|
||||
|
||||
void History::onFileInserted(int64_t dbId, QString fileId)
|
||||
{
|
||||
auto& fileInfo = fileInfos[fileId];
|
||||
if (fileInfo.finished) {
|
||||
db->execLater(
|
||||
generateFileFinished(dbId, fileInfo.success, fileInfo.filePath, fileInfo.fileHash));
|
||||
fileInfos.remove(fileId);
|
||||
} else {
|
||||
fileInfo.finished = false;
|
||||
fileInfo.fileId = dbId;
|
||||
}
|
||||
}
|
||||
|
||||
RawDatabase::Query History::generateFileFinished(int64_t id, bool success, const QString& filePath,
|
||||
const QByteArray& fileHash)
|
||||
{
|
||||
auto file_state = success ? ToxFile::FINISHED : ToxFile::CANCELED;
|
||||
if (filePath.length()) {
|
||||
return RawDatabase::Query(QStringLiteral("UPDATE file_transfers "
|
||||
"SET file_state = %1, file_path = ?, file_hash = ?"
|
||||
"WHERE id = %2")
|
||||
.arg(file_state)
|
||||
.arg(id),
|
||||
{filePath.toUtf8(), fileHash});
|
||||
} else {
|
||||
return RawDatabase::Query(QStringLiteral("UPDATE file_transfers "
|
||||
"SET finished = %1 "
|
||||
"WHERE id = %2")
|
||||
.arg(file_state)
|
||||
.arg(id));
|
||||
}
|
||||
}
|
||||
|
||||
void History::addNewFileMessage(const QString& friendPk, const QString& fileId,
|
||||
const QString& fileName, const QString& filePath, int64_t size,
|
||||
const QString& sender, const QDateTime& time, QString const& dispName)
|
||||
{
|
||||
// This is an incredibly far from an optimal way of implementing this,
|
||||
// but given the frequency that people are going to be initiating a file
|
||||
// transfer we can probably live with it.
|
||||
|
||||
// Since both inserting an alias for a user and inserting a file transfer
|
||||
// will generate new ids, there is no good way to inject both new ids into the
|
||||
// history query without refactoring our RawDatabase::Query and processor loops.
|
||||
|
||||
// What we will do instead is chain callbacks to try to get reasonable behavior.
|
||||
// We can call the generateNewMessageQueries() fn to insert a message with an empty
|
||||
// message in it, and get the id with the callbck. Once we have the id we can ammend
|
||||
// the data to have our newly inserted file_id as well
|
||||
|
||||
ToxFile::FileDirection direction;
|
||||
if (sender == friendPk) {
|
||||
direction = ToxFile::RECEIVING;
|
||||
} else {
|
||||
direction = ToxFile::SENDING;
|
||||
}
|
||||
|
||||
std::weak_ptr<History> weakThis = shared_from_this();
|
||||
FileDbInsertionData insertionData;
|
||||
insertionData.friendPk = friendPk;
|
||||
insertionData.fileId = fileId;
|
||||
insertionData.fileName = fileName;
|
||||
insertionData.filePath = filePath;
|
||||
insertionData.size = size;
|
||||
insertionData.direction = direction;
|
||||
|
||||
auto insertFileTransferFn = [weakThis, insertionData](int64_t messageId) {
|
||||
auto insertionDataRw = std::move(insertionData);
|
||||
|
||||
insertionDataRw.historyId = messageId;
|
||||
|
||||
auto thisPtr = weakThis.lock();
|
||||
if (thisPtr)
|
||||
emit thisPtr->fileInsertionReady(std::move(insertionDataRw));
|
||||
};
|
||||
|
||||
addNewMessage(friendPk, "", sender, time, true, dispName, insertFileTransferFn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Saves a chat message in the database.
|
||||
* @param friendPk Friend publick key to save.
|
||||
|
@ -254,6 +409,21 @@ void History::addNewMessage(const QString& friendPk, const QString& message, con
|
|||
insertIdCallback));
|
||||
}
|
||||
|
||||
void History::setFileFinished(const QString& fileId, bool success, const QString& filePath,
|
||||
const QByteArray& fileHash)
|
||||
{
|
||||
auto& fileInfo = fileInfos[fileId];
|
||||
if (fileInfo.fileId == -1) {
|
||||
fileInfo.finished = true;
|
||||
fileInfo.success = success;
|
||||
fileInfo.filePath = filePath;
|
||||
fileInfo.fileHash = fileHash;
|
||||
} else {
|
||||
db->execLater(generateFileFinished(fileInfo.fileId, success, filePath, fileHash));
|
||||
}
|
||||
|
||||
fileInfos.remove(fileId);
|
||||
}
|
||||
/**
|
||||
* @brief Fetches chat messages from the database.
|
||||
* @param friendPk Friend publick key to fetch.
|
||||
|
@ -261,8 +431,8 @@ void History::addNewMessage(const QString& friendPk, const QString& message, con
|
|||
* @param to End of period to fetch.
|
||||
* @return List of messages.
|
||||
*/
|
||||
QList<History::HistMessage> History::getChatHistoryFromDate(const QString& friendPk, const QDateTime& from,
|
||||
const QDateTime& to)
|
||||
QList<History::HistMessage> History::getChatHistoryFromDate(const QString& friendPk,
|
||||
const QDateTime& from, const QDateTime& to)
|
||||
{
|
||||
if (!isValid()) {
|
||||
return {};
|
||||
|
@ -280,7 +450,8 @@ QList<History::HistMessage> History::getChatHistoryDefaultNum(const QString& fri
|
|||
if (!isValid()) {
|
||||
return {};
|
||||
}
|
||||
return getChatHistory(friendPk, QDateTime::fromMSecsSinceEpoch(0), QDateTime::currentDateTime(), NUM_MESSAGES_DEFAULT);
|
||||
return getChatHistory(friendPk, QDateTime::fromMSecsSinceEpoch(0), QDateTime::currentDateTime(),
|
||||
NUM_MESSAGES_DEFAULT);
|
||||
}
|
||||
|
||||
|
||||
|
@ -333,7 +504,8 @@ QList<History::DateMessages> History::getChatHistoryCounts(const ToxPk& friendPk
|
|||
* @param parameter for search
|
||||
* @return date of the message where the phrase was found
|
||||
*/
|
||||
QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTime& from, QString phrase, const ParameterSearch& parameter)
|
||||
QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTime& from,
|
||||
QString phrase, const ParameterSearch& parameter)
|
||||
{
|
||||
QDateTime result;
|
||||
auto rowCallback = [&result](const QVector<QVariant>& row) {
|
||||
|
@ -349,10 +521,12 @@ QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTi
|
|||
message = QStringLiteral("message LIKE '%%1%'").arg(phrase);
|
||||
break;
|
||||
case FilterSearch::WordsOnly:
|
||||
message = QStringLiteral("message REGEXP '%1'").arg(SearchExtraFunctions::generateFilterWordsOnly(phrase).toLower());
|
||||
message = QStringLiteral("message REGEXP '%1'")
|
||||
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase).toLower());
|
||||
break;
|
||||
case FilterSearch::RegisterAndWordsOnly:
|
||||
message = QStringLiteral("REGEXPSENSITIVE(message, '%1')").arg(SearchExtraFunctions::generateFilterWordsOnly(phrase));
|
||||
message = QStringLiteral("REGEXPSENSITIVE(message, '%1')")
|
||||
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase));
|
||||
break;
|
||||
case FilterSearch::Regular:
|
||||
message = QStringLiteral("message REGEXP '%1'").arg(phrase);
|
||||
|
@ -376,13 +550,16 @@ QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTi
|
|||
period = QStringLiteral("ORDER BY timestamp ASC LIMIT 1;");
|
||||
break;
|
||||
case PeriodSearch::AfterDate:
|
||||
period = QStringLiteral("AND timestamp > '%1' ORDER BY timestamp ASC LIMIT 1;").arg(date.toMSecsSinceEpoch());
|
||||
period = QStringLiteral("AND timestamp > '%1' ORDER BY timestamp ASC LIMIT 1;")
|
||||
.arg(date.toMSecsSinceEpoch());
|
||||
break;
|
||||
case PeriodSearch::BeforeDate:
|
||||
period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;").arg(date.toMSecsSinceEpoch());
|
||||
period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;")
|
||||
.arg(date.toMSecsSinceEpoch());
|
||||
break;
|
||||
default:
|
||||
period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;").arg(date.toMSecsSinceEpoch());
|
||||
period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;")
|
||||
.arg(date.toMSecsSinceEpoch());
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -460,31 +637,50 @@ QList<History::HistMessage> History::getChatHistory(const QString& friendPk, con
|
|||
auto rowCallback = [&messages](const QVector<QVariant>& row) {
|
||||
// dispName and message could have null bytes, QString::fromUtf8
|
||||
// truncates on null bytes so we strip them
|
||||
messages += {row[0].toLongLong(),
|
||||
row[1].isNull(),
|
||||
QDateTime::fromMSecsSinceEpoch(row[2].toLongLong()),
|
||||
row[3].toString(),
|
||||
QString::fromUtf8(row[4].toByteArray().replace('\0', "")),
|
||||
row[5].toString(),
|
||||
QString::fromUtf8(row[6].toByteArray().replace('\0', ""))};
|
||||
auto id = row[0].toLongLong();
|
||||
auto isOfflineMessage = row[1].isNull();
|
||||
auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong());
|
||||
auto friend_key = row[3].toString();
|
||||
auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', ""));
|
||||
auto sender_key = row[5].toString();
|
||||
if (row[7].isNull()) {
|
||||
messages +=
|
||||
{id, isOfflineMessage, timestamp, friend_key, display_name, sender_key, row[6].toString()};
|
||||
} else {
|
||||
ToxFile file;
|
||||
file.fileKind = TOX_FILE_KIND_DATA;
|
||||
file.resumeFileId = row[7].toString().toUtf8();
|
||||
file.filePath = row[8].toString();
|
||||
file.fileName = row[9].toString();
|
||||
file.filesize = row[10].toLongLong();
|
||||
file.direction = static_cast<ToxFile::FileDirection>(row[11].toLongLong());
|
||||
file.status = static_cast<ToxFile::FileStatus>(row[12].toInt());
|
||||
messages +=
|
||||
{id, isOfflineMessage, timestamp, friend_key, display_name, sender_key, file};
|
||||
}
|
||||
};
|
||||
|
||||
// Don't forget to update the rowCallback if you change the selected columns!
|
||||
QString queryText =
|
||||
QString("SELECT history.id, faux_offline_pending.id, timestamp, "
|
||||
"chat.public_key, aliases.display_name, sender.public_key, "
|
||||
"message FROM history "
|
||||
"message, file_transfers.file_restart_id, "
|
||||
"file_transfers.file_path, file_transfers.file_name, "
|
||||
"file_transfers.file_size, file_transfers.direction, "
|
||||
"file_transfers.file_state FROM history "
|
||||
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
||||
"JOIN peers chat ON chat_id = chat.id "
|
||||
"JOIN peers chat ON history.chat_id = chat.id "
|
||||
"JOIN aliases ON sender_alias = aliases.id "
|
||||
"JOIN peers sender ON aliases.owner = sender.id "
|
||||
"LEFT JOIN file_transfers ON history.file_id = file_transfers.id "
|
||||
"WHERE timestamp BETWEEN %1 AND %2 AND chat.public_key='%3'")
|
||||
.arg(from.toMSecsSinceEpoch())
|
||||
.arg(to.toMSecsSinceEpoch())
|
||||
.arg(friendPk);
|
||||
if (numMessages) {
|
||||
queryText = "SELECT * FROM (" + queryText +
|
||||
QString(" ORDER BY history.id DESC limit %1) AS T1 ORDER BY T1.id ASC;").arg(numMessages);
|
||||
queryText =
|
||||
"SELECT * FROM (" + queryText
|
||||
+ QString(" ORDER BY history.id DESC limit %1) AS T1 ORDER BY T1.id ASC;").arg(numMessages);
|
||||
} else {
|
||||
queryText = queryText + ";";
|
||||
}
|
||||
|
@ -493,3 +689,44 @@ QList<History::HistMessage> History::getChatHistory(const QString& friendPk, con
|
|||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Upgrade the db schema
|
||||
* @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION
|
||||
* variable and add another case to the switch statement below. Make sure to fall through on each case.
|
||||
*/
|
||||
void History::dbSchemaUpgrade()
|
||||
{
|
||||
int64_t databaseSchemaVersion;
|
||||
db->execNow(RawDatabase::Query("PRAGMA user_version", [&](const QVector<QVariant>& row) {
|
||||
databaseSchemaVersion = row[0].toLongLong();
|
||||
}));
|
||||
|
||||
if (databaseSchemaVersion > SCHEMA_VERSION) {
|
||||
qWarning() << "Database version is newer than we currently support. Please upgrade qTox";
|
||||
// We don't know what future versions have done, we have to disable db access until we re-upgrade
|
||||
db.reset();
|
||||
return;
|
||||
} else if (databaseSchemaVersion == SCHEMA_VERSION) {
|
||||
// No work to do
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure to handle the un-created case as well in the following upgrade code
|
||||
switch (databaseSchemaVersion) {
|
||||
case 0:
|
||||
// This will generate a warning on new profiles, but we have no easy way to chain execs. I
|
||||
// don't want to block the rest of the program on db creation so I guess we can just live with the warning for now
|
||||
db->execLater(RawDatabase::Query("ALTER TABLE history ADD file_id INTEGER;"));
|
||||
// fallthrough
|
||||
// case 1:
|
||||
// do 1 -> 2 upgrade
|
||||
// //fallthrough
|
||||
// etc.
|
||||
default:
|
||||
db->execLater(
|
||||
RawDatabase::Query(QStringLiteral("PRAGMA user_version = %1;").arg(SCHEMA_VERSION)));
|
||||
qDebug() << "Database upgrade finished (databaseSchemaVersion " << databaseSchemaVersion
|
||||
<< " -> " << SCHEMA_VERSION << ")";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,11 +22,14 @@
|
|||
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QPointer>
|
||||
#include <QVector>
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <tox/toxencryptsave.h>
|
||||
|
||||
#include "src/core/toxfile.h"
|
||||
#include "src/core/toxpk.h"
|
||||
#include "src/persistence/db/rawdatabase.h"
|
||||
#include "src/widget/searchtypes.h"
|
||||
|
@ -34,8 +37,77 @@
|
|||
class Profile;
|
||||
class HistoryKeeper;
|
||||
|
||||
class History
|
||||
enum class HistMessageContentType
|
||||
{
|
||||
message,
|
||||
file
|
||||
};
|
||||
|
||||
class HistMessageContent
|
||||
{
|
||||
public:
|
||||
HistMessageContent(QString message)
|
||||
: data(std::make_shared<QString>(std::move(message)))
|
||||
, type(HistMessageContentType::message)
|
||||
{}
|
||||
|
||||
HistMessageContent(ToxFile file)
|
||||
: data(std::make_shared<ToxFile>(std::move(file)))
|
||||
, type(HistMessageContentType::file)
|
||||
{}
|
||||
|
||||
HistMessageContentType getType() const
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
QString& asMessage()
|
||||
{
|
||||
assert(type == HistMessageContentType::message);
|
||||
return *static_cast<QString*>(data.get());
|
||||
}
|
||||
|
||||
ToxFile& asFile()
|
||||
{
|
||||
assert(type == HistMessageContentType::file);
|
||||
return *static_cast<ToxFile*>(data.get());
|
||||
}
|
||||
|
||||
const QString& asMessage() const
|
||||
{
|
||||
assert(type == HistMessageContentType::message);
|
||||
return *static_cast<QString*>(data.get());
|
||||
}
|
||||
|
||||
const ToxFile& asFile() const
|
||||
{
|
||||
assert(type == HistMessageContentType::file);
|
||||
return *static_cast<ToxFile*>(data.get());
|
||||
}
|
||||
|
||||
private:
|
||||
// Not really shared but shared_ptr has support for shared_ptr<void>
|
||||
std::shared_ptr<void> data;
|
||||
HistMessageContentType type;
|
||||
};
|
||||
|
||||
struct FileDbInsertionData
|
||||
{
|
||||
FileDbInsertionData();
|
||||
|
||||
int64_t historyId;
|
||||
QString friendPk;
|
||||
QString fileId;
|
||||
QString fileName;
|
||||
QString filePath;
|
||||
int64_t size;
|
||||
int direction;
|
||||
};
|
||||
Q_DECLARE_METATYPE(FileDbInsertionData);
|
||||
|
||||
class History : public QObject, public std::enable_shared_from_this<History>
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
struct HistMessage
|
||||
{
|
||||
|
@ -43,21 +115,32 @@ public:
|
|||
QString sender, QString message)
|
||||
: chat{chat}
|
||||
, sender{sender}
|
||||
, message{message}
|
||||
, dispName{dispName}
|
||||
, timestamp{timestamp}
|
||||
, id{id}
|
||||
, isSent{isSent}
|
||||
{
|
||||
}
|
||||
, content(std::move(message))
|
||||
{}
|
||||
|
||||
HistMessage(qint64 id, bool isSent, QDateTime timestamp, QString chat, QString dispName,
|
||||
QString sender, ToxFile file)
|
||||
: chat{chat}
|
||||
, sender{sender}
|
||||
, dispName{dispName}
|
||||
, timestamp{timestamp}
|
||||
, id{id}
|
||||
, isSent{isSent}
|
||||
, content(std::move(file))
|
||||
{}
|
||||
|
||||
|
||||
QString chat;
|
||||
QString sender;
|
||||
QString message;
|
||||
QString dispName;
|
||||
QDateTime timestamp;
|
||||
qint64 id;
|
||||
bool isSent;
|
||||
HistMessageContent content;
|
||||
};
|
||||
|
||||
struct DateMessages
|
||||
|
@ -80,6 +163,12 @@ public:
|
|||
const QDateTime& time, bool isSent, QString dispName,
|
||||
const std::function<void(int64_t)>& insertIdCallback = {});
|
||||
|
||||
void addNewFileMessage(const QString& friendPk, const QString& fileId,
|
||||
const QString& fileName, const QString& filePath, int64_t size,
|
||||
const QString& sender, const QDateTime& time, QString const& dispName);
|
||||
|
||||
void setFileFinished(const QString& fileId, bool success, const QString& filePath, const QByteArray& fileHash);
|
||||
|
||||
QList<HistMessage> getChatHistoryFromDate(const QString& friendPk, const QDateTime& from,
|
||||
const QDateTime& to);
|
||||
QList<HistMessage> getChatHistoryDefaultNum(const QString& friendPk);
|
||||
|
@ -96,11 +185,37 @@ protected:
|
|||
const QString& sender, const QDateTime& time, bool isSent,
|
||||
QString dispName, std::function<void(int64_t)> insertIdCallback = {});
|
||||
|
||||
signals:
|
||||
void fileInsertionReady(FileDbInsertionData data);
|
||||
void fileInserted(int64_t dbId, QString fileId);
|
||||
|
||||
private slots:
|
||||
void onFileInsertionReady(FileDbInsertionData data);
|
||||
void onFileInserted(int64_t dbId, QString fileId);
|
||||
|
||||
private:
|
||||
QList<HistMessage> getChatHistory(const QString& friendPk, const QDateTime& from,
|
||||
const QDateTime& to, int numMessages);
|
||||
|
||||
static RawDatabase::Query generateFileFinished(int64_t fileId, bool success,
|
||||
const QString& filePath, const QByteArray& fileHash);
|
||||
void dbSchemaUpgrade();
|
||||
|
||||
std::shared_ptr<RawDatabase> db;
|
||||
|
||||
|
||||
QHash<QString, int64_t> peers;
|
||||
struct FileInfo
|
||||
{
|
||||
bool finished = false;
|
||||
bool success = false;
|
||||
QString filePath;
|
||||
QByteArray fileHash;
|
||||
int64_t fileId = -1;
|
||||
};
|
||||
|
||||
// This needs to be a shared pointer to avoid callback lifetime issues
|
||||
QHash<QString, FileInfo> fileInfos;
|
||||
};
|
||||
|
||||
#endif // HISTORY_H
|
||||
|
|
|
@ -748,7 +748,7 @@ QStringList Profile::remove()
|
|||
qWarning() << "Could not remove file " << dbPath;
|
||||
}
|
||||
|
||||
history.release();
|
||||
history.reset();
|
||||
database.reset();
|
||||
|
||||
return ret;
|
||||
|
|
|
@ -109,7 +109,7 @@ private:
|
|||
QString name;
|
||||
std::unique_ptr<ToxEncrypt> passkey = nullptr;
|
||||
std::shared_ptr<RawDatabase> database;
|
||||
std::unique_ptr<History> history;
|
||||
std::shared_ptr<History> history;
|
||||
bool isRemoved;
|
||||
bool encrypted = false;
|
||||
static QStringList profiles;
|
||||
|
|
|
@ -161,6 +161,9 @@ ChatForm::ChatForm(Friend* chatFriend, History* history)
|
|||
connect(core, &Core::fileReceiveRequested, this, &ChatForm::onFileRecvRequest);
|
||||
connect(profile, &Profile::friendAvatarChanged, this, &ChatForm::onAvatarChanged);
|
||||
connect(core, &Core::fileSendStarted, this, &ChatForm::startFileSend);
|
||||
connect(core, &Core::fileTransferFinished, this, &ChatForm::onFileTransferFinished);
|
||||
connect(core, &Core::fileTransferCancelled, this, &ChatForm::onFileTransferCancelled);
|
||||
connect(core, &Core::fileTransferBrokenUnbroken, this, &ChatForm::onFileTransferBrokenUnbroken);
|
||||
connect(core, &Core::fileSendFailed, this, &ChatForm::onFileSendFailed);
|
||||
connect(core, &Core::receiptRecieved, this, &ChatForm::onReceiptReceived);
|
||||
connect(core, &Core::friendMessageReceived, this, &ChatForm::onFriendMessageReceived);
|
||||
|
@ -312,9 +315,35 @@ void ChatForm::startFileSend(ToxFile file)
|
|||
|
||||
insertChatMessage(
|
||||
ChatMessage::createFileTransferMessage(name, file, true, QDateTime::currentDateTime()));
|
||||
|
||||
if (history && Settings::getInstance().getEnableLogging()) {
|
||||
auto selfPk = Core::getInstance()->getSelfId().toString();
|
||||
auto pk = f->getPublicKey().toString();
|
||||
auto name = Core::getInstance()->getUsername();
|
||||
history->addNewFileMessage(pk, file.resumeFileId, file.fileName, file.filePath,
|
||||
file.filesize, selfPk, QDateTime::currentDateTime(), name);
|
||||
}
|
||||
|
||||
Widget::getInstance()->updateFriendActivity(f);
|
||||
}
|
||||
|
||||
void ChatForm::onFileTransferFinished(ToxFile file)
|
||||
{
|
||||
history->setFileFinished(file.resumeFileId, true, file.filePath, file.hashGenerator->result());
|
||||
}
|
||||
|
||||
void ChatForm::onFileTransferBrokenUnbroken(ToxFile file, bool broken)
|
||||
{
|
||||
if (broken) {
|
||||
history->setFileFinished(file.resumeFileId, false, file.filePath, file.hashGenerator->result());
|
||||
}
|
||||
}
|
||||
|
||||
void ChatForm::onFileTransferCancelled(ToxFile file)
|
||||
{
|
||||
history->setFileFinished(file.resumeFileId, false, file.filePath, file.hashGenerator->result());
|
||||
}
|
||||
|
||||
void ChatForm::onFileRecvRequest(ToxFile file)
|
||||
{
|
||||
if (file.friendId != f->getId()) {
|
||||
|
@ -331,9 +360,17 @@ void ChatForm::onFileRecvRequest(ToxFile file)
|
|||
|
||||
ChatMessage::Ptr msg =
|
||||
ChatMessage::createFileTransferMessage(name, file, false, QDateTime::currentDateTime());
|
||||
|
||||
insertChatMessage(msg);
|
||||
|
||||
if (history && Settings::getInstance().getEnableLogging()) {
|
||||
auto pk = f->getPublicKey().toString();
|
||||
auto name = f->getDisplayedName();
|
||||
history->addNewFileMessage(pk, file.resumeFileId, file.fileName, file.filePath,
|
||||
file.filesize, pk, QDateTime::currentDateTime(), name);
|
||||
}
|
||||
ChatLineContentProxy* proxy = static_cast<ChatLineContentProxy*>(msg->getContent(1));
|
||||
|
||||
assert(proxy->getWidgetType() == ChatLineContentProxy::FileTransferWidgetType);
|
||||
FileTransferWidget* tfWidget = static_cast<FileTransferWidget*>(proxy->getWidget());
|
||||
|
||||
|
@ -794,6 +831,11 @@ void ChatForm::handleLoadedMessages(QList<History::HistMessage> newHistMsgs, boo
|
|||
MessageMetadata const metadata = getMessageMetadata(histMessage);
|
||||
lastDate = addDateLineIfNeeded(chatLines, lastDate, histMessage, metadata);
|
||||
auto msg = chatMessageFromHistMessage(histMessage, metadata);
|
||||
|
||||
if (!msg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processUndelivered) {
|
||||
sendLoadedMessage(msg, metadata);
|
||||
}
|
||||
|
@ -838,7 +880,9 @@ ChatForm::MessageMetadata ChatForm::getMessageMetadata(History::HistMessage cons
|
|||
const QDateTime msgDateTime = histMessage.timestamp.toLocalTime();
|
||||
const bool isSelf = Core::getInstance()->getSelfId().getPublicKey() == authorPk;
|
||||
const bool needSending = !histMessage.isSent && isSelf;
|
||||
const bool isAction = histMessage.message.startsWith(ACTION_PREFIX, Qt::CaseInsensitive);
|
||||
const bool isAction =
|
||||
histMessage.content.getType() == HistMessageContentType::message
|
||||
&& histMessage.content.asMessage().startsWith(ACTION_PREFIX, Qt::CaseInsensitive);
|
||||
const qint64 id = histMessage.id;
|
||||
return {isSelf, needSending, isAction, id, authorPk, msgDateTime};
|
||||
}
|
||||
|
@ -848,11 +892,31 @@ ChatMessage::Ptr ChatForm::chatMessageFromHistMessage(History::HistMessage const
|
|||
{
|
||||
ToxPk authorPk(ToxId(histMessage.sender).getPublicKey());
|
||||
QString authorStr = getMsgAuthorDispName(authorPk, histMessage.dispName);
|
||||
QString messageText =
|
||||
metadata.isAction ? histMessage.message.mid(ACTION_PREFIX.length()) : histMessage.message;
|
||||
ChatMessage::MessageType type = metadata.isAction ? ChatMessage::ACTION : ChatMessage::NORMAL;
|
||||
QDateTime dateTime = metadata.needSending ? QDateTime() : metadata.msgDateTime;
|
||||
auto msg = ChatMessage::createChatMessage(authorStr, messageText, type, metadata.isSelf, dateTime);
|
||||
|
||||
|
||||
ChatMessage::Ptr msg;
|
||||
|
||||
switch (histMessage.content.getType()) {
|
||||
case HistMessageContentType::message: {
|
||||
ChatMessage::MessageType type = metadata.isAction ? ChatMessage::ACTION : ChatMessage::NORMAL;
|
||||
auto& message = histMessage.content.asMessage();
|
||||
QString messageText = metadata.isAction ? message.mid(ACTION_PREFIX.length()) : message;
|
||||
|
||||
msg = ChatMessage::createChatMessage(authorStr, messageText, type, metadata.isSelf, dateTime);
|
||||
break;
|
||||
}
|
||||
case HistMessageContentType::file: {
|
||||
auto& file = histMessage.content.asFile();
|
||||
bool isMe = file.direction == ToxFile::SENDING;
|
||||
msg = ChatMessage::createFileTransferMessage(authorStr, file, isMe, dateTime);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
qCritical() << "Invalid HistMessageContentType";
|
||||
assert(false);
|
||||
}
|
||||
|
||||
if (!metadata.isAction && needsToHideName(authorPk, metadata.msgDateTime)) {
|
||||
msg->hideSender();
|
||||
}
|
||||
|
@ -1135,13 +1199,17 @@ void ChatForm::onExportChat()
|
|||
|
||||
QString buffer;
|
||||
for (const auto& it : msgs) {
|
||||
if (it.content.getType() != HistMessageContentType::message) {
|
||||
continue;
|
||||
}
|
||||
QString timestamp = it.timestamp.time().toString("hh:mm:ss");
|
||||
QString datestamp = it.timestamp.date().toString("yyyy-MM-dd");
|
||||
ToxPk authorPk(ToxId(it.sender).getPublicKey());
|
||||
QString author = getMsgAuthorDispName(authorPk, it.dispName);
|
||||
|
||||
buffer = buffer
|
||||
% QString{datestamp % '\t' % timestamp % '\t' % author % '\t' % it.message % '\n'};
|
||||
% QString{datestamp % '\t' % timestamp % '\t' % author % '\t'
|
||||
% it.content.asMessage() % '\n'};
|
||||
}
|
||||
file.write(buffer.toUtf8());
|
||||
file.close();
|
||||
|
|
|
@ -68,6 +68,9 @@ signals:
|
|||
|
||||
public slots:
|
||||
void startFileSend(ToxFile file);
|
||||
void onFileTransferFinished(ToxFile file);
|
||||
void onFileTransferCancelled(ToxFile file);
|
||||
void onFileTransferBrokenUnbroken(ToxFile file, bool broken);
|
||||
void onFileRecvRequest(ToxFile file);
|
||||
void onAvInvite(uint32_t friendId, bool video);
|
||||
void onAvStart(uint32_t friendId, bool video);
|
||||
|
|
Loading…
Reference in New Issue
Block a user