diff --git a/core/account.cpp b/core/account.cpp index 9517c39..22b8e2e 100644 --- a/core/account.cpp +++ b/core/account.cpp @@ -1,6 +1,7 @@ #include "account.h" #include #include +#include using namespace Core; @@ -25,6 +26,7 @@ Account::Account(const QString& p_login, const QString& p_server, const QString& QObject::connect(&client, SIGNAL(disconnected()), this, SLOT(onClientDisconnected())); QObject::connect(&client, SIGNAL(presenceReceived(const QXmppPresence&)), this, SLOT(onPresenceReceived(const QXmppPresence&))); QObject::connect(&client, SIGNAL(messageReceived(const QXmppMessage&)), this, SLOT(onMessageReceived(const QXmppMessage&))); + QObject::connect(&client, SIGNAL(error(QXmppClient::Error)), this, SLOT(onClientError(QXmppClient::Error))); QXmppRosterManager& rm = client.rosterManager(); @@ -212,6 +214,8 @@ void Core::Account::addedAccount(const QString& jid) QObject::connect(contact, SIGNAL(nameChanged(const QString&)), this, SLOT(onContactNameChanged(const QString&))); QObject::connect(contact, SIGNAL(subscriptionStateChanged(Shared::SubscriptionState)), this, SLOT(onContactSubscriptionStateChanged(Shared::SubscriptionState))); + QObject::connect(contact, SIGNAL(needHistory(const QString&, const QString&)), this, SLOT(onContactNeedHistory(const QString&, const QString&))); + QObject::connect(contact, SIGNAL(historyResponse(const std::list&)), this, SLOT(onContactHistoryResponse(const std::list&))); } } @@ -368,6 +372,10 @@ void Core::Account::sendMessage(const Shared::Message& data) QXmppMessage msg(data.getFrom(), data.getTo(), data.getBody(), data.getThread()); msg.setId(data.getId()); msg.setType(static_cast(data.getType())); //it is safe here, my type is compatible + + std::map::const_iterator itr = contacts.find(data.getPenPalJid()); + itr->second->appendMessageToArchive(data); + client.sendPacket(msg); } else { qDebug() << "An attempt to send message with not connected account " << name << ", skipping"; @@ -386,27 +394,14 @@ void Core::Account::onCarbonMessageSent(const QXmppMessage& msg) bool Core::Account::handleChatMessage(const QXmppMessage& msg, bool outgoing, bool forwarded, bool guessing) { - QString body(msg.body()); + const QString& body(msg.body()); if (body.size() != 0) { - QString id(msg.id()); - QDateTime time(msg.stamp()); + const QString& id(msg.id()); Shared::Message sMsg(Shared::Message::chat); - sMsg.setId(id); - sMsg.setFrom(msg.from()); - sMsg.setTo(msg.to()); - sMsg.setBody(body); - sMsg.setForwarded(forwarded); - if (guessing) { - if (sMsg.getFromJid() == getLogin() + "@" + getServer()) { - outgoing = true; - } else { - outgoing = false; - } - } - sMsg.setOutgoing(outgoing); - if (time.isValid()) { - sMsg.setTime(time); - } + initializeMessage(sMsg, msg, outgoing, forwarded, guessing); + std::map::const_iterator itr = contacts.find(sMsg.getPenPalJid()); + itr->second->appendMessageToArchive(sMsg); + emit message(sMsg); if (!forwarded && !outgoing) { @@ -422,32 +417,108 @@ bool Core::Account::handleChatMessage(const QXmppMessage& msg, bool outgoing, bo return false; } -void Core::Account::onMamMessageReceived(const QString& bareJid, const QXmppMessage& msg) +void Core::Account::initializeMessage(Shared::Message& target, const QXmppMessage& source, bool outgoing, bool forwarded, bool guessing) const { - handleChatMessage(msg, false, true, true); + const QDateTime& time(source.stamp()); + target.setId(source.id()); + target.setFrom(source.from()); + target.setTo(source.to()); + target.setBody(source.body()); + target.setForwarded(forwarded); + if (guessing) { + if (target.getFromJid() == getLogin() + "@" + getServer()) { + outgoing = true; + } else { + outgoing = false; + } + } + target.setOutgoing(outgoing); + if (time.isValid()) { + target.setTime(time); + } else { + target.setCurrentTime(); + } } -void Core::Account::requestAchive(const QString& jid) +void Core::Account::onMamMessageReceived(const QString& queryId, const QXmppMessage& msg) { + std::map::const_iterator itr = achiveQueries.find(queryId); + QString jid = itr->second; + std::map::const_iterator citr = contacts.find(jid); + + if (citr != contacts.end()) { + Contact* cnt = citr->second; + if (msg.id().size() > 0 && msg.body().size() > 0) { + Shared::Message sMsg(Shared::Message::chat); + initializeMessage(sMsg, msg, false, true, true); + + cnt->addMessageToArchive(sMsg); + } + + } + //handleChatMessage(msg, false, true, true); +} + +void Core::Account::requestArchive(const QString& jid, int count, const QString& before) +{ + qDebug() << "An archive request for " << jid << ", before " << before; + std::map::const_iterator itr = contacts.find(jid); + if (itr == contacts.end()) { + qDebug() << "An attempt to request archive for" << jid << "in account" << name << ", but the contact with such id wasn't found, skipping"; + return; + } + Contact* contact = itr->second; + + if (contact->getArchiveState() == Contact::empty && before.size() == 0) { + QXmppMessage msg(getFullJid(), jid, "", ""); + QString last = Shared::generateUUID(); + msg.setId(last); + msg.setType(QXmppMessage::Chat); + msg.setState(QXmppMessage::Active); + client.sendPacket(msg); + QTimer* timer = new QTimer; + QObject::connect(timer, &QTimer::timeout, [timer, contact, count, last](){ + contact->requestFromEmpty(count, last); + timer->deleteLater(); + }); + + timer->setSingleShot(true); + timer->start(1000); + } else { + contact->requestHistory(count, before); + } +} + +void Core::Account::onContactNeedHistory(const QString& before, const QString& after) +{ + Contact* contact = static_cast(sender()); QXmppResultSetQuery query; query.setMax(100); - QDateTime from = QDateTime::currentDateTime().addDays(-7); - - QString q = am->retrieveArchivedMessages("", "", jid, from, QDateTime(), query); - achiveQueries.insert(std::make_pair(q, jid)); + if (before.size() > 0) { + query.setBefore(before); + } + if (after.size() > 0) { + query.setAfter(after); + } + + qDebug() << "Remote query from\"" << after << "\", to" << before; + + QString q = am->retrieveArchivedMessages("", "", contact->jid, QDateTime(), QDateTime(), query); + achiveQueries.insert(std::make_pair(q, contact->jid)); } + void Core::Account::onMamResultsReceived(const QString& queryId, const QXmppResultSetReply& resultSetReply, bool complete) { std::map::const_iterator itr = achiveQueries.find(queryId); QString jid = itr->second; achiveQueries.erase(itr); - if (!complete) { - QXmppResultSetQuery q; - q.setAfter(resultSetReply.last()); - q.setMax(100); - QString nQ = am->retrieveArchivedMessages("", "", jid, QDateTime::currentDateTime().addDays(-7), QDateTime(), q); - achiveQueries.insert(std::make_pair(nQ, jid)); + std::map::const_iterator citr = contacts.find(jid); + if (citr != contacts.end()) { + Contact* cnt = citr->second; + + qDebug() << "Flushing messages for" << jid; + cnt->flushMessagesToArchive(complete, resultSetReply.first(), resultSetReply.last()); } } @@ -534,3 +605,29 @@ Shared::SubscriptionState Core::Account::castSubscriptionState(QXmppRosterIq::It } return state; } + +void Core::Account::onContactHistoryResponse(const std::list& list) +{ + Contact* contact = static_cast(sender()); + + qDebug() << "Collected history for contact " << contact->jid << list.size() << "elements"; + emit responseArchive(contact->jid, list); +} + +void Core::Account::onClientError(QXmppClient::Error err) +{ + switch (err) { + case QXmppClient::SocketError: + qDebug() << "Client socket error" << client.socketErrorString(); + break; + case QXmppClient::XmppStreamError: + qDebug() << "Client stream error" << client.socketErrorString(); + break; + case QXmppClient::KeepAliveError: + qDebug() << "Client keep alive error"; + break; + } + + //onClientDisconnected(); +} + diff --git a/core/account.h b/core/account.h index 72abb55..04f14b6 100644 --- a/core/account.h +++ b/core/account.h @@ -41,7 +41,7 @@ public: void setAvailability(Shared::Availability avail); QString getFullJid() const; void sendMessage(const Shared::Message& data); - void requestAchive(const QString& jid); + void requestArchive(const QString& jid, int count, const QString& before); signals: void connectionStateChanged(int); @@ -55,6 +55,7 @@ signals: void addPresence(const QString& jid, const QString& name, const QMap& data); void removePresence(const QString& jid, const QString& name); void message(const Shared::Message& data); + void responseArchive(const QString& jid, const std::list& list); private: QString name; @@ -71,6 +72,7 @@ private: private slots: void onClientConnected(); void onClientDisconnected(); + void onClientError(QXmppClient::Error err); void onRosterReceived(); void onRosterItemAdded(const QString& bareJid); @@ -91,12 +93,15 @@ private slots: void onContactGroupRemoved(const QString& group); void onContactNameChanged(const QString& name); void onContactSubscriptionStateChanged(Shared::SubscriptionState state); + void onContactHistoryResponse(const std::list& list); + void onContactNeedHistory(const QString& before, const QString& after); private: void addedAccount(const QString &bareJid); bool handleChatMessage(const QXmppMessage& msg, bool outgoing = false, bool forwarded = false, bool guessing = false); void addToGroup(const QString& jid, const QString& group); void removeFromGroup(const QString& jid, const QString& group); + void initializeMessage(Shared::Message& target, const QXmppMessage& source, bool outgoing = false, bool forwarded = false, bool guessing = false) const; Shared::SubscriptionState castSubscriptionState(QXmppRosterIq::Item::SubscriptionType qs) const; }; diff --git a/core/archive.cpp b/core/archive.cpp index 70327a7..21d1658 100644 --- a/core/archive.cpp +++ b/core/archive.cpp @@ -28,6 +28,7 @@ Core::Archive::Archive(const QString& p_jid, QObject* parent): QObject(parent), jid(p_jid), opened(false), + fromTheBeginning(false), environment(), main(), order() @@ -54,14 +55,16 @@ void Core::Archive::open(const QString& account) } } - mdb_env_set_maxdbs(environment, 2); + mdb_env_set_maxdbs(environment, 3); mdb_env_open(environment, path.toStdString().c_str(), 0, 0664); MDB_txn *txn; mdb_txn_begin(environment, NULL, 0, &txn); mdb_dbi_open(txn, "main", MDB_CREATE, &main); mdb_dbi_open(txn, "order", MDB_CREATE | MDB_INTEGERKEY, &order); + mdb_dbi_open(txn, "order", MDB_CREATE, &stats); mdb_txn_commit(txn); + fromTheBeginning = _isFromTheBeginning(); opened = true; } } @@ -69,6 +72,7 @@ void Core::Archive::open(const QString& account) void Core::Archive::close() { if (opened) { + mdb_dbi_close(environment, stats); mdb_dbi_close(environment, order); mdb_dbi_close(environment, main); mdb_env_close(environment); @@ -76,7 +80,7 @@ void Core::Archive::close() } } -void Core::Archive::addElement(const Shared::Message& message) +bool Core::Archive::addElement(const Shared::Message& message) { if (!opened) { throw Closed("addElement", jid.toStdString()); @@ -95,7 +99,7 @@ void Core::Archive::addElement(const Shared::Message& message) MDB_txn *txn; mdb_txn_begin(environment, NULL, 0, &txn); int rc; - rc = mdb_put(txn, main, &lmdbKey, &lmdbData, 0); + rc = mdb_put(txn, main, &lmdbKey, &lmdbData, MDB_NOOVERWRITE); if (rc == 0) { MDB_val orderKey; orderKey.mv_size = 8; @@ -105,15 +109,19 @@ void Core::Archive::addElement(const Shared::Message& message) if (rc) { qDebug() << "An element couldn't be inserted into the index" << mdb_strerror(rc); mdb_txn_abort(txn); + return false; } else { rc = mdb_txn_commit(txn); if (rc) { qDebug() << "A transaction error: " << mdb_strerror(rc); + return false; } + return true; } } else { qDebug() << "An element couldn't been added to the archive, skipping" << mdb_strerror(rc); mdb_txn_abort(txn); + return false; } } @@ -184,7 +192,7 @@ QString Core::Archive::newestId() qDebug() << "Error geting newestId " << mdb_strerror(rc); mdb_cursor_close(cursor); mdb_txn_abort(txn); - throw new Empty(jid.toStdString()); + throw Empty(jid.toStdString()); } else { std::string sId((char*)lmdbData.mv_data, lmdbData.mv_size); mdb_cursor_close(cursor); @@ -198,6 +206,65 @@ Shared::Message Core::Archive::oldest() return getElement(oldestId()); } +unsigned int Core::Archive::addElements(const std::list& messages) +{ + if (!opened) { + throw Closed("addElements", jid.toStdString()); + } + + int success = 0; + int rc = 0; + MDB_val lmdbKey, lmdbData; + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + std::list::const_iterator itr = messages.begin(); + while (rc == 0 && itr != messages.end()) { + const Shared::Message& message = *itr; + + QByteArray ba; + QDataStream ds(&ba, QIODevice::WriteOnly); + message.serialize(ds); + quint64 stamp = message.getTime().toMSecsSinceEpoch(); + const std::string& id = message.getId().toStdString(); + + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + lmdbData.mv_size = ba.size(); + lmdbData.mv_data = (uint8_t*)ba.data(); + + rc = mdb_put(txn, main, &lmdbKey, &lmdbData, MDB_NOOVERWRITE); + if (rc == 0) { + MDB_val orderKey; + orderKey.mv_size = 8; + orderKey.mv_data = (uint8_t*) &stamp; + + rc = mdb_put(txn, order, &orderKey, &lmdbKey, 0); + if (rc) { + qDebug() << "An element couldn't be inserted into the index, aborting the transaction" << mdb_strerror(rc); + } else { + success++; + } + } else { + if (rc == MDB_KEYEXIST) { + rc = 0; + } else { + qDebug() << "An element couldn't been added to the archive, aborting the transaction" << mdb_strerror(rc); + } + } + itr++; + } + + if (rc != 0) { + mdb_txn_abort(txn); + success = 0; + } else { + mdb_txn_commit(txn); + } + + return success; +} + + QString Core::Archive::oldestId() { if (!opened) { @@ -215,7 +282,7 @@ QString Core::Archive::oldestId() qDebug() << "Error geting oldestId " << mdb_strerror(rc); mdb_cursor_close(cursor); mdb_txn_abort(txn); - throw new Empty(jid.toStdString()); + throw Empty(jid.toStdString()); } else { std::string sId((char*)lmdbData.mv_data, lmdbData.mv_size); mdb_cursor_close(cursor); @@ -237,3 +304,202 @@ long unsigned int Core::Archive::size() const mdb_txn_abort(txn); return stat.ms_entries; } + +std::list Core::Archive::getBefore(int count, const QString& id) +{ + if (!opened) { + throw Closed("getBefore", jid.toStdString()); + } + std::list res; + MDB_cursor* cursor; + MDB_txn *txn; + MDB_val lmdbKey, lmdbData; + int rc; + rc = mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); + if (id == "") { + rc = mdb_cursor_open(txn, order, &cursor); + rc = mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_LAST); + if (rc) { + qDebug() << "Error getting before " << mdb_strerror(rc) << ", id:" << id; + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + throw Empty(jid.toStdString()); + } + } else { + std::string stdId(id.toStdString()); + lmdbKey.mv_size = stdId.size(); + lmdbKey.mv_data = (char*)stdId.c_str(); + rc = mdb_get(txn, main, &lmdbKey, &lmdbData); + if (rc) { + qDebug() <<"Error getting before: no reference message" << mdb_strerror(rc) << ", id:" << id; + mdb_txn_abort(txn); + printKeys(); + throw NotFound(stdId, jid.toStdString()); + } else { + QByteArray ba((char*)lmdbData.mv_data, lmdbData.mv_size); + QDataStream ds(&ba, QIODevice::ReadOnly); + + Shared::Message msg; + msg.deserialize(ds); + quint64 stamp = msg.getTime().toMSecsSinceEpoch(); + lmdbKey.mv_data = (quint8*)&stamp; + lmdbKey.mv_size = 8; + + rc = mdb_cursor_open(txn, order, &cursor); + rc = mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_SET); + + if (rc) { + qDebug() << "Error getting before: couldn't set " << mdb_strerror(rc); + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + throw NotFound(stdId, jid.toStdString()); + } else { + rc = mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_PREV); + if (rc) { + qDebug() << "Error getting before, couldn't prev " << mdb_strerror(rc); + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + throw NotFound(stdId, jid.toStdString()); + } + } + } + } + + do { + MDB_val dKey, dData; + dKey.mv_size = lmdbData.mv_size; + dKey.mv_data = lmdbData.mv_data; + rc = mdb_get(txn, main, &dKey, &dData); + if (rc) { + qDebug() <<"Get error: " << mdb_strerror(rc); + std::string sId((char*)lmdbData.mv_data, lmdbData.mv_size); + mdb_txn_abort(txn); + throw NotFound(sId, jid.toStdString()); + } else { + QByteArray ba((char*)dData.mv_data, dData.mv_size); + QDataStream ds(&ba, QIODevice::ReadOnly); + + res.emplace_back(); + Shared::Message& msg = res.back(); + msg.deserialize(ds); + } + + --count; + + } while (count > 0 && mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_PREV) == 0); + + mdb_cursor_close(cursor); + mdb_txn_abort(txn); + return res; +} + +bool Core::Archive::_isFromTheBeginning() +{ + std::string strKey = "beginning"; + + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = strKey.size(); + lmdbKey.mv_data = (char*)strKey.c_str(); + + MDB_txn *txn; + int rc; + mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); + rc = mdb_get(txn, stats, &lmdbKey, &lmdbData); + if (rc == MDB_NOTFOUND) { + mdb_txn_abort(txn); + return false; + } else if (rc) { + qDebug() <<"isFromTheBeginning error: " << mdb_strerror(rc); + mdb_txn_abort(txn); + throw NotFound(strKey, jid.toStdString()); + } else { + uint8_t value = *(uint8_t*)(lmdbData.mv_data); + bool is; + if (value == 144) { + is = false; + } else if (value == 72) { + is = true; + } else { + qDebug() <<"isFromTheBeginning error: stored value doesn't match any magic number, the answer is most probably wrong"; + } + return is; + } +} + +bool Core::Archive::isFromTheBeginning() +{ + if (!opened) { + throw Closed("isFromTheBeginning", jid.toStdString()); + } + return fromTheBeginning; +} + +void Core::Archive::setFromTheBeginning(bool is) +{ + if (!opened) { + throw Closed("setFromTheBeginning", jid.toStdString()); + } + if (fromTheBeginning != is) { + fromTheBeginning = is; + const std::string& id = "beginning"; + uint8_t value = 144; + if (is) { + value = 72; + } + + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + lmdbData.mv_size = sizeof value; + lmdbData.mv_data = &value; + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + int rc; + rc = mdb_put(txn, stats, &lmdbKey, &lmdbData, 0); + if (rc != 0) { + qDebug() << "Couldn't store beginning key into stat database:" << mdb_strerror(rc); + mdb_txn_abort(txn); + } + } +} + +void Core::Archive::printOrder() +{ + qDebug() << "Printing order"; + MDB_txn *txn; + int rc; + rc = mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); + MDB_cursor* cursor; + rc = mdb_cursor_open(txn, order, &cursor); + MDB_val lmdbKey, lmdbData; + + rc = mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_FIRST); + + do { + std::string sId((char*)lmdbData.mv_data, lmdbData.mv_size); + qDebug() << QString(sId.c_str()); + } while (mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_NEXT) == 0); + + mdb_cursor_close(cursor); + mdb_txn_abort(txn); +} + +void Core::Archive::printKeys() +{ + MDB_txn *txn; + int rc; + rc = mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); + MDB_cursor* cursor; + rc = mdb_cursor_open(txn, main, &cursor); + MDB_val lmdbKey, lmdbData; + + rc = mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_FIRST); + + do { + std::string sId((char*)lmdbKey.mv_data, lmdbKey.mv_size); + qDebug() << QString(sId.c_str()); + } while (mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_NEXT) == 0); + + mdb_cursor_close(cursor); + mdb_txn_abort(txn); +} diff --git a/core/archive.h b/core/archive.h index 614ce40..a55c2ee 100644 --- a/core/archive.h +++ b/core/archive.h @@ -23,6 +23,7 @@ #include "../global.h" #include #include "../exception.h" +#include namespace Core { @@ -36,7 +37,8 @@ public: void open(const QString& account); void close(); - void addElement(const Shared::Message& message); + bool addElement(const Shared::Message& message); + unsigned int addElements(const std::list& messages); Shared::Message getElement(const QString& id); Shared::Message oldest(); QString oldestId(); @@ -44,6 +46,9 @@ public: QString newestId(); void clear(); long unsigned int size() const; + std::list getBefore(int count, const QString& id); + bool isFromTheBeginning(); + void setFromTheBeginning(bool is); public: const QString jid; @@ -97,9 +102,15 @@ public: private: bool opened; + bool fromTheBeginning; MDB_env* environment; MDB_dbi main; MDB_dbi order; + MDB_dbi stats; + + bool _isFromTheBeginning(); + void printOrder(); + void printKeys(); }; } diff --git a/core/contact.cpp b/core/contact.cpp index 1cc7e8a..1a1203a 100644 --- a/core/contact.cpp +++ b/core/contact.cpp @@ -17,6 +17,7 @@ */ #include "contact.h" +#include Core::Contact::Contact(const QString& pJid, const QString& account, QObject* parent): QObject(parent), @@ -25,9 +26,23 @@ Core::Contact::Contact(const QString& pJid, const QString& account, QObject* par groups(), archiveState(empty), archive(new Archive(jid)), - subscriptionState(Shared::unknown) + subscriptionState(Shared::unknown), + syncronizing(false), + requestedCount(0), + requestedBefore(), + hisoryCache(), + appendCache(), + responseCache(), + requestCache() { archive->open(account); + if (archive->isFromTheBeginning()) { + archiveState = beginning; + } else { + if (archive->size() != 0) { + archiveState = chunk; + } + } } Core::Contact::~Contact() @@ -91,3 +106,236 @@ unsigned int Core::Contact::groupsCount() const { return groups.size(); } + +void Core::Contact::addMessageToArchive(const Shared::Message& msg) +{ + if (msg.getId().size() > 0 && msg.getBody().size() > 0) { + hisoryCache.emplace_back(msg); + } +} + +void Core::Contact::requestHistory(int count, const QString& before) +{ + if (syncronizing) { + requestCache.emplace_back(count, before); + } else { + performRequest(count, before); + } +} + +void Core::Contact::nextRequest() +{ + if (syncronizing) { + if (requestedCount != -1) { + emit historyResponse(responseCache); + } + } + if (requestCache.size() > 0) { + std::pair request = requestCache.front(); + requestCache.pop_front(); + performRequest(request.first, request.second); + } else { + syncronizing = false; + requestedCount = 0; + requestedBefore = ""; + hisoryCache.clear(); + responseCache.clear(); + } +} + +void Core::Contact::performRequest(int count, const QString& before) +{ + syncronizing = true; + requestedCount = count; + requestedBefore = before; + hisoryCache.clear(); + responseCache.clear(); + + switch (archiveState) { + case empty: + emit needHistory(before, ""); + break; + case chunk: + case beginning: + if (count != -1) { + requestCache.emplace_back(requestedCount, before); + requestedCount = -1; + } + emit needHistory("", archive->newestId()); + break; + case end: + if (count != -1) { + QString lBefore; + if (responseCache.size() > 0) { + lBefore = responseCache.front().getId(); + } else { + lBefore = before; + } + bool found = false; + try { + std::list arc = archive->getBefore(requestedCount - responseCache.size(), lBefore); + responseCache.insert(responseCache.begin(), arc.begin(), arc.end()); + found = true; + } catch (Archive::NotFound e) { + requestCache.emplace_back(requestedCount, before); + requestedCount = -1; + emit needHistory(archive->oldestId(), ""); + } + + if (found) { + int rSize = responseCache.size(); + if (rSize < count) { + if (rSize != 0) { + emit needHistory(responseCache.front().getId(), ""); + } else { + emit needHistory(before, ""); + } + } else { + nextRequest(); + } + } + } else { + emit needHistory(archive->oldestId(), ""); + } + break; + case complete: + try { + std::list arc = archive->getBefore(requestedCount - responseCache.size(), before); + responseCache.insert(responseCache.begin(), arc.begin(), arc.end()); + } catch (Archive::NotFound e) { + qDebug("requesting id hasn't been found in archive, skipping"); + } + nextRequest(); + break; + } +} + +void Core::Contact::appendMessageToArchive(const Shared::Message& msg) +{ + const QString& id = msg.getId(); + if (id.size() > 0) { + if (msg.getBody().size() > 0) { + switch (archiveState) { + case empty: + if (archive->addElement(msg)) { + archiveState = end; + }; + if (!syncronizing) { + requestHistory(-1, id); + } + break; + case beginning: + appendCache.emplace_back(msg); + if (!syncronizing) { + requestHistory(-1, id); + } + break; + case end: + archive->addElement(msg); + break; + case chunk: + appendCache.emplace_back(msg); + if (!syncronizing) { + requestHistory(-1, id); + } + break; + case complete: + archive->addElement(msg); + break; + } + } else if (!syncronizing && archiveState == empty) { + requestHistory(-1, id); + } + } +} + +void Core::Contact::flushMessagesToArchive(bool finished, const QString& firstId, const QString& lastId) +{ + unsigned int added(0); + if (hisoryCache.size() > 0) { + added = archive->addElements(hisoryCache); + qDebug() << "Added" << added << "messages to the archive"; + hisoryCache.clear(); + } + + switch (archiveState) { + break; + case beginning: + if (finished) { + archiveState = complete; + nextRequest(); + } else { + emit needHistory("", lastId); + } + case chunk: + if (finished) { + archiveState = end; + nextRequest(); + } else { + emit needHistory("", lastId); + } + break; + case empty: + archiveState = end; + case end: + if (finished) { + archiveState = complete; + archive->setFromTheBeginning(true); + } + if (requestedCount != -1) { + QString before; + if (responseCache.size() > 0) { + before = responseCache.front().getId(); + } else { + before = requestedBefore; + } + + bool found = false; + try { + std::list arc = archive->getBefore(requestedCount - responseCache.size(), before); + responseCache.insert(responseCache.begin(), arc.begin(), arc.end()); + found = true; + } catch (Archive::NotFound e) {} + if (!found || requestedCount < responseCache.size()) { + if (archiveState == complete) { + nextRequest(); + } else { + emit needHistory(firstId, ""); + } + } else { + nextRequest(); + } + } else { + if (added != 0) { + nextRequest(); + } else { + emit needHistory(firstId, ""); + } + } + break; + case complete: + nextRequest(); + break; + } +} + +void Core::Contact::requestFromEmpty(int count, const QString& before) +{ + if (syncronizing) { + qDebug("perform from empty didn't work, another request queued"); + } else { + if (archiveState != empty) { + qDebug("perform from empty didn't work, the state is not empty"); + requestHistory(count, before); + } else { + syncronizing = true; + requestedCount = count; + requestedBefore = ""; + hisoryCache.clear(); + responseCache.clear(); + + emit needHistory(before, ""); + } + } +} + diff --git a/core/contact.h b/core/contact.h index b245d45..650b06d 100644 --- a/core/contact.h +++ b/core/contact.h @@ -21,8 +21,10 @@ #include #include +#include #include "archive.h" #include "../global.h" +#include namespace Core { @@ -39,7 +41,7 @@ public: }; Contact(const QString& pJid, const QString& account, QObject* parent = 0); ~Contact(); - + ArchiveState getArchiveState() const; QString getName() const; void setName(const QString& n); @@ -48,13 +50,20 @@ public: void setSubscriptionState(Shared::SubscriptionState state); Shared::SubscriptionState getSubscriptionState() const; unsigned int groupsCount() const; - + void addMessageToArchive(const Shared::Message& msg); + void appendMessageToArchive(const Shared::Message& msg); + void flushMessagesToArchive(bool finished, const QString& firstId, const QString& lastId); + void requestHistory(int count, const QString& before); + void requestFromEmpty(int count, const QString& before); + signals: void groupAdded(const QString& name); void groupRemoved(const QString& name); void nameChanged(const QString& name); void subscriptionStateChanged(Shared::SubscriptionState state); - + void historyResponse(const std::list& messages); + void needHistory(const QString& before, const QString& after); + public: const QString jid; @@ -64,6 +73,18 @@ private: ArchiveState archiveState; Archive* archive; Shared::SubscriptionState subscriptionState; + + bool syncronizing; + int requestedCount; + QString requestedBefore; + std::list hisoryCache; + std::list appendCache; + std::list responseCache; + std::list> requestCache; + +private: + void nextRequest(); + void performRequest(int count, const QString& before); }; } diff --git a/core/squawk.cpp b/core/squawk.cpp index 25d8eb5..2d20941 100644 --- a/core/squawk.cpp +++ b/core/squawk.cpp @@ -96,6 +96,8 @@ void Core::Squawk::addAccount(const QString& login, const QString& server, const this, SLOT(onAccountAddPresence(const QString&, const QString&, const QMap&))); connect(acc, SIGNAL(removePresence(const QString&, const QString&)), this, SLOT(onAccountRemovePresence(const QString&, const QString&))); connect(acc, SIGNAL(message(const Shared::Message&)), this, SLOT(onAccountMessage(const Shared::Message&))); + connect(acc, SIGNAL(responseArchive(const QString&, const std::list&)), + this, SLOT(onAccountResponseArchive(const QString&, const std::list&))); QMap map = { {"login", login}, @@ -221,12 +223,19 @@ void Core::Squawk::sendMessage(const QString& account, const Shared::Message& da itr->second->sendMessage(data); } -void Core::Squawk::requestArchive(const QString& account, const QString& jid) +void Core::Squawk::requestArchive(const QString& account, const QString& jid, int count, const QString& before) { AccountsMap::const_iterator itr = amap.find(account); if (itr == amap.end()) { qDebug("An attempt to request an archive of non existing account, skipping"); return; } - itr->second->requestAchive(jid); + itr->second->requestArchive(jid, count, before); } + +void Core::Squawk::onAccountResponseArchive(const QString& jid, const std::list& list) +{ + Account* acc = static_cast(sender()); + emit responseArchive(acc->getName(), jid, list); +} + diff --git a/core/squawk.h b/core/squawk.h index 9b8235e..3daf26f 100644 --- a/core/squawk.h +++ b/core/squawk.h @@ -36,6 +36,7 @@ signals: void removePresence(const QString& account, const QString& jid, const QString& name); void stateChanged(int state); void accountMessage(const QString& account, const Shared::Message& data); + void responseArchive(const QString& account, const QString& jid, const std::list& list); public slots: void start(); @@ -45,7 +46,7 @@ public slots: void disconnectAccount(const QString& account); void changeState(int state); void sendMessage(const QString& account, const Shared::Message& data); - void requestArchive(const QString& account, const QString& jid); + void requestArchive(const QString& account, const QString& jid, int count, const QString& before); private: typedef std::deque Accounts; @@ -70,6 +71,7 @@ private slots: void onAccountAddPresence(const QString& jid, const QString& name, const QMap& data); void onAccountRemovePresence(const QString& jid, const QString& name); void onAccountMessage(const Shared::Message& data); + void onAccountResponseArchive(const QString& jid, const std::list& list); }; } diff --git a/global.cpp b/global.cpp index ce5556a..5d03fef 100644 --- a/global.cpp +++ b/global.cpp @@ -176,12 +176,7 @@ bool Shared::Message::getForwarded() const void Shared::Message::generateRandomId() { - uuid_t uuid; - uuid_generate(uuid); - - char uuid_str[36]; - uuid_unparse_lower(uuid, uuid_str); - id = uuid_str; + id = generateUUID(); } QString Shared::Message::getThread() const @@ -241,3 +236,19 @@ void Shared::Message::deserialize(QDataStream& data) data >> outgoing; data >> forwarded; } + +QString Shared::generateUUID() +{ + uuid_t uuid; + uuid_generate(uuid); + + char uuid_str[36]; + uuid_unparse_lower(uuid, uuid_str); + return uuid_str; +} + +void Shared::Message::setCurrentTime() +{ + time = QDateTime::currentDateTime(); +} + diff --git a/global.h b/global.h index 922c242..e399907 100644 --- a/global.h +++ b/global.h @@ -53,6 +53,8 @@ static const std::deque availabilityNames = {"Online", "Away", "Absent" static const std::deque subscriptionStateThemeIcons = {"edit-none", "arrow-down-double", "arrow-up-double", "dialog-ok", "question"}; +QString generateUUID(); + class Message { public: enum Type { @@ -78,6 +80,7 @@ public: void setOutgoing(bool og); void setForwarded(bool fwd); void setType(Type t); + void setCurrentTime(); QString getFrom() const; QString getFromJid() const; diff --git a/main.cpp b/main.cpp index 7d0dae2..87ee2b5 100644 --- a/main.cpp +++ b/main.cpp @@ -9,6 +9,7 @@ int main(int argc, char *argv[]) { qRegisterMetaType("Shared::Message"); + qRegisterMetaType>("std::list"); QApplication app(argc, argv); SignalCatcher sc(&app); @@ -35,7 +36,7 @@ int main(int argc, char *argv[]) QObject::connect(&w, SIGNAL(disconnectAccount(const QString&)), squawk, SLOT(disconnectAccount(const QString&))); QObject::connect(&w, SIGNAL(changeState(int)), squawk, SLOT(changeState(int))); QObject::connect(&w, SIGNAL(sendMessage(const QString&, const Shared::Message&)), squawk, SLOT(sendMessage(const QString&, const Shared::Message&))); - QObject::connect(&w, SIGNAL(requestArchive(const QString&, const QString&)), squawk, SLOT(requestArchive(const QString&, const QString&))); + QObject::connect(&w, SIGNAL(requestArchive(const QString&, const QString&, int, const QString&)), squawk, SLOT(requestArchive(const QString&, const QString&, int, const QString&))); QObject::connect(squawk, SIGNAL(newAccount(const QMap&)), &w, SLOT(newAccount(const QMap&))); QObject::connect(squawk, SIGNAL(accountAvailabilityChanged(const QString&, int)), &w, SLOT(accountAvailabilityChanged(const QString&, int))); @@ -53,6 +54,8 @@ int main(int argc, char *argv[]) QObject::connect(squawk, SIGNAL(removePresence(const QString&, const QString&, const QString&)), &w, SLOT(removePresence(const QString&, const QString&, const QString&))); QObject::connect(squawk, SIGNAL(stateChanged(int)), &w, SLOT(stateChanged(int))); QObject::connect(squawk, SIGNAL(accountMessage(const QString&, const Shared::Message&)), &w, SLOT(accountMessage(const QString&, const Shared::Message&))); + QObject::connect(squawk, SIGNAL(responseArchive(const QString&, const QString&, const std::list&)), + &w, SLOT(responseArchive(const QString&, const QString&, const std::list&))); //qDebug() << QStandardPaths::writableLocation(QStandardPaths::CacheLocation); diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index c5f7a42..e2fbf4c 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -8,6 +8,10 @@ set(CMAKE_AUTOUIC ON) # Find the QtWidgets library find_package(Qt5Widgets CONFIG REQUIRED) +find_package(Qt5Qml CONFIG REQUIRED) +find_package(Qt5QuickCompiler) +find_package(Qt5Quick CONFIG REQUIRED) +find_package(Qt5QuickWidgets CONFIG REQUIRED) set(squawkUI_SRC squawk.cpp @@ -28,6 +32,9 @@ add_library(squawkUI ${squawkUI_SRC}) # Use the Widgets module from Qt 5. target_link_libraries(squawkUI Qt5::Widgets) +target_link_libraries(squawkUI Qt5::Quick) +target_link_libraries(squawkUI Qt5::Qml) +target_link_libraries(squawkUI Qt5::QuickWidgets) # Install the executable install(TARGETS squawkUI DESTINATION lib) diff --git a/ui/conversation.cpp b/ui/conversation.cpp index b7ba1d8..6c2cb4f 100644 --- a/ui/conversation.cpp +++ b/ui/conversation.cpp @@ -20,6 +20,7 @@ #include "ui_conversation.h" #include #include +#include Conversation::Conversation(Models::Contact* p_contact, QWidget* parent): QWidget(parent), @@ -30,7 +31,9 @@ Conversation::Conversation(Models::Contact* p_contact, QWidget* parent): activePalResource(), thread(), scroll(down), - manualSliderChange(false) + manualSliderChange(false), + requestingHistory(false), + everShown(false) { m_ui->setupUi(this); m_ui->splitter->setSizes({300, 0}); @@ -116,6 +119,7 @@ void Conversation::addMessage(const Shared::Message& data) { int pos = m_ui->scrollArea->verticalScrollBar()->sliderPosition(); int max = m_ui->scrollArea->verticalScrollBar()->maximum(); + MessageLine::Position place = line->message(data); if (place == MessageLine::invalid) { return; @@ -183,6 +187,7 @@ void Conversation::onEnterPressed() msg.setBody(body); msg.setOutgoing(true); msg.generateRandomId(); + msg.setCurrentTime(); addMessage(msg); emit sendMessage(msg); } @@ -195,6 +200,18 @@ void Conversation::onMessagesResize(int amount) case down: m_ui->scrollArea->verticalScrollBar()->setValue(m_ui->scrollArea->verticalScrollBar()->maximum()); break; + case keep: { + int max = m_ui->scrollArea->verticalScrollBar()->maximum(); + int value = m_ui->scrollArea->verticalScrollBar()->value() + amount; + m_ui->scrollArea->verticalScrollBar()->setValue(value); + + if (value == max) { + scroll = down; + } else { + scroll = nothing; + } + } + break; default: break; } @@ -207,7 +224,35 @@ void Conversation::onSliderValueChanged(int value) if (value == m_ui->scrollArea->verticalScrollBar()->maximum()) { scroll = down; } else { + if (!requestingHistory && value == 0) { + m_ui->historyStatus->setPixmap(QIcon::fromTheme("view-refresh").pixmap(25)); + requestingHistory = true; + emit requestArchive(line->firstMessageId()); + } scroll = nothing; } } } + +void Conversation::responseArchive(const std::list list) +{ + requestingHistory = false; + scroll = keep; + + m_ui->historyStatus->clear(); + for (std::list::const_iterator itr = list.begin(), end = list.end(); itr != end; ++itr) { + addMessage(*itr); + } +} + +void Conversation::showEvent(QShowEvent* event) +{ + if (!everShown) { + everShown = true; + m_ui->historyStatus->setPixmap(QIcon::fromTheme("view-refresh").pixmap(25)); + requestingHistory = true; + emit requestArchive(line->firstMessageId()); + } + + QWidget::showEvent(event); +} diff --git a/ui/conversation.h b/ui/conversation.h index e88007b..3f5a315 100644 --- a/ui/conversation.h +++ b/ui/conversation.h @@ -56,9 +56,12 @@ public: void addMessage(const Shared::Message& data); void setPalResource(const QString& res); + void responseArchive(const std::list list); + void showEvent(QShowEvent * event) override; signals: void sendMessage(const Shared::Message& message); + void requestArchive(const QString& before); protected: void setState(Shared::Availability state); @@ -85,6 +88,8 @@ private: QString thread; Scroll scroll; bool manualSliderChange; + bool requestingHistory; + bool everShown; }; #endif // CONVERSATION_H diff --git a/ui/conversation.ui b/ui/conversation.ui index e1b2a52..de6604e 100644 --- a/ui/conversation.ui +++ b/ui/conversation.ui @@ -105,6 +105,13 @@ + + + + + + + @@ -134,7 +141,7 @@ 0 0 572 - 162 + 95 diff --git a/ui/messageline.cpp b/ui/messageline.cpp index 4fd7205..7cb53bb 100644 --- a/ui/messageline.cpp +++ b/ui/messageline.cpp @@ -76,6 +76,12 @@ MessageLine::Position MessageLine::message(const Shared::Message& msg) QLabel* body = new QLabel(msg.getBody()); QLabel* sender = new QLabel(); + QLabel* time = new QLabel(msg.getTime().toLocalTime().toString()); + QFont dFont = time->font(); + dFont.setItalic(true); + dFont.setPointSize(dFont.pointSize() - 2); + time->setFont(dFont); + time->setForegroundRole(QPalette::ToolTipText); QFont f; f.setBold(true); sender->setFont(f); @@ -84,10 +90,12 @@ MessageLine::Position MessageLine::message(const Shared::Message& msg) vBox->addWidget(sender); vBox->addWidget(body); + vBox->addWidget(time); if (msg.getOutgoing()) { - body->setAlignment(Qt::AlignRight); + //body->setAlignment(Qt::AlignRight); sender->setAlignment(Qt::AlignRight); + time->setAlignment(Qt::AlignRight); sender->setText(myName); hBox->addStretch(); hBox->addWidget(message); @@ -132,3 +140,13 @@ void MessageLine::resizeEvent(QResizeEvent* event) QWidget::resizeEvent(event); emit resize(event->size().height() - event->oldSize().height()); } + + +QString MessageLine::firstMessageId() const +{ + if (messageOrder.size() == 0) { + return ""; + } else { + return messageOrder.begin()->second->getId(); + } +} diff --git a/ui/messageline.h b/ui/messageline.h index c0fd0d5..9dbc192 100644 --- a/ui/messageline.h +++ b/ui/messageline.h @@ -25,6 +25,7 @@ #include #include #include "../global.h" +#include class MessageLine : public QWidget { @@ -42,6 +43,9 @@ public: Position message(const Shared::Message& msg); void setMyName(const QString& name); void setPalName(const QString& jid, const QString& name); + QString firstMessageId() const; + void showBusyIndicator(); + void hideBusyIndicator(); signals: void resize(int amount); @@ -67,6 +71,7 @@ private: QString myName; std::map palNames; std::deque views; + QQuickWidget busy; }; #endif // MESSAGELINE_H diff --git a/ui/squawk.cpp b/ui/squawk.cpp index 0d91ca3..6dcd78d 100644 --- a/ui/squawk.cpp +++ b/ui/squawk.cpp @@ -189,6 +189,7 @@ void Squawk::onRosterItemDoubleClicked(const QModelIndex& item) conv->setAttribute(Qt::WA_DeleteOnClose); connect(conv, SIGNAL(destroyed(QObject*)), this, SLOT(onConversationClosed(QObject*))); connect(conv, SIGNAL(sendMessage(const Shared::Message&)), this, SLOT(onConversationMessage(const Shared::Message&))); + connect(conv, SIGNAL(requestArchive(const QString&)), this, SLOT(onConversationRequestArchive(const QString&))); conversations.insert(std::make_pair(id, conv)); rosterModel.dropMessages(account, jid); @@ -198,7 +199,6 @@ void Squawk::onRosterItemDoubleClicked(const QModelIndex& item) if (res.size() > 0) { itr->second->setPalResource(res); } - requestArchive(account, jid); } } } @@ -234,3 +234,19 @@ void Squawk::onConversationMessage(const Shared::Message& msg) emit sendMessage(conv->getAccount(), msg); } + +void Squawk::onConversationRequestArchive(const QString& before) +{ + Conversation* conv = static_cast(sender()); + requestArchive(conv->getAccount(), conv->getJid(), 20, before); //TODO amount as a settings value +} + +void Squawk::responseArchive(const QString& account, const QString& jid, const std::list& list) +{ + Models::Roster::ElId id(account, jid); + + Conversations::const_iterator itr = conversations.find(id); + if (itr != conversations.end()) { + itr->second->responseArchive(list); + } +} diff --git a/ui/squawk.h b/ui/squawk.h index b9d53bd..573e1ac 100644 --- a/ui/squawk.h +++ b/ui/squawk.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "accounts.h" #include "conversation.h" @@ -31,7 +32,7 @@ signals: void disconnectAccount(const QString&); void changeState(int state); void sendMessage(const QString& account, const Shared::Message& data); - void requestArchive(const QString& account, const QString& jid); + void requestArchive(const QString& account, const QString& jid, int count, const QString& before); public slots: void newAccount(const QMap& account); @@ -47,6 +48,7 @@ public slots: void removePresence(const QString& account, const QString& jid, const QString& name); void stateChanged(int state); void accountMessage(const QString& account, const Shared::Message& data); + void responseArchive(const QString& account, const QString& jid, const std::list& list); private: typedef std::map Conversations; @@ -66,6 +68,7 @@ private slots: void onComboboxActivated(int index); void onRosterItemDoubleClicked(const QModelIndex& item); void onConversationMessage(const Shared::Message& msg); + void onConversationRequestArchive(const QString& before); };