mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
Merge branch 'v1.17-dev'
This commit is contained in:
commit
f7d82a425d
|
@ -290,6 +290,8 @@ set(${PROJECT_NAME}_SOURCES
|
|||
src/chatlog/content/text.h
|
||||
src/chatlog/content/timestamp.cpp
|
||||
src/chatlog/content/timestamp.h
|
||||
src/chatlog/content/broken.cpp
|
||||
src/chatlog/content/broken.h
|
||||
src/chatlog/customtextdocument.cpp
|
||||
src/chatlog/customtextdocument.h
|
||||
src/chatlog/documentcache.cpp
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include "content/spinner.h"
|
||||
#include "content/text.h"
|
||||
#include "content/timestamp.h"
|
||||
#include "content/broken.h"
|
||||
#include "src/widget/style.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
@ -33,6 +34,7 @@
|
|||
|
||||
#include "src/persistence/settings.h"
|
||||
#include "src/persistence/smileypack.h"
|
||||
#include "src/persistence/history.h"
|
||||
|
||||
#define NAME_COL_WIDTH 90.0
|
||||
#define TIME_COL_WIDTH 90.0
|
||||
|
@ -43,7 +45,8 @@ ChatMessage::ChatMessage()
|
|||
}
|
||||
|
||||
ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QString& rawMessage,
|
||||
MessageType type, bool isMe, const QDateTime& date, bool colorizeName)
|
||||
MessageType type, bool isMe, MessageState state,
|
||||
const QDateTime& date, bool colorizeName)
|
||||
{
|
||||
ChatMessage::Ptr msg = ChatMessage::Ptr(new ChatMessage);
|
||||
|
||||
|
@ -105,12 +108,21 @@ ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QSt
|
|||
? QString("%1 %2").arg(sender, rawMessage)
|
||||
: rawMessage),
|
||||
ColumnFormat(1.0, ColumnFormat::VariableSize));
|
||||
|
||||
switch (state) {
|
||||
case MessageState::complete:
|
||||
msg->addColumn(new Timestamp(date, Settings::getInstance().getTimestampFormat(), baseFont),
|
||||
ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right));
|
||||
break;
|
||||
case MessageState::pending:
|
||||
msg->addColumn(new Spinner(Style::getImagePath("chatArea/spinner.svg"), QSize(16, 16), 360.0 / 1.6),
|
||||
ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right));
|
||||
|
||||
if (!date.isNull())
|
||||
msg->markAsSent(date);
|
||||
|
||||
break;
|
||||
case MessageState::broken:
|
||||
msg->addColumn(new Broken(Style::getImagePath("chatArea/error.svg"), QSize(16, 16)),
|
||||
ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right));
|
||||
break;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
|
@ -207,7 +219,7 @@ ChatMessage::Ptr ChatMessage::createBusyNotification()
|
|||
return msg;
|
||||
}
|
||||
|
||||
void ChatMessage::markAsSent(const QDateTime& time)
|
||||
void ChatMessage::markAsDelivered(const QDateTime& time)
|
||||
{
|
||||
QFont baseFont = Settings::getInstance().getChatMessageFont();
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
|
||||
#include "chatline.h"
|
||||
#include "src/core/toxfile.h"
|
||||
#include "src/persistence/history.h"
|
||||
|
||||
#include <QDateTime>
|
||||
|
||||
class QGraphicsScene;
|
||||
|
@ -48,8 +50,8 @@ public:
|
|||
ChatMessage();
|
||||
|
||||
static ChatMessage::Ptr createChatMessage(const QString& sender, const QString& rawMessage,
|
||||
MessageType type, bool isMe,
|
||||
const QDateTime& date = QDateTime(), bool colorizeName = false);
|
||||
MessageType type, bool isMe, MessageState state,
|
||||
const QDateTime& date, bool colorizeName = false);
|
||||
static ChatMessage::Ptr createChatInfoMessage(const QString& rawMessage, SystemMessageType type,
|
||||
const QDateTime& date);
|
||||
static ChatMessage::Ptr createFileTransferMessage(const QString& sender, ToxFile file,
|
||||
|
@ -57,7 +59,7 @@ public:
|
|||
static ChatMessage::Ptr createTypingNotification();
|
||||
static ChatMessage::Ptr createBusyNotification();
|
||||
|
||||
void markAsSent(const QDateTime& time);
|
||||
void markAsDelivered(const QDateTime& time);
|
||||
QString toString() const;
|
||||
bool isAction() const;
|
||||
void setAsAction();
|
||||
|
|
61
src/chatlog/content/broken.cpp
Normal file
61
src/chatlog/content/broken.cpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright © 2019 by The qTox Project Contributors
|
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox.
|
||||
|
||||
qTox is libre software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
qTox is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "broken.h"
|
||||
#include "src/chatlog/pixmapcache.h"
|
||||
#include <QPainter>
|
||||
|
||||
class QStyleOptionGraphicsItem;
|
||||
|
||||
Broken::Broken(const QString& img, QSize size)
|
||||
: pmap{PixmapCache::getInstance().get(img, size)}
|
||||
, size{size}
|
||||
{
|
||||
}
|
||||
|
||||
QRectF Broken::boundingRect() const
|
||||
{
|
||||
return QRectF(QPointF(-size.width() / 2.0, -size.height() / 2.0), size);
|
||||
}
|
||||
|
||||
void Broken::paint(QPainter* painter, const QStyleOptionGraphicsItem* option,
|
||||
QWidget* widget)
|
||||
{
|
||||
painter->setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
painter->drawPixmap(0, 0, pmap);
|
||||
|
||||
Q_UNUSED(option)
|
||||
Q_UNUSED(widget)
|
||||
|
||||
}
|
||||
|
||||
void Broken::setWidth(qreal width)
|
||||
{
|
||||
Q_UNUSED(width);
|
||||
}
|
||||
|
||||
void Broken::visibilityChanged(bool visible)
|
||||
{
|
||||
Q_UNUSED(visible);
|
||||
}
|
||||
|
||||
qreal Broken::getAscent() const
|
||||
{
|
||||
return 0.0;
|
||||
}
|
45
src/chatlog/content/broken.h
Normal file
45
src/chatlog/content/broken.h
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Copyright © 2019 by The qTox Project Contributors
|
||||
|
||||
This file is part of qTox, a Qt-based graphical interface for Tox.
|
||||
|
||||
qTox is libre software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
qTox is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BROKEN_H
|
||||
#define BROKEN_H
|
||||
|
||||
#include "../chatlinecontent.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QPixmap>
|
||||
|
||||
class Broken : public ChatLineContent
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Broken(const QString& img, QSize size);
|
||||
QRectF boundingRect() const override;
|
||||
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option,
|
||||
QWidget* widget) override;
|
||||
void setWidth(qreal width) override;
|
||||
void visibilityChanged(bool visible) override;
|
||||
qreal getAscent() const override;
|
||||
|
||||
private:
|
||||
QSize size;
|
||||
QPixmap pmap;
|
||||
};
|
||||
|
||||
#endif // BROKEN_H
|
|
@ -391,8 +391,8 @@ int main(int argc, char* argv[])
|
|||
} else {
|
||||
nexus.setParser(&parser);
|
||||
int returnval = nexus.showLogin(profileName);
|
||||
if (returnval != 0) {
|
||||
return returnval;
|
||||
if (returnval == QDialog::Rejected) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -94,9 +94,10 @@ ChatHistory::ChatHistory(Friend& f_, History* history_, const ICoreIdHandler& co
|
|||
, settings(settings_)
|
||||
, coreIdHandler(coreIdHandler)
|
||||
{
|
||||
connect(&messageDispatcher, &IMessageDispatcher::messageSent, this, &ChatHistory::onMessageSent);
|
||||
connect(&messageDispatcher, &IMessageDispatcher::messageComplete, this,
|
||||
&ChatHistory::onMessageComplete);
|
||||
connect(&messageDispatcher, &IMessageDispatcher::messageReceived, this,
|
||||
&ChatHistory::onMessageReceived);
|
||||
|
||||
if (canUseHistory()) {
|
||||
// Defer messageSent callback until we finish firing off all our unsent messages.
|
||||
|
@ -105,8 +106,7 @@ ChatHistory::ChatHistory(Friend& f_, History* history_, const ICoreIdHandler& co
|
|||
}
|
||||
|
||||
// Now that we've fired off our unsent messages we can connect the message
|
||||
connect(&messageDispatcher, &IMessageDispatcher::messageReceived, this,
|
||||
&ChatHistory::onMessageReceived);
|
||||
connect(&messageDispatcher, &IMessageDispatcher::messageSent, this, &ChatHistory::onMessageSent);
|
||||
|
||||
// NOTE: this has to be done _after_ sending all sent messages since initial
|
||||
// state of the message has to be marked according to our dispatch state
|
||||
|
@ -385,15 +385,23 @@ void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const
|
|||
std::find_if(dispatchedMessageRowIdMap.begin(), dispatchedMessageRowIdMap.end(),
|
||||
[&](RowId dispatchedId) { return dispatchedId == message.id; });
|
||||
|
||||
bool isComplete = dispatchedMessageIt == dispatchedMessageRowIdMap.end();
|
||||
assert((message.state != MessageState::pending && dispatchedMessageIt == dispatchedMessageRowIdMap.end()) ||
|
||||
(message.state == MessageState::pending && dispatchedMessageIt != dispatchedMessageRowIdMap.end()));
|
||||
|
||||
if (isComplete) {
|
||||
auto chatLogMessage = ChatLogMessage{true, processedMessage};
|
||||
sessionChatLog.insertMessageAtIdx(currentIdx, sender, message.dispName, chatLogMessage);
|
||||
} else {
|
||||
// If the message is incomplete we have to pretend we sent it to ensure
|
||||
// sessionChatLog state is correct
|
||||
sessionChatLog.onMessageSent(dispatchedMessageIt.key(), processedMessage);
|
||||
auto chatLogMessage = ChatLogMessage{message.state, processedMessage};
|
||||
switch (message.state) {
|
||||
case MessageState::complete:
|
||||
sessionChatLog.insertCompleteMessageAtIdx(currentIdx, sender, message.dispName,
|
||||
chatLogMessage);
|
||||
break;
|
||||
case MessageState::pending:
|
||||
sessionChatLog.insertIncompleteMessageAtIdx(currentIdx, sender, message.dispName,
|
||||
chatLogMessage, dispatchedMessageIt.key());
|
||||
break;
|
||||
case MessageState::broken:
|
||||
sessionChatLog.insertBrokenMessageAtIdx(currentIdx, sender, message.dispName,
|
||||
chatLogMessage);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -409,7 +417,7 @@ void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const
|
|||
*/
|
||||
void ChatHistory::dispatchUnsentMessages(IMessageDispatcher& messageDispatcher)
|
||||
{
|
||||
auto unsentMessages = history->getUnsentMessagesForFriend(f.getPublicKey());
|
||||
auto unsentMessages = history->getUndeliveredMessagesForFriend(f.getPublicKey());
|
||||
for (auto& message : unsentMessages) {
|
||||
// We should only store messages as unsent, if this changes in the
|
||||
// future we need to extend this logic
|
||||
|
@ -443,7 +451,7 @@ void ChatHistory::handleDispatchedMessage(DispatchedMessageId dispatchId, RowId
|
|||
if (completedMessageIt == completedMessages.end()) {
|
||||
dispatchedMessageRowIdMap.insert(dispatchId, historyId);
|
||||
} else {
|
||||
history->markAsSent(historyId);
|
||||
history->markAsDelivered(historyId);
|
||||
completedMessages.erase(completedMessageIt);
|
||||
}
|
||||
}
|
||||
|
@ -455,7 +463,7 @@ void ChatHistory::completeMessage(DispatchedMessageId id)
|
|||
if (dispatchedMessageIt == dispatchedMessageRowIdMap.end()) {
|
||||
completedMessages.insert(id);
|
||||
} else {
|
||||
history->markAsSent(*dispatchedMessageIt);
|
||||
history->markAsDelivered(*dispatchedMessageIt);
|
||||
dispatchedMessageRowIdMap.erase(dispatchedMessageIt);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,12 +23,13 @@
|
|||
#include "src/core/toxfile.h"
|
||||
#include "src/core/toxpk.h"
|
||||
#include "src/model/message.h"
|
||||
#include "src/persistence/history.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
struct ChatLogMessage
|
||||
{
|
||||
bool isComplete;
|
||||
MessageState state;
|
||||
Message message;
|
||||
};
|
||||
|
||||
|
|
|
@ -289,8 +289,8 @@ std::vector<IChatLog::DateChatLogIdxPair> SessionChatLog::getDateIdxs(const QDat
|
|||
return ret;
|
||||
}
|
||||
|
||||
void SessionChatLog::insertMessageAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName,
|
||||
ChatLogMessage message)
|
||||
void SessionChatLog::insertCompleteMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
|
||||
const ChatLogMessage& message)
|
||||
{
|
||||
auto item = ChatLogItem(sender, message);
|
||||
|
||||
|
@ -298,10 +298,42 @@ void SessionChatLog::insertMessageAtIdx(ChatLogIdx idx, ToxPk sender, QString se
|
|||
item.setDisplayName(senderName);
|
||||
}
|
||||
|
||||
assert(message.state == MessageState::complete);
|
||||
|
||||
items.emplace(idx, std::move(item));
|
||||
}
|
||||
|
||||
void SessionChatLog::insertFileAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName, ChatLogFile file)
|
||||
void SessionChatLog::insertIncompleteMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
|
||||
const ChatLogMessage& message,
|
||||
DispatchedMessageId dispatchId)
|
||||
{
|
||||
auto item = ChatLogItem(sender, message);
|
||||
|
||||
if (!senderName.isEmpty()) {
|
||||
item.setDisplayName(senderName);
|
||||
}
|
||||
|
||||
assert(message.state == MessageState::pending);
|
||||
|
||||
items.emplace(idx, std::move(item));
|
||||
outgoingMessages.insert(dispatchId, idx);
|
||||
}
|
||||
|
||||
void SessionChatLog::insertBrokenMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
|
||||
const ChatLogMessage& message)
|
||||
{
|
||||
auto item = ChatLogItem(sender, message);
|
||||
|
||||
if (!senderName.isEmpty()) {
|
||||
item.setDisplayName(senderName);
|
||||
}
|
||||
|
||||
assert(message.state == MessageState::broken);
|
||||
|
||||
items.emplace(idx, std::move(item));
|
||||
}
|
||||
|
||||
void SessionChatLog::insertFileAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName, const ChatLogFile& file)
|
||||
{
|
||||
auto item = ChatLogItem(sender, file);
|
||||
|
||||
|
@ -321,7 +353,7 @@ void SessionChatLog::onMessageReceived(const ToxPk& sender, const Message& messa
|
|||
auto messageIdx = nextIdx++;
|
||||
|
||||
ChatLogMessage chatLogMessage;
|
||||
chatLogMessage.isComplete = true;
|
||||
chatLogMessage.state = MessageState::complete;
|
||||
chatLogMessage.message = message;
|
||||
items.emplace(messageIdx, ChatLogItem(sender, chatLogMessage));
|
||||
|
||||
|
@ -337,7 +369,7 @@ void SessionChatLog::onMessageSent(DispatchedMessageId id, const Message& messag
|
|||
auto messageIdx = nextIdx++;
|
||||
|
||||
ChatLogMessage chatLogMessage;
|
||||
chatLogMessage.isComplete = false;
|
||||
chatLogMessage.state = MessageState::pending;
|
||||
chatLogMessage.message = message;
|
||||
items.emplace(messageIdx, ChatLogItem(coreIdHandler.getSelfPublicKey(), chatLogMessage));
|
||||
|
||||
|
@ -367,7 +399,7 @@ void SessionChatLog::onMessageComplete(DispatchedMessageId id)
|
|||
return;
|
||||
}
|
||||
|
||||
messageIt->second.getContentAsMessage().isComplete = true;
|
||||
messageIt->second.getContentAsMessage().state = MessageState::complete;
|
||||
|
||||
emit this->itemUpdated(messageIt->first);
|
||||
}
|
||||
|
|
|
@ -46,8 +46,13 @@ public:
|
|||
ChatLogIdx getNextIdx() const override;
|
||||
std::vector<DateChatLogIdxPair> getDateIdxs(const QDate& startDate, size_t maxDates) const override;
|
||||
|
||||
void insertMessageAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName, ChatLogMessage message);
|
||||
void insertFileAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName, ChatLogFile file);
|
||||
void insertCompleteMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
|
||||
const ChatLogMessage& message);
|
||||
void insertIncompleteMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
|
||||
const ChatLogMessage& message, DispatchedMessageId dispatchId);
|
||||
void insertBrokenMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
|
||||
const ChatLogMessage& message);
|
||||
void insertFileAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName, const ChatLogFile& file);
|
||||
|
||||
public slots:
|
||||
void onMessageReceived(const ToxPk& sender, const Message& message);
|
||||
|
|
|
@ -165,6 +165,11 @@ int Nexus::showLogin(const QString& profileName)
|
|||
// The connection order ensures profile will be ready for bootstrap for now
|
||||
connect(this, &Nexus::currentProfileChanged, this, &Nexus::bootstrapWithProfile);
|
||||
int returnval = loginScreen.exec();
|
||||
if (returnval == QDialog::Rejected) {
|
||||
// Kriby: This will terminate the main application loop, necessary until we refactor
|
||||
// away the split startup/return to login behavior.
|
||||
qApp->quit();
|
||||
}
|
||||
disconnect(this, &Nexus::currentProfileChanged, this, &Nexus::bootstrapWithProfile);
|
||||
return returnval;
|
||||
}
|
||||
|
|
|
@ -721,7 +721,8 @@ void RawDatabase::process()
|
|||
&stmt, &compileTail))
|
||||
!= SQLITE_OK) {
|
||||
qWarning() << "Failed to prepare statement" << anonymizeQuery(query.query)
|
||||
<< "with error" << r;
|
||||
<< "and returned" << r;
|
||||
qWarning("The full error is %d: %s", sqlite3_errcode(sqlite), sqlite3_errmsg(sqlite));
|
||||
goto cleanupStatements;
|
||||
}
|
||||
query.statements += stmt;
|
||||
|
|
|
@ -26,10 +26,11 @@
|
|||
#include "db/rawdatabase.h"
|
||||
|
||||
namespace {
|
||||
static constexpr int SCHEMA_VERSION = 1;
|
||||
static constexpr int SCHEMA_VERSION = 2;
|
||||
|
||||
void generateCurrentSchema(QVector<RawDatabase::Query>& queries)
|
||||
bool createCurrentSchema(RawDatabase& db)
|
||||
{
|
||||
QVector<RawDatabase::Query> queries;
|
||||
queries += RawDatabase::Query(QStringLiteral(
|
||||
"CREATE TABLE peers (id INTEGER PRIMARY KEY, "
|
||||
"public_key TEXT NOT NULL UNIQUE);"
|
||||
|
@ -60,10 +61,13 @@ void generateCurrentSchema(QVector<RawDatabase::Query>& queries)
|
|||
"file_size INTEGER NOT NULL, "
|
||||
"direction INTEGER NOT NULL, "
|
||||
"file_state INTEGER NOT NULL);"
|
||||
"CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY);"));
|
||||
"CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY);"
|
||||
"CREATE TABLE broken_messages (id INTEGER PRIMARY KEY);"));
|
||||
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = %1;").arg(SCHEMA_VERSION));
|
||||
return db.execNow(queries);
|
||||
}
|
||||
|
||||
bool isNewDb(std::shared_ptr<RawDatabase> db)
|
||||
bool isNewDb(std::shared_ptr<RawDatabase>& db, bool& success)
|
||||
{
|
||||
bool newDb;
|
||||
if (!db->execNow(RawDatabase::Query("SELECT COUNT(*) FROM sqlite_master;",
|
||||
|
@ -71,13 +75,16 @@ bool isNewDb(std::shared_ptr<RawDatabase> db)
|
|||
newDb = row[0].toLongLong() == 0;
|
||||
}))) {
|
||||
db.reset();
|
||||
return false; // TODO: propogate error
|
||||
success = false;
|
||||
return false;
|
||||
}
|
||||
success = true;
|
||||
return newDb;
|
||||
}
|
||||
|
||||
void dbSchema0to1(std::shared_ptr<RawDatabase> db, QVector<RawDatabase::Query>& queries)
|
||||
bool dbSchema0to1(RawDatabase& db)
|
||||
{
|
||||
QVector<RawDatabase::Query> queries;
|
||||
queries +=
|
||||
RawDatabase::Query(QStringLiteral(
|
||||
"CREATE TABLE file_transfers "
|
||||
|
@ -92,6 +99,55 @@ void dbSchema0to1(std::shared_ptr<RawDatabase> db, QVector<RawDatabase::Query>&
|
|||
"file_state INTEGER NOT NULL);"));
|
||||
queries +=
|
||||
RawDatabase::Query(QStringLiteral("ALTER TABLE history ADD file_id INTEGER;"));
|
||||
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 1;"));
|
||||
return db.execNow(queries);
|
||||
}
|
||||
|
||||
bool dbSchema1to2(RawDatabase& db)
|
||||
{
|
||||
// Any faux_offline_pending message, in a chat that has newer delivered
|
||||
// message is decided to be broken. It must be moved from
|
||||
// faux_offline_pending to broken_messages
|
||||
|
||||
// the last non-pending message in each chat
|
||||
QString lastDeliveredQuery = QString(
|
||||
"SELECT chat_id, MAX(history.id) FROM "
|
||||
"history JOIN peers chat ON chat_id = chat.id "
|
||||
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
||||
"WHERE faux_offline_pending.id IS NULL "
|
||||
"GROUP BY chat_id;");
|
||||
|
||||
QVector<RawDatabase::Query> upgradeQueries;
|
||||
upgradeQueries +=
|
||||
RawDatabase::Query(QStringLiteral(
|
||||
"CREATE TABLE broken_messages "
|
||||
"(id INTEGER PRIMARY KEY);"));
|
||||
|
||||
auto rowCallback = [&upgradeQueries](const QVector<QVariant>& row) {
|
||||
auto chatId = row[0].toLongLong();
|
||||
auto lastDeliveredHistoryId = row[1].toLongLong();
|
||||
|
||||
upgradeQueries += QString("INSERT INTO broken_messages "
|
||||
"SELECT faux_offline_pending.id FROM "
|
||||
"history JOIN faux_offline_pending "
|
||||
"ON faux_offline_pending.id = history.id "
|
||||
"WHERE history.chat_id=%1 "
|
||||
"AND history.id < %2;").arg(chatId).arg(lastDeliveredHistoryId);
|
||||
};
|
||||
// note this doesn't modify the db, just generate new queries, so is safe
|
||||
// to run outside of our upgrade transaction
|
||||
if (!db.execNow({lastDeliveredQuery, rowCallback})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
upgradeQueries += QString(
|
||||
"DELETE FROM faux_offline_pending "
|
||||
"WHERE id in ("
|
||||
"SELECT id FROM broken_messages);");
|
||||
|
||||
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 2;"));
|
||||
|
||||
return db.execNow(upgradeQueries);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -99,7 +155,7 @@ void dbSchema0to1(std::shared_ptr<RawDatabase> db, QVector<RawDatabase::Query>&
|
|||
* @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 dbSchemaUpgrade(std::shared_ptr<RawDatabase> db)
|
||||
void dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db)
|
||||
{
|
||||
int64_t databaseSchemaVersion;
|
||||
|
||||
|
@ -121,35 +177,65 @@ void dbSchemaUpgrade(std::shared_ptr<RawDatabase> db)
|
|||
return;
|
||||
}
|
||||
|
||||
QVector<RawDatabase::Query> queries;
|
||||
// Make sure to handle the un-created case as well in the following upgrade code
|
||||
switch (databaseSchemaVersion) {
|
||||
case 0:
|
||||
case 0: {
|
||||
// Note: 0 is a special version that is actually two versions.
|
||||
// possibility 1) it is a newly created database and it neesds the current schema to be created.
|
||||
// possibility 2) it is a old existing database, before version 1 and before we saved schema version,
|
||||
// and need to be updated.
|
||||
if (isNewDb(db)) {
|
||||
generateCurrentSchema(queries);
|
||||
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = %1;").arg(SCHEMA_VERSION));
|
||||
db->execLater(queries);
|
||||
// and needs to be updated.
|
||||
bool success = false;
|
||||
const bool newDb = isNewDb(db, success);
|
||||
if (!success) {
|
||||
qCritical() << "Failed to create current db schema";
|
||||
db.reset();
|
||||
return;
|
||||
}
|
||||
if (newDb) {
|
||||
if (!createCurrentSchema(*db)) {
|
||||
qCritical() << "Failed to create current db schema";
|
||||
db.reset();
|
||||
return;
|
||||
}
|
||||
qDebug() << "Database created at schema version" << SCHEMA_VERSION;
|
||||
break; // new db is the only case where we don't incrementally upgrade through each version
|
||||
} else {
|
||||
dbSchema0to1(db, queries);
|
||||
if (!dbSchema0to1(*db)) {
|
||||
qCritical() << "Failed to upgrade db to schema version 1, aborting";
|
||||
db.reset();
|
||||
return;
|
||||
}
|
||||
qDebug() << "Database upgraded incrementally to schema version 1";
|
||||
}
|
||||
}
|
||||
// fallthrough
|
||||
// case 1:
|
||||
// dbSchema1to2(queries);
|
||||
// //fallthrough
|
||||
case 1:
|
||||
if (!dbSchema1to2(*db)) {
|
||||
qCritical() << "Failed to upgrade db to schema version 2, aborting";
|
||||
db.reset();
|
||||
return;
|
||||
}
|
||||
qDebug() << "Database upgraded incrementally to schema version 2";
|
||||
//fallthrough
|
||||
// etc.
|
||||
default:
|
||||
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = %1;").arg(SCHEMA_VERSION));
|
||||
db->execLater(queries);
|
||||
qDebug() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion
|
||||
qInfo() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion
|
||||
<< "->" << SCHEMA_VERSION << ")";
|
||||
}
|
||||
}
|
||||
|
||||
MessageState getMessageState(bool isPending, bool isBroken)
|
||||
{
|
||||
assert(!(isPending && isBroken));
|
||||
MessageState messageState;
|
||||
if (isPending) {
|
||||
messageState = MessageState::pending;
|
||||
} else if (isBroken) {
|
||||
messageState = MessageState::broken;
|
||||
} else {
|
||||
messageState = MessageState::complete;
|
||||
}
|
||||
return messageState;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
|
@ -243,6 +329,7 @@ void History::eraseHistory()
|
|||
"DELETE FROM aliases;"
|
||||
"DELETE FROM peers;"
|
||||
"DELETE FROM file_transfers;"
|
||||
"DELETE FROM broken_messages;"
|
||||
"VACUUM;");
|
||||
}
|
||||
|
||||
|
@ -268,6 +355,12 @@ void History::removeFriendHistory(const QString& friendPk)
|
|||
" LEFT JOIN history ON faux_offline_pending.id = history.id "
|
||||
" WHERE chat_id=%1 "
|
||||
"); "
|
||||
"DELETE FROM broken_messages "
|
||||
"WHERE broken_messages.id IN ( "
|
||||
" SELECT broken_messages.id FROM broken_messages "
|
||||
" LEFT JOIN history ON broken_messages.id = history.id "
|
||||
" WHERE chat_id=%1 "
|
||||
"); "
|
||||
"DELETE FROM history WHERE chat_id=%1; "
|
||||
"DELETE FROM aliases WHERE owner=%1; "
|
||||
"DELETE FROM peers WHERE id=%1; "
|
||||
|
@ -288,13 +381,13 @@ void History::removeFriendHistory(const QString& friendPk)
|
|||
* @param message Message to save.
|
||||
* @param sender Sender to save.
|
||||
* @param time Time of message sending.
|
||||
* @param isSent True if message was already sent.
|
||||
* @param isDelivered True if message was already delivered.
|
||||
* @param dispName Name, which should be displayed.
|
||||
* @param insertIdCallback Function, called after query execution.
|
||||
*/
|
||||
QVector<RawDatabase::Query>
|
||||
History::generateNewMessageQueries(const QString& friendPk, const QString& message,
|
||||
const QString& sender, const QDateTime& time, bool isSent,
|
||||
const QString& sender, const QDateTime& time, bool isDelivered,
|
||||
QString dispName, std::function<void(RowId)> insertIdCallback)
|
||||
{
|
||||
QVector<RawDatabase::Query> queries;
|
||||
|
@ -355,7 +448,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
|
|||
.arg(senderId),
|
||||
{message.toUtf8(), dispName.toUtf8()}, insertIdCallback);
|
||||
|
||||
if (!isSent) {
|
||||
if (!isDelivered) {
|
||||
queries += RawDatabase::Query{"INSERT INTO faux_offline_pending (id) VALUES ("
|
||||
" last_insert_rowid()"
|
||||
");"};
|
||||
|
@ -485,12 +578,12 @@ void History::addNewFileMessage(const QString& friendPk, const QString& fileId,
|
|||
* @param message Message to save.
|
||||
* @param sender Sender to save.
|
||||
* @param time Time of message sending.
|
||||
* @param isSent True if message was already sent.
|
||||
* @param isDelivered True if message was already delivered.
|
||||
* @param dispName Name, which should be displayed.
|
||||
* @param insertIdCallback Function, called after query execution.
|
||||
*/
|
||||
void History::addNewMessage(const QString& friendPk, const QString& message, const QString& sender,
|
||||
const QDateTime& time, bool isSent, QString dispName,
|
||||
const QDateTime& time, bool isDelivered, QString dispName,
|
||||
const std::function<void(RowId)>& insertIdCallback)
|
||||
{
|
||||
if (!Settings::getInstance().getEnableLogging()) {
|
||||
|
@ -501,7 +594,7 @@ void History::addNewMessage(const QString& friendPk, const QString& message, con
|
|||
return;
|
||||
}
|
||||
|
||||
db->execLater(generateNewMessageQueries(friendPk, message, sender, time, isSent, dispName,
|
||||
db->execLater(generateNewMessageQueries(friendPk, message, sender, time, isDelivered, dispName,
|
||||
insertIdCallback));
|
||||
}
|
||||
|
||||
|
@ -562,12 +655,13 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
|
|||
"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 "
|
||||
"file_transfers.file_state, broken_messages.id FROM history "
|
||||
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
||||
"JOIN peers chat ON history.chat_id = chat.id "
|
||||
"JOIN aliases ON sender_alias = aliases.id "
|
||||
"JOIN peers sender ON aliases.owner = sender.id "
|
||||
"LEFT JOIN file_transfers ON history.file_id = file_transfers.id "
|
||||
"LEFT JOIN broken_messages ON history.id = broken_messages.id "
|
||||
"WHERE chat.public_key='%1' "
|
||||
"LIMIT %2 OFFSET %3;")
|
||||
.arg(friendPk.toString())
|
||||
|
@ -578,13 +672,17 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
|
|||
// dispName and message could have null bytes, QString::fromUtf8
|
||||
// truncates on null bytes so we strip them
|
||||
auto id = RowId{row[0].toLongLong()};
|
||||
auto isOfflineMessage = row[1].isNull();
|
||||
auto isPending = !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();
|
||||
auto isBroken = !row[13].isNull();
|
||||
|
||||
MessageState messageState = getMessageState(isPending, isBroken);
|
||||
|
||||
if (row[7].isNull()) {
|
||||
messages += {id, isOfflineMessage, timestamp, friend_key,
|
||||
messages += {id, messageState, timestamp, friend_key,
|
||||
display_name, sender_key, row[6].toString()};
|
||||
} else {
|
||||
ToxFile file;
|
||||
|
@ -596,7 +694,7 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
|
|||
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};
|
||||
{id, messageState, timestamp, friend_key, display_name, sender_key, file};
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -605,16 +703,17 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
|
|||
return messages;
|
||||
}
|
||||
|
||||
QList<History::HistMessage> History::getUnsentMessagesForFriend(const ToxPk& friendPk)
|
||||
QList<History::HistMessage> History::getUndeliveredMessagesForFriend(const ToxPk& friendPk)
|
||||
{
|
||||
auto queryText =
|
||||
QString("SELECT history.id, faux_offline_pending.id, timestamp, chat.public_key, "
|
||||
"aliases.display_name, sender.public_key, message "
|
||||
"aliases.display_name, sender.public_key, message, broken_messages.id "
|
||||
"FROM history "
|
||||
"JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
|
||||
"JOIN peers chat on history.chat_id = chat.id "
|
||||
"JOIN aliases on sender_alias = aliases.id "
|
||||
"JOIN peers sender on aliases.owner = sender.id "
|
||||
"LEFT JOIN broken_messages ON history.id = broken_messages.id "
|
||||
"WHERE chat.public_key='%1';")
|
||||
.arg(friendPk.toString());
|
||||
|
||||
|
@ -623,15 +722,17 @@ QList<History::HistMessage> History::getUnsentMessagesForFriend(const ToxPk& fri
|
|||
// dispName and message could have null bytes, QString::fromUtf8
|
||||
// truncates on null bytes so we strip them
|
||||
auto id = RowId{row[0].toLongLong()};
|
||||
auto isOfflineMessage = row[1].isNull();
|
||||
auto isPending = !row[1].isNull();
|
||||
auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong());
|
||||
auto friend_key = row[3].toString();
|
||||
auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', ""));
|
||||
auto sender_key = row[5].toString();
|
||||
if (row[6].isNull()) {
|
||||
ret += {id, isOfflineMessage, timestamp, friend_key,
|
||||
auto isBroken = !row[7].isNull();
|
||||
|
||||
MessageState messageState = getMessageState(isPending, isBroken);
|
||||
|
||||
ret += {id, messageState, timestamp, friend_key,
|
||||
display_name, sender_key, row[6].toString()};
|
||||
}
|
||||
};
|
||||
|
||||
db->execNow({queryText, rowCallback});
|
||||
|
@ -788,12 +889,12 @@ QList<History::DateIdx> History::getNumMessagesForFriendBeforeDateBoundaries(con
|
|||
}
|
||||
|
||||
/**
|
||||
* @brief Marks a message as sent.
|
||||
* @brief Marks a message as delivered.
|
||||
* Removing message from the faux-offline pending messages list.
|
||||
*
|
||||
* @param id Message ID.
|
||||
*/
|
||||
void History::markAsSent(RowId messageId)
|
||||
void History::markAsDelivered(RowId messageId)
|
||||
{
|
||||
if (!isValid()) {
|
||||
return;
|
||||
|
|
|
@ -105,31 +105,38 @@ struct FileDbInsertionData
|
|||
};
|
||||
Q_DECLARE_METATYPE(FileDbInsertionData);
|
||||
|
||||
enum class MessageState
|
||||
{
|
||||
complete,
|
||||
pending,
|
||||
broken
|
||||
};
|
||||
|
||||
class History : public QObject, public std::enable_shared_from_this<History>
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
struct HistMessage
|
||||
{
|
||||
HistMessage(RowId id, bool isSent, QDateTime timestamp, QString chat, QString dispName,
|
||||
HistMessage(RowId id, MessageState state, QDateTime timestamp, QString chat, QString dispName,
|
||||
QString sender, QString message)
|
||||
: chat{chat}
|
||||
, sender{sender}
|
||||
, dispName{dispName}
|
||||
, timestamp{timestamp}
|
||||
, id{id}
|
||||
, isSent{isSent}
|
||||
, state{state}
|
||||
, content(std::move(message))
|
||||
{}
|
||||
|
||||
HistMessage(RowId id, bool isSent, QDateTime timestamp, QString chat, QString dispName,
|
||||
HistMessage(RowId id, MessageState state, QDateTime timestamp, QString chat, QString dispName,
|
||||
QString sender, ToxFile file)
|
||||
: chat{chat}
|
||||
, sender{sender}
|
||||
, dispName{dispName}
|
||||
, timestamp{timestamp}
|
||||
, id{id}
|
||||
, isSent{isSent}
|
||||
, state{state}
|
||||
, content(std::move(file))
|
||||
{}
|
||||
|
||||
|
@ -139,7 +146,7 @@ public:
|
|||
QString dispName;
|
||||
QDateTime timestamp;
|
||||
RowId id;
|
||||
bool isSent;
|
||||
MessageState state;
|
||||
HistMessageContent content;
|
||||
};
|
||||
|
||||
|
@ -160,7 +167,7 @@ public:
|
|||
void eraseHistory();
|
||||
void removeFriendHistory(const QString& friendPk);
|
||||
void addNewMessage(const QString& friendPk, const QString& message, const QString& sender,
|
||||
const QDateTime& time, bool isSent, QString dispName,
|
||||
const QDateTime& time, bool isDelivered, QString dispName,
|
||||
const std::function<void(RowId)>& insertIdCallback = {});
|
||||
|
||||
void addNewFileMessage(const QString& friendPk, const QString& fileId,
|
||||
|
@ -171,18 +178,18 @@ public:
|
|||
size_t getNumMessagesForFriend(const ToxPk& friendPk);
|
||||
size_t getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const QDateTime& date);
|
||||
QList<HistMessage> getMessagesForFriend(const ToxPk& friendPk, size_t firstIdx, size_t lastIdx);
|
||||
QList<HistMessage> getUnsentMessagesForFriend(const ToxPk& friendPk);
|
||||
QList<HistMessage> getUndeliveredMessagesForFriend(const ToxPk& friendPk);
|
||||
QDateTime getDateWhereFindPhrase(const QString& friendPk, const QDateTime& from, QString phrase,
|
||||
const ParameterSearch& parameter);
|
||||
QList<DateIdx> getNumMessagesForFriendBeforeDateBoundaries(const ToxPk& friendPk,
|
||||
const QDate& from, size_t maxNum);
|
||||
|
||||
void markAsSent(RowId messageId);
|
||||
void markAsDelivered(RowId messageId);
|
||||
|
||||
protected:
|
||||
QVector<RawDatabase::Query>
|
||||
generateNewMessageQueries(const QString& friendPk, const QString& message,
|
||||
const QString& sender, const QDateTime& time, bool isSent,
|
||||
const QString& sender, const QDateTime& time, bool isDelivered,
|
||||
QString dispName, std::function<void(RowId)> insertIdCallback = {});
|
||||
|
||||
signals:
|
||||
|
|
|
@ -178,11 +178,9 @@ ChatMessage::Ptr createMessage(const QString& displayName, bool isSelf, bool col
|
|||
messageType = ChatMessage::MessageType::ALERT;
|
||||
}
|
||||
|
||||
// Spinner is displayed by passing in an empty date
|
||||
auto timestamp = chatLogMessage.isComplete ? chatLogMessage.message.timestamp : QDateTime();
|
||||
|
||||
const auto timestamp = chatLogMessage.message.timestamp;
|
||||
return ChatMessage::createChatMessage(displayName, chatLogMessage.message.content, messageType,
|
||||
isSelf, timestamp, colorizeNames);
|
||||
isSelf, chatLogMessage.state, timestamp, colorizeNames);
|
||||
}
|
||||
|
||||
void renderMessage(const QString& displayName, bool isSelf, bool colorizeNames,
|
||||
|
@ -190,8 +188,8 @@ void renderMessage(const QString& displayName, bool isSelf, bool colorizeNames,
|
|||
{
|
||||
|
||||
if (chatMessage) {
|
||||
if (chatLogMessage.isComplete) {
|
||||
chatMessage->markAsSent(chatLogMessage.message.timestamp);
|
||||
if (chatLogMessage.state == MessageState::complete) {
|
||||
chatMessage->markAsDelivered(chatLogMessage.message.timestamp);
|
||||
}
|
||||
} else {
|
||||
chatMessage = createMessage(displayName, isSelf, colorizeNames, chatLogMessage);
|
||||
|
@ -543,9 +541,8 @@ void GenericChatForm::onSendTriggered()
|
|||
|
||||
/**
|
||||
* @brief Show, is it needed to hide message author name or not
|
||||
* @param messageAuthor Author of the sent message
|
||||
* @oaran messageTime DateTime of the sent message
|
||||
* @return True if it's needed to hide name, false otherwise
|
||||
* @param idx ChatLogIdx of the message
|
||||
* @return True if the name should be hidden, false otherwise
|
||||
*/
|
||||
bool GenericChatForm::needsToHideName(ChatLogIdx idx) const
|
||||
{
|
||||
|
|
|
@ -72,15 +72,6 @@ LoginScreen::~LoginScreen()
|
|||
delete ui;
|
||||
}
|
||||
|
||||
void LoginScreen::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
|
||||
// If we are in the bootstrap, returning -1 will give us something to exit with in main.cpp
|
||||
this->setResult(-1);
|
||||
// If we are in application exec, we can quit by closing it, instead.
|
||||
qApp->quit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Resets the UI, clears all fields.
|
||||
*/
|
||||
|
@ -110,10 +101,11 @@ void LoginScreen::reset(const QString& initialProfileName)
|
|||
|
||||
void LoginScreen::onProfileLoaded()
|
||||
{
|
||||
done(0);
|
||||
done(QDialog::Accepted);
|
||||
}
|
||||
|
||||
void LoginScreen::onProfileLoadFailed() {
|
||||
void LoginScreen::onProfileLoadFailed()
|
||||
{
|
||||
QMessageBox::critical(this, tr("Couldn't load this profile"), tr("Wrong password."));
|
||||
ui->loginPassword->setFocus();
|
||||
ui->loginPassword->selectAll();
|
||||
|
|
|
@ -21,9 +21,9 @@
|
|||
#ifndef LOGINSCREEN_H
|
||||
#define LOGINSCREEN_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QShortcut>
|
||||
#include <QToolButton>
|
||||
#include <QDialog>
|
||||
|
||||
class Profile;
|
||||
|
||||
|
@ -47,9 +47,6 @@ signals:
|
|||
void createNewProfile(QString name, const QString& pass);
|
||||
void loadProfile(QString name, const QString& pass);
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent* event) final;
|
||||
|
||||
public slots:
|
||||
void onProfileLoaded();
|
||||
void onProfileLoadFailed();
|
||||
|
|
|
@ -35,9 +35,11 @@ private slots:
|
|||
void testCreation();
|
||||
void testIsNewDb();
|
||||
void test0to1();
|
||||
void test1to2();
|
||||
void cleanupTestCase();
|
||||
private:
|
||||
bool initSucess{false};
|
||||
void createSchemaAtVersion(std::shared_ptr<RawDatabase>, const QMap<QString, QString>& schema);
|
||||
void verifyDb(std::shared_ptr<RawDatabase> db, const QMap<QString, QString>& expectedSql);
|
||||
};
|
||||
|
||||
|
@ -45,7 +47,34 @@ const QString testFileList[] = {
|
|||
"testCreation.db",
|
||||
"testIsNewDbTrue.db",
|
||||
"testIsNewDbFalse.db",
|
||||
"test0to1.db"
|
||||
"test0to1.db",
|
||||
"test1to2.db"
|
||||
};
|
||||
|
||||
const QMap<QString, QString> schema0 {
|
||||
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name))"},
|
||||
{"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY)"},
|
||||
{"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL)"},
|
||||
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"}
|
||||
};
|
||||
|
||||
// added file transfer history
|
||||
const QMap<QString, QString> schema1 {
|
||||
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name))"},
|
||||
{"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY)"},
|
||||
{"file_transfers", "CREATE TABLE 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)"},
|
||||
{"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL, file_id INTEGER)"},
|
||||
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"}
|
||||
};
|
||||
|
||||
// move stuck faux offline messages do a table of "broken" messages
|
||||
const QMap<QString, QString> schema2 {
|
||||
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name))"},
|
||||
{"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY)"},
|
||||
{"file_transfers", "CREATE TABLE 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)"},
|
||||
{"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL, file_id INTEGER)"},
|
||||
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"},
|
||||
{"broken_messages", "CREATE TABLE broken_messages (id INTEGER PRIMARY KEY)"}
|
||||
};
|
||||
|
||||
void TestDbSchema::initTestCase()
|
||||
|
@ -69,77 +98,153 @@ void TestDbSchema::cleanupTestCase()
|
|||
|
||||
void TestDbSchema::verifyDb(std::shared_ptr<RawDatabase> db, const QMap<QString, QString>& expectedSql)
|
||||
{
|
||||
QVERIFY(db->execNow(RawDatabase::Query(QStringLiteral("SELECT name, sql FROM sqlite_master "
|
||||
"WHERE type='table' "
|
||||
"ORDER BY name;"),
|
||||
QVERIFY(db->execNow(RawDatabase::Query(QStringLiteral(
|
||||
"SELECT name, sql FROM sqlite_master "
|
||||
"WHERE type='table';"),
|
||||
[&](const QVector<QVariant>& row) {
|
||||
const QString tableName = row[0].toString();
|
||||
const QString tableSql = row[1].toString();
|
||||
QString tableSql = row[1].toString();
|
||||
QVERIFY(expectedSql.contains(tableName));
|
||||
QVERIFY(expectedSql.value(tableName) == tableSql);
|
||||
// table and column names can be quoted. UPDATE TEABLE automatically quotes the new names, but this
|
||||
// has no functional impact on the schema. Strip quotes for comparison so that our created schema
|
||||
// matches schema made from UPDATE TABLEs.
|
||||
const QString unquotedTableSql = tableSql.remove("\"");
|
||||
QVERIFY(expectedSql.value(tableName) == unquotedTableSql);
|
||||
})));
|
||||
}
|
||||
|
||||
void TestDbSchema::createSchemaAtVersion(std::shared_ptr<RawDatabase> db, const QMap<QString, QString>& schema)
|
||||
{
|
||||
QVector<RawDatabase::Query> queries;
|
||||
for (auto const& tableCreation : schema.values()) {
|
||||
queries += tableCreation;
|
||||
}
|
||||
QVERIFY(db->execNow(queries));
|
||||
}
|
||||
|
||||
void TestDbSchema::testCreation()
|
||||
{
|
||||
QVector<RawDatabase::Query> queries;
|
||||
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"testCreation.db", {}, {}}};
|
||||
generateCurrentSchema(queries);
|
||||
QVERIFY(db->execNow(queries));
|
||||
const QMap<QString, QString> expectedSql {
|
||||
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name))"},
|
||||
{"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY)"},
|
||||
{"file_transfers", "CREATE TABLE 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)"},
|
||||
{"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL, file_id INTEGER)"},
|
||||
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"}
|
||||
};
|
||||
verifyDb(db, expectedSql);
|
||||
QVERIFY(createCurrentSchema(*db));
|
||||
verifyDb(db, schema2);
|
||||
}
|
||||
|
||||
void TestDbSchema::testIsNewDb()
|
||||
{
|
||||
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"testIsNewDbTrue.db", {}, {}}};
|
||||
QVERIFY(isNewDb(db) == true);
|
||||
bool success = false;
|
||||
bool newDb = isNewDb(db, success);
|
||||
QVERIFY(success);
|
||||
QVERIFY(newDb == true);
|
||||
db = std::shared_ptr<RawDatabase>{new RawDatabase{"testIsNewDbFalse.db", {}, {}}};
|
||||
QVector<RawDatabase::Query> queries;
|
||||
generateCurrentSchema(queries);
|
||||
QVERIFY(db->execNow(queries));
|
||||
QVERIFY(isNewDb(db) == false);
|
||||
createSchemaAtVersion(db, schema0);
|
||||
newDb = isNewDb(db, success);
|
||||
QVERIFY(success);
|
||||
QVERIFY(newDb == false);
|
||||
}
|
||||
|
||||
void TestDbSchema::test0to1()
|
||||
{
|
||||
const QMap<QString, QString> expectedSql {
|
||||
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name))"},
|
||||
{"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY)"},
|
||||
{"file_transfers", "CREATE TABLE 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)"},
|
||||
{"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL, file_id INTEGER)"},
|
||||
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"}
|
||||
};
|
||||
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"test0to1.db", {}, {}}};
|
||||
createSchemaAtVersion(db, schema0);
|
||||
QVERIFY(dbSchema0to1(*db));
|
||||
verifyDb(db, schema1);
|
||||
}
|
||||
|
||||
void TestDbSchema::test1to2()
|
||||
{
|
||||
/*
|
||||
Due to a long standing bug, faux offline message have been able to become stuck
|
||||
going back years. Because of recent fixes to history loading, faux offline
|
||||
messages will correctly all be sent on connection, but this causes an issue of
|
||||
long stuck messages suddenly being delivered to a friend, out of context,
|
||||
creating a confusing interaction. To work around this, this upgrade moves any
|
||||
faux offline messages in a chat that are older than the last successfully
|
||||
delivered message, indicating they were stuck, to a new table,
|
||||
`broken_messages`, preventing them from ever being sent in the future.
|
||||
|
||||
https://github.com/qTox/qTox/issues/5776
|
||||
*/
|
||||
|
||||
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"test1to2.db", {}, {}}};
|
||||
createSchemaAtVersion(db, schema1);
|
||||
|
||||
const QString myPk = "AC18841E56CCDEE16E93E10E6AB2765BE54277D67F1372921B5B418A6B330D3D";
|
||||
const QString friend1Pk = "FE34BC6D87B66E958C57BBF205F9B79B62BE0AB8A4EFC1F1BB9EC4D0D8FB0663";
|
||||
const QString friend2Pk = "2A1CBCE227549459C0C20F199DB86AD9BCC436D35BAA1825FFD4B9CA3290D200";
|
||||
|
||||
QVector<RawDatabase::Query> queries;
|
||||
queries += RawDatabase::Query(QStringLiteral(
|
||||
"CREATE TABLE peers "
|
||||
"(id INTEGER PRIMARY KEY, "
|
||||
"public_key TEXT NOT NULL UNIQUE);"
|
||||
"CREATE TABLE aliases "
|
||||
"(id INTEGER PRIMARY KEY, "
|
||||
"owner INTEGER, "
|
||||
"display_name BLOB NOT NULL, "
|
||||
"UNIQUE(owner, display_name));"
|
||||
"CREATE TABLE 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 faux_offline_pending "
|
||||
"(id INTEGER PRIMARY KEY);"));
|
||||
queries += QString("INSERT INTO peers (id, public_key) VALUES (%1, '%2')").arg(0).arg(myPk);
|
||||
queries += QString("INSERT INTO peers (id, public_key) VALUES (%1, '%2')").arg(1).arg(friend1Pk);
|
||||
queries += QString("INSERT INTO peers (id, public_key) VALUES (%1, '%2')").arg(2).arg(friend2Pk);
|
||||
|
||||
// friend 1
|
||||
// first message in chat is pending - but the second is delivered. This message is "broken"
|
||||
queries += RawDatabase::Query{
|
||||
"INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (1, 1, 1, ?, 0)",
|
||||
{"first message in chat, pending and stuck"}};
|
||||
queries += {"INSERT INTO faux_offline_pending (id) VALUES ("
|
||||
" last_insert_rowid()"
|
||||
");"};
|
||||
// second message is delivered, causing the first to be considered broken
|
||||
queries += RawDatabase::Query{
|
||||
"INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (2, 2, 1, ?, 0)",
|
||||
{"second message in chat, delivered"}};
|
||||
|
||||
// third message is pending - this is a normal pending message. It should be untouched.
|
||||
queries += RawDatabase::Query{
|
||||
"INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (3, 3, 1, ?, 0)",
|
||||
{"third message in chat, pending"}};
|
||||
queries += {"INSERT INTO faux_offline_pending (id) VALUES ("
|
||||
" last_insert_rowid()"
|
||||
");"};
|
||||
|
||||
// friend 2
|
||||
// first message is delivered.
|
||||
queries += RawDatabase::Query{
|
||||
"INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (4, 4, 2, ?, 2)",
|
||||
{"first message by friend in chat, delivered"}};
|
||||
|
||||
// second message is also delivered.
|
||||
queries += RawDatabase::Query{
|
||||
"INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (5, 5, 2, ?, 0)",
|
||||
{"first message by us in chat, delivered"}};
|
||||
|
||||
// third message is pending, but not broken since there are no delivered messages after it.
|
||||
queries += RawDatabase::Query{
|
||||
"INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (6, 6, 2, ?, 0)",
|
||||
{"last message in chat, by us, pending"}};
|
||||
queries += {"INSERT INTO faux_offline_pending (id) VALUES ("
|
||||
" last_insert_rowid()"
|
||||
");"};
|
||||
|
||||
QVERIFY(db->execNow(queries));
|
||||
queries.clear();
|
||||
dbSchema0to1(db, queries);
|
||||
QVERIFY(db->execNow(queries));
|
||||
verifyDb(db, expectedSql);
|
||||
QVERIFY(dbSchema1to2(*db));
|
||||
verifyDb(db, schema2);
|
||||
|
||||
long brokenCount = -1;
|
||||
RawDatabase::Query brokenCountQuery = {"SELECT COUNT(*) FROM broken_messages;", [&](const QVector<QVariant>& row) {
|
||||
brokenCount = row[0].toLongLong();
|
||||
}};
|
||||
QVERIFY(db->execNow(brokenCountQuery));
|
||||
QVERIFY(brokenCount == 1); // only friend 1's first message is "broken"
|
||||
|
||||
int fauxOfflineCount = -1;
|
||||
RawDatabase::Query fauxOfflineCountQuery = {"SELECT COUNT(*) FROM faux_offline_pending;", [&](const QVector<QVariant>& row) {
|
||||
fauxOfflineCount = row[0].toLongLong();
|
||||
}};
|
||||
QVERIFY(db->execNow(fauxOfflineCountQuery));
|
||||
// both friend 1's third message and friend 2's third message should still be pending.
|
||||
//The broken message should no longer be pending.
|
||||
QVERIFY(fauxOfflineCount == 2);
|
||||
|
||||
int totalHisoryCount = -1;
|
||||
RawDatabase::Query totalHistoryCountQuery = {"SELECT COUNT(*) FROM history;", [&](const QVector<QVariant>& row) {
|
||||
totalHisoryCount = row[0].toLongLong();
|
||||
}};
|
||||
QVERIFY(db->execNow(totalHistoryCountQuery));
|
||||
QVERIFY(totalHisoryCount == 6); // all messages should still be in history.
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TestDbSchema)
|
||||
|
|
Loading…
Reference in New Issue
Block a user