diff --git a/src/lib/qutim/history.cpp b/src/lib/qutim/history.cpp
index 4f99dc260..0da282be3 100644
--- a/src/lib/qutim/history.cpp
+++ b/src/lib/qutim/history.cpp
@@ -49,10 +49,6 @@ namespace qutim_sdk_0_3
return AsyncResult::create(MessageList());
}
- void showHistory(const ChatUnit *) override
- {
- }
-
AsyncResult> accounts() override
{
return AsyncResult>::create(QVector());
@@ -63,12 +59,12 @@ namespace qutim_sdk_0_3
return AsyncResult>::create(QVector());
}
- AsyncResult> months(const ContactInfo &, const QRegularExpression &) override
+ AsyncResult> months(const ContactInfo &, const QString &) override
{
return AsyncResult>::create(QList());
}
- AsyncResult> dates(const ContactInfo &, const QDate &, const QRegularExpression &) override
+ AsyncResult> dates(const ContactInfo &, const QDate &, const QString &) override
{
return AsyncResult>::create(QList());
}
diff --git a/src/lib/qutim/history.h b/src/lib/qutim/history.h
index 1fe4ac69e..b777f8d5b 100644
--- a/src/lib/qutim/history.h
+++ b/src/lib/qutim/history.h
@@ -34,6 +34,9 @@ namespace qutim_sdk_0_3
{
class ChatUnit;
+ /**
+ * @brief The History class is base class to implement history storage plugins
+ */
class LIBQUTIM_EXPORT History : public QObject
{
Q_OBJECT
@@ -61,11 +64,24 @@ namespace qutim_sdk_0_3
};
virtual void store(const Message &message) = 0;
+ /**
+ * Read messages from history.
+ * \warning \a from or/and \a to parameter can be invalid, which means that
+ * you should ignore that parameter
+ */
virtual AsyncResult read(const ContactInfo &contact, const QDateTime &from, const QDateTime &to, int max_num) = 0;
virtual AsyncResult> accounts() = 0;
virtual AsyncResult> contacts(const AccountInfo &account) = 0;
- virtual AsyncResult> months(const ContactInfo &contact, const QRegularExpression ®ex) = 0;
- virtual AsyncResult> dates(const ContactInfo &contact, const QDate &month, const QRegularExpression ®ex) = 0;
+ /**
+ * Returns list of date(year, month). If search string is presented, you may return list of months where
+ * at least message contains search string.
+ */
+ virtual AsyncResult> months(const ContactInfo &contact, const QString &needle) = 0;
+ /**
+ * Returns list of days in month of certain year. If search string is not empty, you should return list of days
+ * where at least one message with search string is presented
+ */
+ virtual AsyncResult> dates(const ContactInfo &contact, const QDate &month, const QString &needle) = 0;
AsyncResult read(const ChatUnit *unit, const QDateTime &to, int max_num);
AsyncResult read(const ChatUnit *unit, int max_num);
@@ -75,9 +91,6 @@ namespace qutim_sdk_0_3
static ContactInfo info(const ChatUnit *unit);
- public slots:
- virtual void showHistory(const ChatUnit *unit) = 0;
-
protected:
History();
diff --git a/src/plugins/generic/generic.qbs b/src/plugins/generic/generic.qbs
index c709f9933..a1dd6dadb 100644
--- a/src/plugins/generic/generic.qbs
+++ b/src/plugins/generic/generic.qbs
@@ -32,6 +32,7 @@ Project {
"formula/formula.qbs",
"highlighter/highlighter.qbs",
"histman/histman.qbs",
+ "histview/histview.qbs",
"hunspeller/hunspeller.qbs",
"idledetector/idledetector.qbs",
"idlestatuschanger/idlestatuschanger.qbs",
@@ -75,6 +76,7 @@ Project {
"simplecontactlist/simplecontactlist.qbs",
"simplerosterstorage/simplerosterstorage.qbs",
"soundthemeselector/soundthemeselector.qbs",
+ "sqlitehistory/sqlitehistory.qbs",
"trayicon/trayicon.qbs",
"unreadmessageskeeper/unreadmessageskeeper.qbs",
"updater/updater.qbs",
diff --git a/src/plugins/generic/histview/histview.plugin.json b/src/plugins/generic/histview/histview.plugin.json
new file mode 100644
index 000000000..de5feb5ab
--- /dev/null
+++ b/src/plugins/generic/histview/histview.plugin.json
@@ -0,0 +1,7 @@
+{
+ "pluginIcon": "",
+ "pluginName": "History View",
+ "pluginDescription": "Simple separate window plugin for viewing history",
+ "extensionHeader": "src/histview.h",
+ "extensionClass": "Core::HistView"
+}
diff --git a/src/plugins/generic/histview/histview.qbs b/src/plugins/generic/histview/histview.qbs
new file mode 100644
index 000000000..89b40c682
--- /dev/null
+++ b/src/plugins/generic/histview/histview.qbs
@@ -0,0 +1,5 @@
+import "../GenericPlugin.qbs" as GenericPlugin
+
+GenericPlugin {
+
+}
diff --git a/src/plugins/generic/jsonhistory/historywindow.cpp b/src/plugins/generic/histview/src/historywindow.cpp
similarity index 90%
rename from src/plugins/generic/jsonhistory/historywindow.cpp
rename to src/plugins/generic/histview/src/historywindow.cpp
index 55fbe4ef2..3fffaa9b1 100644
--- a/src/plugins/generic/jsonhistory/historywindow.cpp
+++ b/src/plugins/generic/histview/src/historywindow.cpp
@@ -69,7 +69,7 @@ HistoryWindow::HistoryWindow(const ChatUnit *unit)
ui.setupUi(this);
ui.historyLog->setHtml(""
- + tr("No History") + "
");
+ + tr("No History") + "
");
ui.label_in->setText( tr( "In: %L1").arg( 0 ) );
ui.label_out->setText( tr( "Out: %L1").arg( 0 ) );
ui.label_all->setText( tr( "All: %L1").arg( 0 ) );
@@ -92,6 +92,8 @@ HistoryWindow::HistoryWindow(const ChatUnit *unit)
connect(ui.dateTreeWidget, &QTreeWidget::itemExpanded, this, &HistoryWindow::fillMonth);
+ connect(ui.dateTreeWidget, &QTreeWidget::itemCollapsed, this, &HistoryWindow::clearMonth);
+
fillAccountComboBox();
setParent(QApplication::activeWindow());
@@ -126,7 +128,21 @@ void HistoryWindow::setUnit(const ChatUnit *unit)
void HistoryWindow::setIcons()
{
// setWindowIcon(Icon("history"));
-// ui.searchButton->setIcon(Icon("search"));
+ // ui.searchButton->setIcon(Icon("search"));
+}
+
+QTreeWidgetItem *HistoryWindow::findChild(QTreeWidgetItem *parent, const QVariant &value)
+{
+ if (!parent)
+ return nullptr;
+
+ for (int i = 0; i < parent->childCount(); ++i) {
+ QTreeWidgetItem *child = parent->child(i);
+ if (child->data(0, Qt::UserRole) == value)
+ return child;
+ }
+
+ return nullptr;
}
void HistoryWindow::fillAccountComboBox()
@@ -146,10 +162,14 @@ void HistoryWindow::fillAccountComboBox()
connect(ui.accountComboBox, static_cast(&QComboBox::currentIndexChanged),
this, &HistoryWindow::fillContactComboBox);
int accountIndex = ui.accountComboBox->findData(QVariant::fromValue(m_unitInfo));
+
if (accountIndex < 0)
fillContactComboBox(0);
- else
+ else {
ui.accountComboBox->setCurrentIndex(accountIndex);
+ fillContactComboBox(accountIndex);
+ }
+
});
}
@@ -201,7 +221,7 @@ void HistoryWindow::fillDateTreeWidget(int index)
setWindowTitle(QStringLiteral("%1 (%2)").arg(ui.fromComboBox->currentText(), ui.accountComboBox->currentText()));
- history()->months(contactInfo, m_search).connect(this, [this, contactInfo] (const QList &months) {
+ history()->months(contactInfo, m_search_word).connect(this, [this, contactInfo] (const QList &months) {
int index = ui.fromComboBox->currentIndex();
auto currentContactInfo = ui.fromComboBox->itemData(index).value();
if (!(currentContactInfo == contactInfo))
@@ -244,25 +264,12 @@ void HistoryWindow::fillMonth(QTreeWidgetItem *monthItem)
auto contactInfo = ui.fromComboBox->itemData(contactIndex).value();
auto month = monthItem->data(0, Qt::UserRole).toDate();
- history()->dates(contactInfo, month, m_search).connect(this, [this, contactInfo, month] (const QList &dates) {
+ history()->dates(contactInfo, month, m_search_word).connect(this, [this, contactInfo, month] (const QList &dates) {
int contactIndex = ui.fromComboBox->currentIndex();
auto currentContactInfo = ui.fromComboBox->itemData(contactIndex).value();
if (!(currentContactInfo == contactInfo))
return;
- auto findChild = [] (QTreeWidgetItem *parent, const QVariant &value) -> QTreeWidgetItem * {
- if (!parent)
- return nullptr;
-
- for (int i = 0; i < parent->childCount(); ++i) {
- QTreeWidgetItem *child = parent->child(i);
- if (child->data(0, Qt::UserRole) == value)
- return child;
- }
-
- return nullptr;
- };
-
auto monthItem = findChild(findChild(ui.dateTreeWidget->invisibleRootItem(), month.year()), month);
if (!monthItem)
return;
@@ -276,6 +283,18 @@ void HistoryWindow::fillMonth(QTreeWidgetItem *monthItem)
});
}
+void HistoryWindow::clearMonth(QTreeWidgetItem *monthItem) {
+ auto month = monthItem->data(0, Qt::UserRole).toDate();
+
+ auto item = findChild(findChild(ui.dateTreeWidget->invisibleRootItem(), month.year()), month);
+
+ if(item) {
+ auto items = item->takeChildren();
+ qDeleteAll(items);
+ }
+}
+
+
void HistoryWindow::on_dateTreeWidget_currentItemChanged(QTreeWidgetItem *dayItem, QTreeWidgetItem *)
{
QTreeWidgetItem *monthItem = dayItem ? dayItem->parent() : nullptr;
@@ -351,7 +370,11 @@ void HistoryWindow::on_dateTreeWidget_currentItemChanged(QTreeWidgetItem *dayIte
cursor.insertHtml(historyMessage);
cursor.insertText(QStringLiteral("\n"));
} else {
- cursor.insertHtml(historyMessage.replace(m_search, resultString));
+ QRegularExpression expr;
+ expr.setPattern(QLatin1Char('(') + QRegularExpression::escape(m_search_word) + QLatin1Char(')'));
+ expr.setPatternOptions(QRegularExpression::MultilineOption | QRegularExpression::CaseInsensitiveOption);
+
+ cursor.insertHtml(historyMessage.replace(expr, resultString));
cursor.insertText(QStringLiteral("\n"));
}
}
@@ -382,8 +405,6 @@ void HistoryWindow::on_searchButton_clicked()
}
} else {
m_search_word = searchWord;
- m_search.setPattern(QLatin1Char('(') + QRegularExpression::escape(searchWord) + QLatin1Char(')'));
- m_search.setPatternOptions(QRegularExpression::MultilineOption | QRegularExpression::CaseInsensitiveOption);
fillDateTreeWidget(ui.fromComboBox->currentIndex());
}
}
diff --git a/src/plugins/generic/jsonhistory/historywindow.h b/src/plugins/generic/histview/src/historywindow.h
similarity index 94%
rename from src/plugins/generic/jsonhistory/historywindow.h
rename to src/plugins/generic/histview/src/historywindow.h
index 5404fbebe..c14a105d4 100644
--- a/src/plugins/generic/jsonhistory/historywindow.h
+++ b/src/plugins/generic/histview/src/historywindow.h
@@ -38,7 +38,6 @@ using namespace qutim_sdk_0_3;
namespace Core
{
-class JsonEngine;
class HistoryWindow : public QWidget
{
@@ -52,6 +51,7 @@ private slots:
void fillContactComboBox(int index);
void fillDateTreeWidget(int index);
void fillMonth(QTreeWidgetItem *month);
+ void clearMonth(QTreeWidgetItem *month);
void on_dateTreeWidget_currentItemChanged( QTreeWidgetItem* current, QTreeWidgetItem* previous );
void on_searchButton_clicked();
void findPrevious();
@@ -62,8 +62,8 @@ private slots:
Ui::HistoryWindowClass ui;
QMetaObject::Connection m_contactConnection;
History::ContactInfo m_unitInfo;
- QRegularExpression m_search;
QString m_search_word;
+ QTreeWidgetItem* findChild(QTreeWidgetItem *parent, const QVariant &value);
};
}
diff --git a/src/plugins/generic/jsonhistory/historywindow.ui b/src/plugins/generic/histview/src/historywindow.ui
similarity index 85%
rename from src/plugins/generic/jsonhistory/historywindow.ui
rename to src/plugins/generic/histview/src/historywindow.ui
index 2251d2a5f..f82306f3f 100644
--- a/src/plugins/generic/jsonhistory/historywindow.ui
+++ b/src/plugins/generic/histview/src/historywindow.ui
@@ -14,7 +14,16 @@
HistoryWindow
-
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
4
-
@@ -40,10 +49,18 @@
-
-
-
+
+
+ QComboBox::AdjustToContents
+
+
-
-
+
+
+ QComboBox::AdjustToContents
+
+
diff --git a/src/plugins/generic/histview/src/histview.cpp b/src/plugins/generic/histview/src/histview.cpp
new file mode 100644
index 000000000..ab18df1da
--- /dev/null
+++ b/src/plugins/generic/histview/src/histview.cpp
@@ -0,0 +1,81 @@
+/****************************************************************************
+**
+** qutIM - instant messenger
+**
+** Copyright © 2015 Nicolay Izoderov
+**
+*****************************************************************************
+**
+** $QUTIM_BEGIN_LICENSE$
+** This program is free 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.
+**
+** This program 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 this program. If not, see http://www.gnu.org/licenses/.
+** $QUTIM_END_LICENSE$
+**
+****************************************************************************/
+
+#include "histview.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Core
+{
+
+HistView::HistView()
+{
+ m_historyAction = new ActionGenerator(Icon("view-history"),
+ QT_TRANSLATE_NOOP("Chat", "View History"),
+ this,
+ SLOT(onHistoryActionTriggered(QObject*)));
+ m_historyAction->setType(ActionTypeChatButton|ActionTypeContactList);
+ m_historyAction->setPriority(512);
+ MenuController::addAction(m_historyAction);
+}
+
+HistView::~HistView()
+{
+ delete m_historyAction;
+}
+
+void HistView::onHistoryActionTriggered(QObject* object)
+{
+ ChatUnit *unit = qobject_cast(object);
+ Q_ASSERT(unit);
+
+ showHistory(unit);
+}
+
+void HistView::showHistory(const ChatUnit *unit)
+{
+ unit = unit->getHistoryUnit();
+ if (m_historyWindow) {
+ m_historyWindow.data()->setUnit(unit);
+ m_historyWindow.data()->raise();
+ } else {
+ m_historyWindow = new HistoryWindow(unit);
+ m_historyWindow.data()->show();
+ }
+}
+
+}
diff --git a/src/plugins/generic/histview/src/histview.h b/src/plugins/generic/histview/src/histview.h
new file mode 100644
index 000000000..27dd3cb96
--- /dev/null
+++ b/src/plugins/generic/histview/src/histview.h
@@ -0,0 +1,61 @@
+/****************************************************************************
+**
+** qutIM - instant messenger
+**
+** Copyright © 2008 Rustam Chakin
+** Copyright © 2011 Ruslan Nigmatullin
+** Copyright © 2015 Nicolay Izoderov
+**
+*****************************************************************************
+**
+** $QUTIM_BEGIN_LICENSE$
+** This program is free 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.
+**
+** This program 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 this program. If not, see http://www.gnu.org/licenses/.
+** $QUTIM_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef HISTVIEW_H
+#define HISTVIEW_H
+
+#include
+#include
+#include
+#include
+#include
+#include "historywindow.h"
+
+using namespace qutim_sdk_0_3;
+
+namespace Core
+{
+
+class HistView : public QObject
+{
+ Q_OBJECT
+ Q_CLASSINFO("Service", "HistView")
+public:
+ HistView();
+ ~HistView();
+public slots:
+ void onHistoryActionTriggered(QObject *object);
+
+private:
+ QPointer m_historyWindow;
+ ActionGenerator* m_historyAction;
+ void showHistory(const ChatUnit *unit);
+};
+
+}
+
+#endif // HISTVIEW_H
diff --git a/src/plugins/generic/jsonhistory/jsonhistory.cpp b/src/plugins/generic/jsonhistory/jsonhistory.cpp
index 9ae6c087b..6074def3f 100644
--- a/src/plugins/generic/jsonhistory/jsonhistory.cpp
+++ b/src/plugins/generic/jsonhistory/jsonhistory.cpp
@@ -31,7 +31,6 @@
#include
#include
#include
-#include "historywindow.h"
#include
#include
//#include
@@ -75,25 +74,8 @@ static void runJob(JsonHistoryScope::Ptr scope, Method method)
}
}
-void init(History *history)
-{
- ActionGenerator *gen = new ActionGenerator(Icon("view-history"),
- QT_TRANSLATE_NOOP("Chat", "View History"),
- history,
- SLOT(onHistoryActionTriggered(QObject*)));
- gen->setType(ActionTypeChatButton|ActionTypeContactList);
- gen->setPriority(512);
- MenuController::addAction(gen);
-}
-
JsonHistory::JsonHistory() : m_scope(new JsonHistoryScope)
{
- static bool inited = false;
- if (!inited) {
- inited = true;
- init(this);
- }
-
m_scope->hasJobRunnable = false;
}
@@ -396,9 +378,9 @@ AsyncResult> JsonHistory::contacts(const AccountIn
return handler.result();
}
-AsyncResult> JsonHistory::months(const ContactInfo &contact, const QRegularExpression ®ex)
+AsyncResult> JsonHistory::months(const ContactInfo &contact, const QString &needle)
{
- Q_UNUSED(regex);
+ Q_UNUSED(needle);
AsyncResultHandler> handler;
auto scope = m_scope;
@@ -430,12 +412,12 @@ AsyncResult> JsonHistory::months(const ContactInfo &contact, const
return handler.result();
}
-AsyncResult> JsonHistory::dates(const ContactInfo &contact, const QDate &month, const QRegularExpression ®ex)
+AsyncResult> JsonHistory::dates(const ContactInfo &contact, const QDate &month, const QString &needle)
{
AsyncResultHandler> handler;
auto scope = m_scope;
- runJob(m_scope, [handler, scope, contact, month, regex] () {
+ runJob(m_scope, [handler, scope, contact, month, needle] () {
QSet result;
QFile file(scope->getFileName(contact, month));
@@ -479,7 +461,7 @@ AsyncResult> JsonHistory::dates(const ContactInfo &contact, const Q
const QDate date = QDateTime::fromString(message.value("datetime").toString(), Qt::ISODate).date();
const QString text = message.value("text").toString();
- if (!regex.isValid() || text.contains(regex))
+ if (needle.isEmpty() || text.contains(needle, Qt::CaseInsensitive))
result.insert(date);
}
}
@@ -493,18 +475,6 @@ AsyncResult> JsonHistory::dates(const ContactInfo &contact, const Q
return handler.result();
}
-void JsonHistory::showHistory(const ChatUnit *unit)
-{
- unit = unit->getHistoryUnit();
- if (m_historyWindow) {
- m_historyWindow.data()->setUnit(unit);
- m_historyWindow.data()->raise();
- } else {
- m_historyWindow = new Core::HistoryWindow(unit);
- m_historyWindow.data()->show();
- }
-}
-
QString JsonHistory::quote(const QString &str)
{
const static bool true_chars[128] =
@@ -561,11 +531,5 @@ QString JsonHistory::unquote(const QString &str)
return result;
}
-void JsonHistory::onHistoryActionTriggered(QObject* object)
-{
- ChatUnit *unit = qobject_cast(object);
- showHistory(unit);
-}
-
}
diff --git a/src/plugins/generic/jsonhistory/jsonhistory.h b/src/plugins/generic/jsonhistory/jsonhistory.h
index 6ce8c8ef5..76beb7ff2 100644
--- a/src/plugins/generic/jsonhistory/jsonhistory.h
+++ b/src/plugins/generic/jsonhistory/jsonhistory.h
@@ -38,8 +38,6 @@ using namespace qutim_sdk_0_3;
namespace Core
{
-class HistoryWindow;
-
class JsonHistoryScope
{
public:
@@ -86,18 +84,14 @@ class JsonHistory : public History
AsyncResult read(const ContactInfo &info, const QDateTime &from, const QDateTime &to, int max_num) override;
AsyncResult> accounts() override;
AsyncResult> contacts(const AccountInfo &account) override;
- AsyncResult> months(const ContactInfo &contact, const QRegularExpression ®ex) override;
- AsyncResult> dates(const ContactInfo &contact, const QDate &month, const QRegularExpression ®ex) override;
- void showHistory(const ChatUnit *unit) override;
+ AsyncResult> months(const ContactInfo &contact, const QString &needle) override;
+ AsyncResult> dates(const ContactInfo &contact, const QDate &month, const QString &needle) override;
static QString quote(const QString &str);
static QString unquote(const QString &str);
-private slots:
- void onHistoryActionTriggered(QObject *object);
private:
JsonHistoryScope::Ptr m_scope;
- QPointer m_historyWindow;
};
}
diff --git a/src/plugins/generic/sqlitehistory/sqlitehistory.cpp b/src/plugins/generic/sqlitehistory/sqlitehistory.cpp
new file mode 100644
index 000000000..b2893a23a
--- /dev/null
+++ b/src/plugins/generic/sqlitehistory/sqlitehistory.cpp
@@ -0,0 +1,515 @@
+/****************************************************************************
+**
+** qutIM - instant messenger
+**
+** Copyright © 2015 Nicolay Izoderov
+**
+*****************************************************************************
+**
+** $QUTIM_BEGIN_LICENSE$
+** This program is free 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.
+**
+** This program 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 this program. If not, see http://www.gnu.org/licenses/.
+** $QUTIM_END_LICENSE$
+**
+****************************************************************************/
+
+#include "sqlitehistory.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+//#include
+#include
+#include
+#include
+#include
+
+namespace Core
+{
+
+SqliteHistory::SqliteHistory() :
+ m_thread(new QThread),
+ m_worker(new SqliteWorker)
+{
+ m_worker->moveToThread(m_thread);
+
+ connect(m_worker, SIGNAL(error(QString)), this, SLOT(errorHandler(QString)));
+
+ connect(m_thread, SIGNAL(started()), m_worker, SLOT(process()));
+
+ connect(m_worker, SIGNAL(finished()), m_thread, SLOT(quit()));
+ connect(m_worker, SIGNAL(finished()), m_worker, SLOT(deleteLater()));
+ connect(m_thread, SIGNAL(finished()), m_thread, SLOT(deleteLater()));
+
+ m_thread->start();
+}
+
+SqliteHistory::~SqliteHistory()
+{
+ m_worker->shutdown();
+ m_thread->wait();
+
+ delete m_worker;
+ delete m_thread;
+}
+
+void SqliteHistory::store(const Message &message)
+{
+ if (!message.chatUnit())
+ return;
+
+ auto contactInfo = info(message.chatUnit());
+
+ m_worker->runJob([message, contactInfo] () {
+ QSqlQuery query;
+
+ query.prepare(QStringLiteral("INSERT INTO qutim_history (account, protocol, contact, year, month, day, time, incoming, message, html, type, sendername) "
+ "VALUES (:account, :protocol, :contact, :year, :month, :day, :time, :incoming, :message, :html, :type, :sendername)"));
+
+ query.bindValue(QStringLiteral(":account"), contactInfo.account);
+ query.bindValue(QStringLiteral(":protocol"), contactInfo.protocol);
+ query.bindValue(QStringLiteral(":contact"), contactInfo.contact);
+
+ query.bindValue(QStringLiteral(":year"), message.time().date().year());
+ query.bindValue(QStringLiteral(":month"), message.time().date().month());
+ query.bindValue(QStringLiteral(":day"), message.time().date().day());
+ query.bindValue(QStringLiteral(":time"), message.time().toTime_t());
+
+ query.bindValue(QStringLiteral(":incoming"), message.isIncoming());
+ query.bindValue(QStringLiteral(":message"), message.text());
+ query.bindValue(QStringLiteral(":html"), message.html());
+
+ SqliteWorker::MessageTypes type = SqliteWorker::Message;
+ if(message.property("topic", false))
+ type |= SqliteWorker::Topic;
+ if(message.property("service", false))
+ type |= SqliteWorker::Service;
+
+ query.bindValue(QStringLiteral(":type"), static_cast(type));
+ query.bindValue(QStringLiteral(":sendername"), message.property("senderName", QString()));
+ query.exec();
+ });
+}
+
+AsyncResult SqliteHistory::read(const ContactInfo &info, const QDateTime &from, const QDateTime &to, int max_num)
+{
+ AsyncResultHandler handler;
+
+ m_worker->runJob([info, from, to, max_num, handler] () {
+ QSqlQuery query;
+
+ QString queryString = "SELECT time, incoming, message, html, type, sendername FROM qutim_history WHERE protocol = :protocol "
+ "AND account = :account "
+ "AND contact = :contact ";
+ if(from.isValid())
+ queryString += "AND time >= :time_from ";
+
+ if(to.isValid())
+ queryString += "AND time <= :time_to ";
+
+ queryString += "ORDER BY time DESC ";
+
+ if(max_num != -1)
+ queryString += "LIMIT :max";
+ query.prepare(queryString);
+
+ if(from.isValid())
+ query.bindValue(QStringLiteral(":time_from"), from.toTime_t());
+ if(to.isValid())
+ query.bindValue(QStringLiteral(":time_to"), to.toTime_t());
+
+ qDebug() << "Trying to read messages";
+
+ query.bindValue(QStringLiteral(":account"), info.account);
+ query.bindValue(QStringLiteral(":protocol"), info.protocol);
+ query.bindValue(QStringLiteral(":contact"), info.contact);
+
+ query.bindValue(QStringLiteral(":max"), max_num);
+
+ query.exec();
+
+ auto error = query.lastError();
+ if (error.isValid()) {
+ qDebug() << "Error type: " << error.type();
+ qDebug() << "Database: " << error.databaseText();
+ qDebug() << "Driver: " << error.driverText();
+ }
+
+ MessageList items;
+
+ while(query.next()) {
+ Message msg;
+ msg.setTime(QDateTime::fromTime_t(query.value(0).toUInt()));
+ msg.setIncoming(query.value(1).toBool());
+ msg.setText(query.value(2).toString());
+ msg.setHtml(query.value(3).toString());
+
+ if(!query.value(5).isNull())
+ msg.setProperty("senderName", query.value(5).toString());
+
+ SqliteWorker::MessageTypes type = static_cast(query.value(4).toInt());
+
+ if(type.testFlag(SqliteWorker::Topic))
+ msg.setProperty("topic", true);
+
+ if(type.testFlag(SqliteWorker::Service))
+ msg.setProperty("service", true);
+
+ items.append(msg);
+ }
+
+ std::stable_sort(items.begin(), items.end(), [](const Message &left, const Message &right) {
+ return left.time() < right.time();
+ });
+
+ handler.handle(items);
+ });
+
+ return handler.result();
+}
+
+AsyncResult> SqliteHistory::accounts()
+{
+ AsyncResultHandler> handler;
+
+ m_worker->runJob([handler] () {
+ QVector result;
+
+ QSqlQuery query;
+ query.prepare(QStringLiteral("SELECT DISTINCT account, protocol FROM qutim_history"));
+ query.exec();
+ qDebug() << "accounts()";
+
+ while(query.next()) {
+ AccountInfo info;
+ info.account = query.value(0).toString();
+ info.protocol = query.value(1).toString();
+ qDebug() << "AccountInfo" << info.protocol << info.account;
+ result << info;
+ }
+
+ handler.handle(result);
+ });
+
+ return handler.result();
+}
+
+AsyncResult> SqliteHistory::contacts(const AccountInfo &account)
+{
+ AsyncResultHandler> handler;
+
+ m_worker->runJob([handler, account] () {
+ QVector result;
+
+ qDebug() << "contacts()";
+ QSqlQuery query;
+ query.prepare(QStringLiteral("SELECT DISTINCT contact FROM qutim_history WHERE account = :account AND protocol = :protocol"));
+ query.bindValue(QStringLiteral(":account"), account.account);
+ query.bindValue(QStringLiteral(":protocol"), account.protocol);
+ query.exec();
+
+ auto error = query.lastError();
+ if (error.isValid()) {
+ qDebug() << "Error type: " << error.type();
+ qDebug() << "Database: " << error.databaseText();
+ qDebug() << "Driver: " << error.driverText();
+ }
+
+
+ while(query.next()) {
+ ContactInfo info;
+ info.account = account.account;
+ info.protocol = account.protocol;
+ info.contact = query.value(0).toString();
+
+ qDebug() << info.contact;
+ result << info;
+ }
+
+ handler.handle(result);
+ });
+
+ return handler.result();
+}
+
+AsyncResult> SqliteHistory::months(const ContactInfo &contact, const QString &needle)
+{
+ AsyncResultHandler> handler;
+
+ m_worker->runJob([handler, contact, needle] () {
+ QList result;
+
+ QString queryString = QStringLiteral("SELECT DISTINCT year, month FROM qutim_history WHERE account = :account "
+ "AND protocol = :protocol "
+ "AND contact = :contact ");
+
+ if(!needle.isEmpty())
+ queryString += QStringLiteral("AND message LIKE :needle ESCAPE '@'");
+ QSqlQuery query;
+ query.prepare(queryString);
+ query.bindValue(QStringLiteral(":account"), contact.account);
+ query.bindValue(QStringLiteral(":protocol"), contact.protocol);
+ query.bindValue(QStringLiteral(":contact"), contact.contact);
+ if(!needle.isEmpty())
+ query.bindValue(QStringLiteral(":needle"), QLatin1Char('%') + SqliteWorker::escapeSqliteLike(needle) + QLatin1Char('%'));
+ query.exec();
+ qDebug() << "months()";
+
+ while(query.next()) {
+ int year = query.value(0).toInt();
+ int month = query.value(1).toInt();
+
+ qDebug() << QDate(year, month, 1);
+ result << QDate(year, month, 1);
+ }
+
+ std::sort(result.begin(), result.end());
+
+ handler.handle(result);
+ });
+
+ return handler.result();
+}
+
+AsyncResult> SqliteHistory::dates(const ContactInfo &contact, const QDate &month, const QString &needle)
+{
+ AsyncResultHandler> handler;
+
+ m_worker->runJob([handler, contact, month, needle] () {
+ QList result;
+
+ QString queryString = QStringLiteral("SELECT DISTINCT day FROM qutim_history WHERE account = :account "
+ "AND protocol = :protocol "
+ "AND contact = :contact "
+ "AND year = :year "
+ "AND month = :month ");
+ if(!needle.isEmpty())
+ queryString += QStringLiteral("AND message LIKE :needle ESCAPE '@'");
+
+ QSqlQuery query;
+ query.prepare(queryString);
+ query.bindValue(QStringLiteral(":account"), contact.account);
+ query.bindValue(QStringLiteral(":protocol"), contact.protocol);
+ query.bindValue(QStringLiteral(":contact"), contact.contact);
+ query.bindValue(QStringLiteral(":year"), month.year());
+ query.bindValue(QStringLiteral(":month"), month.month());
+ if(!needle.isEmpty())
+ query.bindValue(QStringLiteral(":needle"), QLatin1Char('%') + SqliteWorker::escapeSqliteLike(needle) + QLatin1Char('%'));
+ query.exec();
+
+ while(query.next()) {
+ result << QDate(month.year(), month.month(), query.value(0).toInt());
+ }
+
+ std::sort(result.begin(), result.end());
+
+ handler.handle(result);
+ });
+
+ return handler.result();
+}
+
+void SqliteHistory::errorHandler(const QString &error)
+{
+ qDebug() << error;
+}
+
+void SqliteWorker::prepareDb()
+{
+ QString dbScheme = QStringLiteral("CREATE TABLE IF NOT EXISTS qutim_history ("
+ "`id` INTEGER PRIMARY KEY AUTOINCREMENT, "
+ "`account` TEXT NOT NULL, "
+ "`protocol` TEXT NOT NULL, "
+ "`contact` TEXT NOT NULL, "
+ "`year` INTEGER(2) NOT NULL, "
+ "`month` INTEGER(1) NOT NULL, "
+ "`day` INTEGER(1) NOT NULL, "
+ "`time` TIMESTAMP NOT NULL, "
+ "`incoming` INTEGER(1) NOT NULL, "
+ "`message` TEXT NOT NULL, "
+ "`html` TEXT NOT NULL, "
+ "`type` INTEGER(1) NOT NULL, "
+ "`sendername` TEXT DEFAULT NULL"
+ ");");
+
+ QSqlQuery res = m_db.exec(dbScheme);
+ auto error = res.lastError();
+ if(error.isValid())
+ qDebug() << "Error during db init:" << error;
+
+ QString dbMigrations = QStringLiteral("CREATE TABLE IF NOT EXISTS qutim_history_version ("
+ "`key` VARCHAR(32) NOT NULL, "
+ "`value` INTEGER(1) NOT NULL, "
+ "primary key (`key`)"
+ ");");
+ QSqlQuery migrationsResult = m_db.exec(dbMigrations);
+ auto migrationsError = migrationsResult.lastError();
+ if(migrationsError.isValid())
+ qDebug() << "Error during creation of qutim_history_version:" << error;
+
+ qDebug() << "Checking version of database...";
+
+ QSqlQuery query;
+ query.prepare(QStringLiteral("SELECT value FROM qutim_history_version WHERE key = :key LIMIT 1"));
+ query.bindValue(QStringLiteral(":key"), QStringLiteral("sqlitehistory"));
+ query.exec();
+
+ if(query.first()) {
+ int version = query.value(0).toInt();
+ if(version == currentVersion()) {
+ qDebug() << "No migration needed";
+ return;
+ } else if(version > currentVersion()) {
+ qDebug() << "Current version of sqlitehistory plugin is" << currentVersion();
+ qDebug() << "But qutim.sqlite version = " << version;
+ qFatal("qutIM sqlite database is older than plugin. Cannot proceed. Please upgrade qutIM to last version");
+ } else if(version < currentVersion()) {
+ qDebug() << "Database older than plugin, executing migrations";
+
+ for(int i = version + 1; i < currentVersion(); ++i)
+ makeMigration(i);
+ }
+ } else {
+ qDebug() << "First run, inserting current version";
+ QSqlQuery query;
+
+ query.prepare(QStringLiteral("INSERT INTO qutim_history_version (key, value) "
+ "VALUES (:key, :value)"));
+ query.bindValue(QStringLiteral(":key"), QStringLiteral("sqlitehistory"));
+ query.bindValue(QStringLiteral(":value"), currentVersion());
+ query.exec();
+
+ qDebug() << "No migration needed";
+ }
+}
+
+void SqliteWorker::exec()
+{
+ forever {
+ m_queueLock.lock();
+ if(m_queue.isEmpty()) {
+ m_runningLock.lock();
+ m_isRunning = false;
+ m_runningLock.unlock();
+
+ m_queueLock.unlock();
+ break;
+ }
+
+ auto f = m_queue.dequeue();
+ m_queueLock.unlock();
+
+ f();
+ }
+}
+
+void SqliteWorker::runJob(std::function job)
+{
+ m_queueLock.lock();
+ m_queue.enqueue(std::move(job));
+ m_queueLock.unlock();
+
+ m_runningLock.lock();
+ if(!m_isRunning) {
+ m_isRunning = true;
+ m_runningLock.unlock();
+ QTimer::singleShot(0, this, SLOT(exec()));
+ } else
+ m_runningLock.unlock();
+}
+
+void SqliteWorker::shutdown()
+{
+ emit finished();
+}
+
+/**
+ * Escapes string to use in LIKE query. You need to add ESCAPE '@' to end of your query
+ */
+QString SqliteWorker::escapeSqliteLike(const QString &str)
+{
+ QString escaped = str;
+ escaped.replace(QLatin1Char('@'), QStringLiteral("@@"));
+ escaped.replace(QLatin1Char('_'), QStringLiteral("@_"));
+ escaped.replace(QLatin1Char('%'), QStringLiteral("@%"));
+
+ return escaped;
+}
+
+void SqliteWorker::process()
+{
+ m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"));
+ QDir history_dir = SystemInfo::getDir(SystemInfo::HistoryDir);
+ QString pathToHistory = history_dir.absolutePath() + QDir::separator() + "qutim.sqlite";
+ qDebug() << "Trying to open database" << pathToHistory;
+ m_db.setDatabaseName(pathToHistory);
+ bool openSuccess = m_db.open();
+
+ if(!openSuccess) {
+ qDebug() << m_db.lastError();
+ qFatal("Cannot open sqlite database");
+ } else
+ qDebug() << "Database opened!";
+
+ prepareDb();
+
+ m_runningLock.lock();
+ m_isRunning = true;
+ m_runningLock.unlock();
+ exec();
+}
+
+void SqliteWorker::makeMigration(int version)
+{
+ qDebug() << "Executing migration to version" << version;
+ m_db.transaction();
+ QSqlQuery query;
+
+ switch(version) {
+ case 0:
+ query.prepare(QStringLiteral("ALTER TABLE qutim_history ADD COLUMN `sendername` TEXT DEFAULT NULL"));
+ break;
+ default:
+ qFatal("Unhandled migration to version %i! Seems like a bug", version);
+ break;
+ }
+
+ query.exec();
+
+ auto error = query.lastError();
+ if(error.isValid()) {
+ qDebug() << "Error during migration from" << version - 1 << "to" << version;
+ qDebug() << "Error type: " << error.type();
+ qDebug() << "Database: " << error.databaseText();
+ qDebug() << "Driver: " << error.driverText();
+
+ m_db.rollback();
+ qFatal("Exiting...");
+ } else {
+ qDebug() << "Successful migration to version" << version;
+ QSqlQuery q;
+ q.prepare(QStringLiteral("UPDATE qutim_history_version SET value = :value WHERE key = :key"));
+ q.bindValue(QStringLiteral(":key"), QStringLiteral("sqlitehistory"));
+ q.bindValue(QStringLiteral(":value"), version);
+ q.exec();
+
+ m_db.commit();
+ }
+}
+
+}
+
diff --git a/src/plugins/generic/sqlitehistory/sqlitehistory.h b/src/plugins/generic/sqlitehistory/sqlitehistory.h
new file mode 100644
index 000000000..06983c5cb
--- /dev/null
+++ b/src/plugins/generic/sqlitehistory/sqlitehistory.h
@@ -0,0 +1,106 @@
+/****************************************************************************
+**
+** qutIM - instant messenger
+**
+** Copyright © 2015 Nicolay Izoderov
+**
+*****************************************************************************
+**
+** $QUTIM_BEGIN_LICENSE$
+** This program is free 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.
+**
+** This program 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 this program. If not, see http://www.gnu.org/licenses/.
+** $QUTIM_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef SQLITEHISTORY_H
+#define SQLITEHISTORY_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace qutim_sdk_0_3;
+
+namespace Core
+{
+class SqliteWorker;
+
+class SqliteHistory : public History
+{
+ Q_OBJECT
+public:
+ SqliteHistory();
+ virtual ~SqliteHistory();
+
+ void store(const Message &message) override;
+ AsyncResult read(const ContactInfo &info, const QDateTime &from, const QDateTime &to, int max_num) override;
+ AsyncResult> accounts() override;
+ AsyncResult> contacts(const AccountInfo &account) override;
+ AsyncResult> months(const ContactInfo &contact, const QString &needle) override;
+ AsyncResult> dates(const ContactInfo &contact, const QDate &month, const QString &needle) override;
+
+public slots:
+ void errorHandler(const QString & error);
+
+private:
+ QThread* m_thread;
+ SqliteWorker* m_worker;
+};
+
+class SqliteWorker : public QObject
+{
+ Q_OBJECT
+public:
+
+ enum MessageType {
+ Message = 0,
+ Topic = 1,
+ Service = 2
+ };
+ Q_DECLARE_FLAGS(MessageTypes, MessageType)
+
+ static QString escapeSqliteLike(const QString &str);
+ void runJob(std::function job);
+ void shutdown();
+public slots:
+ void process();
+signals:
+ void finished();
+ void error(const QString &error);
+
+private slots:
+ void exec();
+private:
+ inline int currentVersion() { return 1; }
+ void makeMigration(int version);
+ QQueue> m_queue;
+ QMutex m_queueLock;
+ QMutex m_runningLock;
+ QSqlDatabase m_db;
+ void prepareDb();
+ bool m_isRunning = false;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(SqliteWorker::MessageTypes)
+
+}
+
+#endif // SQLITEHISTORY_H
+
diff --git a/src/plugins/generic/sqlitehistory/sqlitehistory.plugin.json b/src/plugins/generic/sqlitehistory/sqlitehistory.plugin.json
new file mode 100644
index 000000000..cf5076953
--- /dev/null
+++ b/src/plugins/generic/sqlitehistory/sqlitehistory.plugin.json
@@ -0,0 +1,7 @@
+{
+ "pluginIcon": "",
+ "pluginName": "Sqlite History",
+ "pluginDescription": "New qutIM history implementation using sqlite",
+ "extensionHeader": "sqlitehistory.h",
+ "extensionClass": "Core::SqliteHistory"
+}
diff --git a/src/plugins/generic/sqlitehistory/sqlitehistory.qbs b/src/plugins/generic/sqlitehistory/sqlitehistory.qbs
new file mode 100644
index 000000000..f268769d8
--- /dev/null
+++ b/src/plugins/generic/sqlitehistory/sqlitehistory.qbs
@@ -0,0 +1,8 @@
+import "../GenericPlugin.qbs" as GenericPlugin
+
+GenericPlugin {
+ sourcePath: ''
+ Depends {
+ name: "Qt.sql"
+ }
+}