diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c4ce1..bf90231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,14 @@ - requesting the history of the current chat after reconnection - global availability (in drop down list) gets restored after reconnection - status icon in active chat changes when presence of the pen pal changes +- infinite progress when open the dialogue with something that has no history to show ### Improvements - slightly reduced the traffic on the startup by not requesting history of all MUCs - +- completely rewritten message feed, now it works way faster +- OPTIONAL RUNTIME dependency: "KIO Widgets" that is supposed to allow you to open a file in your default file manager +- show in folder now is supposed to try it's best to show file in folder, even you don't have KIO installed +- once uploaded local files don't get second time uploaded - the remote URL is reused ## Squawk 0.1.5 (Jul 29, 2020) ### Bug fixes diff --git a/CMakeLists.txt b/CMakeLists.txt index 771481f..e88fdc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,8 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.4) project(squawk) set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) @@ -32,6 +32,7 @@ set(squawk_SRC shared/message.cpp shared/vcard.cpp shared/icons.cpp + shared/messageinfo.cpp ) set(squawk_HEAD @@ -44,6 +45,7 @@ set(squawk_HEAD shared/utils.h shared/vcard.h shared/icons.h + shared/messageinfo.h ) configure_file(resources/images/logo.svg squawk.svg COPYONLY) @@ -64,6 +66,7 @@ qt5_add_resources(RCC resources/resources.qrc) option(SYSTEM_QXMPP "Use system qxmpp lib" ON) option(WITH_KWALLET "Build KWallet support module" ON) +option(WITH_KIO "Build KIO support module" ON) if (SYSTEM_QXMPP) find_package(QXmpp CONFIG) @@ -95,8 +98,21 @@ endif() add_executable(squawk ${squawk_SRC} ${squawk_HEAD} ${RCC}) target_link_libraries(squawk Qt5::Widgets) +if (WITH_KIO) + find_package(KF5KIO CONFIG) + + if (NOT KF5KIO_FOUND) + set(WITH_KIO OFF) + message("KIO package wasn't found, KIO support modules wouldn't be built") + else() + add_definitions(-DWITH_KIO) + message("Building with support of KIO") + endif() +endif() + add_subdirectory(ui) add_subdirectory(core) +add_subdirectory(plugins) add_subdirectory(external/simpleCrypt) @@ -104,6 +120,8 @@ target_link_libraries(squawk squawkUI) target_link_libraries(squawk squawkCORE) target_link_libraries(squawk uuid) + + add_dependencies(${CMAKE_PROJECT_NAME} translations) # Install the executable diff --git a/README.md b/README.md index 30c6473..f2101d6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ - lmdb - CMake 3.0 or higher - qxmpp 1.1.0 or higher -- kwallet (optional) +- KDE Frameworks: kwallet (optional) +- KDE Frameworks: KIO (optional) ### Getting @@ -67,6 +68,7 @@ Here is the list of keys you can pass to configuration phase of `cmake ..`. - `CMAKE_BUILD_TYPE` - `Debug` just builds showing all warnings, `Release` builds with no warnings and applies optimizations (default is `Debug`) - `SYSTEM_QXMPP` - `True` tries to link against `qxmpp` installed in the system, `False` builds bundled `qxmpp` library (default is `True`) - `WITH_KWALLET` - `True` builds the `KWallet` capability module if `KWallet` is installed and if not goes to `False`. `False` disables `KWallet` support (default is `True`) +- `WITH_KIO` - `True` builds the `KIO` capability module if `KIO` is installed and if not goes to `False`. `False` disables `KIO` support (default is `True`) ## License diff --git a/cmake/FindLMDB.cmake b/cmake/FindLMDB.cmake new file mode 100644 index 0000000..8bf48b4 --- /dev/null +++ b/cmake/FindLMDB.cmake @@ -0,0 +1,47 @@ +#This file is taken from here https://gitlab.ralph.or.at/causal-rt/causal-cpp/, it was GPLv3 license +#Thank you so much, mr. Ralph Alexander Bariz, I hope you don't mind me using your code + +# Try to find LMDB headers and library. +# +# Usage of this module as follows: +# +# find_package(LMDB) +# +# Variables used by this module, they can change the default behaviour and need +# to be set before calling find_package: +# +# LMDB_ROOT_DIR Set this variable to the root installation of +# LMDB if the module has problems finding the +# proper installation path. +# +# Variables defined by this module: +# +# LMDB_FOUND System has LMDB library/headers. +# LMDB_LIBRARIES The LMDB library. +# LMDB_INCLUDE_DIRS The location of LMDB headers. + +find_path(LMDB_ROOT_DIR + NAMES include/lmdb.h +) + +find_library(LMDB_LIBRARIES + NAMES lmdb + HINTS ${LMDB_ROOT_DIR}/lib +) + +find_path(LMDB_INCLUDE_DIRS + NAMES lmdb.h + HINTS ${LMDB_ROOT_DIR}/include +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LMDB DEFAULT_MSG + LMDB_LIBRARIES + LMDB_INCLUDE_DIRS +) + +mark_as_advanced( + LMDB_ROOT_DIR + LMDB_LIBRARIES + LMDB_INCLUDE_DIRS +) diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index b74a055..f8aa267 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,12 +1,15 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.3) project(squawkCORE) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") + set(CMAKE_AUTOMOC ON) find_package(Qt5Core CONFIG REQUIRED) find_package(Qt5Gui CONFIG REQUIRED) find_package(Qt5Network CONFIG REQUIRED) find_package(Qt5Xml CONFIG REQUIRED) +find_package(LMDB REQUIRED) set(squawkCORE_SRC squawk.cpp @@ -15,7 +18,7 @@ set(squawkCORE_SRC rosteritem.cpp contact.cpp conference.cpp - storage.cpp + urlstorage.cpp networkaccess.cpp adapterFuctions.cpp handlers/messagehandler.cpp @@ -25,7 +28,7 @@ set(squawkCORE_SRC add_subdirectory(passwordStorageEngines) # Tell CMake to create the helloworld executable -add_library(squawkCORE ${squawkCORE_SRC}) +add_library(squawkCORE STATIC ${squawkCORE_SRC}) if(SYSTEM_QXMPP) diff --git a/core/account.cpp b/core/account.cpp index 094fd3c..5ce29ee 100644 --- a/core/account.cpp +++ b/core/account.cpp @@ -84,8 +84,9 @@ Account::Account(const QString& p_login, const QString& p_server, const QString& QObject::connect(dm, &QXmppDiscoveryManager::itemsReceived, this, &Account::onDiscoveryItemsReceived); QObject::connect(dm, &QXmppDiscoveryManager::infoReceived, this, &Account::onDiscoveryInfoReceived); - QObject::connect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onFileUploaded); - QObject::connect(network, &NetworkAccess::uploadFileError, mh, &MessageHandler::onFileUploadError); + QObject::connect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onUploadFileComplete); + QObject::connect(network, &NetworkAccess::downloadFileComplete, mh, &MessageHandler::onDownloadFileComplete); + QObject::connect(network, &NetworkAccess::loadFileError, mh, &MessageHandler::onLoadFileError); client.addExtension(rcpm); QObject::connect(rcpm, &QXmppMessageReceiptManager::messageDelivered, mh, &MessageHandler::onReceiptReceived); @@ -155,8 +156,9 @@ Account::~Account() reconnectTimer->stop(); } - QObject::disconnect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onFileUploaded); - QObject::disconnect(network, &NetworkAccess::uploadFileError, mh, &MessageHandler::onFileUploadError); + QObject::disconnect(network, &NetworkAccess::uploadFileComplete, mh, &MessageHandler::onUploadFileComplete); + QObject::disconnect(network, &NetworkAccess::downloadFileComplete, mh, &MessageHandler::onDownloadFileComplete); + QObject::disconnect(network, &NetworkAccess::loadFileError, mh, &MessageHandler::onLoadFileError); delete mh; delete rh; @@ -402,9 +404,6 @@ QString Core::Account::getFullJid() const { void Core::Account::sendMessage(const Shared::Message& data) { mh->sendMessage(data);} -void Core::Account::sendMessage(const Shared::Message& data, const QString& path) { - mh->sendMessage(data, path);} - void Core::Account::onMamMessageReceived(const QString& queryId, const QXmppMessage& msg) { if (msg.id().size() > 0 && (msg.body().size() > 0 || msg.outOfBandUrl().size() > 0)) { @@ -434,13 +433,13 @@ void Core::Account::requestArchive(const QString& jid, int count, const QString& if (contact == 0) { qDebug() << "An attempt to request archive for" << jid << "in account" << name << ", but the contact with such id wasn't found, skipping"; - emit responseArchive(jid, std::list()); + emit responseArchive(jid, std::list(), true); return; } if (state != Shared::ConnectionState::connected) { qDebug() << "An attempt to request archive for" << jid << "in account" << name << ", but the account is not online, skipping"; - emit responseArchive(contact->jid, std::list()); + emit responseArchive(contact->jid, std::list(), false); } contact->requestHistory(count, before); @@ -552,9 +551,11 @@ void Core::Account::onClientError(QXmppClient::Error err) case QXmppStanza::Error::NotAuthorized: errorText = "Authentication error"; break; +#if (QXMPP_VERSION) < QT_VERSION_CHECK(1, 3, 0) case QXmppStanza::Error::PaymentRequired: errorText = "Payment is required"; break; +#endif case QXmppStanza::Error::RecipientUnavailable: errorText = "Recipient is unavailable"; break; @@ -909,3 +910,16 @@ void Core::Account::handleDisconnection() ownVCardRequestInProgress = false; } +void Core::Account::onContactHistoryResponse(const std::list& list, bool last) +{ + RosterItem* contact = static_cast(sender()); + + qDebug() << "Collected history for contact " << contact->jid << list.size() << "elements"; + if (last) { + qDebug() << "The response contains the first accounted message"; + } + emit responseArchive(contact->jid, list, last); +} + +void Core::Account::requestChangeMessage(const QString& jid, const QString& messageId, const QMap& data){ + mh->requestChangeMessage(jid, messageId, data);} diff --git a/core/account.h b/core/account.h index 49c7ca9..ce3b754 100644 --- a/core/account.h +++ b/core/account.h @@ -88,7 +88,6 @@ public: void setPasswordType(Shared::AccountPassword pt); QString getFullJid() const; void sendMessage(const Shared::Message& data); - void sendMessage(const Shared::Message& data, const QString& path); void requestArchive(const QString& jid, int count, const QString& before); void subscribeToContact(const QString& jid, const QString& reason); void unsubscribeFromContact(const QString& jid, const QString& reason); @@ -97,6 +96,7 @@ public: void addContactToGroupRequest(const QString& jid, const QString& groupName); void removeContactFromGroupRequest(const QString& jid, const QString& groupName); void renameContactRequest(const QString& jid, const QString& newName); + void requestChangeMessage(const QString& jid, const QString& messageId, const QMap& data); void setRoomJoined(const QString& jid, bool joined); void setRoomAutoJoin(const QString& jid, bool joined); @@ -127,14 +127,14 @@ signals: void removePresence(const QString& jid, const QString& name); void message(const Shared::Message& data); void changeMessage(const QString& jid, const QString& id, const QMap& data); - void responseArchive(const QString& jid, const std::list& list); + void responseArchive(const QString& jid, const std::list& list, bool last); void error(const QString& text); void addRoomParticipant(const QString& jid, const QString& nickName, const QMap& data); void changeRoomParticipant(const QString& jid, const QString& nickName, const QMap& data); void removeRoomParticipant(const QString& jid, const QString& nickName); void receivedVCard(const QString& jid, const Shared::VCard& card); void uploadFile(const QFileInfo& file, const QUrl& set, const QUrl& get, QMap headers); - void uploadFileError(const QString& messageId, const QString& error); + void uploadFileError(const QString& jid, const QString& messageId, const QString& error); private: QString name; @@ -183,6 +183,7 @@ private slots: void onDiscoveryItemsReceived (const QXmppDiscoveryIq& items); void onDiscoveryInfoReceived (const QXmppDiscoveryIq& info); + void onContactHistoryResponse(const std::list& list, bool last); private: void handleDisconnection(); diff --git a/core/archive.cpp b/core/archive.cpp index a1f8b76..c29c9a0 100644 --- a/core/archive.cpp +++ b/core/archive.cpp @@ -271,6 +271,8 @@ void Core::Archive::changeMessage(const QString& id, const QMap 0; QDateTime oTime = msg.getTime(); bool idChange = msg.change(data); + QDateTime nTime = msg.getTime(); + bool orderChange = oTime != nTime; MDB_val lmdbKey, lmdbData; QByteArray ba; @@ -280,15 +282,21 @@ void Core::Archive::changeMessage(const QString& id, const QMap Core::Archive::getBefore(int count, const QString& id) @@ -603,10 +612,10 @@ void Core::Archive::setFromTheBeginning(bool is) MDB_txn *txn; mdb_txn_begin(environment, NULL, 0, &txn); bool success = setStatValue("beginning", is, txn); - if (success != 0) { - mdb_txn_abort(txn); - } else { + if (success) { mdb_txn_commit(txn); + } else { + mdb_txn_abort(txn); } } } diff --git a/core/handlers/messagehandler.cpp b/core/handlers/messagehandler.cpp index 0f0e09d..dc37c3b 100644 --- a/core/handlers/messagehandler.cpp +++ b/core/handlers/messagehandler.cpp @@ -23,7 +23,6 @@ Core::MessageHandler::MessageHandler(Core::Account* account): QObject(), acc(account), pendingStateMessages(), - pendingMessages(), uploadingSlotsQueue() { } @@ -54,7 +53,7 @@ void Core::MessageHandler::onMessageReceived(const QXmppMessage& msg) if (cnt != 0) { cnt->changeMessage(id, cData); } - ; + emit acc->changeMessage(jid, id, cData); pendingStateMessages.erase(itr); handled = true; @@ -168,14 +167,16 @@ void Core::MessageHandler::initializeMessage(Shared::Message& target, const QXmp id = source.id(); #endif target.setId(id); - if (target.getId().size() == 0) { + QString messageId = target.getId(); + if (messageId.size() == 0) { target.generateRandomId(); //TODO out of desperation, I need at least a random ID + messageId = target.getId(); } target.setFrom(source.from()); target.setTo(source.to()); target.setBody(source.body()); target.setForwarded(forwarded); - target.setOutOfBandUrl(source.outOfBandUrl()); + if (guessing) { if (target.getFromJid() == acc->getLogin() + "@" + acc->getServer()) { outgoing = true; @@ -189,6 +190,12 @@ void Core::MessageHandler::initializeMessage(Shared::Message& target, const QXmp } else { target.setCurrentTime(); } + + QString oob = source.outOfBandUrl(); + if (oob.size() > 0) { + target.setAttachPath(acc->network->addMessageAndCheckForPath(oob, acc->getName(), target.getPenPalJid(), messageId)); + } + target.setOutOfBandUrl(oob); } void Core::MessageHandler::logMessage(const QXmppMessage& msg, const QString& reason) @@ -232,11 +239,23 @@ void Core::MessageHandler::onReceiptReceived(const QString& jid, const QString& } } -void Core::MessageHandler::sendMessage(Shared::Message data) +void Core::MessageHandler::sendMessage(const Shared::Message& data) +{ + if (data.getOutOfBandUrl().size() == 0 && data.getAttachPath().size() > 0) { + prepareUpload(data); + } else { + performSending(data); + } +} + +void Core::MessageHandler::performSending(Shared::Message data, bool newMessage) { QString jid = data.getPenPalJid(); QString id = data.getId(); + QString oob = data.getOutOfBandUrl(); RosterItem* ri = acc->rh->getRosterItem(jid); + bool sent = false; + QMap changes; if (acc->state == Shared::ConnectionState::connected) { QXmppMessage msg(acc->getFullJid(), data.getTo(), data.getBody(), data.getThread()); @@ -245,23 +264,16 @@ void Core::MessageHandler::sendMessage(Shared::Message data) #endif msg.setId(id); msg.setType(static_cast(data.getType())); //it is safe here, my type is compatible - msg.setOutOfBandUrl(data.getOutOfBandUrl()); + msg.setOutOfBandUrl(oob); msg.setReceiptRequested(true); - bool sent = acc->client.sendPacket(msg); + sent = acc->client.sendPacket(msg); if (sent) { data.setState(Shared::Message::State::sent); } else { data.setState(Shared::Message::State::error); - data.setErrorText("Couldn't send message via QXMPP library check out logs"); - } - - if (ri != 0) { - ri->appendMessageToArchive(data); - if (sent) { - pendingStateMessages.insert(std::make_pair(id, jid)); - } + data.setErrorText("Couldn't send message: internal QXMPP library error, probably need to check out the logs"); } } else { @@ -269,41 +281,74 @@ void Core::MessageHandler::sendMessage(Shared::Message data) data.setErrorText("You are is offline or reconnecting"); } - emit acc->changeMessage(jid, id, { - {"state", static_cast(data.getState())}, - {"errorText", data.getErrorText()} - }); + Shared::Message::State mstate = data.getState(); + changes.insert("state", static_cast(mstate)); + if (mstate == Shared::Message::State::error) { + changes.insert("errorText", data.getErrorText()); + } + if (oob.size() > 0) { + changes.insert("outOfBandUrl", oob); + } + if (!newMessage) { + changes.insert("stamp", data.getTime()); + } + + if (ri != 0) { + if (newMessage) { + ri->appendMessageToArchive(data); + } else { + ri->changeMessage(id, changes); + } + if (sent) { + pendingStateMessages.insert(std::make_pair(id, jid)); + } else { + pendingStateMessages.erase(id); + } + } + + emit acc->changeMessage(jid, id, changes); } -void Core::MessageHandler::sendMessage(const Shared::Message& data, const QString& path) +void Core::MessageHandler::prepareUpload(const Shared::Message& data) { if (acc->state == Shared::ConnectionState::connected) { + QString jid = data.getPenPalJid(); + QString id = data.getId(); + RosterItem* ri = acc->rh->getRosterItem(jid); + if (!ri) { + qDebug() << "An attempt to initialize upload in" << acc->name << "for pal" << jid << "but the object for this pal wasn't found, something went terrebly wrong, skipping send"; + return; + } + QString path = data.getAttachPath(); QString url = acc->network->getFileRemoteUrl(path); if (url.size() != 0) { sendMessageWithLocalUploadedFile(data, url); } else { - if (acc->network->isUploading(path, data.getId())) { - pendingMessages.emplace(data.getId(), data); + if (acc->network->checkAndAddToUploading(acc->getName(), jid, id, path)) { + ri->appendMessageToArchive(data); + pendingStateMessages.insert(std::make_pair(id, jid)); } else { if (acc->um->serviceFound()) { QFileInfo file(path); if (file.exists() && file.isReadable()) { - uploadingSlotsQueue.emplace_back(path, data); + ri->appendMessageToArchive(data); + pendingStateMessages.insert(std::make_pair(id, jid)); + uploadingSlotsQueue.emplace_back(path, id); if (uploadingSlotsQueue.size() == 1) { acc->um->requestUploadSlot(file); } } else { - onFileUploadError(data.getId(), "Uploading file no longer exists or your system user has no permission to read it"); + handleUploadError(jid, id, "Uploading file no longer exists or your system user has no permission to read it"); qDebug() << "Requested upload slot in account" << acc->name << "for file" << path << "but the file doesn't exist or is not readable"; } } else { - onFileUploadError(data.getId(), "Your server doesn't support file upload service, or it's prohibited for your account"); + handleUploadError(jid, id, "Your server doesn't support file upload service, or it's prohibited for your account"); qDebug() << "Requested upload slot in account" << acc->name << "for file" << path << "but upload manager didn't discover any upload services"; } } } } else { - onFileUploadError(data.getId(), "Account is offline or reconnecting"); + handleUploadError(data.getPenPalJid(), data.getId(), "Account is offline or reconnecting"); qDebug() << "An attempt to send message with not connected account " << acc->name << ", skipping"; } } @@ -314,12 +359,12 @@ void Core::MessageHandler::onUploadSlotReceived(const QXmppHttpUploadSlotIq& slo if (uploadingSlotsQueue.size() == 0) { qDebug() << "HTTP Upload manager of account" << acc->name << "reports about success requesting upload slot, but none was requested"; } else { - const std::pair& pair = uploadingSlotsQueue.front(); - const QString& mId = pair.second.getId(); - acc->network->uploadFile(mId, pair.first, slot.putUrl(), slot.getUrl(), slot.putHeaders()); - pendingMessages.emplace(mId, pair.second); - uploadingSlotsQueue.pop_front(); + const std::pair& pair = uploadingSlotsQueue.front(); + const QString& mId = pair.second; + QString palJid = pendingStateMessages.at(mId); + acc->network->uploadFile({acc->name, palJid, mId}, pair.first, slot.putUrl(), slot.getUrl(), slot.putHeaders()); + uploadingSlotsQueue.pop_front(); if (uploadingSlotsQueue.size() > 0) { acc->um->requestUploadSlot(uploadingSlotsQueue.front().first); } @@ -328,44 +373,111 @@ void Core::MessageHandler::onUploadSlotReceived(const QXmppHttpUploadSlotIq& slo void Core::MessageHandler::onUploadSlotRequestFailed(const QXmppHttpUploadRequestIq& request) { + QString err(request.error().text()); if (uploadingSlotsQueue.size() == 0) { qDebug() << "HTTP Upload manager of account" << acc->name << "reports about an error requesting upload slot, but none was requested"; - qDebug() << request.error().text(); + qDebug() << err; } else { - const std::pair& pair = uploadingSlotsQueue.front(); - qDebug() << "Error requesting upload slot for file" << pair.first << "in account" << acc->name << ":" << request.error().text(); - emit acc->uploadFileError(pair.second.getId(), "Error requesting slot to upload file: " + request.error().text()); + const std::pair& pair = uploadingSlotsQueue.front(); + qDebug() << "Error requesting upload slot for file" << pair.first << "in account" << acc->name << ":" << err; + handleUploadError(pendingStateMessages.at(pair.second), pair.second, err); + uploadingSlotsQueue.pop_front(); if (uploadingSlotsQueue.size() > 0) { acc->um->requestUploadSlot(uploadingSlotsQueue.front().first); } - uploadingSlotsQueue.pop_front(); } } -void Core::MessageHandler::onFileUploaded(const QString& messageId, const QString& url) +void Core::MessageHandler::onDownloadFileComplete(const std::list& msgs, const QString& path) { - std::map::const_iterator itr = pendingMessages.find(messageId); - if (itr != pendingMessages.end()) { - sendMessageWithLocalUploadedFile(itr->second, url); - pendingMessages.erase(itr); + QMap cData = { + {"attachPath", path} + }; + for (const Shared::MessageInfo& info : msgs) { + if (info.account == acc->getName()) { + RosterItem* cnt = acc->rh->getRosterItem(info.jid); + if (cnt != 0) { + if (cnt->changeMessage(info.messageId, cData)) { + emit acc->changeMessage(info.jid, info.messageId, cData); + } + } + } } } -void Core::MessageHandler::onFileUploadError(const QString& messageId, const QString& errMsg) +void Core::MessageHandler::onLoadFileError(const std::list& msgs, const QString& text, bool up) { - std::map::const_iterator itr = pendingMessages.find(messageId); - if (itr != pendingMessages.end()) { - pendingMessages.erase(itr); + if (up) { + for (const Shared::MessageInfo& info : msgs) { + if (info.account == acc->getName()) { + handleUploadError(info.jid, info.messageId, text); + } + } } } -void Core::MessageHandler::sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url) +void Core::MessageHandler::handleUploadError(const QString& jid, const QString& messageId, const QString& errorText) +{ + emit acc->uploadFileError(jid, messageId, "Error requesting slot to upload file: " + errorText); + pendingStateMessages.erase(jid); + requestChangeMessage(jid, messageId, { + {"state", static_cast(Shared::Message::State::error)}, + {"errorText", errorText} + }); +} + +void Core::MessageHandler::onUploadFileComplete(const std::list& msgs, const QString& path) +{ + for (const Shared::MessageInfo& info : msgs) { + if (info.account == acc->getName()) { + RosterItem* ri = acc->rh->getRosterItem(info.jid); + if (ri != 0) { + Shared::Message msg = ri->getMessage(info.messageId); + sendMessageWithLocalUploadedFile(msg, path, false); + } else { + qDebug() << "A signal received about complete upload to" << acc->name << "for pal" << info.jid << "but the object for this pal wasn't found, something went terrebly wrong, skipping send"; + } + } + } +} + +void Core::MessageHandler::sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url, bool newMessage) { msg.setOutOfBandUrl(url); - if (msg.getBody().size() == 0) { - msg.setBody(url); - } - sendMessage(msg); + if (msg.getBody().size() == 0) { //not sure why, but most messages do that + msg.setBody(url); //they duplicate oob in body, some of them wouldn't even show an attachment if you don't do that + } + performSending(msg, newMessage); //TODO removal/progress update } + +static const std::set allowerToChangeKeys({ + "attachPath", + "outOfBandUrl", + "state", + "errorText" +}); + +void Core::MessageHandler::requestChangeMessage(const QString& jid, const QString& messageId, const QMap& data) +{ + RosterItem* cnt = acc->rh->getRosterItem(jid); + if (cnt != 0) { + bool allSupported = true; + QString unsupportedString; + for (QMap::const_iterator itr = data.begin(); itr != data.end(); ++itr) { //I need all this madness + if (allowerToChangeKeys.count(itr.key()) != 1) { //to not allow this method + allSupported = false; //to make a message to look like if it was edited + unsupportedString = itr.key(); //basically I needed to control who exaclty calls this method + break; //because the underlying tech assumes that the change is initiated by user + } //not by system + } + if (allSupported) { + cnt->changeMessage(messageId, data); + emit acc->changeMessage(jid, messageId, data); + } else { + qDebug() << "A request to change message" << messageId << "of conversation" << jid << "with following data" << data; + qDebug() << "only limited set of dataFields are supported yet here, and" << unsupportedString << "isn't one of them, skipping"; + } + } +} diff --git a/core/handlers/messagehandler.h b/core/handlers/messagehandler.h index be1545f..9138245 100644 --- a/core/handlers/messagehandler.h +++ b/core/handlers/messagehandler.h @@ -28,6 +28,7 @@ #include #include +#include namespace Core { @@ -44,8 +45,7 @@ public: MessageHandler(Account* account); public: - void sendMessage(Shared::Message data); - void sendMessage(const Shared::Message& data, const QString& path); + void sendMessage(const Shared::Message& data); void initializeMessage(Shared::Message& target, const QXmppMessage& source, bool outgoing = false, bool forwarded = false, bool guessing = false) const; public slots: @@ -55,20 +55,24 @@ public slots: void onReceiptReceived(const QString& jid, const QString& id); void onUploadSlotReceived(const QXmppHttpUploadSlotIq& slot); void onUploadSlotRequestFailed(const QXmppHttpUploadRequestIq& request); - void onFileUploaded(const QString& messageId, const QString& url); - void onFileUploadError(const QString& messageId, const QString& errMsg); + void onDownloadFileComplete(const std::list& msgs, const QString& path); + void onUploadFileComplete(const std::list& msgs, const QString& path); + void onLoadFileError(const std::list& msgs, const QString& path, bool up); + void requestChangeMessage(const QString& jid, const QString& messageId, const QMap& data); private: bool handleChatMessage(const QXmppMessage& msg, bool outgoing = false, bool forwarded = false, bool guessing = false); bool handleGroupMessage(const QXmppMessage& msg, bool outgoing = false, bool forwarded = false, bool guessing = false); void logMessage(const QXmppMessage& msg, const QString& reason = "Message wasn't handled: "); - void sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url); + void sendMessageWithLocalUploadedFile(Shared::Message msg, const QString& url, bool newMessage = true); + void performSending(Shared::Message data, bool newMessage = true); + void prepareUpload(const Shared::Message& data); + void handleUploadError(const QString& jid, const QString& messageId, const QString& errorText); private: Account* acc; - std::map pendingStateMessages; - std::map pendingMessages; - std::deque> uploadingSlotsQueue; + std::map pendingStateMessages; //key is message id, value is JID + std::deque> uploadingSlotsQueue; }; } diff --git a/core/handlers/rosterhandler.cpp b/core/handlers/rosterhandler.cpp index 82ca8c3..ce5f1b7 100644 --- a/core/handlers/rosterhandler.cpp +++ b/core/handlers/rosterhandler.cpp @@ -190,7 +190,7 @@ void Core::RosterHandler::removeContactRequest(const QString& jid) void Core::RosterHandler::handleNewRosterItem(Core::RosterItem* contact) { connect(contact, &RosterItem::needHistory, this->acc, &Account::onContactNeedHistory); - connect(contact, &RosterItem::historyResponse, this, &RosterHandler::onContactHistoryResponse); + connect(contact, &RosterItem::historyResponse, this->acc, &Account::onContactHistoryResponse); connect(contact, &RosterItem::nameChanged, this, &RosterHandler::onContactNameChanged); connect(contact, &RosterItem::avatarChanged, this, &RosterHandler::onContactAvatarChanged); connect(contact, &RosterItem::requestVCard, this->acc, &Account::requestVCard); @@ -315,14 +315,6 @@ void Core::RosterHandler::removeFromGroup(const QString& jid, const QString& gro } } -void Core::RosterHandler::onContactHistoryResponse(const std::list& list) -{ - RosterItem* contact = static_cast(sender()); - - qDebug() << "Collected history for contact " << contact->jid << list.size() << "elements"; - emit acc->responseArchive(contact->jid, list); -} - Core::RosterItem * Core::RosterHandler::getRosterItem(const QString& jid) { RosterItem* item = 0; diff --git a/core/handlers/rosterhandler.h b/core/handlers/rosterhandler.h index c01f396..b1dfc45 100644 --- a/core/handlers/rosterhandler.h +++ b/core/handlers/rosterhandler.h @@ -86,7 +86,6 @@ private slots: void onContactGroupRemoved(const QString& group); void onContactNameChanged(const QString& name); void onContactSubscriptionStateChanged(Shared::SubscriptionState state); - void onContactHistoryResponse(const std::list& list); void onContactAvatarChanged(Shared::Avatar, const QString& path); private: diff --git a/core/networkaccess.cpp b/core/networkaccess.cpp index 2d66a70..eece379 100644 --- a/core/networkaccess.cpp +++ b/core/networkaccess.cpp @@ -16,13 +16,17 @@ * along with this program. If not, see . */ + +#include +#include + #include "networkaccess.h" Core::NetworkAccess::NetworkAccess(QObject* parent): QObject(parent), running(false), manager(0), - files("files"), + storage("fileURLStorage"), downloads(), uploads() { @@ -33,60 +37,31 @@ Core::NetworkAccess::~NetworkAccess() stop(); } -void Core::NetworkAccess::fileLocalPathRequest(const QString& messageId, const QString& url) +void Core::NetworkAccess::downladFile(const QString& url) { std::map::iterator itr = downloads.find(url); if (itr != downloads.end()) { - Transfer* dwn = itr->second; - std::set::const_iterator mItr = dwn->messages.find(messageId); - if (mItr == dwn->messages.end()) { - dwn->messages.insert(messageId); - } - emit downloadFileProgress(messageId, dwn->progress); + qDebug() << "NetworkAccess received a request to download a file" << url << ", but the file is currently downloading, skipping"; } else { try { - QString path = files.getRecord(url); - QFileInfo info(path); - if (info.exists() && info.isFile()) { - emit fileLocalPathResponse(messageId, path); + std::pair> p = storage.getPath(url); + if (p.first.size() > 0) { + QFileInfo info(p.first); + if (info.exists() && info.isFile()) { + emit downloadFileComplete(p.second, p.first); + } else { + startDownload(p.second, url); + } } else { - files.removeRecord(url); - emit fileLocalPathResponse(messageId, ""); + startDownload(p.second, url); } } catch (const Archive::NotFound& e) { - emit fileLocalPathResponse(messageId, ""); + qDebug() << "NetworkAccess received a request to download a file" << url << ", but there is now record of which message uses that file, downloading anyway"; + storage.addFile(url); + startDownload(std::list(), url); } catch (const Archive::Unknown& e) { qDebug() << "Error requesting file path:" << e.what(); - emit fileLocalPathResponse(messageId, ""); - } - } -} - -void Core::NetworkAccess::downladFileRequest(const QString& messageId, const QString& url) -{ - std::map::iterator itr = downloads.find(url); - if (itr != downloads.end()) { - Transfer* dwn = itr->second; - std::set::const_iterator mItr = dwn->messages.find(messageId); - if (mItr == dwn->messages.end()) { - dwn->messages.insert(messageId); - } - emit downloadFileProgress(messageId, dwn->progress); - } else { - try { - QString path = files.getRecord(url); - QFileInfo info(path); - if (info.exists() && info.isFile()) { - emit fileLocalPathResponse(messageId, path); - } else { - files.removeRecord(url); - startDownload(messageId, url); - } - } catch (const Archive::NotFound& e) { - startDownload(messageId, url); - } catch (const Archive::Unknown& e) { - qDebug() << "Error requesting file path:" << e.what(); - emit downloadFileError(messageId, QString("Database error: ") + e.what()); + emit loadFileError(std::list(), QString("Database error: ") + e.what(), false); } } } @@ -95,7 +70,7 @@ void Core::NetworkAccess::start() { if (!running) { manager = new QNetworkAccessManager(); - files.open(); + storage.open(); running = true; } } @@ -103,7 +78,7 @@ void Core::NetworkAccess::start() void Core::NetworkAccess::stop() { if (running) { - files.close(); + storage.close(); manager->deleteLater(); manager = 0; running = false; @@ -128,9 +103,7 @@ void Core::NetworkAccess::onDownloadProgress(qint64 bytesReceived, qint64 bytesT qreal total = bytesTotal; qreal progress = received/total; dwn->progress = progress; - for (std::set::const_iterator mItr = dwn->messages.begin(), end = dwn->messages.end(); mItr != end; ++mItr) { - emit downloadFileProgress(*mItr, progress); - } + emit loadFileProgress(dwn->messages, progress, false); } } @@ -146,9 +119,7 @@ void Core::NetworkAccess::onDownloadError(QNetworkReply::NetworkError code) if (errorText.size() > 0) { itr->second->success = false; Transfer* dwn = itr->second; - for (std::set::const_iterator mItr = dwn->messages.begin(), end = dwn->messages.end(); mItr != end; ++mItr) { - emit downloadFileError(*mItr, errorText); - } + emit loadFileError(dwn->messages, errorText, false); } } } @@ -276,61 +247,54 @@ QString Core::NetworkAccess::getErrorText(QNetworkReply::NetworkError code) void Core::NetworkAccess::onDownloadFinished() { - QString path(""); QNetworkReply* rpl = static_cast(sender()); QString url = rpl->url().toString(); std::map::const_iterator itr = downloads.find(url); if (itr == downloads.end()) { - qDebug() << "an error downloading" << url << ": the request is done but seems like noone is waiting for it, skipping"; + qDebug() << "an error downloading" << url << ": the request is done but there is no record of it being downloaded, ignoring"; } else { Transfer* dwn = itr->second; if (dwn->success) { qDebug() << "download success for" << url; QStringList hops = url.split("/"); QString fileName = hops.back(); - QStringList parts = fileName.split("."); - path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + "/"; - QString suffix(""); - QStringList::const_iterator sItr = parts.begin(); - QString realName = *sItr; - ++sItr; - for (QStringList::const_iterator sEnd = parts.end(); sItr != sEnd; ++sItr) { - suffix += "." + (*sItr); + QString jid; + if (dwn->messages.size() > 0) { + jid = dwn->messages.front().jid; } - QString postfix(""); - QFileInfo proposedName(path + realName + postfix + suffix); - int counter = 0; - while (proposedName.exists()) { - postfix = QString("(") + std::to_string(++counter).c_str() + ")"; - proposedName = QFileInfo(path + realName + postfix + suffix); + QString path = prepareDirectory(jid); + if (path.size() > 0) { + path = checkFileName(fileName, path); + + QFile file(path); + if (file.open(QIODevice::WriteOnly)) { + file.write(dwn->reply->readAll()); + file.close(); + storage.setPath(url, path); + qDebug() << "file" << path << "was successfully downloaded"; + } else { + qDebug() << "couldn't save file" << path; + path = QString(); + } } - path = proposedName.absoluteFilePath(); - QFile file(path); - if (file.open(QIODevice::WriteOnly)) { - file.write(dwn->reply->readAll()); - file.close(); - files.addRecord(url, path); - qDebug() << "file" << path << "was successfully downloaded"; + if (path.size() > 0) { + emit downloadFileComplete(dwn->messages, path); } else { - qDebug() << "couldn't save file" << path; - path = ""; + //TODO do I need to handle the failure here or it's already being handled in error? + //emit loadFileError(dwn->messages, path, false); } } - for (std::set::const_iterator mItr = dwn->messages.begin(), end = dwn->messages.end(); mItr != end; ++mItr) { - emit fileLocalPathResponse(*mItr, path); - } - dwn->reply->deleteLater(); delete dwn; downloads.erase(itr); } } -void Core::NetworkAccess::startDownload(const QString& messageId, const QString& url) +void Core::NetworkAccess::startDownload(const std::list& msgs, const QString& url) { - Transfer* dwn = new Transfer({{messageId}, 0, 0, true, "", url, 0}); + Transfer* dwn = new Transfer({msgs, 0, 0, true, "", url, 0}); QNetworkRequest req(url); dwn->reply = manager->get(req); connect(dwn->reply, &QNetworkReply::downloadProgress, this, &NetworkAccess::onDownloadProgress); @@ -341,7 +305,7 @@ void Core::NetworkAccess::startDownload(const QString& messageId, const QString& #endif connect(dwn->reply, &QNetworkReply::finished, this, &NetworkAccess::onDownloadFinished); downloads.insert(std::make_pair(url, dwn)); - emit downloadFileProgress(messageId, 0); + emit loadFileProgress(dwn->messages, 0, false); } void Core::NetworkAccess::onUploadError(QNetworkReply::NetworkError code) @@ -350,16 +314,16 @@ void Core::NetworkAccess::onUploadError(QNetworkReply::NetworkError code) QString url = rpl->url().toString(); std::map::const_iterator itr = uploads.find(url); if (itr == uploads.end()) { - qDebug() << "an error uploading" << url << ": the request is reporting an error but seems like noone is waiting for it, skipping"; + qDebug() << "an error uploading" << url << ": the request is reporting an error but there is no record of it being uploading, ignoring"; } else { QString errorText = getErrorText(code); if (errorText.size() > 0) { itr->second->success = false; Transfer* upl = itr->second; - for (std::set::const_iterator mItr = upl->messages.begin(), end = upl->messages.end(); mItr != end; ++mItr) { - emit uploadFileError(*mItr, errorText); - } + emit loadFileError(upl->messages, errorText, true); } + + //TODO deletion? } } @@ -369,17 +333,14 @@ void Core::NetworkAccess::onUploadFinished() QString url = rpl->url().toString(); std::map::const_iterator itr = uploads.find(url); if (itr == downloads.end()) { - qDebug() << "an error uploading" << url << ": the request is done but seems like no one is waiting for it, skipping"; + qDebug() << "an error uploading" << url << ": the request is done there is no record of it being uploading, ignoring"; } else { Transfer* upl = itr->second; if (upl->success) { qDebug() << "upload success for" << url; - files.addRecord(upl->url, upl->path); - - for (std::set::const_iterator mItr = upl->messages.begin(), end = upl->messages.end(); mItr != end; ++mItr) { - emit fileLocalPathResponse(*mItr, upl->path); - emit uploadFileComplete(*mItr, upl->url); - } + + storage.addFile(upl->messages, upl->url, upl->path); + emit uploadFileComplete(upl->messages, upl->url); } upl->reply->deleteLater(); @@ -403,94 +364,29 @@ void Core::NetworkAccess::onUploadProgress(qint64 bytesReceived, qint64 bytesTot qreal total = bytesTotal; qreal progress = received/total; upl->progress = progress; - for (std::set::const_iterator mItr = upl->messages.begin(), end = upl->messages.end(); mItr != end; ++mItr) { - emit uploadFileProgress(*mItr, progress); - } - } -} - -void Core::NetworkAccess::startUpload(const QString& messageId, const QString& url, const QString& path) -{ - Transfer* upl = new Transfer({{messageId}, 0, 0, true, path, url, 0}); - QNetworkRequest req(url); - QFile* file = new QFile(path); - if (file->open(QIODevice::ReadOnly)) { - upl->reply = manager->put(req, file); - - connect(upl->reply, &QNetworkReply::uploadProgress, this, &NetworkAccess::onUploadProgress); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - connect(upl->reply, qOverload(&QNetworkReply::errorOccurred), this, &NetworkAccess::onUploadError); -#else - connect(upl->reply, qOverload(&QNetworkReply::error), this, &NetworkAccess::onUploadError); -#endif - - connect(upl->reply, &QNetworkReply::finished, this, &NetworkAccess::onUploadFinished); - uploads.insert(std::make_pair(url, upl)); - emit downloadFileProgress(messageId, 0); - } else { - qDebug() << "couldn't upload file" << path; - emit uploadFileError(messageId, "Error opening file"); - delete file; - } -} - -void Core::NetworkAccess::uploadFileRequest(const QString& messageId, const QString& url, const QString& path) -{ - std::map::iterator itr = uploads.find(url); - if (itr != uploads.end()) { - Transfer* upl = itr->second; - std::set::const_iterator mItr = upl->messages.find(messageId); - if (mItr == upl->messages.end()) { - upl->messages.insert(messageId); - } - emit uploadFileProgress(messageId, upl->progress); - } else { - try { - QString ePath = files.getRecord(url); - if (ePath == path) { - QFileInfo info(path); - if (info.exists() && info.isFile()) { - emit fileLocalPathResponse(messageId, path); - } else { - files.removeRecord(url); - startUpload(messageId, url, path); - } - } else { - QFileInfo info(path); - if (info.exists() && info.isFile()) { - files.changeRecord(url, path); - emit fileLocalPathResponse(messageId, path); - } else { - files.removeRecord(url); - startUpload(messageId, url, path); - } - } - } catch (const Archive::NotFound& e) { - startUpload(messageId, url, path); - } catch (const Archive::Unknown& e) { - qDebug() << "Error requesting file path on upload:" << e.what(); - emit uploadFileError(messageId, QString("Database error: ") + e.what()); - } + emit loadFileProgress(upl->messages, progress, true); } } QString Core::NetworkAccess::getFileRemoteUrl(const QString& path) { - return ""; //TODO this is a way not to upload some file more then 1 time, here I'm supposed to return that file GET url + QString p; + + try { + p = storage.getUrl(path); + } catch (const Archive::NotFound& err) { + + } catch (...) { + throw; + } + + return p; } -bool Core::NetworkAccess::isUploading(const QString& path, const QString& messageId) -{ - return false; //TODO this is a way to avoid parallel uploading of the same files by different chats - // message is is supposed to be added to the uploading messageids list - // the result should be true if there was an uploading file with this path - // message id can be empty, then it's just to check and not to add -} - -void Core::NetworkAccess::uploadFile(const QString& messageId, const QString& path, const QUrl& put, const QUrl& get, const QMap headers) +void Core::NetworkAccess::uploadFile(const Shared::MessageInfo& info, const QString& path, const QUrl& put, const QUrl& get, const QMap headers) { QFile* file = new QFile(path); - Transfer* upl = new Transfer({{messageId}, 0, 0, true, path, get.toString(), file}); + Transfer* upl = new Transfer({{info}, 0, 0, true, path, get.toString(), file}); QNetworkRequest req(put); for (QMap::const_iterator itr = headers.begin(), end = headers.end(); itr != end; itr++) { req.setRawHeader(itr.key().toUtf8(), itr.value().toUtf8()); @@ -506,10 +402,99 @@ void Core::NetworkAccess::uploadFile(const QString& messageId, const QString& pa #endif connect(upl->reply, &QNetworkReply::finished, this, &NetworkAccess::onUploadFinished); uploads.insert(std::make_pair(put.toString(), upl)); - emit downloadFileProgress(messageId, 0); + emit loadFileProgress(upl->messages, 0, true); } else { qDebug() << "couldn't upload file" << path; - emit uploadFileError(messageId, "Error opening file"); + emit loadFileError(upl->messages, "Error opening file", true); delete file; + delete upl; } } + +void Core::NetworkAccess::registerFile(const QString& url, const QString& account, const QString& jid, const QString& id) +{ + storage.addFile(url, account, jid, id); + std::map::iterator itr = downloads.find(url); + if (itr != downloads.end()) { + itr->second->messages.emplace_back(account, jid, id); //TODO notification is going to happen the next tick, is that okay? + } +} + +void Core::NetworkAccess::registerFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id) +{ + storage.addFile(url, path, account, jid, id); +} + +bool Core::NetworkAccess::checkAndAddToUploading(const QString& acc, const QString& jid, const QString id, const QString path) +{ + for (const std::pair& pair : uploads) { + Transfer* info = pair.second; + if (pair.second->path == path) { + std::list& messages = info->messages; + bool dup = false; + for (const Shared::MessageInfo& info : messages) { + if (info.account == acc && info.jid == jid && info.messageId == id) { + dup = true; + break; + } + } + if (!dup) { + info->messages.emplace_back(acc, jid, id); //TODO notification is going to happen the next tick, is that okay? + return true; + } + } + } + + return false; +} + +QString Core::NetworkAccess::prepareDirectory(const QString& jid) +{ + QString path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + path += "/" + QApplication::applicationName(); + if (jid.size() > 0) { + path += "/" + jid; + } + QDir location(path); + + if (!location.exists()) { + bool res = location.mkpath(path); + if (!res) { + return ""; + } else { + return path; + } + } + return path; +} + +QString Core::NetworkAccess::checkFileName(const QString& name, const QString& path) +{ + QStringList parts = name.split("."); + QString suffix(""); + QStringList::const_iterator sItr = parts.begin(); + QString realName = *sItr; + ++sItr; + for (QStringList::const_iterator sEnd = parts.end(); sItr != sEnd; ++sItr) { + suffix += "." + (*sItr); + } + QString postfix(""); + QFileInfo proposedName(path + "/" + realName + suffix); + int counter = 0; + while (proposedName.exists()) { + QString count = QString("(") + std::to_string(++counter).c_str() + ")"; + proposedName = QFileInfo(path + "/" + realName + count + suffix); + } + + return proposedName.absoluteFilePath(); +} + +QString Core::NetworkAccess::addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id) +{ + return storage.addMessageAndCheckForPath(url, account, jid, id); +} + +std::list Core::NetworkAccess::reportPathInvalid(const QString& path) +{ + return storage.deletedFile(path); +} diff --git a/core/networkaccess.h b/core/networkaccess.h index 824b1af..5b9eae2 100644 --- a/core/networkaccess.h +++ b/core/networkaccess.h @@ -29,13 +29,15 @@ #include -#include "storage.h" +#include "urlstorage.h" namespace Core { /** * @todo write docs */ + +//TODO Need to describe how to get rid of records when file is no longer reachable; class NetworkAccess : public QObject { Q_OBJECT @@ -48,26 +50,27 @@ public: void stop(); QString getFileRemoteUrl(const QString& path); - bool isUploading(const QString& path, const QString& messageId = ""); - void uploadFile(const QString& messageId, const QString& path, const QUrl& put, const QUrl& get, const QMap headers); + QString addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id); + void uploadFile(const Shared::MessageInfo& info, const QString& path, const QUrl& put, const QUrl& get, const QMap headers); + bool checkAndAddToUploading(const QString& acc, const QString& jid, const QString id, const QString path); + std::list reportPathInvalid(const QString& path); signals: - void fileLocalPathResponse(const QString& messageId, const QString& path); - void downloadFileProgress(const QString& messageId, qreal value); - void downloadFileError(const QString& messageId, const QString& path); - void uploadFileProgress(const QString& messageId, qreal value); - void uploadFileError(const QString& messageId, const QString& path); - void uploadFileComplete(const QString& messageId, const QString& url); + void loadFileProgress(const std::list& msgs, qreal value, bool up); + void loadFileError(const std::list& msgs, const QString& text, bool up); + void uploadFileComplete(const std::list& msgs, const QString& url); + void downloadFileComplete(const std::list& msgs, const QString& path); public slots: - void fileLocalPathRequest(const QString& messageId, const QString& url); - void downladFileRequest(const QString& messageId, const QString& url); - void uploadFileRequest(const QString& messageId, const QString& url, const QString& path); + void downladFile(const QString& url); + void registerFile(const QString& url, const QString& account, const QString& jid, const QString& id); + void registerFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id); private: - void startDownload(const QString& messageId, const QString& url); - void startUpload(const QString& messageId, const QString& url, const QString& path); + void startDownload(const std::list& msgs, const QString& url); QString getErrorText(QNetworkReply::NetworkError code); + QString prepareDirectory(const QString& jid); + QString checkFileName(const QString& name, const QString& path); private slots: void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); @@ -80,12 +83,12 @@ private slots: private: bool running; QNetworkAccessManager* manager; - Storage files; + UrlStorage storage; std::map downloads; std::map uploads; struct Transfer { - std::set messages; + std::list messages; qreal progress; QNetworkReply* reply; bool success; diff --git a/core/passwordStorageEngines/CMakeLists.txt b/core/passwordStorageEngines/CMakeLists.txt index e824f77..735c0ad 100644 --- a/core/passwordStorageEngines/CMakeLists.txt +++ b/core/passwordStorageEngines/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.3) project(pse) if (WITH_KWALLET) @@ -14,7 +14,7 @@ if (WITH_KWALLET) kwallet.cpp ) - add_library(kwalletPSE ${kwalletPSE_SRC}) + add_library(kwalletPSE STATIC ${kwalletPSE_SRC}) target_include_directories(kwalletPSE PUBLIC ${KWALLET_INTERFACE_INCLUDE_DIRECTORIES}) target_include_directories(kwalletPSE PUBLIC ${Qt5GUI_INTERFACE_INCLUDE_DIRECTORIES}) diff --git a/core/rosteritem.cpp b/core/rosteritem.cpp index 32b70f4..b1951d6 100644 --- a/core/rosteritem.cpp +++ b/core/rosteritem.cpp @@ -122,7 +122,22 @@ void Core::RosterItem::nextRequest() { if (syncronizing) { if (requestedCount != -1) { - emit historyResponse(responseCache); + bool last = false; + if (archiveState == beginning || archiveState == complete) { + QString firstId = archive->oldestId(); + if (responseCache.size() == 0) { + if (requestedBefore == firstId) { + last = true; + } + } else { + if (responseCache.front().getId() == firstId) { + last = true; + } + } + } else if (archiveState == empty && responseCache.size() == 0) { + last = true; + } + emit historyResponse(responseCache, last); } } if (requestCache.size() > 0) { @@ -360,6 +375,11 @@ void Core::RosterItem::flushMessagesToArchive(bool finished, const QString& firs archiveState = complete; archive->setFromTheBeginning(true); } + if (added == 0 && wasEmpty) { + archiveState = empty; + nextRequest(); + break; + } if (requestedCount != -1) { QString before; if (responseCache.size() > 0) { @@ -378,7 +398,7 @@ void Core::RosterItem::flushMessagesToArchive(bool finished, const QString& firs } catch (const Archive::Empty& e) { } - if (!found || requestedCount > responseCache.size()) { + if (!found || requestedCount > int(responseCache.size())) { if (archiveState == complete) { nextRequest(); } else { @@ -529,7 +549,7 @@ void Core::RosterItem::clearArchiveRequests() requestedBefore = ""; for (const std::pair& pair : requestCache) { if (pair.first != -1) { - emit historyResponse(responseCache); //just to notify those who still waits with whatever happened to be left in caches yet + emit historyResponse(responseCache, false); //just to notify those who still waits with whatever happened to be left in caches yet } responseCache.clear(); } @@ -549,3 +569,20 @@ void Core::RosterItem::downgradeDatabaseState() archiveState = ArchiveState::chunk; } } + +Shared::Message Core::RosterItem::getMessage(const QString& id) +{ + for (const Shared::Message& msg : appendCache) { + if (msg.getId() == id) { + return msg; + } + } + + for (Shared::Message& msg : hisoryCache) { + if (msg.getId() == id) { + return msg; + } + } + + return archive->getElement(id); +} diff --git a/core/rosteritem.h b/core/rosteritem.h index 4113b37..237a46a 100644 --- a/core/rosteritem.h +++ b/core/rosteritem.h @@ -78,10 +78,12 @@ public: void clearArchiveRequests(); void downgradeDatabaseState(); + Shared::Message getMessage(const QString& id); + signals: void nameChanged(const QString& name); void subscriptionStateChanged(Shared::SubscriptionState state); - void historyResponse(const std::list& messages); + void historyResponse(const std::list& messages, bool last); void needHistory(const QString& before, const QString& after, const QDateTime& afterTime = QDateTime()); void avatarChanged(Shared::Avatar, const QString& path); void requestVCard(const QString& jid); diff --git a/core/squawk.cpp b/core/squawk.cpp index 1689d71..411d4ab 100644 --- a/core/squawk.cpp +++ b/core/squawk.cpp @@ -32,11 +32,10 @@ Core::Squawk::Squawk(QObject* parent): ,kwallet() #endif { - connect(&network, &NetworkAccess::fileLocalPathResponse, this, &Squawk::fileLocalPathResponse); - connect(&network, &NetworkAccess::downloadFileProgress, this, &Squawk::downloadFileProgress); - connect(&network, &NetworkAccess::downloadFileError, this, &Squawk::downloadFileError); - connect(&network, &NetworkAccess::uploadFileProgress, this, &Squawk::uploadFileProgress); - connect(&network, &NetworkAccess::uploadFileError, this, &Squawk::uploadFileError); + connect(&network, &NetworkAccess::loadFileProgress, this, &Squawk::fileProgress); + connect(&network, &NetworkAccess::loadFileError, this, &Squawk::fileError); + connect(&network, &NetworkAccess::downloadFileComplete, this, &Squawk::fileDownloadComplete); + connect(&network, &NetworkAccess::uploadFileComplete, this, &Squawk::fileUploadComplete); #ifdef WITH_KWALLET if (kwallet.supportState() == PSE::KWallet::success) { @@ -168,7 +167,7 @@ void Core::Squawk::addAccount( connect(acc, &Account::receivedVCard, this, &Squawk::responseVCard); - connect(acc, &Account::uploadFileError, this, &Squawk::uploadFileError); + connect(acc, &Account::uploadFileError, this, &Squawk::onAccountUploadFileError); QMap map = { {"login", login}, @@ -336,17 +335,6 @@ void Core::Squawk::sendMessage(const QString& account, const Shared::Message& da itr->second->sendMessage(data); } -void Core::Squawk::sendMessage(const QString& account, const Shared::Message& data, const QString& path) -{ - AccountsMap::const_iterator itr = amap.find(account); - if (itr == amap.end()) { - qDebug("An attempt to send a message with non existing account, skipping"); - return; - } - - itr->second->sendMessage(data, path); -} - void Core::Squawk::requestArchive(const QString& account, const QString& jid, int count, const QString& before) { AccountsMap::const_iterator itr = amap.find(account); @@ -357,10 +345,10 @@ void Core::Squawk::requestArchive(const QString& account, const QString& jid, in itr->second->requestArchive(jid, count, before); } -void Core::Squawk::onAccountResponseArchive(const QString& jid, const std::list& list) +void Core::Squawk::onAccountResponseArchive(const QString& jid, const std::list& list, bool last) { Account* acc = static_cast(sender()); - emit responseArchive(acc->getName(), jid, list); + emit responseArchive(acc->getName(), jid, list, last); } void Core::Squawk::modifyAccountRequest(const QString& name, const QMap& map) @@ -604,14 +592,9 @@ void Core::Squawk::addRoomRequest(const QString& account, const QString& jid, co itr->second->addRoomRequest(jid, nick, password, autoJoin); } -void Core::Squawk::fileLocalPathRequest(const QString& messageId, const QString& url) +void Core::Squawk::fileDownloadRequest(const QString& url) { - network.fileLocalPathRequest(messageId, url); -} - -void Core::Squawk::downloadFileRequest(const QString& messageId, const QString& url) -{ - network.downladFileRequest(messageId, url); + network.downladFile(url); } void Core::Squawk::addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName) @@ -688,7 +671,7 @@ void Core::Squawk::readSettings() settings.value("login").toString(), settings.value("server").toString(), settings.value("password", "").toString(), - settings.value("name").toString(), + settings.value("name").toString(), settings.value("resource").toString(), Shared::Global::fromInt(settings.value("passwordType", static_cast(Shared::AccountPassword::plain)).toInt()) ); @@ -762,3 +745,26 @@ void Core::Squawk::onWalletResponsePassword(const QString& login, const QString& emit changeAccount(login, {{"password", password}}); accountReady(); } + +void Core::Squawk::onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText) +{ + Account* acc = static_cast(sender()); + emit fileError({{acc->getName(), jid, id}}, errorText, true); +} + +void Core::Squawk::onLocalPathInvalid(const QString& path) +{ + std::list list = network.reportPathInvalid(path); + + QMap data({ + {"attachPath", ""} + }); + for (const Shared::MessageInfo& info : list) { + AccountsMap::const_iterator itr = amap.find(info.account); + if (itr != amap.end()) { + itr->second->requestChangeMessage(info.jid, info.messageId, data); + } else { + qDebug() << "Reacting on failure to reach file" << path << "there was an attempt to change message in account" << info.account << "which doesn't exist, skipping"; + } + } +} diff --git a/core/squawk.h b/core/squawk.h index 31812d2..25fdbda 100644 --- a/core/squawk.h +++ b/core/squawk.h @@ -51,31 +51,39 @@ public: signals: void quit(); void ready(); + void newAccount(const QMap&); void changeAccount(const QString& account, const QMap& data); void removeAccount(const QString& account); + void addGroup(const QString& account, const QString& name); void removeGroup(const QString& account, const QString& name); + void addContact(const QString& account, const QString& jid, const QString& group, const QMap& data); void removeContact(const QString& account, const QString& jid); void removeContact(const QString& account, const QString& jid, const QString& group); void changeContact(const QString& account, const QString& jid, const QMap& data); + void addPresence(const QString& account, const QString& jid, const QString& name, const QMap& data); void removePresence(const QString& account, const QString& jid, const QString& name); + void stateChanged(Shared::Availability state); + void accountMessage(const QString& account, const Shared::Message& data); - void responseArchive(const QString& account, const QString& jid, const std::list& list); + void responseArchive(const QString& account, const QString& jid, const std::list& list, bool last); + void addRoom(const QString& account, const QString jid, const QMap& data); void changeRoom(const QString& account, const QString jid, const QMap& data); void removeRoom(const QString& account, const QString jid); void addRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap& data); void changeRoomParticipant(const QString& account, const QString& jid, const QString& name, const QMap& data); void removeRoomParticipant(const QString& account, const QString& jid, const QString& name); - void fileLocalPathResponse(const QString& messageId, const QString& path); - void downloadFileError(const QString& messageId, const QString& error); - void downloadFileProgress(const QString& messageId, qreal value); - void uploadFileError(const QString& messageId, const QString& error); - void uploadFileProgress(const QString& messageId, qreal value); + + void fileError(const std::list msgs, const QString& error, bool up); + void fileProgress(const std::list msgs, qreal value, bool up); + void fileDownloadComplete(const std::list msgs, const QString& path); + void fileUploadComplete(const std::list msgs, const QString& path); + void responseVCard(const QString& jid, const Shared::VCard& card); void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap& data); void requestPassword(const QString& account); @@ -83,15 +91,18 @@ signals: public slots: void start(); void stop(); + void newAccountRequest(const QMap& map); void modifyAccountRequest(const QString& name, const QMap& map); void removeAccountRequest(const QString& name); void connectAccount(const QString& account); void disconnectAccount(const QString& account); + void changeState(Shared::Availability state); + void sendMessage(const QString& account, const Shared::Message& data); - void sendMessage(const QString& account, const Shared::Message& data, const QString& path); void requestArchive(const QString& account, const QString& jid, int count, const QString& before); + void subscribeContact(const QString& account, const QString& jid, const QString& reason); void unsubscribeContact(const QString& account, const QString& jid, const QString& reason); void addContactToGroupRequest(const QString& account, const QString& jid, const QString& groupName); @@ -99,15 +110,18 @@ public slots: void removeContactRequest(const QString& account, const QString& jid); void renameContactRequest(const QString& account, const QString& jid, const QString& newName); void addContactRequest(const QString& account, const QString& jid, const QString& name, const QSet& groups); + void setRoomJoined(const QString& account, const QString& jid, bool joined); void setRoomAutoJoin(const QString& account, const QString& jid, bool joined); void addRoomRequest(const QString& account, const QString& jid, const QString& nick, const QString& password, bool autoJoin); void removeRoomRequest(const QString& account, const QString& jid); - void fileLocalPathRequest(const QString& messageId, const QString& url); - void downloadFileRequest(const QString& messageId, const QString& url); + + void fileDownloadRequest(const QString& url); + void requestVCard(const QString& account, const QString& jid); void uploadVCard(const QString& account, const Shared::VCard& card); void responsePassword(const QString& account, const QString& password); + void onLocalPathInvalid(const QString& path); private: typedef std::deque Accounts; @@ -146,7 +160,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); + void onAccountResponseArchive(const QString& jid, const std::list& list, bool last); void onAccountAddRoom(const QString jid, const QMap& data); void onAccountChangeRoom(const QString jid, const QMap& data); void onAccountRemoveRoom(const QString jid); @@ -155,6 +169,8 @@ private slots: void onAccountRemoveRoomPresence(const QString& jid, const QString& nick); void onAccountChangeMessage(const QString& jid, const QString& id, const QMap& data); + void onAccountUploadFileError(const QString& jid, const QString id, const QString& errorText); + void onWalletOpened(bool success); void onWalletResponsePassword(const QString& login, const QString& password); void onWalletRejectPassword(const QString& login); diff --git a/core/urlstorage.cpp b/core/urlstorage.cpp new file mode 100644 index 0000000..109cfea --- /dev/null +++ b/core/urlstorage.cpp @@ -0,0 +1,491 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * 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 . + */ + +#include +#include +#include + +#include "urlstorage.h" + +Core::UrlStorage::UrlStorage(const QString& p_name): + name(p_name), + opened(false), + environment(), + base(), + map() +{ +} + +Core::UrlStorage::~UrlStorage() +{ + close(); +} + +void Core::UrlStorage::open() +{ + if (!opened) { + mdb_env_create(&environment); + QString path(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + path += "/" + name; + QDir cache(path); + + if (!cache.exists()) { + bool res = cache.mkpath(path); + if (!res) { + throw Archive::Directory(path.toStdString()); + } + } + + mdb_env_set_maxdbs(environment, 2); + mdb_env_set_mapsize(environment, 10UL * 1024UL * 1024UL); + mdb_env_open(environment, path.toStdString().c_str(), 0, 0664); + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + mdb_dbi_open(txn, "base", MDB_CREATE, &base); + mdb_dbi_open(txn, "map", MDB_CREATE, &map); + mdb_txn_commit(txn); + opened = true; + } +} + +void Core::UrlStorage::close() +{ + if (opened) { + mdb_dbi_close(environment, map); + mdb_dbi_close(environment, base); + mdb_env_close(environment); + opened = false; + } +} + +void Core::UrlStorage::writeInfo(const QString& key, const Core::UrlStorage::UrlInfo& info, bool overwrite) +{ + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + + try { + writeInfo(key, info, txn, overwrite); + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } +} + +void Core::UrlStorage::writeInfo(const QString& key, const Core::UrlStorage::UrlInfo& info, MDB_txn* txn, bool overwrite) +{ + QByteArray ba; + QDataStream ds(&ba, QIODevice::WriteOnly); + info.serialize(ds); + + const std::string& id = key.toStdString(); + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + lmdbData.mv_size = ba.size(); + lmdbData.mv_data = (uint8_t*)ba.data(); + + int rc; + rc = mdb_put(txn, base, &lmdbKey, &lmdbData, overwrite ? 0 : MDB_NOOVERWRITE); + + if (rc != 0) { + if (rc == MDB_KEYEXIST) { + if (!overwrite) { + throw Archive::Exist(name.toStdString(), id); + } + } else { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + } + + if (info.hasPath()) { + std::string sp = info.getPath().toStdString(); + lmdbData.mv_size = sp.size(); + lmdbData.mv_data = (char*)sp.c_str(); + rc = mdb_put(txn, map, &lmdbData, &lmdbKey, 0); + if (rc != 0) { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + } +} + +void Core::UrlStorage::readInfo(const QString& key, Core::UrlStorage::UrlInfo& info, MDB_txn* txn) +{ + const std::string& id = key.toStdString(); + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + int rc = mdb_get(txn, base, &lmdbKey, &lmdbData); + + if (rc == 0) { + QByteArray ba((char*)lmdbData.mv_data, lmdbData.mv_size); + QDataStream ds(&ba, QIODevice::ReadOnly); + + info.deserialize(ds); + } else if (rc == MDB_NOTFOUND) { + throw Archive::NotFound(id, name.toStdString()); + } else { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } +} + +void Core::UrlStorage::readInfo(const QString& key, Core::UrlStorage::UrlInfo& info) +{ + MDB_txn *txn; + mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); + + try { + readInfo(key, info, txn); + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } +} + +void Core::UrlStorage::addFile(const QString& url) +{ + if (!opened) { + throw Archive::Closed("addFile(no message, no path)", name.toStdString()); + } + + addToInfo(url, "", "", ""); +} + +void Core::UrlStorage::addFile(const QString& url, const QString& path) +{ + if (!opened) { + throw Archive::Closed("addFile(no message, with path)", name.toStdString()); + } + + addToInfo(url, "", "", "", path); +} + +void Core::UrlStorage::addFile(const QString& url, const QString& account, const QString& jid, const QString& id) +{ + if (!opened) { + throw Archive::Closed("addFile(with message, no path)", name.toStdString()); + } + + addToInfo(url, account, jid, id); +} + +void Core::UrlStorage::addFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id) +{ + if (!opened) { + throw Archive::Closed("addFile(with message, with path)", name.toStdString()); + } + + addToInfo(url, account, jid, id, path); +} + +void Core::UrlStorage::addFile(const std::list& msgs, const QString& url, const QString& path) +{ + if (!opened) { + throw Archive::Closed("addFile(with list)", name.toStdString()); + } + + UrlInfo info (path, msgs); + writeInfo(url, info, true); +} + +QString Core::UrlStorage::addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id) +{ + if (!opened) { + throw Archive::Closed("addMessageAndCheckForPath", name.toStdString()); + } + + return addToInfo(url, account, jid, id).getPath(); +} + +Core::UrlStorage::UrlInfo Core::UrlStorage::addToInfo(const QString& url, const QString& account, const QString& jid, const QString& id, const QString& path) +{ + UrlInfo info; + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + + try { + readInfo(url, info, txn); + } catch (const Archive::NotFound& e) { + + } catch (...) { + mdb_txn_abort(txn); + throw; + } + + bool pathChange = false; + bool listChange = false; + if (path != "-s") { + if (info.getPath() != path) { + info.setPath(path); + pathChange = true; + } + } + + if (account.size() > 0 && jid.size() > 0 && id.size() > 0) { + listChange = info.addMessage(account, jid, id); + } + + if (pathChange || listChange) { + try { + writeInfo(url, info, txn, true); + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } + } else { + mdb_txn_abort(txn); + } + + return info; +} + +std::list Core::UrlStorage::setPath(const QString& url, const QString& path) +{ + std::list list; + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + UrlInfo info; + + try { + readInfo(url, info, txn); + info.getMessages(list); + } catch (const Archive::NotFound& e) { + } catch (...) { + mdb_txn_abort(txn); + throw; + } + + info.setPath(path); + try { + writeInfo(url, info, txn, true); + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } + + return list; +} + +std::list Core::UrlStorage::removeFile(const QString& url) +{ + std::list list; + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + UrlInfo info; + + try { + std::string id = url.toStdString(); + readInfo(url, info, txn); + info.getMessages(list); + + MDB_val lmdbKey; + lmdbKey.mv_size = id.size(); + lmdbKey.mv_data = (char*)id.c_str(); + int rc = mdb_del(txn, base, &lmdbKey, NULL); + if (rc != 0) { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + + if (info.hasPath()) { + std::string path = info.getPath().toStdString(); + lmdbKey.mv_size = path.size(); + lmdbKey.mv_data = (char*)path.c_str(); + + int rc = mdb_del(txn, map, &lmdbKey, NULL); + if (rc != 0) { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + } + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } + + return list; +} + +std::list Core::UrlStorage::deletedFile(const QString& path) +{ + std::list list; + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, 0, &txn); + + try { + std::string spath = path.toStdString(); + + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = spath.size(); + lmdbKey.mv_data = (char*)spath.c_str(); + + QString url; + int rc = mdb_get(txn, map, &lmdbKey, &lmdbData); + + if (rc == 0) { + std::string surl((char*)lmdbData.mv_data, lmdbData.mv_size); + url = QString(surl.c_str()); + } else if (rc == MDB_NOTFOUND) { + qDebug() << "Have been asked to remove file" << path << ", which isn't in the database, skipping"; + mdb_txn_abort(txn); + return list; + } else { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + + UrlInfo info; + std::string id = url.toStdString(); + readInfo(url, info, txn); + info.getMessages(list); + info.setPath(QString()); + writeInfo(url, info, txn, true); + + rc = mdb_del(txn, map, &lmdbKey, NULL); + if (rc != 0) { + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } + + mdb_txn_commit(txn); + } catch (...) { + mdb_txn_abort(txn); + throw; + } + + return list; +} + + +QString Core::UrlStorage::getUrl(const QString& path) +{ + std::list list; + + MDB_txn *txn; + mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); + + std::string spath = path.toStdString(); + + MDB_val lmdbKey, lmdbData; + lmdbKey.mv_size = spath.size(); + lmdbKey.mv_data = (char*)spath.c_str(); + + QString url; + int rc = mdb_get(txn, map, &lmdbKey, &lmdbData); + + if (rc == 0) { + std::string surl((char*)lmdbData.mv_data, lmdbData.mv_size); + url = QString(surl.c_str()); + + mdb_txn_abort(txn); + return url; + } else if (rc == MDB_NOTFOUND) { + mdb_txn_abort(txn); + throw Archive::NotFound(spath, name.toStdString()); + } else { + mdb_txn_abort(txn); + throw Archive::Unknown(name.toStdString(), mdb_strerror(rc)); + } +} + +std::pair> Core::UrlStorage::getPath(const QString& url) +{ + UrlInfo info; + readInfo(url, info); + std::list container; + info.getMessages(container); + return std::make_pair(info.getPath(), container); +} + +Core::UrlStorage::UrlInfo::UrlInfo(): + localPath(), + messages() {} + +Core::UrlStorage::UrlInfo::UrlInfo(const QString& path): + localPath(path), + messages() {} + +Core::UrlStorage::UrlInfo::UrlInfo(const QString& path, const std::list& msgs): + localPath(path), + messages(msgs) {} + +Core::UrlStorage::UrlInfo::~UrlInfo() {} + +bool Core::UrlStorage::UrlInfo::addMessage(const QString& acc, const QString& jid, const QString& id) +{ + for (const Shared::MessageInfo& info : messages) { + if (info.account == acc && info.jid == jid && info.messageId == id) { + return false; + } + } + messages.emplace_back(acc, jid, id); + return true; +} + +void Core::UrlStorage::UrlInfo::serialize(QDataStream& data) const +{ + data << localPath; + std::list::size_type size = messages.size(); + data << quint32(size); + for (const Shared::MessageInfo& info : messages) { + data << info.account; + data << info.jid; + data << info.messageId; + } +} + +void Core::UrlStorage::UrlInfo::deserialize(QDataStream& data) +{ + data >> localPath; + quint32 size; + data >> size; + for (quint32 i = 0; i < size; ++i) { + messages.emplace_back(); + Shared::MessageInfo& info = messages.back(); + data >> info.account; + data >> info.jid; + data >> info.messageId; + } +} + +void Core::UrlStorage::UrlInfo::getMessages(std::list& container) const +{ + for (const Shared::MessageInfo& info : messages) { + container.emplace_back(info); + } +} + +QString Core::UrlStorage::UrlInfo::getPath() const +{ + return localPath; +} + +bool Core::UrlStorage::UrlInfo::hasPath() const +{ + return localPath.size() > 0; +} + + +void Core::UrlStorage::UrlInfo::setPath(const QString& path) +{ + localPath = path; +} diff --git a/core/urlstorage.h b/core/urlstorage.h new file mode 100644 index 0000000..3dc5c21 --- /dev/null +++ b/core/urlstorage.h @@ -0,0 +1,99 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * 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 . + */ + +#ifndef CORE_URLSTORAGE_H +#define CORE_URLSTORAGE_H + +#include +#include +#include +#include + +#include "archive.h" +#include + +namespace Core { + +/** + * @todo write docs + */ +class UrlStorage +{ + class UrlInfo; +public: + UrlStorage(const QString& name); + ~UrlStorage(); + + void open(); + void close(); + + void addFile(const QString& url); + void addFile(const QString& url, const QString& path); + void addFile(const QString& url, const QString& account, const QString& jid, const QString& id); + void addFile(const QString& url, const QString& path, const QString& account, const QString& jid, const QString& id); + void addFile(const std::list& msgs, const QString& url, const QString& path); //this one overwrites all that was + std::list removeFile(const QString& url); //removes entry like it never was in the database, returns affected message infos + std::list deletedFile(const QString& path); //empties the localPath of the entry, returns affected message infos + std::list setPath(const QString& url, const QString& path); + QString getUrl(const QString& path); + QString addMessageAndCheckForPath(const QString& url, const QString& account, const QString& jid, const QString& id); + std::pair> getPath(const QString& url); + +private: + QString name; + bool opened; + MDB_env* environment; + MDB_dbi base; + MDB_dbi map; + +private: + void writeInfo(const QString& key, const UrlInfo& info, bool overwrite = false); + void writeInfo(const QString& key, const UrlInfo& info, MDB_txn* txn, bool overwrite = false); + void readInfo(const QString& key, UrlInfo& info); + void readInfo(const QString& key, UrlInfo& info, MDB_txn* txn); + UrlInfo addToInfo(const QString& url, const QString& account, const QString& jid, const QString& id, const QString& path = "-s"); + +private: + class UrlInfo { + public: + UrlInfo(const QString& path); + UrlInfo(const QString& path, const std::list& msgs); + UrlInfo(); + ~UrlInfo(); + + void serialize(QDataStream& data) const; + void deserialize(QDataStream& data); + + QString getPath() const; + bool hasPath() const; + void setPath(const QString& path); + + bool addMessage(const QString& acc, const QString& jid, const QString& id); + void getMessages(std::list& container) const; + + private: + QString localPath; + std::list messages; + }; + + +}; + +} + +#endif // CORE_URLSTORAGE_H diff --git a/external/simpleCrypt/CMakeLists.txt b/external/simpleCrypt/CMakeLists.txt index bdb62c6..88f5d23 100644 --- a/external/simpleCrypt/CMakeLists.txt +++ b/external/simpleCrypt/CMakeLists.txt @@ -10,7 +10,7 @@ set(simplecrypt_SRC ) # Tell CMake to create the helloworld executable -add_library(simpleCrypt ${simplecrypt_SRC}) +add_library(simpleCrypt STATIC ${simplecrypt_SRC}) # Use the Widgets module from Qt 5. target_link_libraries(simpleCrypt Qt5::Core) diff --git a/main.cpp b/main.cpp index 4c4b3ea..210dd70 100644 --- a/main.cpp +++ b/main.cpp @@ -20,6 +20,7 @@ #include "core/squawk.h" #include "signalcatcher.h" #include "shared/global.h" +#include "shared/messageinfo.h" #include #include #include @@ -31,8 +32,10 @@ int main(int argc, char *argv[]) { qRegisterMetaType("Shared::Message"); + qRegisterMetaType("Shared::MessageInfo"); qRegisterMetaType("Shared::VCard"); qRegisterMetaType>("std::list"); + qRegisterMetaType>("std::list"); qRegisterMetaType>("QSet"); qRegisterMetaType("Shared::ConnectionState"); qRegisterMetaType("Shared::Availability"); @@ -96,10 +99,7 @@ int main(int argc, char *argv[]) QObject::connect(&w, &Squawk::connectAccount, squawk, &Core::Squawk::connectAccount); QObject::connect(&w, &Squawk::disconnectAccount, squawk, &Core::Squawk::disconnectAccount); QObject::connect(&w, &Squawk::changeState, squawk, &Core::Squawk::changeState); - QObject::connect(&w, qOverload(&Squawk::sendMessage), - squawk, qOverload(&Core::Squawk::sendMessage)); - QObject::connect(&w, qOverload(&Squawk::sendMessage), - squawk, qOverload(&Core::Squawk::sendMessage)); + QObject::connect(&w, &Squawk::sendMessage, squawk,&Core::Squawk::sendMessage); QObject::connect(&w, &Squawk::requestArchive, squawk, &Core::Squawk::requestArchive); QObject::connect(&w, &Squawk::subscribeContact, squawk, &Core::Squawk::subscribeContact); QObject::connect(&w, &Squawk::unsubscribeContact, squawk, &Core::Squawk::unsubscribeContact); @@ -109,14 +109,14 @@ int main(int argc, char *argv[]) QObject::connect(&w, &Squawk::setRoomAutoJoin, squawk, &Core::Squawk::setRoomAutoJoin); QObject::connect(&w, &Squawk::removeRoomRequest, squawk, &Core::Squawk::removeRoomRequest); QObject::connect(&w, &Squawk::addRoomRequest, squawk, &Core::Squawk::addRoomRequest); - QObject::connect(&w, &Squawk::fileLocalPathRequest, squawk, &Core::Squawk::fileLocalPathRequest); - QObject::connect(&w, &Squawk::downloadFileRequest, squawk, &Core::Squawk::downloadFileRequest); + QObject::connect(&w, &Squawk::fileDownloadRequest, squawk, &Core::Squawk::fileDownloadRequest); QObject::connect(&w, &Squawk::addContactToGroupRequest, squawk, &Core::Squawk::addContactToGroupRequest); QObject::connect(&w, &Squawk::removeContactFromGroupRequest, squawk, &Core::Squawk::removeContactFromGroupRequest); QObject::connect(&w, &Squawk::renameContactRequest, squawk, &Core::Squawk::renameContactRequest); QObject::connect(&w, &Squawk::requestVCard, squawk, &Core::Squawk::requestVCard); QObject::connect(&w, &Squawk::uploadVCard, squawk, &Core::Squawk::uploadVCard); QObject::connect(&w, &Squawk::responsePassword, squawk, &Core::Squawk::responsePassword); + QObject::connect(&w, &Squawk::localPathInvalid, squawk, &Core::Squawk::onLocalPathInvalid); QObject::connect(squawk, &Core::Squawk::newAccount, &w, &Squawk::newAccount); QObject::connect(squawk, &Core::Squawk::addContact, &w, &Squawk::addContact); @@ -141,11 +141,10 @@ int main(int argc, char *argv[]) QObject::connect(squawk, &Core::Squawk::addRoomParticipant, &w, &Squawk::addRoomParticipant); QObject::connect(squawk, &Core::Squawk::changeRoomParticipant, &w, &Squawk::changeRoomParticipant); QObject::connect(squawk, &Core::Squawk::removeRoomParticipant, &w, &Squawk::removeRoomParticipant); - QObject::connect(squawk, &Core::Squawk::fileLocalPathResponse, &w, &Squawk::fileLocalPathResponse); - QObject::connect(squawk, &Core::Squawk::downloadFileProgress, &w, &Squawk::fileProgress); - QObject::connect(squawk, &Core::Squawk::downloadFileError, &w, &Squawk::fileError); - QObject::connect(squawk, &Core::Squawk::uploadFileProgress, &w, &Squawk::fileProgress); - QObject::connect(squawk, &Core::Squawk::uploadFileError, &w, &Squawk::fileError); + QObject::connect(squawk, &Core::Squawk::fileDownloadComplete, &w, &Squawk::fileDownloadComplete); + QObject::connect(squawk, &Core::Squawk::fileUploadComplete, &w, &Squawk::fileUploadComplete); + QObject::connect(squawk, &Core::Squawk::fileProgress, &w, &Squawk::fileProgress); + QObject::connect(squawk, &Core::Squawk::fileError, &w, &Squawk::fileError); QObject::connect(squawk, &Core::Squawk::responseVCard, &w, &Squawk::responseVCard); QObject::connect(squawk, &Core::Squawk::requestPassword, &w, &Squawk::requestPassword); QObject::connect(squawk, &Core::Squawk::ready, &w, &Squawk::readSettings); diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..69a5e94 --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.3) +project(plugins) + +if (WITH_KIO) + set(CMAKE_AUTOMOC ON) + + find_package(Qt5Core CONFIG REQUIRED) + + set(openFileManagerWindowJob_SRC + openfilemanagerwindowjob.cpp + ) + + add_library(openFileManagerWindowJob SHARED ${openFileManagerWindowJob_SRC}) + + get_target_property(Qt5CORE_INTERFACE_INCLUDE_DIRECTORIES Qt5::Core INTERFACE_INCLUDE_DIRECTORIES) + get_target_property(KIO_WIDGETS_INTERFACE_INCLUDE_DIRECTORIES KF5::KIOWidgets INTERFACE_INCLUDE_DIRECTORIES) + get_target_property(CORE_ADDONS_INTERFACE_INCLUDE_DIRECTORIES KF5::CoreAddons INTERFACE_INCLUDE_DIRECTORIES) + target_include_directories(openFileManagerWindowJob PUBLIC ${KIO_WIDGETS_INTERFACE_INCLUDE_DIRECTORIES}) + target_include_directories(openFileManagerWindowJob PUBLIC ${CORE_ADDONS_INTERFACE_INCLUDE_DIRECTORIES}) + target_include_directories(openFileManagerWindowJob PUBLIC ${Qt5CORE_INTERFACE_INCLUDE_DIRECTORIES}) + + target_link_libraries(openFileManagerWindowJob KF5::KIOWidgets) + target_link_libraries(openFileManagerWindowJob Qt5::Core) + + install(TARGETS openFileManagerWindowJob DESTINATION ${CMAKE_INSTALL_LIBDIR}) +endif() diff --git a/plugins/openfilemanagerwindowjob.cpp b/plugins/openfilemanagerwindowjob.cpp new file mode 100644 index 0000000..904fbcf --- /dev/null +++ b/plugins/openfilemanagerwindowjob.cpp @@ -0,0 +1,8 @@ +#include +#include +#include + +extern "C" void highlightInFileManager(const QUrl& url) { + KIO::OpenFileManagerWindowJob* job = KIO::highlightInFileManager({url}); + QObject::connect(job, &KIO::OpenFileManagerWindowJob::result, job, &KIO::OpenFileManagerWindowJob::deleteLater); +} diff --git a/resources/images/fallback/dark/big/document-preview.svg b/resources/images/fallback/dark/big/document-preview.svg new file mode 100644 index 0000000..49a3feb --- /dev/null +++ b/resources/images/fallback/dark/big/document-preview.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/resources/images/fallback/dark/big/folder.svg b/resources/images/fallback/dark/big/folder.svg new file mode 100644 index 0000000..2acb4ab --- /dev/null +++ b/resources/images/fallback/dark/big/folder.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/fallback/dark/small/document-preview.svg b/resources/images/fallback/dark/small/document-preview.svg new file mode 100644 index 0000000..43d19bf --- /dev/null +++ b/resources/images/fallback/dark/small/document-preview.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/resources/images/fallback/dark/small/folder.svg b/resources/images/fallback/dark/small/folder.svg new file mode 100644 index 0000000..1061f4d --- /dev/null +++ b/resources/images/fallback/dark/small/folder.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/images/fallback/light/big/document-preview.svg b/resources/images/fallback/light/big/document-preview.svg new file mode 100644 index 0000000..6f6e346 --- /dev/null +++ b/resources/images/fallback/light/big/document-preview.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/resources/images/fallback/light/big/folder.svg b/resources/images/fallback/light/big/folder.svg new file mode 100644 index 0000000..2acb4ab --- /dev/null +++ b/resources/images/fallback/light/big/folder.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/fallback/light/small/document-preview.svg b/resources/images/fallback/light/small/document-preview.svg new file mode 100644 index 0000000..f40fcdf --- /dev/null +++ b/resources/images/fallback/light/small/document-preview.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/resources/images/fallback/light/small/folder.svg b/resources/images/fallback/light/small/folder.svg new file mode 100644 index 0000000..a5f66cd --- /dev/null +++ b/resources/images/fallback/light/small/folder.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/resources/resources.qrc b/resources/resources.qrc index 4fb3e5b..58565fc 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -40,6 +40,8 @@ images/fallback/dark/big/favorite.svg images/fallback/dark/big/unfavorite.svg images/fallback/dark/big/add.svg + images/fallback/dark/big/folder.svg + images/fallback/dark/big/document-preview.svg images/fallback/dark/small/absent.svg @@ -80,6 +82,8 @@ images/fallback/dark/small/favorite.svg images/fallback/dark/small/unfavorite.svg images/fallback/dark/small/add.svg + images/fallback/dark/small/folder.svg + images/fallback/dark/small/document-preview.svg images/fallback/light/big/absent.svg @@ -120,6 +124,8 @@ images/fallback/light/big/favorite.svg images/fallback/light/big/unfavorite.svg images/fallback/light/big/add.svg + images/fallback/light/big/folder.svg + images/fallback/light/big/document-preview.svg images/fallback/light/small/absent.svg @@ -160,5 +166,7 @@ images/fallback/light/small/favorite.svg images/fallback/light/small/unfavorite.svg images/fallback/light/small/add.svg + images/fallback/light/small/folder.svg + images/fallback/light/small/document-preview.svg diff --git a/shared.h b/shared.h index 83bcd76..3925ce2 100644 --- a/shared.h +++ b/shared.h @@ -25,5 +25,6 @@ #include "shared/message.h" #include "shared/vcard.h" #include "shared/global.h" +#include "shared/messageinfo.h" #endif // SHARED_H diff --git a/shared/global.cpp b/shared/global.cpp index a6b7b60..62843ed 100644 --- a/shared/global.cpp +++ b/shared/global.cpp @@ -23,6 +23,11 @@ Shared::Global* Shared::Global::instance = 0; const std::set Shared::Global::supportedImagesExts = {"png", "jpg", "webp", "jpeg", "gif", "svg"}; +#ifdef WITH_KIO +QLibrary Shared::Global::openFileManagerWindowJob("openFileManagerWindowJob"); +Shared::Global::HighlightInFileManager Shared::Global::hfm = 0; +#endif + Shared::Global::Global(): availability({ tr("Online", "Availability"), @@ -80,16 +85,61 @@ Shared::Global::Global(): tr("Your password is going to be stored in KDE wallet storage (KWallet). You're going to be queried for permissions", "AccountPasswordDescription") }), pluginSupport({ - {"KWallet", false} - }) + {"KWallet", false}, + {"openFileManagerWindowJob", false} + }), + fileCache() { if (instance != 0) { throw 551; } instance = this; + +#ifdef WITH_KIO + openFileManagerWindowJob.load(); + if (openFileManagerWindowJob.isLoaded()) { + hfm = (HighlightInFileManager) openFileManagerWindowJob.resolve("highlightInFileManager"); + if (hfm) { + setSupported("openFileManagerWindowJob", true); + qDebug() << "KIO::OpenFileManagerWindow support enabled"; + } else { + qDebug() << "KIO::OpenFileManagerWindow support disabled: couldn't resolve required methods in the library"; + } + } else { + qDebug() << "KIO::OpenFileManagerWindow support disabled: couldn't load the library" << openFileManagerWindowJob.errorString(); + } +#endif } +Shared::Global::FileInfo Shared::Global::getFileInfo(const QString& path) +{ + std::map::const_iterator itr = instance->fileCache.find(path); + if (itr == instance->fileCache.end()) { + QMimeDatabase db; + QMimeType type = db.mimeTypeForFile(path); + QStringList parts = type.name().split("/"); + QString big = parts.front(); + QFileInfo info(path); + + FileInfo::Preview p = FileInfo::Preview::none; + QSize size; + if (big == "image") { + if (parts.back() == "gif") { + //TODO need to consider GIF as a movie + } + p = FileInfo::Preview::picture; + QImage img(path); + size = img.size(); + } + + itr = instance->fileCache.insert(std::make_pair(path, FileInfo({info.fileName(), size, type, p}))).first; + } + + return itr->second; +} + + Shared::Global * Shared::Global::getInstance() { return instance; @@ -152,6 +202,69 @@ QString Shared::Global::getDescription(Shared::AccountPassword ap) return instance->accountPasswordDescription[static_cast(ap)]; } + +static const QStringList query = {"query", "default", "inode/directory"}; +static const QRegularExpression dolphinReg("[Dd]olphin"); +static const QRegularExpression nautilusReg("[Nn]autilus"); +static const QRegularExpression cajaReg("[Cc]aja"); +static const QRegularExpression nemoReg("[Nn]emo"); +static const QRegularExpression konquerorReg("kfmclient"); +static const QRegularExpression pcmanfmQtReg("pcmanfm-qt"); +static const QRegularExpression pcmanfmReg("pcmanfm"); +static const QRegularExpression thunarReg("thunar"); + +void Shared::Global::highlightInFileManager(const QString& path) +{ +#ifdef WITH_KIO + if (supported("openFileManagerWindowJob")) { + hfm(path); + return; + } else { + qDebug() << "requested to highlight in file manager url" << path << "but it's not supported: KIO plugin isn't loaded, trying fallback"; + } +#else + qDebug() << "requested to highlight in file manager url" << path << "but it's not supported: squawk wasn't compiled to support it, trying fallback"; +#endif + + QFileInfo info = path; + if (info.exists()) { + QProcess proc; + proc.start("xdg-mime", query); + proc.waitForFinished(); + QString output = proc.readLine().simplified(); + + QString folder; + if (info.isDir()) { + folder = info.canonicalFilePath(); + } else { + folder = info.canonicalPath(); + } + + if (output.contains(dolphinReg)) { + //there is a bug on current (21.04.0) dolphin, it works correct only if you already have dolphin launched + proc.startDetached("dolphin", QStringList() << "--select" << info.canonicalFilePath()); + //KIO::highlightInFileManager({QUrl(info.canonicalFilePath())}); + } else if (output.contains(nautilusReg)) { + proc.startDetached("nautilus", QStringList() << "--select" << info.canonicalFilePath()); //this worked on nautilus + } else if (output.contains(cajaReg)) { + proc.startDetached("caja", QStringList() << folder); //caja doesn't seem to support file selection command line, gonna just open directory + } else if (output.contains(nemoReg)) { + proc.startDetached("nemo", QStringList() << info.canonicalFilePath()); //nemo supports selecting files without keys + } else if (output.contains(konquerorReg)) { + proc.startDetached("konqueror", QStringList() << "--select" << info.canonicalFilePath()); //this worked on konqueror + } else if (output.contains(pcmanfmQtReg)) { + proc.startDetached("pcmanfm-qt", QStringList() << folder); //pcmanfm-qt doesn't seem to support open with selection, gonna just open directory + } else if (output.contains(pcmanfmReg)) { + proc.startDetached("pcmanfm", QStringList() << folder); //pcmanfm also doesn't seem to support open with selection, gonna just open directory + } else if (output.contains(thunarReg)) { + proc.startDetached("thunar", QStringList() << folder); //thunar doesn't seem to support open with selection, gonna just open directory + } else { + QDesktopServices::openUrl(QUrl::fromLocalFile(folder)); + } + } +} + + #define FROM_INT_INPL(Enum) \ template<> \ Enum Shared::Global::fromInt(int src) \ diff --git a/shared/global.h b/shared/global.h index 54e1584..b6bbe37 100644 --- a/shared/global.h +++ b/shared/global.h @@ -29,6 +29,17 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace Shared { @@ -36,6 +47,19 @@ namespace Shared { Q_DECLARE_TR_FUNCTIONS(Global) public: + struct FileInfo { + enum class Preview { + none, + picture, + movie + }; + + QString name; + QSize size; + QMimeType mime; + Preview preview; + }; + Global(); static Global* getInstance(); @@ -64,6 +88,9 @@ namespace Shared { static const std::set supportedImagesExts; + static FileInfo getFileInfo(const QString& path); + static void highlightInFileManager(const QString& path); + template static T fromInt(int src); @@ -87,6 +114,15 @@ namespace Shared { static Global* instance; std::map pluginSupport; + std::map fileCache; + +#ifdef WITH_KIO + static QLibrary openFileManagerWindowJob; + + typedef void (*HighlightInFileManager)(const QUrl &); + + static HighlightInFileManager hfm; +#endif }; } diff --git a/shared/icons.h b/shared/icons.h index 48ecc37..540d3e9 100644 --- a/shared/icons.h +++ b/shared/icons.h @@ -170,6 +170,8 @@ static const std::map> icons = { {"favorite", {"favorite", "favorite"}}, {"unfavorite", {"draw-star", "unfavorite"}}, {"list-add", {"list-add", "add"}}, + {"folder", {"folder", "folder"}}, + {"document-preview", {"document-preview", "document-preview"}} }; } diff --git a/shared/message.cpp b/shared/message.cpp index af4f9e0..e6b47b2 100644 --- a/shared/message.cpp +++ b/shared/message.cpp @@ -36,7 +36,8 @@ Shared::Message::Message(Shared::Message::Type p_type): errorText(), originalMessage(), lastModified(), - stanzaId() + stanzaId(), + attachPath() {} Shared::Message::Message(): @@ -56,7 +57,8 @@ Shared::Message::Message(): errorText(), originalMessage(), lastModified(), - stanzaId() + stanzaId(), + attachPath() {} QString Shared::Message::getBody() const @@ -311,6 +313,7 @@ void Shared::Message::serialize(QDataStream& data) const data << lastModified; } data << stanzaId; + data << attachPath; } void Shared::Message::deserialize(QDataStream& data) @@ -341,6 +344,7 @@ void Shared::Message::deserialize(QDataStream& data) data >> lastModified; } data >> stanzaId; + data >> attachPath; } bool Shared::Message::change(const QMap& data) @@ -350,6 +354,16 @@ bool Shared::Message::change(const QMap& data) setState(static_cast(itr.value().toUInt())); } + itr = data.find("outOfBandUrl"); + if (itr != data.end()) { + setOutOfBandUrl(itr.value().toString()); + } + + itr = data.find("attachPath"); + if (itr != data.end()) { + setAttachPath(itr.value().toString()); + } + if (state == State::error) { itr = data.find("errorText"); if (itr != data.end()) { @@ -380,18 +394,29 @@ bool Shared::Message::change(const QMap& data) itr = data.find("body"); if (itr != data.end()) { - QMap::const_iterator dItr = data.find("stamp"); - QDateTime correctionDate; - if (dItr != data.end()) { - correctionDate = dItr.value().toDateTime(); - } else { - correctionDate = QDateTime::currentDateTimeUtc(); //in case there is no information about time of this correction it's applied + QString b = itr.value().toString(); + if (body != b) { + QMap::const_iterator dItr = data.find("stamp"); + QDateTime correctionDate; + if (dItr != data.end()) { + correctionDate = dItr.value().toDateTime(); + } else { + correctionDate = QDateTime::currentDateTimeUtc(); //in case there is no information about time of this correction it's applied + } + if (!edited || lastModified < correctionDate) { + originalMessage = body; + lastModified = correctionDate; + setBody(body); + setEdited(true); + } } - if (!edited || lastModified < correctionDate) { - originalMessage = body; - lastModified = correctionDate; - setBody(itr.value().toString()); - setEdited(true); + } else { + QMap::const_iterator dItr = data.find("stamp"); + if (dItr != data.end()) { + QDateTime ntime = dItr.value().toDateTime(); + if (time != ntime) { + setTime(ntime); + } } } @@ -420,7 +445,7 @@ void Shared::Message::setOutOfBandUrl(const QString& url) bool Shared::Message::storable() const { - return id.size() > 0 && (body.size() > 0 || oob.size()) > 0; + return id.size() > 0 && (body.size() > 0 || oob.size() > 0 || attachPath.size() > 0); } void Shared::Message::setStanzaId(const QString& sid) @@ -432,3 +457,33 @@ QString Shared::Message::getStanzaId() const { return stanzaId; } + +QString Shared::Message::getAttachPath() const +{ + return attachPath; +} + +void Shared::Message::setAttachPath(const QString& path) +{ + attachPath = path; +} + +Shared::Message::Change::Change(const QMap& _data): + data(_data), + idModified(false) {} + +void Shared::Message::Change::operator()(Shared::Message& msg) +{ + idModified = msg.change(data); +} + +void Shared::Message::Change::operator()(Shared::Message* msg) +{ + idModified = msg->change(data); +} + +bool Shared::Message::Change::hasIdBeenModified() const +{ + return idModified; +} + diff --git a/shared/message.h b/shared/message.h index d84053f..aa91af6 100644 --- a/shared/message.h +++ b/shared/message.h @@ -16,15 +16,15 @@ * along with this program. If not, see . */ +#ifndef SHAPER_MESSAGE_H +#define SHAPER_MESSAGE_H + #include #include #include #include #include -#ifndef SHAPER_MESSAGE_H -#define SHAPER_MESSAGE_H - namespace Shared { /** @@ -46,9 +46,22 @@ public: delivered, error }; + static const State StateHighest = State::error; static const State StateLowest = State::pending; + struct Change //change functor, stores in idModified if ID has been modified during change + { + Change(const QMap& _data); + void operator() (Message& msg); + void operator() (Message* msg); + bool hasIdBeenModified() const; + + private: + const QMap& data; + bool idModified; + }; + Message(Type p_type); Message(); @@ -72,6 +85,7 @@ public: void setErrorText(const QString& err); bool change(const QMap& data); void setStanzaId(const QString& sid); + void setAttachPath(const QString& path); QString getFrom() const; QString getFromJid() const; @@ -100,6 +114,7 @@ public: QDateTime getLastModified() const; QString getOriginalBody() const; QString getStanzaId() const; + QString getAttachPath() const; void serialize(QDataStream& data) const; void deserialize(QDataStream& data); @@ -123,6 +138,7 @@ private: QString originalMessage; QDateTime lastModified; QString stanzaId; + QString attachPath; }; } diff --git a/shared/messageinfo.cpp b/shared/messageinfo.cpp new file mode 100644 index 0000000..7502a6e --- /dev/null +++ b/shared/messageinfo.cpp @@ -0,0 +1,45 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * 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 . + */ + +#include "messageinfo.h" + +using namespace Shared; + +Shared::MessageInfo::MessageInfo(): + account(), + jid(), + messageId() {} + +Shared::MessageInfo::MessageInfo(const QString& acc, const QString& j, const QString& id): + account(acc), + jid(j), + messageId(id) {} + +Shared::MessageInfo::MessageInfo(const Shared::MessageInfo& other): + account(other.account), + jid(other.jid), + messageId(other.messageId) {} + +Shared::MessageInfo & Shared::MessageInfo::operator=(const Shared::MessageInfo& other) +{ + account = other.account; + jid = other.jid; + messageId = other.messageId; + + return *this; +} diff --git a/shared/messageinfo.h b/shared/messageinfo.h new file mode 100644 index 0000000..942d88c --- /dev/null +++ b/shared/messageinfo.h @@ -0,0 +1,43 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * 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 . + */ + +#ifndef SHARED_MESSAGEINFO_H +#define SHARED_MESSAGEINFO_H + +#include + +namespace Shared { + +/** + * @todo write docs + */ +struct MessageInfo { + MessageInfo(); + MessageInfo(const QString& acc, const QString& j, const QString& id); + MessageInfo(const MessageInfo& other); + + QString account; + QString jid; + QString messageId; + + MessageInfo& operator=(const MessageInfo& other); +}; + +} + +#endif // SHARED_MESSAGEINFO_H diff --git a/shared/utils.h b/shared/utils.h index e9e3d29..a8a17d5 100644 --- a/shared/utils.h +++ b/shared/utils.h @@ -20,11 +20,14 @@ #define SHARED_UTILS_H #include +#include #include #include +//#include "KIO/OpenFileManagerWindowJob" + #include -#include +#include namespace Shared { diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index 52913a8..11b8f3d 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.3) project(squawkUI) # Instruct CMake to run moc automatically when needed. @@ -7,9 +7,13 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) # Find the QtWidgets library -find_package(Qt5Widgets CONFIG REQUIRED) -find_package(Qt5DBus CONFIG REQUIRED) +find_package(Qt5 CONFIG REQUIRED COMPONENTS Widgets DBus Core) +find_package(Boost 1.36.0 REQUIRED) +if(Boost_FOUND) + include_directories(${Boost_INCLUDE_DIRS}) +endif() +add_subdirectory(utils) add_subdirectory(widgets) set(squawkUI_SRC @@ -25,21 +29,15 @@ set(squawkUI_SRC models/abstractparticipant.cpp models/participant.cpp models/reference.cpp - utils/messageline.cpp - utils//message.cpp - utils/resizer.cpp - utils/image.cpp - utils/flowlayout.cpp - utils/badge.cpp - utils/progress.cpp - utils/comboboxdelegate.cpp - utils/dropshadoweffect.cpp + models/messagefeed.cpp + models/element.cpp ) # Tell CMake to create the helloworld executable -add_library(squawkUI ${squawkUI_SRC}) +add_library(squawkUI STATIC ${squawkUI_SRC}) # Use the Widgets module from Qt 5. target_link_libraries(squawkUI squawkWidgets) +target_link_libraries(squawkUI squawkUIUtils) target_link_libraries(squawkUI Qt5::Widgets) target_link_libraries(squawkUI Qt5::DBus) diff --git a/ui/models/account.cpp b/ui/models/account.cpp index 00dd6b2..f8d0c37 100644 --- a/ui/models/account.cpp +++ b/ui/models/account.cpp @@ -231,7 +231,7 @@ void Models::Account::toOfflineState() Item::toOfflineState(); } -QString Models::Account::getAvatarPath() +QString Models::Account::getAvatarPath() const { return avatarPath; } diff --git a/ui/models/account.h b/ui/models/account.h index 2563382..686d4da 100644 --- a/ui/models/account.h +++ b/ui/models/account.h @@ -57,7 +57,7 @@ namespace Models { QString getError() const; void setAvatarPath(const QString& path); - QString getAvatarPath(); + QString getAvatarPath() const; void setAvailability(Shared::Availability p_avail); void setAvailability(unsigned int p_avail); diff --git a/ui/models/contact.cpp b/ui/models/contact.cpp index 57744d8..4c3432b 100644 --- a/ui/models/contact.cpp +++ b/ui/models/contact.cpp @@ -17,55 +17,26 @@ */ #include "contact.h" -#include "account.h" #include Models::Contact::Contact(const Account* acc, const QString& p_jid ,const QMap &data, Item *parentItem): - Item(Item::contact, data, parentItem), - jid(p_jid), + Element(Item::contact, acc, p_jid, data, parentItem), availability(Shared::Availability::offline), state(Shared::SubscriptionState::none), - avatarState(Shared::Avatar::empty), presences(), - messages(), - childMessages(0), - status(), - avatarPath(), - account(acc) + status() { QMap::const_iterator itr = data.find("state"); if (itr != data.end()) { setState(itr.value().toUInt()); } - - itr = data.find("avatarState"); - if (itr != data.end()) { - setAvatarState(itr.value().toUInt()); - } - itr = data.find("avatarPath"); - if (itr != data.end()) { - setAvatarPath(itr.value().toString()); - } } Models::Contact::~Contact() { } -QString Models::Contact::getJid() const -{ - return jid; -} - -void Models::Contact::setJid(const QString p_jid) -{ - if (jid != p_jid) { - jid = p_jid; - changed(1); - } -} - void Models::Contact::setAvailability(unsigned int p_state) { setAvailability(Shared::Global::fromInt(p_state)); @@ -144,16 +115,12 @@ void Models::Contact::update(const QString& field, const QVariant& value) { if (field == "name") { setName(value.toString()); - } else if (field == "jid") { - setJid(value.toString()); } else if (field == "availability") { setAvailability(value.toUInt()); } else if (field == "state") { setState(value.toUInt()); - } else if (field == "avatarState") { - setAvatarState(value.toUInt()); - } else if (field == "avatarPath") { - setAvatarPath(value.toString()); + } else { + Element::update(field, value); } } @@ -192,11 +159,9 @@ void Models::Contact::refresh() { QDateTime lastActivity; Presence* presence = 0; - unsigned int count = 0; for (QMap::iterator itr = presences.begin(), end = presences.end(); itr != end; ++itr) { Presence* pr = itr.value(); QDateTime la = pr->getLastActivity(); - count += pr->getMessagesCount(); if (la > lastActivity) { lastActivity = la; @@ -211,11 +176,6 @@ void Models::Contact::refresh() setAvailability(Shared::Availability::offline); setStatus(""); } - - if (childMessages != count) { - childMessages = count; - changed(4); - } } void Models::Contact::_removeChild(int index) @@ -251,87 +211,12 @@ QIcon Models::Contact::getStatusIcon(bool big) const if (getMessagesCount() > 0) { return Shared::icon("mail-message", big); } else if (state == Shared::SubscriptionState::both || state == Shared::SubscriptionState::to) { - return Shared::availabilityIcon(availability, big);; + return Shared::availabilityIcon(availability, big); } else { return Shared::subscriptionStateIcon(state, big); } } -void Models::Contact::addMessage(const Shared::Message& data) -{ - const QString& res = data.getPenPalResource(); - if (res.size() > 0) { - QMap::iterator itr = presences.find(res); - if (itr == presences.end()) { - // this is actually the place when I can spot someone's invisible presence, and there is nothing criminal in it, cuz the sender sent us a message - // therefore he have revealed himself - // the only issue is to find out when the sender is gone offline - Presence* pr = new Presence({}); - pr->setName(res); - pr->setAvailability(Shared::Availability::invisible); - pr->setLastActivity(QDateTime::currentDateTimeUtc()); - presences.insert(res, pr); - appendChild(pr); - pr->addMessage(data); - return; - } - itr.value()->addMessage(data); - } else { - messages.emplace_back(data); - changed(4); - } -} - -void Models::Contact::changeMessage(const QString& id, const QMap& data) -{ - - bool found = false; - for (Shared::Message& msg : messages) { - if (msg.getId() == id) { - msg.change(data); - found = true; - break; - } - } - if (!found) { - for (Presence* pr : presences) { - found = pr->changeMessage(id, data); - if (found) { - break; - } - } - } -} - -unsigned int Models::Contact::getMessagesCount() const -{ - return messages.size() + childMessages; -} - -void Models::Contact::dropMessages() -{ - if (messages.size() > 0) { - messages.clear(); - changed(4); - } - - for (QMap::iterator itr = presences.begin(), end = presences.end(); itr != end; ++itr) { - itr.value()->dropMessages(); - } -} - -void Models::Contact::getMessages(Models::Contact::Messages& container) const -{ - for (Messages::const_iterator itr = messages.begin(), end = messages.end(); itr != end; ++itr) { - const Shared::Message& msg = *itr; - container.push_back(msg); - } - - for (QMap::const_iterator itr = presences.begin(), end = presences.end(); itr != end; ++itr) { - itr.value()->getMessages(container); - } -} - void Models::Contact::toOfflineState() { std::deque::size_type size = childItems.size(); @@ -355,75 +240,3 @@ QString Models::Contact::getDisplayedName() const return getContactName(); } -bool Models::Contact::columnInvolvedInDisplay(int col) -{ - return Item::columnInvolvedInDisplay(col) && col == 1; -} - -Models::Contact * Models::Contact::copy() const -{ - Contact* cnt = new Contact(*this); - return cnt; -} - -Models::Contact::Contact(const Models::Contact& other): - Item(other), - jid(other.jid), - availability(other.availability), - state(other.state), - presences(), - messages(other.messages), - childMessages(0), - account(other.account) -{ - for (const Presence* pres : other.presences) { - Presence* pCopy = new Presence(*pres); - presences.insert(pCopy->getName(), pCopy); - Item::appendChild(pCopy); - connect(pCopy, &Item::childChanged, this, &Contact::refresh); - } - - refresh(); -} - -QString Models::Contact::getAvatarPath() const -{ - return avatarPath; -} - -Shared::Avatar Models::Contact::getAvatarState() const -{ - return avatarState; -} - -void Models::Contact::setAvatarPath(const QString& path) -{ - if (path != avatarPath) { - avatarPath = path; - changed(7); - } -} - -void Models::Contact::setAvatarState(Shared::Avatar p_state) -{ - if (avatarState != p_state) { - avatarState = p_state; - changed(6); - } -} - -void Models::Contact::setAvatarState(unsigned int p_state) -{ - if (p_state <= static_cast(Shared::Avatar::valid)) { - Shared::Avatar state = static_cast(p_state); - setAvatarState(state); - } else { - qDebug() << "An attempt to set invalid avatar state" << p_state << "to the contact" << jid << ", skipping"; - } -} - -const Models::Account * Models::Contact::getParentAccount() const -{ - return account; -} - diff --git a/ui/models/contact.h b/ui/models/contact.h index c8c99b5..7e76f5b 100644 --- a/ui/models/contact.h +++ b/ui/models/contact.h @@ -19,7 +19,7 @@ #ifndef MODELS_CONTACT_H #define MODELS_CONTACT_H -#include "item.h" +#include "element.h" #include "presence.h" #include "shared/enums.h" #include "shared/message.h" @@ -31,49 +31,34 @@ #include namespace Models { -class Account; -class Contact : public Item +class Contact : public Element { Q_OBJECT public: - typedef std::deque Messages; Contact(const Account* acc, const QString& p_jid, const QMap &data, Item *parentItem = 0); - Contact(const Contact& other); ~Contact(); - QString getJid() const; Shared::Availability getAvailability() const; Shared::SubscriptionState getState() const; - Shared::Avatar getAvatarState() const; - QString getAvatarPath() const; + QIcon getStatusIcon(bool big = false) const; int columnCount() const override; QVariant data(int column) const override; - void update(const QString& field, const QVariant& value); + void update(const QString& field, const QVariant& value) override; void addPresence(const QString& name, const QMap& data); void removePresence(const QString& name); QString getContactName() const; QString getStatus() const; - - void addMessage(const Shared::Message& data); - void changeMessage(const QString& id, const QMap& data); - unsigned int getMessagesCount() const; - void dropMessages(); - void getMessages(Messages& container) const; QString getDisplayedName() const override; - Contact* copy() const; - protected: void _removeChild(int index) override; void _appendChild(Models::Item * child) override; - bool columnInvolvedInDisplay(int col) override; - const Account* getParentAccount() const override; protected slots: void refresh(); @@ -84,23 +69,13 @@ protected: void setAvailability(unsigned int p_state); void setState(Shared::SubscriptionState p_state); void setState(unsigned int p_state); - void setAvatarState(Shared::Avatar p_state); - void setAvatarState(unsigned int p_state); - void setAvatarPath(const QString& path); - void setJid(const QString p_jid); void setStatus(const QString& p_state); private: - QString jid; Shared::Availability availability; Shared::SubscriptionState state; - Shared::Avatar avatarState; QMap presences; - Messages messages; - unsigned int childMessages; QString status; - QString avatarPath; - const Account* account; }; } diff --git a/ui/models/element.cpp b/ui/models/element.cpp new file mode 100644 index 0000000..4e741a4 --- /dev/null +++ b/ui/models/element.cpp @@ -0,0 +1,184 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * 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 . + */ + +#include "element.h" +#include "account.h" + +#include + +Models::Element::Element(Type p_type, const Models::Account* acc, const QString& p_jid, const QMap& data, Models::Item* parentItem): + Item(p_type, data, parentItem), + jid(p_jid), + avatarPath(), + avatarState(Shared::Avatar::empty), + account(acc), + feed(new MessageFeed(this)) +{ + connect(feed, &MessageFeed::requestArchive, this, &Element::requestArchive); + connect(feed, &MessageFeed::fileDownloadRequest, this, &Element::fileDownloadRequest); + connect(feed, &MessageFeed::unreadMessagesCountChanged, this, &Element::onFeedUnreadMessagesCountChanged); + connect(feed, &MessageFeed::unnoticedMessage, this, &Element::onFeedUnnoticedMessage); + connect(feed, &MessageFeed::localPathInvalid, this, &Element::localPathInvalid); + + QMap::const_iterator itr = data.find("avatarState"); + if (itr != data.end()) { + setAvatarState(itr.value().toUInt()); + } + itr = data.find("avatarPath"); + if (itr != data.end()) { + setAvatarPath(itr.value().toString()); + } +} + +Models::Element::~Element() +{ + delete feed; +} + + +QString Models::Element::getJid() const +{ + return jid; +} + +void Models::Element::setJid(const QString& p_jid) +{ + if (jid != p_jid) { + jid = p_jid; + changed(1); + } +} + +void Models::Element::update(const QString& field, const QVariant& value) +{ + if (field == "jid") { + setJid(value.toString()); + } else if (field == "avatarState") { + setAvatarState(value.toUInt()); + } else if (field == "avatarPath") { + setAvatarPath(value.toString()); + } +} + +QString Models::Element::getAvatarPath() const +{ + return avatarPath; +} + +Shared::Avatar Models::Element::getAvatarState() const +{ + return avatarState; +} + +void Models::Element::setAvatarPath(const QString& path) +{ + if (path != avatarPath) { + avatarPath = path; + if (type == contact) { + changed(7); + } else if (type == room) { + changed(8); + } + } +} + +void Models::Element::setAvatarState(Shared::Avatar p_state) +{ + if (avatarState != p_state) { + avatarState = p_state; + if (type == contact) { + changed(6); + } else if (type == room) { + changed(7); + } + } +} + +void Models::Element::setAvatarState(unsigned int p_state) +{ + if (p_state <= static_cast(Shared::Avatar::valid)) { + Shared::Avatar state = static_cast(p_state); + setAvatarState(state); + } else { + qDebug() << "An attempt to set invalid avatar state" << p_state << "to the element" << jid << ", skipping"; + } +} + +bool Models::Element::columnInvolvedInDisplay(int col) +{ + return Item::columnInvolvedInDisplay(col) && col == 1; +} + +const Models::Account * Models::Element::getParentAccount() const +{ + return account; +} + +unsigned int Models::Element::getMessagesCount() const +{ + return feed->unreadMessagesCount(); +} + +void Models::Element::addMessage(const Shared::Message& data) +{ + feed->addMessage(data); +} + +void Models::Element::changeMessage(const QString& id, const QMap& data) +{ + feed->changeMessage(id, data); +} + +void Models::Element::responseArchive(const std::list list, bool last) +{ + feed->responseArchive(list, last); +} + +bool Models::Element::isRoom() const +{ + return type != contact; +} + +void Models::Element::fileProgress(const QString& messageId, qreal value, bool up) +{ + feed->fileProgress(messageId, value, up); +} + +void Models::Element::fileComplete(const QString& messageId, bool up) +{ + feed->fileComplete(messageId, up); +} + +void Models::Element::fileError(const QString& messageId, const QString& error, bool up) +{ + feed->fileError(messageId, error, up); +} + +void Models::Element::onFeedUnreadMessagesCountChanged() +{ + if (type == contact) { + changed(4); + } else if (type == room) { + changed(5); + } +} + +void Models::Element::onFeedUnnoticedMessage(const Shared::Message& msg) +{ + emit unnoticedMessage(getAccountName(), msg); +} diff --git a/ui/models/element.h b/ui/models/element.h new file mode 100644 index 0000000..af44791 --- /dev/null +++ b/ui/models/element.h @@ -0,0 +1,81 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * 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 . + */ + +#ifndef ELEMENT_H +#define ELEMENT_H + +#include "item.h" +#include "messagefeed.h" + +namespace Models { + +class Element : public Item +{ + Q_OBJECT +protected: + Element(Type p_type, const Account* acc, const QString& p_jid, const QMap &data, Item *parentItem = 0); + ~Element(); + +public: + QString getJid() const; + Shared::Avatar getAvatarState() const; + QString getAvatarPath() const; + + virtual void update(const QString& field, const QVariant& value); + + void addMessage(const Shared::Message& data); + void changeMessage(const QString& id, const QMap& data); + unsigned int getMessagesCount() const; + void responseArchive(const std::list list, bool last); + bool isRoom() const; + void fileProgress(const QString& messageId, qreal value, bool up); + void fileError(const QString& messageId, const QString& error, bool up); + void fileComplete(const QString& messageId, bool up); + +signals: + void requestArchive(const QString& before); + void fileDownloadRequest(const QString& url); + void unnoticedMessage(const QString& account, const Shared::Message& msg); + void localPathInvalid(const QString& path); + +protected: + void setJid(const QString& p_jid); + void setAvatarState(Shared::Avatar p_state); + void setAvatarState(unsigned int p_state); + void setAvatarPath(const QString& path); + bool columnInvolvedInDisplay(int col) override; + const Account* getParentAccount() const override; + +protected slots: + void onFeedUnreadMessagesCountChanged(); + void onFeedUnnoticedMessage(const Shared::Message& msg); + +protected: + QString jid; + QString avatarPath; + Shared::Avatar avatarState; + + const Account* account; + +public: + MessageFeed* feed; +}; + +} + +#endif // ELEMENT_H diff --git a/ui/models/item.cpp b/ui/models/item.cpp index e006ad0..4a88dd2 100644 --- a/ui/models/item.cpp +++ b/ui/models/item.cpp @@ -283,6 +283,15 @@ Shared::ConnectionState Models::Item::getAccountConnectionState() const return acc->getState(); } +QString Models::Item::getAccountAvatarPath() const +{ + const Account* acc = getParentAccount(); + if (acc == nullptr) { + return ""; + } + return acc->getAvatarPath(); +} + QString Models::Item::getDisplayedName() const { return name; diff --git a/ui/models/item.h b/ui/models/item.h index 4f3e29a..4661479 100644 --- a/ui/models/item.h +++ b/ui/models/item.h @@ -80,6 +80,7 @@ class Item : public QObject{ QString getAccountName() const; QString getAccountJid() const; QString getAccountResource() const; + QString getAccountAvatarPath() const; Shared::ConnectionState getAccountConnectionState() const; Shared::Availability getAccountAvailability() const; diff --git a/ui/models/messagefeed.cpp b/ui/models/messagefeed.cpp new file mode 100644 index 0000000..4187af8 --- /dev/null +++ b/ui/models/messagefeed.cpp @@ -0,0 +1,576 @@ +/* + * Squawk messenger. + * Copyright (C) 2019 Yury Gubich + * + * 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 . + */ + +#include "messagefeed.h" +#include "element.h" +#include "room.h" + +#include + +const QHash Models::MessageFeed::roles = { + {Text, "text"}, + {Sender, "sender"}, + {Date, "date"}, + {DeliveryState, "deliveryState"}, + {Correction, "correction"}, + {SentByMe,"sentByMe"}, + {Avatar, "avatar"}, + {Attach, "attach"}, + {Id, "id"}, + {Error, "error"}, + {Bulk, "bulk"} +}; + +Models::MessageFeed::MessageFeed(const Element* ri, QObject* parent): + QAbstractListModel(parent), + storage(), + indexById(storage.get()), + indexByTime(storage.get