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..b9349d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,117 +1,106 @@ -cmake_minimum_required(VERSION 3.0) -project(squawk) +cmake_minimum_required(VERSION 3.4) +project(squawk VERSION 0.1.6 LANGUAGES CXX) -set(CMAKE_INCLUDE_CURRENT_DIR ON) -set(CMAKE_CXX_STANDARD 14) +cmake_policy(SET CMP0076 NEW) +cmake_policy(SET CMP0079 NEW) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) include(GNUInstallDirs) -include_directories(.) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") -find_package(Qt5Widgets CONFIG REQUIRED) -find_package(Qt5LinguistTools) +add_executable(squawk) +target_include_directories(squawk PRIVATE ${CMAKE_SOURCE_DIR}) -if(NOT CMAKE_BUILD_TYPE) +option(SYSTEM_QXMPP "Use system qxmpp lib" ON) +option(WITH_KWALLET "Build KWallet support module" ON) +option(WITH_KIO "Build KIO support module" ON) + +# Dependencies +## Qt +find_package(Qt5 COMPONENTS Widgets DBus Gui Xml Network Core REQUIRED) + +## QXmpp +if (SYSTEM_QXMPP) + find_package(QXmpp CONFIG) + + if (NOT QXmpp_FOUND) + set(SYSTEM_QXMPP OFF) + message("QXmpp package wasn't found, trying to build with bundled QXmpp") + else () + message("Building with system QXmpp") + endif () +endif () + +if (NOT SYSTEM_QXMPP) + target_link_libraries(squawk PRIVATE qxmpp) + add_subdirectory(external/qxmpp) +else () + target_link_libraries(squawk PRIVATE QXmpp::QXmpp) +endif () + +## KIO +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 () + target_compile_definitions(squawk PRIVATE WITH_KIO) + message("Building with support of KIO") + endif () +endif () + +## KWallet +if (WITH_KWALLET) + find_package(KF5Wallet CONFIG) + + if (NOT KF5Wallet_FOUND) + set(WITH_KWALLET OFF) + message("KWallet package wasn't found, KWallet support module wouldn't be built") + else () + target_compile_definitions(squawk PRIVATE WITH_KWALLET) + message("Building with support of KWallet") + endif () +endif () + +## Signal (TODO) +# find_package(Signal REQUIRED) + +## LMDB +find_package(LMDB REQUIRED) + +# Linking +target_link_libraries(squawk PRIVATE Qt5::Core Qt5::Widgets Qt5::DBus Qt5::Network Qt5::Gui Qt5::Xml) +target_link_libraries(squawk PRIVATE lmdb) +target_link_libraries(squawk PRIVATE simpleCrypt) +target_link_libraries(squawk PRIVATE uuid) + +# Build type +if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) -endif() +endif () -set(CMAKE_CXX_FLAGS_DEBUG "-g -Wall -Wextra") -set(CMAKE_CXX_FLAGS_RELEASE "-O3") message("Build type: ${CMAKE_BUILD_TYPE}") +target_compile_options(squawk PRIVATE + "-Wall;-Wextra" + "$<$:-g>" + "$<$:-O3>" + ) -set(squawk_SRC - main.cpp - exception.cpp - signalcatcher.cpp - shared/global.cpp - shared/utils.cpp - shared/message.cpp - shared/vcard.cpp - shared/icons.cpp -) - -set(squawk_HEAD - exception.h - signalcatcher.h - shared.h - shared/enums.h - shared/message.h - shared/global.h - shared/utils.h - shared/vcard.h - shared/icons.h -) - -configure_file(resources/images/logo.svg squawk.svg COPYONLY) -execute_process(COMMAND convert -background none -size 48x48 squawk.svg squawk48.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) -execute_process(COMMAND convert -background none -size 64x64 squawk.svg squawk64.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) -execute_process(COMMAND convert -background none -size 128x128 squawk.svg squawk128.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) -execute_process(COMMAND convert -background none -size 256x256 squawk.svg squawk256.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -configure_file(packaging/squawk.desktop squawk.desktop COPYONLY) - -set(TS_FILES - translations/squawk.ru.ts -) -qt5_add_translation(QM_FILES ${TS_FILES}) -add_custom_target(translations ALL DEPENDS ${QM_FILES}) - -qt5_add_resources(RCC resources/resources.qrc) - -option(SYSTEM_QXMPP "Use system qxmpp lib" ON) -option(WITH_KWALLET "Build KWallet support module" ON) - -if (SYSTEM_QXMPP) - find_package(QXmpp CONFIG) - - if (NOT QXmpp_FOUND) - set(SYSTEM_QXMPP OFF) - message("QXmpp package wasn't found, trying to build with bundled QXmpp") - else() - message("Building with system QXmpp") - endif() -endif() - -if(NOT SYSTEM_QXMPP) - add_subdirectory(external/qxmpp) -endif() - -if (WITH_KWALLET) - find_package(KF5Wallet CONFIG) - - if (NOT KF5Wallet_FOUND) - set(WITH_KWALLET OFF) - message("KWallet package wasn't found, KWallet support module wouldn't be built") - else() - add_definitions(-DWITH_KWALLET) - message("Building with support of KWallet") - endif() -endif() - -add_executable(squawk ${squawk_SRC} ${squawk_HEAD} ${RCC}) -target_link_libraries(squawk Qt5::Widgets) - -add_subdirectory(ui) add_subdirectory(core) - add_subdirectory(external/simpleCrypt) - -target_link_libraries(squawk squawkUI) -target_link_libraries(squawk squawkCORE) -target_link_libraries(squawk uuid) - -add_dependencies(${CMAKE_PROJECT_NAME} translations) +add_subdirectory(packaging) +add_subdirectory(plugins) +add_subdirectory(resources) +add_subdirectory(shared) +add_subdirectory(translations) +add_subdirectory(ui) # Install the executable install(TARGETS squawk DESTINATION ${CMAKE_INSTALL_BINDIR}) -install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/squawk/l10n) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk48.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/48x48/apps RENAME squawk.png) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk64.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps RENAME squawk.png) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk128.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps RENAME squawk.png) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk256.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/256x256/apps RENAME squawk.png) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) 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..79788f1 --- /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/cmake/FindSignal.cmake b/cmake/FindSignal.cmake new file mode 100644 index 0000000..752fed7 --- /dev/null +++ b/cmake/FindSignal.cmake @@ -0,0 +1,15 @@ +find_path(Signal_INCLUDE_DIR NAMES signal/signal_protocol.h) +find_library(Signal_LIBRARY signal-protocol-c) +mark_as_advanced(Signal_INCLUDE_DIR Signal_LIBRARY) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Signal REQUIRED_VARS Signal_LIBRARY Signal_INCLUDE_DIR) + +if (Signal_FOUND AND NOT TARGET Signal::Signal) + add_library(Signal::Signal UNKNOWN IMPORTED) + set_target_properties(Signal::Signal PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + IMPORTED_LOCATION "${Signal_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Signal_INCLUDE_DIR}" + ) +endif () diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index b74a055..3b160e2 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,46 +1,27 @@ -cmake_minimum_required(VERSION 3.0) -project(squawkCORE) - -set(CMAKE_AUTOMOC ON) - -find_package(Qt5Core CONFIG REQUIRED) -find_package(Qt5Gui CONFIG REQUIRED) -find_package(Qt5Network CONFIG REQUIRED) -find_package(Qt5Xml CONFIG REQUIRED) - -set(squawkCORE_SRC - squawk.cpp - account.cpp - archive.cpp - rosteritem.cpp - contact.cpp - conference.cpp - storage.cpp - networkaccess.cpp - adapterFuctions.cpp - handlers/messagehandler.cpp - handlers/rosterhandler.cpp -) +target_sources(squawk PRIVATE + account.cpp + account.h + adapterFuctions.cpp + archive.cpp + archive.h + conference.cpp + conference.h + contact.cpp + contact.h + main.cpp + networkaccess.cpp + networkaccess.h + rosteritem.cpp + rosteritem.h + signalcatcher.cpp + signalcatcher.h + squawk.cpp + squawk.h + storage.cpp + storage.h + urlstorage.cpp + urlstorage.h + ) +add_subdirectory(handlers) add_subdirectory(passwordStorageEngines) - -# Tell CMake to create the helloworld executable -add_library(squawkCORE ${squawkCORE_SRC}) - - -if(SYSTEM_QXMPP) - get_target_property(QXMPP_INTERFACE_INCLUDE_DIRECTORIES QXmpp::QXmpp INTERFACE_INCLUDE_DIRECTORIES) - target_include_directories(squawkCORE PUBLIC ${QXMPP_INTERFACE_INCLUDE_DIRECTORIES}) -endif() - -# Use the Widgets module from Qt 5. -target_link_libraries(squawkCORE Qt5::Core) -target_link_libraries(squawkCORE Qt5::Network) -target_link_libraries(squawkCORE Qt5::Gui) -target_link_libraries(squawkCORE Qt5::Xml) -target_link_libraries(squawkCORE qxmpp) -target_link_libraries(squawkCORE lmdb) -target_link_libraries(squawkCORE simpleCrypt) -if (WITH_KWALLET) - target_link_libraries(squawkCORE kwalletPSE) -endif() diff --git a/core/account.cpp b/core/account.cpp index 094fd3c..6784674 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,19 @@ 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);} + +void Core::Account::resendMessage(const QString& jid, const QString& id) { + mh->resendMessage(jid, id);} diff --git a/core/account.h b/core/account.h index 49c7ca9..5ba834c 100644 --- a/core/account.h +++ b/core/account.h @@ -43,7 +43,7 @@ #include #include -#include "shared.h" +#include "shared/shared.h" #include "contact.h" #include "conference.h" #include "networkaccess.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,12 +96,14 @@ 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); void removeRoomRequest(const QString& jid); void addRoomRequest(const QString& jid, const QString& nick, const QString& password, bool autoJoin); void uploadVCard(const Shared::VCard& card); + void resendMessage(const QString& jid, const QString& id); public slots: void connect(); @@ -127,14 +128,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 +184,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..2582ff9 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 0 && (idChange || !hadStanzaId)) { - const std::string& szid = msg.getStanzaId().toStdString(); + QString qsid = msg.getStanzaId(); + if (qsid.size() > 0 && (idChange || !hadStanzaId)) { + std::string szid = qsid.toStdString(); lmdbData.mv_size = szid.size(); lmdbData.mv_data = (char*)szid.c_str(); @@ -502,8 +511,9 @@ long unsigned int Core::Archive::size() const mdb_txn_begin(environment, NULL, MDB_RDONLY, &txn); MDB_stat stat; mdb_stat(txn, order, &stat); + size_t amount = stat.ms_entries; mdb_txn_abort(txn); - return stat.ms_entries; + return amount; } std::list Core::Archive::getBefore(int count, const QString& id) @@ -603,10 +613,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/archive.h b/core/archive.h index dd7a167..47c62dc 100644 --- a/core/archive.h +++ b/core/archive.h @@ -25,7 +25,7 @@ #include #include "shared/message.h" -#include "exception.h" +#include "shared/exception.h" #include #include diff --git a/core/handlers/CMakeLists.txt b/core/handlers/CMakeLists.txt new file mode 100644 index 0000000..6da2ef3 --- /dev/null +++ b/core/handlers/CMakeLists.txt @@ -0,0 +1,6 @@ +target_sources(squawk PRIVATE + messagehandler.cpp + messagehandler.h + rosterhandler.cpp + rosterhandler.h + ) diff --git a/core/handlers/messagehandler.cpp b/core/handlers/messagehandler.cpp index 0f0e09d..33b3458 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() { } @@ -74,8 +73,7 @@ void Core::MessageHandler::onMessageReceived(const QXmppMessage& msg) bool Core::MessageHandler::handleChatMessage(const QXmppMessage& msg, bool outgoing, bool forwarded, bool guessing) { - const QString& body(msg.body()); - if (body.size() != 0) { + if (msg.body().size() != 0 || msg.outOfBandUrl().size() > 0) { Shared::Message sMsg(Shared::Message::chat); initializeMessage(sMsg, msg, outgoing, forwarded, guessing); QString jid = sMsg.getPenPalJid(); @@ -168,14 +166,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 +189,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) @@ -227,16 +233,29 @@ void Core::MessageHandler::onReceiptReceived(const QString& jid, const QString& if (ri != 0) { ri->changeMessage(id, cData); } - pendingStateMessages.erase(itr); emit acc->changeMessage(itr->second, id, cData); + pendingStateMessages.erase(itr); } } -void Core::MessageHandler::sendMessage(Shared::Message data) +void Core::MessageHandler::sendMessage(const Shared::Message& data, bool newMessage) +{ + if (data.getOutOfBandUrl().size() == 0 && data.getAttachPath().size() > 0) { + prepareUpload(data, newMessage); + } else { + performSending(data, newMessage); + } +} + +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; + QDateTime sendTime = QDateTime::currentDateTimeUtc(); if (acc->state == Shared::ConnectionState::connected) { QXmppMessage msg(acc->getFullJid(), data.getTo(), data.getBody(), data.getThread()); @@ -245,23 +264,18 @@ 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); + msg.setStamp(sendTime); - bool sent = acc->client.sendPacket(msg); + sent = acc->client.sendPacket(msg); + //sent = false; 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,57 +283,97 @@ 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) { + data.setTime(sendTime); + } + changes.insert("stamp", sendTime); + + 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, bool newMessage) { 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); + sendMessageWithLocalUploadedFile(data, url, newMessage); } else { - if (acc->network->isUploading(path, data.getId())) { - pendingMessages.emplace(data.getId(), data); + pendingStateMessages.insert(std::make_pair(id, jid)); + if (newMessage) { + ri->appendMessageToArchive(data); } else { + QMap changes({ + {"state", (uint)Shared::Message::State::pending} + }); + ri->changeMessage(id, changes); + emit acc->changeMessage(jid, id, changes); + } + //this checks if the file is already uploading, and if so it subscribes to it's success, so, i need to do stuff only if the network knows nothing of this file + if (!acc->network->checkAndAddToUploading(acc->getName(), jid, id, path)) { if (acc->um->serviceFound()) { QFileInfo file(path); if (file.exists() && file.isReadable()) { - uploadingSlotsQueue.emplace_back(path, 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"; } } - void Core::MessageHandler::onUploadSlotReceived(const QXmppHttpUploadSlotIq& slot) { 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 +382,130 @@ 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"; + } + } +} + +void Core::MessageHandler::resendMessage(const QString& jid, const QString& id) +{ + RosterItem* cnt = acc->rh->getRosterItem(jid); + if (cnt != 0) { + try { + Shared::Message msg = cnt->getMessage(id); + if (msg.getState() == Shared::Message::State::error) { + sendMessage(msg, false); + } else { + qDebug() << "An attempt to resend a message to" << jid << "by account" << acc->getName() << ", but this message seems to have been normally sent, this method was made to retry sending failed to be sent messages, skipping"; + } + } catch (const Archive::NotFound& err) { + qDebug() << "An attempt to resend a message to" << jid << "by account" << acc->getName() << ", but this message wasn't found in history, skipping"; + } + } else { + qDebug() << "An attempt to resend a message to" << jid << "by account" << acc->getName() << ", but this jid isn't present in account roster, skipping"; + } +} diff --git a/core/handlers/messagehandler.h b/core/handlers/messagehandler.h index be1545f..4eb9265 100644 --- a/core/handlers/messagehandler.h +++ b/core/handlers/messagehandler.h @@ -28,6 +28,7 @@ #include #include +#include namespace Core { @@ -44,9 +45,9 @@ 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, bool newMessage = true); void initializeMessage(Shared::Message& target, const QXmppMessage& source, bool outgoing = false, bool forwarded = false, bool guessing = false) const; + void resendMessage(const QString& jid, const QString& id); public slots: void onMessageReceived(const QXmppMessage& message); @@ -55,20 +56,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, bool newMessage = true); + 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/main.cpp b/core/main.cpp similarity index 87% rename from main.cpp rename to core/main.cpp index 4c4b3ea..0be020e 100644 --- a/main.cpp +++ b/core/main.cpp @@ -16,23 +16,26 @@ * along with this program. If not, see . */ -#include "ui/squawk.h" -#include "core/squawk.h" +#include "../shared/global.h" +#include "../shared/messageinfo.h" +#include "../ui/squawk.h" #include "signalcatcher.h" -#include "shared/global.h" -#include -#include -#include -#include -#include +#include "squawk.h" #include +#include #include +#include +#include +#include +#include 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,8 @@ 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::resendMessage, squawk,&Core::Squawk::resendMessage); 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 +110,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 +142,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/core/networkaccess.cpp b/core/networkaccess.cpp index 2d66a70..69fe812 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,10 @@ void Core::NetworkAccess::start() { if (!running) { manager = new QNetworkAccessManager(); - files.open(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + manager->setTransferTimeout(); +#endif + storage.open(); running = true; } } @@ -103,7 +81,7 @@ void Core::NetworkAccess::start() void Core::NetworkAccess::stop() { if (running) { - files.close(); + storage.close(); manager->deleteLater(); manager = 0; running = false; @@ -124,35 +102,56 @@ void Core::NetworkAccess::onDownloadProgress(qint64 bytesReceived, qint64 bytesT qDebug() << "an error downloading" << url << ": the request had some progress but seems like no one is waiting for it, skipping"; } else { Transfer* dwn = itr->second; - qreal received = bytesReceived; - 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); + if (dwn->success) { + qreal received = bytesReceived; + qreal total = bytesTotal; + qreal progress = received/total; + dwn->progress = progress; + emit loadFileProgress(dwn->messages, progress, false); } } } void Core::NetworkAccess::onDownloadError(QNetworkReply::NetworkError code) { + qDebug() << "DEBUG: DOWNLOAD ERROR"; QNetworkReply* rpl = static_cast(sender()); + qDebug() << rpl->errorString(); 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 reporting an error but seems like no one is waiting for it, skipping"; } else { QString errorText = getErrorText(code); - if (errorText.size() > 0) { + //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); + //} } } +void Core::NetworkAccess::onDownloadSSLError(const QList& errors) +{ + qDebug() << "DEBUG: DOWNLOAD SSL ERRORS"; + for (const QSslError& err : errors) { + qDebug() << err.errorString(); + } + QNetworkReply* rpl = static_cast(sender()); + QString url = rpl->url().toString(); + std::map::const_iterator itr = downloads.find(url); + if (itr == downloads.end()) { + qDebug() << "an SSL error downloading" << url << ": the request is reporting an error but seems like no one is waiting for it, skipping"; + } else { + //if (errorText.size() > 0) { + itr->second->success = false; + Transfer* dwn = itr->second; + emit loadFileError(dwn->messages, "SSL errors occured", false); + //} + } +} + + QString Core::NetworkAccess::getErrorText(QNetworkReply::NetworkError code) { QString errorText(""); @@ -175,7 +174,11 @@ QString Core::NetworkAccess::getErrorText(QNetworkReply::NetworkError code) errorText = "Connection was closed because it timed out"; break; case QNetworkReply::OperationCanceledError: - //this means I closed it myself by abort() or close(), don't think I need to notify here + //this means I closed it myself by abort() or close() + //I don't call them directory, but this is the error code + //Qt returns when it can not resume donwload after the network failure + //or when the download is canceled by the timout; + errorText = "Connection lost"; break; case QNetworkReply::SslHandshakeFailedError: errorText = "Security error"; //TODO need to handle sslErrors signal to get a better description here @@ -276,64 +279,63 @@ QString Core::NetworkAccess::getErrorText(QNetworkReply::NetworkError code) void Core::NetworkAccess::onDownloadFinished() { - QString path(""); + qDebug() << "DEBUG: DOWNLOAD FINISHED"; 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; + QString err; 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; + } else { + qDebug() << "An attempt to save the file but it doesn't seem to belong to any message, download is definately going to be broken"; } - 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; + err = "Error opening file to write:" + file.errorString(); + } + } else { + err = "Couldn't prepare a directory for file"; } - 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 = ""; + emit loadFileError(dwn->messages, "Error saving file " + url + "; " + err, 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); + connect(dwn->reply, &QNetworkReply::sslErrors, this, &NetworkAccess::onDownloadSSLError); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) connect(dwn->reply, qOverload(&QNetworkReply::errorOccurred), this, &NetworkAccess::onDownloadError); #else @@ -341,7 +343,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 +352,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) { + //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 +371,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(); @@ -399,98 +398,35 @@ void Core::NetworkAccess::onUploadProgress(qint64 bytesReceived, qint64 bytesTot qDebug() << "an error downloading" << url << ": the request had some progress but seems like no one is waiting for it, skipping"; } else { Transfer* upl = itr->second; - qreal received = bytesReceived; - 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()); + if (upl->success) { + qreal received = bytesReceived; + qreal total = bytesTotal; + qreal progress = received/total; + upl->progress = progress; + 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 +442,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..75c189c 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,30 +50,32 @@ 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); void onDownloadError(QNetworkReply::NetworkError code); + void onDownloadSSLError(const QList &errors); void onDownloadFinished(); void onUploadProgress(qint64 bytesReceived, qint64 bytesTotal); void onUploadError(QNetworkReply::NetworkError code); @@ -80,12 +84,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..4da3873 100644 --- a/core/passwordStorageEngines/CMakeLists.txt +++ b/core/passwordStorageEngines/CMakeLists.txt @@ -1,37 +1,9 @@ -cmake_minimum_required(VERSION 3.0) -project(pse) - -if (WITH_KWALLET) - set(CMAKE_AUTOMOC ON) - - find_package(Qt5Core CONFIG REQUIRED) - find_package(Qt5Gui CONFIG REQUIRED) - - get_target_property(KWALLET_INTERFACE_INCLUDE_DIRECTORIES KF5::Wallet INTERFACE_INCLUDE_DIRECTORIES) - get_target_property(Qt5GUI_INTERFACE_INCLUDE_DIRECTORIES Qt5::Gui INTERFACE_INCLUDE_DIRECTORIES) - - set(kwalletPSE_SRC - kwallet.cpp - ) - - add_library(kwalletPSE ${kwalletPSE_SRC}) - - target_include_directories(kwalletPSE PUBLIC ${KWALLET_INTERFACE_INCLUDE_DIRECTORIES}) - target_include_directories(kwalletPSE PUBLIC ${Qt5GUI_INTERFACE_INCLUDE_DIRECTORIES}) - - target_link_libraries(kwalletPSE Qt5::Core) - - set(kwalletW_SRC - wrappers/kwallet.cpp +if (WITH_KWALLET) + target_sources(squawk PRIVATE + kwallet.cpp + kwallet.h ) - add_library(kwalletWrapper SHARED ${kwalletW_SRC}) - - target_include_directories(kwalletWrapper PUBLIC ${KWALLET_INTERFACE_INCLUDE_DIRECTORIES}) - target_include_directories(kwalletWrapper PUBLIC ${Qt5GUI_INTERFACE_INCLUDE_DIRECTORIES}) - - target_link_libraries(kwalletWrapper KF5::Wallet) - target_link_libraries(kwalletWrapper Qt5::Core) - - install(TARGETS kwalletWrapper DESTINATION ${CMAKE_INSTALL_LIBDIR}) -endif() + add_subdirectory(wrappers) + target_include_directories(squawk PRIVATE $) +endif () diff --git a/core/passwordStorageEngines/wrappers/CMakeLists.txt b/core/passwordStorageEngines/wrappers/CMakeLists.txt new file mode 100644 index 0000000..6d486c0 --- /dev/null +++ b/core/passwordStorageEngines/wrappers/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(kwalletWrapper SHARED kwallet.cpp) +target_link_libraries(kwalletWrapper PRIVATE KF5::Wallet) 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/signalcatcher.cpp b/core/signalcatcher.cpp similarity index 100% rename from signalcatcher.cpp rename to core/signalcatcher.cpp diff --git a/signalcatcher.h b/core/signalcatcher.h similarity index 100% rename from signalcatcher.h rename to core/signalcatcher.h diff --git a/core/squawk.cpp b/core/squawk.cpp index 1689d71..6b8af49 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}, @@ -329,22 +328,22 @@ void Core::Squawk::sendMessage(const QString& account, const Shared::Message& da { AccountsMap::const_iterator itr = amap.find(account); if (itr == amap.end()) { - qDebug("An attempt to send a message with non existing account, skipping"); + qDebug() << "An attempt to send a message with non existing account" << account << ", skipping"; return; } itr->second->sendMessage(data); } -void Core::Squawk::sendMessage(const QString& account, const Shared::Message& data, const QString& path) +void Core::Squawk::resendMessage(const QString& account, const QString& jid, const QString& id) { AccountsMap::const_iterator itr = amap.find(account); if (itr == amap.end()) { - qDebug("An attempt to send a message with non existing account, skipping"); + qDebug() << "An attempt to resend a message with non existing account" << account << ", skipping"; return; } - itr->second->sendMessage(data, path); + itr->second->resendMessage(jid, id); } void Core::Squawk::requestArchive(const QString& account, const QString& jid, int count, const QString& before) @@ -357,10 +356,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 +603,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 +682,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 +756,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..338eb40 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,19 @@ 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 resendMessage(const QString& account, const QString& jid, const QString& id); 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 +111,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 +161,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 +170,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..f59ff62 --- /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..274d304 100644 --- a/external/simpleCrypt/CMakeLists.txt +++ b/external/simpleCrypt/CMakeLists.txt @@ -1,16 +1,10 @@ cmake_minimum_required(VERSION 3.0) -project(simplecrypt) +project(simplecrypt LANGUAGES CXX) set(CMAKE_AUTOMOC ON) -find_package(Qt5Core CONFIG REQUIRED) +find_package(Qt5 COMPONENTS Core REQUIRED) -set(simplecrypt_SRC - simplecrypt.cpp -) +add_library(simpleCrypt STATIC simplecrypt.cpp simplecrypt.h) -# Tell CMake to create the helloworld executable -add_library(simpleCrypt ${simplecrypt_SRC}) - -# Use the Widgets module from Qt 5. target_link_libraries(simpleCrypt Qt5::Core) diff --git a/packaging/CMakeLists.txt b/packaging/CMakeLists.txt new file mode 100644 index 0000000..4965b37 --- /dev/null +++ b/packaging/CMakeLists.txt @@ -0,0 +1,3 @@ +configure_file(squawk.desktop squawk.desktop COPYONLY) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) \ No newline at end of file diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..84fc09b --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,4 @@ +if (WITH_KIO) + add_library(openFileManagerWindowJob SHARED openfilemanagerwindowjob.cpp) + target_link_libraries(openFileManagerWindowJob PRIVATE KF5::KIOWidgets) +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/CMakeLists.txt b/resources/CMakeLists.txt new file mode 100644 index 0000000..86433f3 --- /dev/null +++ b/resources/CMakeLists.txt @@ -0,0 +1,14 @@ +target_sources(squawk PRIVATE resources.qrc) + +configure_file(images/logo.svg squawk.svg COPYONLY) + +execute_process(COMMAND convert -background none -size 48x48 squawk.svg squawk48.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND convert -background none -size 64x64 squawk.svg squawk64.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND convert -background none -size 128x128 squawk.svg squawk128.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND convert -background none -size 256x256 squawk.svg squawk256.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk48.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/48x48/apps RENAME squawk.png) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk64.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps RENAME squawk.png) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk128.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps RENAME squawk.png) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/squawk256.png DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/256x256/apps RENAME squawk.png) 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/CMakeLists.txt b/shared/CMakeLists.txt new file mode 100644 index 0000000..a36b516 --- /dev/null +++ b/shared/CMakeLists.txt @@ -0,0 +1,19 @@ +target_sources(squawk PRIVATE + enums.h + global.cpp + global.h + exception.cpp + exception.h + icons.cpp + icons.h + message.cpp + message.h + messageinfo.cpp + messageinfo.h + order.h + shared.h + utils.cpp + utils.h + vcard.cpp + vcard.h + ) diff --git a/exception.cpp b/shared/exception.cpp similarity index 100% rename from exception.cpp rename to shared/exception.cpp diff --git a/exception.h b/shared/exception.h similarity index 100% rename from exception.h rename to shared/exception.h diff --git a/shared/global.cpp b/shared/global.cpp index a6b7b60..67e74d1 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,72 @@ 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 } + +static const QSize defaultIconFileInfoHeight(50, 50); +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") { + QMovie mov(path); + if (mov.isValid() && mov.frameCount() > 1) { + p = FileInfo::Preview::animation; + } else { + p = FileInfo::Preview::picture; + } + QImageReader img(path); + size = img.size(); +// } else if (big == "video") { +// p = FileInfo::Preview::movie; +// QMovie mov(path); +// size = mov.scaledSize(); +// qDebug() << mov.isValid(); + } else { + size = defaultIconFileInfoHeight; + } + + 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 +213,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..03cf84d 100644 --- a/shared/global.h +++ b/shared/global.h @@ -29,6 +29,18 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace Shared { @@ -36,6 +48,19 @@ namespace Shared { Q_DECLARE_TR_FUNCTIONS(Global) public: + struct FileInfo { + enum class Preview { + none, + picture, + animation + }; + + QString name; + QSize size; + QMimeType mime; + Preview preview; + }; + Global(); static Global* getInstance(); @@ -64,6 +89,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 +115,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/order.h b/shared/order.h similarity index 100% rename from order.h rename to shared/order.h diff --git a/shared.h b/shared/shared.h similarity index 83% rename from shared.h rename to shared/shared.h index 83bcd76..1e86c5a 100644 --- a/shared.h +++ b/shared/shared.h @@ -19,11 +19,12 @@ #ifndef SHARED_H #define SHARED_H -#include "shared/enums.h" -#include "shared/utils.h" -#include "shared/icons.h" -#include "shared/message.h" -#include "shared/vcard.h" -#include "shared/global.h" +#include "enums.h" +#include "global.h" +#include "icons.h" +#include "message.h" +#include "messageinfo.h" +#include "utils.h" +#include "vcard.h" #endif // SHARED_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/translations/CMakeLists.txt b/translations/CMakeLists.txt new file mode 100644 index 0000000..c484000 --- /dev/null +++ b/translations/CMakeLists.txt @@ -0,0 +1,8 @@ +find_package(Qt5LinguistTools) + +set(TS_FILES squawk.ru.ts) +qt5_add_translation(QM_FILES ${TS_FILES}) +add_custom_target(translations ALL DEPENDS ${QM_FILES}) +install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/squawk/l10n) + +add_dependencies(${CMAKE_PROJECT_NAME} translations) \ No newline at end of file diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index 52913a8..36207b6 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -1,45 +1,5 @@ -cmake_minimum_required(VERSION 3.0) -project(squawkUI) - -# Instruct CMake to run moc automatically when needed. -set(CMAKE_AUTOMOC ON) -# Instruct CMake to create code from Qt designer ui files -set(CMAKE_AUTOUIC ON) - -# Find the QtWidgets library -find_package(Qt5Widgets CONFIG REQUIRED) -find_package(Qt5DBus CONFIG REQUIRED) +target_sources(squawk PRIVATE squawk.cpp squawk.h squawk.ui) +add_subdirectory(models) +add_subdirectory(utils) add_subdirectory(widgets) - -set(squawkUI_SRC - squawk.cpp - models/accounts.cpp - models/roster.cpp - models/item.cpp - models/account.cpp - models/contact.cpp - models/presence.cpp - models/group.cpp - models/room.cpp - 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 -) - -# Tell CMake to create the helloworld executable -add_library(squawkUI ${squawkUI_SRC}) - -# Use the Widgets module from Qt 5. -target_link_libraries(squawkUI squawkWidgets) -target_link_libraries(squawkUI Qt5::Widgets) -target_link_libraries(squawkUI Qt5::DBus) diff --git a/ui/models/CMakeLists.txt b/ui/models/CMakeLists.txt new file mode 100644 index 0000000..629db32 --- /dev/null +++ b/ui/models/CMakeLists.txt @@ -0,0 +1,26 @@ +target_sources(squawk PRIVATE + abstractparticipant.cpp + abstractparticipant.h + account.cpp + account.h + accounts.cpp + accounts.h + contact.cpp + contact.h + element.cpp + element.h + group.cpp + group.h + item.cpp + item.h + participant.cpp + participant.h + presence.cpp + presence.h + reference.cpp + reference.h + room.cpp + room.h + roster.cpp + roster.h + ) diff --git a/ui/models/account.cpp b/ui/models/account.cpp index 00dd6b2..43cb3ed 100644 --- a/ui/models/account.cpp +++ b/ui/models/account.cpp @@ -31,7 +31,8 @@ Models::Account::Account(const QMap& data, Models::Item* pare avatarPath(data.value("avatarPath").toString()), state(Shared::ConnectionState::disconnected), availability(Shared::Availability::offline), - passwordType(Shared::AccountPassword::plain) + passwordType(Shared::AccountPassword::plain), + wasEverConnected(false) { QMap::const_iterator sItr = data.find("state"); if (sItr != data.end()) { @@ -56,8 +57,19 @@ void Models::Account::setState(Shared::ConnectionState p_state) if (state != p_state) { state = p_state; changed(2); - if (state == Shared::ConnectionState::disconnected) { - toOfflineState(); + switch (state) { + case Shared::ConnectionState::disconnected: + toOfflineState(); + break; + case Shared::ConnectionState::connected: + if (wasEverConnected) { + emit reconnected(); + } else { + wasEverConnected = true; + } + break; + default: + break; } } } @@ -231,7 +243,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..3d2310f 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); @@ -77,6 +77,9 @@ namespace Models { QString getBareJid() const; QString getFullJid() const; + signals: + void reconnected(); + private: QString login; QString password; @@ -87,6 +90,7 @@ namespace Models { Shared::ConnectionState state; Shared::Availability availability; Shared::AccountPassword passwordType; + bool wasEverConnected; protected slots: void toOfflineState() override; diff --git a/ui/models/contact.cpp b/ui/models/contact.cpp index 57744d8..a0c70ac 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) @@ -257,81 +217,6 @@ QIcon Models::Contact::getStatusIcon(bool big) const } } -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,9 @@ QString Models::Contact::getDisplayedName() const return getContactName(); } -bool Models::Contact::columnInvolvedInDisplay(int col) +void Models::Contact::handleRecconnect() { - 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); + if (getMessagesCount() > 0) { + feed->requestLatestMessages(); } } - -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..a8b80a3 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,36 @@ #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; + void handleRecconnect(); //this is a special method Models::Roster calls when reconnect happens 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 +71,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..94d67cb --- /dev/null +++ b/ui/models/element.h @@ -0,0 +1,82 @@ +/* + * 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 "ui/widgets/messageline/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/presence.cpp b/ui/models/presence.cpp index bf931e9..8ba7c47 100644 --- a/ui/models/presence.cpp +++ b/ui/models/presence.cpp @@ -20,82 +20,15 @@ #include "shared/icons.h" Models::Presence::Presence(const QMap& data, Item* parentItem): - AbstractParticipant(Item::presence, data, parentItem), - messages() + AbstractParticipant(Item::presence, data, parentItem) { } -Models::Presence::Presence(const Models::Presence& other): - AbstractParticipant(other), - messages(other.messages) -{ -} - - Models::Presence::~Presence() { } int Models::Presence::columnCount() const { - return 5; -} - -QVariant Models::Presence::data(int column) const -{ - switch (column) { - case 4: - return getMessagesCount(); - default: - return AbstractParticipant::data(column); - } -} - -unsigned int Models::Presence::getMessagesCount() const -{ - return messages.size(); -} - -void Models::Presence::addMessage(const Shared::Message& data) -{ - messages.emplace_back(data); - changed(4); -} - -bool Models::Presence::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; - } - } - return found; -} - -void Models::Presence::dropMessages() -{ - if (messages.size() > 0) { - messages.clear(); - changed(4); - } -} - -QIcon Models::Presence::getStatusIcon(bool big) const -{ - if (getMessagesCount() > 0) { - return Shared::icon("mail-message", big); - } else { - return AbstractParticipant::getStatusIcon(); - } -} - -void Models::Presence::getMessages(Models::Presence::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); - } + return 4; } diff --git a/ui/models/presence.h b/ui/models/presence.h index fc430f0..fb1a31c 100644 --- a/ui/models/presence.h +++ b/ui/models/presence.h @@ -32,25 +32,10 @@ class Presence : public Models::AbstractParticipant { Q_OBJECT public: - typedef std::deque Messages; explicit Presence(const QMap &data, Item *parentItem = 0); - Presence(const Presence& other); ~Presence(); int columnCount() const override; - QVariant data(int column) const override; - - QIcon getStatusIcon(bool big = false) const override; - - unsigned int getMessagesCount() const; - void dropMessages(); - void addMessage(const Shared::Message& data); - bool changeMessage(const QString& id, const QMap& data); - - void getMessages(Messages& container) const; - -private: - Messages messages; }; } diff --git a/ui/models/reference.cpp b/ui/models/reference.cpp index cb8efad..1aaea15 100644 --- a/ui/models/reference.cpp +++ b/ui/models/reference.cpp @@ -104,6 +104,8 @@ void Models::Reference::onChildChanged(Models::Item* item, int row, int col) { if (item == original) { emit childChanged(this, row, col); + } else { + emit childChanged(item, row, col); } } diff --git a/ui/models/room.cpp b/ui/models/room.cpp index cc19d2c..a6a36d0 100644 --- a/ui/models/room.cpp +++ b/ui/models/room.cpp @@ -22,16 +22,12 @@ #include #include -Models::Room::Room(const QString& p_jid, const QMap& data, Models::Item* parentItem): - Item(room, data, parentItem), +Models::Room::Room(const Account* acc, const QString& p_jid, const QMap& data, Models::Item* parentItem): + Element(room, acc, p_jid, data, parentItem), autoJoin(false), joined(false), - jid(p_jid), nick(""), subject(""), - avatarState(Shared::Avatar::empty), - avatarPath(""), - messages(), participants(), exParticipantAvatars() { @@ -55,16 +51,6 @@ Models::Room::Room(const QString& p_jid, const QMap& data, Mo setSubject(itr.value().toString()); } - itr = data.find("avatarState"); - if (itr != data.end()) { - setAvatarState(itr.value().toUInt()); - } - itr = data.find("avatarPath"); - if (itr != data.end()) { - setAvatarPath(itr.value().toString()); - } - - itr = data.find("avatars"); if (itr != data.end()) { QMap avs = itr.value().toMap(); @@ -78,21 +64,11 @@ Models::Room::~Room() { } -unsigned int Models::Room::getUnreadMessagesCount() const -{ - return messages.size(); -} - int Models::Room::columnCount() const { return 7; } -QString Models::Room::getJid() const -{ - return jid; -} - bool Models::Room::getAutoJoin() const { return autoJoin; @@ -151,14 +127,6 @@ void Models::Room::setAutoJoin(bool p_autoJoin) } } -void Models::Room::setJid(const QString& p_jid) -{ - if (jid != p_jid) { - jid = p_jid; - changed(1); - } -} - void Models::Room::setJoined(bool p_joined) { if (joined != p_joined) { @@ -182,8 +150,6 @@ void Models::Room::update(const QString& field, const QVariant& value) { if (field == "name") { setName(value.toString()); - } else if (field == "jid") { - setJid(value.toString()); } else if (field == "joined") { setJoined(value.toBool()); } else if (field == "autoJoin") { @@ -192,16 +158,14 @@ void Models::Room::update(const QString& field, const QVariant& value) setNick(value.toString()); } else if (field == "subject") { setSubject(value.toString()); - } else if (field == "avatarState") { - setAvatarState(value.toUInt()); - } else if (field == "avatarPath") { - setAvatarPath(value.toString()); + } else { + Element::update(field, value); } } QIcon Models::Room::getStatusIcon(bool big) const { - if (messages.size() > 0) { + if (getMessagesCount() > 0) { return Shared::icon("mail-message", big); } else { if (autoJoin) { @@ -237,42 +201,6 @@ QString Models::Room::getStatusText() const } } -unsigned int Models::Room::getMessagesCount() const -{ - return messages.size(); -} - -void Models::Room::addMessage(const Shared::Message& data) -{ - messages.emplace_back(data); - changed(5); -} - -void Models::Room::changeMessage(const QString& id, const QMap& data) -{ - for (Shared::Message& msg : messages) { - if (msg.getId() == id) { - msg.change(data); - break; - } - } -} - -void Models::Room::dropMessages() -{ - if (messages.size() > 0) { - messages.clear(); - changed(5); - } -} - -void Models::Room::getMessages(Models::Room::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); - } -} void Models::Room::toOfflineState() { @@ -367,47 +295,6 @@ QString Models::Room::getDisplayedName() const return getRoomName(); } -bool Models::Room::columnInvolvedInDisplay(int col) -{ - return Item::columnInvolvedInDisplay(col) && col == 1; -} - -QString Models::Room::getAvatarPath() const -{ - return avatarPath; -} - -Shared::Avatar Models::Room::getAvatarState() const -{ - return avatarState; -} - -void Models::Room::setAvatarPath(const QString& path) -{ - if (avatarPath != path) { - avatarPath = path; - changed(8); - } -} - -void Models::Room::setAvatarState(Shared::Avatar p_state) -{ - if (avatarState != p_state) { - avatarState = p_state; - changed(7); - } -} - -void Models::Room::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 room" << jid << ", skipping"; - } -} - std::map Models::Room::getParticipants() const { std::map result; @@ -423,7 +310,12 @@ QString Models::Room::getParticipantIconPath(const QString& name) const { std::map::const_iterator itr = participants.find(name); if (itr == participants.end()) { - return ""; + std::map::const_iterator eitr = exParticipantAvatars.find(name); + if (eitr != exParticipantAvatars.end()) { + return eitr->second; + } else { + return ""; + } } return itr->second->getAvatarPath(); diff --git a/ui/models/room.h b/ui/models/room.h index 9ea70bf..a51a537 100644 --- a/ui/models/room.h +++ b/ui/models/room.h @@ -19,7 +19,7 @@ #ifndef MODELS_ROOM_H #define MODELS_ROOM_H -#include "item.h" +#include "element.h" #include "participant.h" #include "shared/enums.h" #include "shared/message.h" @@ -29,21 +29,18 @@ namespace Models { /** * @todo write docs */ -class Room : public Models::Item +class Room : public Element { Q_OBJECT public: - typedef std::deque Messages; - Room(const QString& p_jid, const QMap &data, Item *parentItem = 0); + Room(const Account* acc, const QString& p_jid, const QMap &data, Item *parentItem = 0); ~Room(); int columnCount() const override; QVariant data(int column) const override; - unsigned int getUnreadMessagesCount() const; bool getJoined() const; bool getAutoJoin() const; - QString getJid() const; QString getNick() const; QString getRoomName() const; QString getSubject() const; @@ -53,17 +50,10 @@ public: void setJoined(bool p_joined); void setAutoJoin(bool p_autoJoin); - void setJid(const QString& p_jid); void setNick(const QString& p_nick); void setSubject(const QString& sub); - 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 dropMessages(); - void getMessages(Messages& container) const; + void update(const QString& field, const QVariant& value) override; void addParticipant(const QString& name, const QMap& data); void changeParticipant(const QString& name, const QMap& data); @@ -71,8 +61,6 @@ public: void toOfflineState() override; QString getDisplayedName() const override; - Shared::Avatar getAvatarState() const; - QString getAvatarPath() const; std::map getParticipants() const; QString getParticipantIconPath(const QString& name) const; std::map getExParticipantAvatars() const; @@ -84,24 +72,14 @@ signals: private: void handleParticipantUpdate(std::map::const_iterator itr, const QMap& data); -protected: - bool columnInvolvedInDisplay(int col) override; - void setAvatarState(Shared::Avatar p_state); - void setAvatarState(unsigned int p_state); - void setAvatarPath(const QString& path); - private: bool autoJoin; bool joined; QString jid; QString nick; QString subject; - Shared::Avatar avatarState; - QString avatarPath; - Messages messages; std::map participants; std::map exParticipantAvatars; - }; } diff --git a/ui/models/roster.cpp b/ui/models/roster.cpp index ac90a50..2d5f99f 100644 --- a/ui/models/roster.cpp +++ b/ui/models/roster.cpp @@ -48,6 +48,7 @@ Models::Roster::~Roster() void Models::Roster::addAccount(const QMap& data) { Account* acc = new Account(data); + connect(acc, &Account::reconnected, this, &Roster::onAccountReconnected); root->appendChild(acc); accounts.insert(std::make_pair(acc->getName(), acc)); accountsModel->addAccount(acc); @@ -215,11 +216,7 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const break; case Item::presence: { Presence* contact = static_cast(item); - QString str(""); - int mc = contact->getMessagesCount(); - if (mc > 0) { - str += tr("New messages: ") + std::to_string(mc).c_str() + "\n"; - } + QString str; Shared::Availability av = contact->getAvailability(); str += tr("Availability: ") + Shared::Global::getName(av); QString s = contact->getStatus(); @@ -232,7 +229,7 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const break; case Item::participant: { Participant* p = static_cast(item); - QString str(""); + QString str; Shared::Availability av = p->getAvailability(); str += tr("Availability: ") + Shared::Global::getName(av) + "\n"; QString s = p->getStatus(); @@ -260,7 +257,7 @@ QVariant Models::Roster::data (const QModelIndex& index, int role) const break; case Item::room: { Room* rm = static_cast(item); - unsigned int count = rm->getUnreadMessagesCount(); + unsigned int count = rm->getMessagesCount(); QString str(""); if (count > 0) { str += tr("New messages: ") + std::to_string(count).c_str() + "\n"; @@ -450,6 +447,10 @@ void Models::Roster::addContact(const QString& account, const QString& jid, cons std::map::iterator itr = contacts.find(id); if (itr == contacts.end()) { contact = new Contact(acc, jid, data); + connect(contact, &Contact::requestArchive, this, &Roster::onElementRequestArchive); + connect(contact, &Contact::fileDownloadRequest, this, &Roster::fileDownloadRequest); + connect(contact, &Contact::unnoticedMessage, this, &Roster::unnoticedMessage); + connect(contact, &Contact::localPathInvalid, this, &Roster::localPathInvalid); contacts.insert(std::make_pair(id, contact)); } else { contact = itr->second; @@ -535,35 +536,19 @@ void Models::Roster::removeGroup(const QString& account, const QString& name) void Models::Roster::changeContact(const QString& account, const QString& jid, const QMap& data) { - ElId id(account, jid); - std::map::iterator cItr = contacts.find(id); - - if (cItr != contacts.end()) { + Element* el = getElement({account, jid}); + if (el != NULL) { for (QMap::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) { - cItr->second->update(itr.key(), itr.value()); - } - } else { - std::map::iterator rItr = rooms.find(id); - if (rItr != rooms.end()) { - for (QMap::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) { - rItr->second->update(itr.key(), itr.value()); - } + el->update(itr.key(), itr.value()); } } } void Models::Roster::changeMessage(const QString& account, const QString& jid, const QString& id, const QMap& data) { - ElId elid(account, jid); - std::map::iterator cItr = contacts.find(elid); - - if (cItr != contacts.end()) { - cItr->second->changeMessage(id, data); - } else { - std::map::iterator rItr = rooms.find(elid); - if (rItr != rooms.end()) { - rItr->second->changeMessage(id, data); - } + Element* el = getElement({account, jid}); + if (el != NULL) { + el->changeMessage(id, data); } } @@ -627,7 +612,6 @@ void Models::Roster::removeContact(const QString& account, const QString& jid, c } else { delete ref; } - if (gr->childCount() == 0) { removeGroup(account, group); } @@ -708,29 +692,9 @@ void Models::Roster::removePresence(const QString& account, const QString& jid, void Models::Roster::addMessage(const QString& account, const Shared::Message& data) { - ElId id(account, data.getPenPalJid()); - std::map::iterator itr = contacts.find(id); - if (itr != contacts.end()) { - itr->second->addMessage(data); - } else { - std::map::const_iterator rItr = rooms.find(id); - if (rItr != rooms.end()) { - rItr->second->addMessage(data); - } - } -} - -void Models::Roster::dropMessages(const QString& account, const QString& jid) -{ - ElId id(account, jid); - std::map::iterator itr = contacts.find(id); - if (itr != contacts.end()) { - itr->second->dropMessages(); - } else { - std::map::const_iterator rItr = rooms.find(id); - if (rItr != rooms.end()) { - rItr->second->dropMessages(); - } + Element* el = getElement({account, data.getPenPalJid()}); + if (el != NULL) { + el->addMessage(data); } } @@ -781,6 +745,7 @@ void Models::Roster::removeAccount(const QString& account) } } + disconnect(acc, &Account::reconnected, this, &Roster::onAccountReconnected); acc->deleteLater(); } @@ -821,7 +786,11 @@ void Models::Roster::addRoom(const QString& account, const QString jid, const QM return; } - Room* room = new Room(jid, data); + Room* room = new Room(acc, jid, data); + connect(room, &Contact::requestArchive, this, &Roster::onElementRequestArchive); + connect(room, &Contact::fileDownloadRequest, this, &Roster::fileDownloadRequest); + connect(room, &Contact::unnoticedMessage, this, &Roster::unnoticedMessage); + connect(room, &Contact::localPathInvalid, this, &Roster::localPathInvalid); rooms.insert(std::make_pair(id, room)); acc->appendChild(room); } @@ -974,3 +943,77 @@ QModelIndex Models::Roster::getGroupIndex(const QString& account, const QString& } } } + +void Models::Roster::onElementRequestArchive(const QString& before) +{ + Element* el = static_cast(sender()); + emit requestArchive(el->getAccountName(), el->getJid(), before); +} + +void Models::Roster::responseArchive(const QString& account, const QString& jid, const std::list& list, bool last) +{ + ElId id(account, jid); + Element* el = getElement(id); + if (el != NULL) { + el->responseArchive(list, last); + } +} + +void Models::Roster::fileProgress(const std::list& msgs, qreal value, bool up) +{ + for (const Shared::MessageInfo& info : msgs) { + Element* el = getElement({info.account, info.jid}); + if (el != NULL) { + el->fileProgress(info.messageId, value, up); + } + } +} + +void Models::Roster::fileComplete(const std::list& msgs, bool up) +{ + for (const Shared::MessageInfo& info : msgs) { + Element* el = getElement({info.account, info.jid}); + if (el != NULL) { + el->fileComplete(info.messageId, up); + } + } +} + +void Models::Roster::fileError(const std::list& msgs, const QString& err, bool up) +{ + for (const Shared::MessageInfo& info : msgs) { + Element* el = getElement({info.account, info.jid}); + if (el != NULL) { + el->fileError(info.messageId, err, up); + } + } +} + +Models::Element * Models::Roster::getElement(const Models::Roster::ElId& id) +{ + std::map::iterator cItr = contacts.find(id); + + if (cItr != contacts.end()) { + return cItr->second; + } else { + std::map::iterator rItr = rooms.find(id); + if (rItr != rooms.end()) { + return rItr->second; + } + } + + return NULL; +} + +void Models::Roster::onAccountReconnected() +{ + Account* acc = static_cast(sender()); + + QString accName = acc->getName(); + for (const std::pair& pair : contacts) { + if (pair.first.account == accName) { + pair.second->handleRecconnect(); + } + } +} + diff --git a/ui/models/roster.h b/ui/models/roster.h index d866b6d..08d5afc 100644 --- a/ui/models/roster.h +++ b/ui/models/roster.h @@ -26,6 +26,7 @@ #include "shared/message.h" #include "shared/global.h" +#include "shared/messageinfo.h" #include "accounts.h" #include "item.h" #include "account.h" @@ -58,7 +59,6 @@ public: void removePresence(const QString& account, const QString& jid, const QString& name); void addMessage(const QString& account, const Shared::Message& data); void changeMessage(const QString& account, const QString& jid, const QString& id, const QMap& data); - void dropMessages(const QString& account, const QString& jid); 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); @@ -81,18 +81,26 @@ public: Account* getAccount(const QString& name); QModelIndex getAccountIndex(const QString& name); QModelIndex getGroupIndex(const QString& account, const QString& name); + void responseArchive(const QString& account, const QString& jid, const std::list& list, bool last); + + void fileProgress(const std::list& msgs, qreal value, bool up); + void fileError(const std::list& msgs, const QString& err, bool up); + void fileComplete(const std::list& msgs, bool up); Accounts* accountsModel; +signals: + void requestArchive(const QString& account, const QString& jid, const QString& before); + void fileDownloadRequest(const QString& url); + void unnoticedMessage(const QString& account, const Shared::Message& msg); + void localPathInvalid(const QString& path); + private: - Item* root; - std::map accounts; - std::map groups; - std::map contacts; - std::map rooms; + Element* getElement(const ElId& id); private slots: void onAccountDataChanged(const QModelIndex& tl, const QModelIndex& br, const QVector& roles); + void onAccountReconnected(); void onChildChanged(Models::Item* item, int row, int col); void onChildIsAboutToBeInserted(Item* parent, int first, int last); void onChildInserted(); @@ -100,6 +108,14 @@ private slots: void onChildRemoved(); void onChildIsAboutToBeMoved(Item* source, int first, int last, Item* destination, int newIndex); void onChildMoved(); + void onElementRequestArchive(const QString& before); + +private: + Item* root; + std::map accounts; + std::map groups; + std::map contacts; + std::map rooms; public: class ElId { diff --git a/ui/squawk.cpp b/ui/squawk.cpp index 41634ad..6a0a676 100644 --- a/ui/squawk.cpp +++ b/ui/squawk.cpp @@ -29,7 +29,6 @@ Squawk::Squawk(QWidget *parent) : conversations(), contextMenu(new QMenu()), dbus("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", QDBusConnection::sessionBus()), - requestedFiles(), vCards(), requestedAccountsForPasswords(), prompt(0), @@ -60,8 +59,12 @@ Squawk::Squawk(QWidget *parent) : connect(m_ui->roster, &QTreeView::customContextMenuRequested, this, &Squawk::onRosterContextMenu); connect(m_ui->roster, &QTreeView::collapsed, this, &Squawk::onItemCollepsed); connect(m_ui->roster->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Squawk::onRosterSelectionChanged); + connect(&rosterModel, &Models::Roster::unnoticedMessage, this, &Squawk::onUnnoticedMessage); connect(rosterModel.accountsModel, &Models::Accounts::sizeChanged, this, &Squawk::onAccountsSizeChanged); + connect(&rosterModel, &Models::Roster::requestArchive, this, &Squawk::onRequestArchive); + connect(&rosterModel, &Models::Roster::fileDownloadRequest, this, &Squawk::fileDownloadRequest); + connect(&rosterModel, &Models::Roster::localPathInvalid, this, &Squawk::localPathInvalid); connect(contextMenu, &QMenu::aboutToHide, this, &Squawk::onContextAboutToHide); //m_ui->mainToolBar->addWidget(m_ui->comboBox); @@ -336,17 +339,14 @@ void Squawk::onRosterItemDoubleClicked(const QModelIndex& item) Models::Account* acc = rosterModel.getAccount(id->account); Conversation* conv = 0; bool created = false; - Models::Contact::Messages deque; if (itr != conversations.end()) { conv = itr->second; } else if (contact != 0) { created = true; conv = new Chat(acc, contact); - contact->getMessages(deque); } else if (room != 0) { created = true; conv = new Room(acc, room); - room->getMessages(deque); if (!room->getJoined()) { emit setRoomJoined(id->account, id->name, true); @@ -358,12 +358,6 @@ void Squawk::onRosterItemDoubleClicked(const QModelIndex& item) conv->setAttribute(Qt::WA_DeleteOnClose); subscribeConversation(conv); conversations.insert(std::make_pair(*id, conv)); - - if (created) { - for (Models::Contact::Messages::const_iterator itr = deque.begin(), end = deque.end(); itr != end; ++itr) { - conv->addMessage(*itr); - } - } } conv->show(); @@ -380,12 +374,6 @@ void Squawk::onRosterItemDoubleClicked(const QModelIndex& item) } } -void Squawk::onConversationShown() -{ - Conversation* conv = static_cast(sender()); - rosterModel.dropMessages(conv->getAccount(), conv->getJid()); -} - void Squawk::onConversationClosed(QObject* parent) { Conversation* conv = static_cast(sender()); @@ -402,164 +390,40 @@ void Squawk::onConversationClosed(QObject* parent) } } -void Squawk::onConversationDownloadFile(const QString& messageId, const QString& url) +void Squawk::fileProgress(const std::list msgs, qreal value, bool up) { - Conversation* conv = static_cast(sender()); - std::map>::iterator itr = requestedFiles.find(messageId); - bool created = false; - if (itr == requestedFiles.end()) { - itr = requestedFiles.insert(std::make_pair(messageId, std::set())).first; - created = true; - } - itr->second.insert(Models::Roster::ElId(conv->getAccount(), conv->getJid())); - if (created) { - emit downloadFileRequest(messageId, url); - } + rosterModel.fileProgress(msgs, value, up); } -void Squawk::fileProgress(const QString& messageId, qreal value) +void Squawk::fileDownloadComplete(const std::list msgs, const QString& path) { - std::map>::const_iterator itr = requestedFiles.find(messageId); - if (itr == requestedFiles.end()) { - qDebug() << "fileProgress in UI Squawk but there is nobody waiting for that id" << messageId << ", skipping"; - return; - } else { - const std::set& convs = itr->second; - for (std::set::const_iterator cItr = convs.begin(), cEnd = convs.end(); cItr != cEnd; ++cItr) { - const Models::Roster::ElId& id = *cItr; - Conversations::const_iterator c = conversations.find(id); - if (c != conversations.end()) { - c->second->responseFileProgress(messageId, value); - } - if (currentConversation != 0 && currentConversation->getId() == id) { - currentConversation->responseFileProgress(messageId, value); - } - } - } + rosterModel.fileComplete(msgs, false); } -void Squawk::fileError(const QString& messageId, const QString& error) +void Squawk::fileError(const std::list msgs, const QString& error, bool up) { - std::map>::const_iterator itr = requestedFiles.find(messageId); - if (itr == requestedFiles.end()) { - qDebug() << "fileError in UI Squawk but there is nobody waiting for that id" << messageId << ", skipping"; - return; - } else { - const std::set& convs = itr->second; - for (std::set::const_iterator cItr = convs.begin(), cEnd = convs.end(); cItr != cEnd; ++cItr) { - const Models::Roster::ElId& id = *cItr; - Conversations::const_iterator c = conversations.find(id); - if (c != conversations.end()) { - c->second->fileError(messageId, error); - } - if (currentConversation != 0 && currentConversation->getId() == id) { - currentConversation->fileError(messageId, error); - } - } - requestedFiles.erase(itr); - } + rosterModel.fileError(msgs, error, up); } -void Squawk::fileLocalPathResponse(const QString& messageId, const QString& path) +void Squawk::fileUploadComplete(const std::list msgs, const QString& path) { - std::map>::const_iterator itr = requestedFiles.find(messageId); - if (itr == requestedFiles.end()) { - qDebug() << "fileLocalPathResponse in UI Squawk but there is nobody waiting for that path, skipping"; - return; - } else { - const std::set& convs = itr->second; - for (std::set::const_iterator cItr = convs.begin(), cEnd = convs.end(); cItr != cEnd; ++cItr) { - const Models::Roster::ElId& id = *cItr; - Conversations::const_iterator c = conversations.find(id); - if (c != conversations.end()) { - c->second->responseLocalFile(messageId, path); - } - if (currentConversation != 0 && currentConversation->getId() == id) { - currentConversation->responseLocalFile(messageId, path); - } - } - - requestedFiles.erase(itr); - } -} - -void Squawk::onConversationRequestLocalFile(const QString& messageId, const QString& url) -{ - Conversation* conv = static_cast(sender()); - std::map>::iterator itr = requestedFiles.find(messageId); - bool created = false; - if (itr == requestedFiles.end()) { - itr = requestedFiles.insert(std::make_pair(messageId, std::set())).first; - created = true; - } - itr->second.insert(Models::Roster::ElId(conv->getAccount(), conv->getJid())); - if (created) { - emit fileLocalPathRequest(messageId, url); - } + rosterModel.fileComplete(msgs, true); } void Squawk::accountMessage(const QString& account, const Shared::Message& data) { - const QString& from = data.getPenPalJid(); - Models::Roster::ElId id({account, from}); - Conversations::iterator itr = conversations.find(id); - bool found = false; - - if (currentConversation != 0 && currentConversation->getId() == id) { - currentConversation->addMessage(data); - QApplication::alert(this); - if (!isVisible() && !data.getForwarded()) { - notify(account, data); - } - found = true; - } - - if (itr != conversations.end()) { - Conversation* conv = itr->second; - conv->addMessage(data); - QApplication::alert(conv); - if (!found && conv->isMinimized()) { - rosterModel.addMessage(account, data); - } - if (!conv->isVisible() && !data.getForwarded()) { - notify(account, data); - } - found = true; - } - - if (!found) { - rosterModel.addMessage(account, data); - if (!data.getForwarded()) { - QApplication::alert(this); - notify(account, data); - } - } + rosterModel.addMessage(account, data); +} + +void Squawk::onUnnoticedMessage(const QString& account, const Shared::Message& msg) +{ + notify(account, msg); //Telegram does this way - notifies even if the app is visible + QApplication::alert(this); } void Squawk::changeMessage(const QString& account, const QString& jid, const QString& id, const QMap& data) { - Models::Roster::ElId eid({account, jid}); - bool found = false; - - if (currentConversation != 0 && currentConversation->getId() == eid) { - currentConversation->changeMessage(id, data); - QApplication::alert(this); - found = true; - } - - Conversations::iterator itr = conversations.find(eid); - if (itr != conversations.end()) { - Conversation* conv = itr->second; - conv->changeMessage(id, data); - if (!found && conv->isMinimized()) { - rosterModel.changeMessage(account, jid, id, data); - } - found = true; - } - - if (!found) { - rosterModel.changeMessage(account, jid, id, data); - } + rosterModel.changeMessage(account, jid, id, data); } void Squawk::notify(const QString& account, const Shared::Message& msg) @@ -596,60 +460,29 @@ void Squawk::notify(const QString& account, const Shared::Message& msg) void Squawk::onConversationMessage(const Shared::Message& msg) { Conversation* conv = static_cast(sender()); - emit sendMessage(conv->getAccount(), msg); - Models::Roster::ElId id = conv->getId(); + QString acc = conv->getAccount(); - if (currentConversation != 0 && currentConversation->getId() == id) { - if (conv == currentConversation) { - Conversations::iterator itr = conversations.find(id); - if (itr != conversations.end()) { - itr->second->addMessage(msg); - } - } else { - currentConversation->addMessage(msg); - } - } + rosterModel.addMessage(acc, msg); + emit sendMessage(acc, msg); } -void Squawk::onConversationMessage(const Shared::Message& msg, const QString& path) +void Squawk::onConversationResend(const QString& id) { Conversation* conv = static_cast(sender()); - Models::Roster::ElId id = conv->getId(); - std::map>::iterator itr = requestedFiles.insert(std::make_pair(msg.getId(), std::set())).first; - itr->second.insert(id); + QString acc = conv->getAccount(); + QString jid = conv->getJid(); - if (currentConversation != 0 && currentConversation->getId() == id) { - if (conv == currentConversation) { - Conversations::iterator itr = conversations.find(id); - if (itr != conversations.end()) { - itr->second->appendMessageWithUpload(msg, path); - } - } else { - currentConversation->appendMessageWithUpload(msg, path); - } - } - - emit sendMessage(conv->getAccount(), msg, path); + emit resendMessage(acc, jid, id); } -void Squawk::onConversationRequestArchive(const QString& before) +void Squawk::onRequestArchive(const QString& account, const QString& jid, const QString& before) { - Conversation* conv = static_cast(sender()); - requestArchive(conv->getAccount(), conv->getJid(), 20, before); //TODO amount as a settings value + emit requestArchive(account, jid, 20, before); //TODO amount as a settings value } -void Squawk::responseArchive(const QString& account, const QString& jid, const std::list& list) +void Squawk::responseArchive(const QString& account, const QString& jid, const std::list& list, bool last) { - Models::Roster::ElId id(account, jid); - - if (currentConversation != 0 && currentConversation->getId() == id) { - currentConversation->responseArchive(list); - } - - Conversations::const_iterator itr = conversations.find(id); - if (itr != conversations.end()) { - itr->second->responseArchive(list); - } + rosterModel.responseArchive(account, jid, list, last); } void Squawk::removeAccount(const QString& account) @@ -661,8 +494,6 @@ void Squawk::removeAccount(const QString& account) ++itr; Conversation* conv = lItr->second; disconnect(conv, &Conversation::destroyed, this, &Squawk::onConversationClosed); - disconnect(conv, &Conversation::requestArchive, this, &Squawk::onConversationRequestArchive); - disconnect(conv, &Conversation::shown, this, &Squawk::onConversationShown); conv->close(); conversations.erase(lItr); } else { @@ -926,7 +757,6 @@ void Squawk::onActivateVCard(const QString& account, const QString& jid, bool ed { std::map::const_iterator itr = vCards.find(jid); VCard* card; - Models::Contact::Messages deque; if (itr != vCards.end()) { card = itr->second; } else { @@ -1092,13 +922,9 @@ void Squawk::onPasswordPromptRejected() void Squawk::subscribeConversation(Conversation* conv) { connect(conv, &Conversation::destroyed, this, &Squawk::onConversationClosed); - connect(conv, qOverload(&Conversation::sendMessage), this, qOverload(&Squawk::onConversationMessage)); - connect(conv, qOverload(&Conversation::sendMessage), - this, qOverload(&Squawk::onConversationMessage)); - connect(conv, &Conversation::requestArchive, this, &Squawk::onConversationRequestArchive); - connect(conv, &Conversation::requestLocalFile, this, &Squawk::onConversationRequestLocalFile); - connect(conv, &Conversation::downloadFile, this, &Squawk::onConversationDownloadFile); - connect(conv, &Conversation::shown, this, &Squawk::onConversationShown); + connect(conv, &Conversation::sendMessage, this, &Squawk::onConversationMessage); + connect(conv, &Conversation::resendMessage, this, &Squawk::onConversationResend); + connect(conv, &Conversation::notifyableMessage, this, &Squawk::notify); } void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous) @@ -1168,13 +994,10 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn } Models::Account* acc = rosterModel.getAccount(id->account); - Models::Contact::Messages deque; if (contact != 0) { currentConversation = new Chat(acc, contact); - contact->getMessages(deque); } else if (room != 0) { currentConversation = new Room(acc, room); - room->getMessages(deque); if (!room->getJoined()) { emit setRoomJoined(id->account, id->name, true); @@ -1185,9 +1008,6 @@ void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIn } subscribeConversation(currentConversation); - for (Models::Contact::Messages::const_iterator itr = deque.begin(), end = deque.end(); itr != end; ++itr) { - currentConversation->addMessage(*itr); - } if (res.size() > 0) { currentConversation->setPalResource(res); diff --git a/ui/squawk.h b/ui/squawk.h index a6a27c0..28389fa 100644 --- a/ui/squawk.h +++ b/ui/squawk.h @@ -39,7 +39,7 @@ #include "models/roster.h" #include "widgets/vcard/vcard.h" -#include "shared.h" +#include "shared/shared.h" namespace Ui { class Squawk; @@ -63,7 +63,7 @@ signals: void disconnectAccount(const QString&); 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 resendMessage(const QString& account, const QString& jid, const QString& id); 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); @@ -76,11 +76,11 @@ signals: 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 localPathInvalid(const QString& path); public slots: void readSettings(); @@ -97,16 +97,17 @@ public slots: 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 fileError(const QString& messageId, const QString& error); - void fileProgress(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); @@ -120,7 +121,6 @@ private: Conversations conversations; QMenu* contextMenu; QDBusInterface dbus; - std::map> requestedFiles; std::map vCards; std::deque requestedAccountsForPasswords; QInputDialog* prompt; @@ -130,6 +130,8 @@ private: protected: void closeEvent(QCloseEvent * event) override; + +protected slots: void notify(const QString& account, const Shared::Message& msg); private slots: @@ -147,18 +149,17 @@ private slots: void onComboboxActivated(int index); void onRosterItemDoubleClicked(const QModelIndex& item); void onConversationMessage(const Shared::Message& msg); - void onConversationMessage(const Shared::Message& msg, const QString& path); - void onConversationRequestArchive(const QString& before); + void onConversationResend(const QString& id); + void onRequestArchive(const QString& account, const QString& jid, const QString& before); void onRosterContextMenu(const QPoint& point); - void onConversationShown(); - void onConversationRequestLocalFile(const QString& messageId, const QString& url); - void onConversationDownloadFile(const QString& messageId, const QString& url); void onItemCollepsed(const QModelIndex& index); void onPasswordPromptAccepted(); void onPasswordPromptRejected(); void onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous); void onContextAboutToHide(); + void onUnnoticedMessage(const QString& account, const Shared::Message& msg); + private: void checkNextAccountForPassword(); void onPasswordPromptDone(); diff --git a/ui/utils/CMakeLists.txt b/ui/utils/CMakeLists.txt new file mode 100644 index 0000000..b46d30d --- /dev/null +++ b/ui/utils/CMakeLists.txt @@ -0,0 +1,18 @@ +target_sources(squawk PRIVATE + badge.cpp + badge.h + comboboxdelegate.cpp + comboboxdelegate.h + exponentialblur.cpp + exponentialblur.h + flowlayout.cpp + flowlayout.h + image.cpp + image.h + progress.cpp + progress.h + resizer.cpp + resizer.h + shadowoverlay.cpp + shadowoverlay.h + ) diff --git a/ui/utils/comboboxdelegate.cpp b/ui/utils/comboboxdelegate.cpp index 7153405..4c96c79 100644 --- a/ui/utils/comboboxdelegate.cpp +++ b/ui/utils/comboboxdelegate.cpp @@ -37,7 +37,7 @@ QWidget* ComboboxDelegate::createEditor(QWidget *parent, const QStyleOptionViewI { QComboBox *cb = new QComboBox(parent); - for (const std::pair pair : entries) { + for (const std::pair& pair : entries) { cb->addItem(pair.second, pair.first); } diff --git a/ui/utils/dropshadoweffect.h b/ui/utils/dropshadoweffect.h deleted file mode 100644 index b2768b7..0000000 --- a/ui/utils/dropshadoweffect.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 DROPSHADOWEFFECT_H -#define DROPSHADOWEFFECT_H - -#include -#include -#include -#include - -class PixmapFilter : public QObject -{ - Q_OBJECT -public: - PixmapFilter(QObject *parent = nullptr); - virtual ~PixmapFilter() = 0; - - virtual QRectF boundingRectFor(const QRectF &rect) const; - virtual void draw(QPainter *painter, const QPointF &p, const QPixmap &src, const QRectF &srcRect = QRectF()) const = 0; -}; - -class PixmapDropShadowFilter : public PixmapFilter -{ - Q_OBJECT - -public: - PixmapDropShadowFilter(QObject *parent = nullptr); - ~PixmapDropShadowFilter(); - - void draw(QPainter *p, const QPointF &pos, const QPixmap &px, const QRectF &src = QRectF()) const override; - - qreal blurRadius() const; - void setBlurRadius(qreal radius); - - QColor color() const; - void setColor(const QColor &color); - - qreal thickness() const; - void setThickness(qreal thickness); - void setFrame(bool top, bool right, bool bottom, bool left); - -protected: - QColor mColor; - qreal mRadius; - qreal mThickness; - bool top; - bool right; - bool bottom; - bool left; -}; - -class DropShadowEffect : public QGraphicsEffect -{ - Q_OBJECT -public: - qreal blurRadius() const; - QColor color() const; - void setFrame(bool top, bool right, bool bottom, bool left); - void setThickness(qreal thickness); - -signals: - void blurRadiusChanged(qreal blurRadius); - void colorChanged(const QColor &color); - -public slots: - void setBlurRadius(qreal blurRadius); - void setColor(const QColor &color); - -protected: - void draw(QPainter * painter) override; - -protected: - PixmapDropShadowFilter filter; - -}; - -#endif // DROPSHADOWEFFECT_H diff --git a/ui/utils/dropshadoweffect.cpp b/ui/utils/exponentialblur.cpp similarity index 85% rename from ui/utils/dropshadoweffect.cpp rename to ui/utils/exponentialblur.cpp index 91a0258..cb222dc 100644 --- a/ui/utils/dropshadoweffect.cpp +++ b/ui/utils/exponentialblur.cpp @@ -16,8 +16,7 @@ * along with this program. If not, see . */ -#include "dropshadoweffect.h" -#include "QtMath" +#include "exponentialblur.h" static const int tileSize = 32; template @@ -574,128 +573,7 @@ void expblur(QImage &img, qreal radius, bool improvedQuality = false, int transp } } -PixmapFilter::PixmapFilter(QObject* parent):QObject(parent) {} -PixmapFilter::~PixmapFilter(){} -QRectF PixmapFilter::boundingRectFor(const QRectF &rect) const {return rect;} - -PixmapDropShadowFilter::PixmapDropShadowFilter(QObject *parent): - PixmapFilter(parent), - mColor(63, 63, 63, 180), - mRadius(1), - mThickness(2), - top(true), - right(true), - bottom(true), - left(true){} - -PixmapDropShadowFilter::~PixmapDropShadowFilter() {} -qreal PixmapDropShadowFilter::blurRadius() const {return mRadius;} -void PixmapDropShadowFilter::setBlurRadius(qreal radius) {mRadius = radius;} -QColor PixmapDropShadowFilter::color() const {return mColor;} -void PixmapDropShadowFilter::setColor(const QColor &color) {mColor = color;} -qreal PixmapDropShadowFilter::thickness() const {return mThickness;} -void PixmapDropShadowFilter::setThickness(qreal thickness) {mThickness = thickness;} -void PixmapDropShadowFilter::setFrame(bool ptop, bool pright, bool pbottom, bool pleft) +void Utils::exponentialblur(QImage& img, qreal radius, bool improvedQuality, int transposed) { - top = ptop; - right = pright; - bottom = pbottom; - left = pleft; -} - -void DropShadowEffect::setThickness(qreal thickness) -{ - if (filter.thickness() == thickness) - return; - - filter.setThickness(thickness); - update(); -} - - -void PixmapDropShadowFilter::draw(QPainter *p, const QPointF &pos, const QPixmap &px, const QRectF &src) const -{ - if (px.isNull()) - return; - - QImage tmp({px.width(), px.height() + int(mThickness)}, QImage::Format_ARGB32_Premultiplied); - tmp.setDevicePixelRatio(px.devicePixelRatioF()); - tmp.fill(0); - QPainter tmpPainter(&tmp); - tmpPainter.setCompositionMode(QPainter::CompositionMode_Source); - if (top) { - QRectF shadow(0, 0, px.width(), mThickness); - tmpPainter.fillRect(shadow, mColor); - } - if (right) { - QRectF shadow(px.width() - mThickness, 0, mThickness, px.height()); - tmpPainter.fillRect(shadow, mColor); - } - if (bottom) { - QRectF shadow(0, px.height() - mThickness, px.width(), mThickness * 2); //i have no idea why, but it leaves some unpainted stripe without some spare space - tmpPainter.fillRect(shadow, mColor); - } - if (left) { - QRectF shadow(0, 0, mThickness, px.height()); - tmpPainter.fillRect(shadow, mColor); - } - - expblur<12, 10, false>(tmp, mRadius, false, 0); - tmpPainter.end(); - - // Draw the actual pixmap... - p->drawPixmap(pos, px, src); - - // draw the blurred drop shadow... - p->drawImage(pos, tmp); -} - -qreal DropShadowEffect::blurRadius() const {return filter.blurRadius();} -void DropShadowEffect::setBlurRadius(qreal blurRadius) -{ - if (qFuzzyCompare(filter.blurRadius(), blurRadius)) - return; - - filter.setBlurRadius(blurRadius); - updateBoundingRect(); - emit blurRadiusChanged(blurRadius); -} - -void DropShadowEffect::setFrame(bool top, bool right, bool bottom, bool left) -{ - filter.setFrame(top, right, bottom, left); - update(); -} - - -QColor DropShadowEffect::color() const {return filter.color();} -void DropShadowEffect::setColor(const QColor &color) -{ - if (filter.color() == color) - return; - - filter.setColor(color); - update(); - emit colorChanged(color); -} - -void DropShadowEffect::draw(QPainter* painter) -{ - if (filter.blurRadius() <= 0 && filter.thickness() == 0) { - drawSource(painter); - return; - } - - PixmapPadMode mode = PadToEffectiveBoundingRect; - - // Draw pixmap in device coordinates to avoid pixmap scaling. - QPoint offset; - const QPixmap pixmap = sourcePixmap(Qt::DeviceCoordinates, &offset, mode); - if (pixmap.isNull()) - return; - - QTransform restoreTransform = painter->worldTransform(); - painter->setWorldTransform(QTransform()); - filter.draw(painter, offset, pixmap); - painter->setWorldTransform(restoreTransform); + expblur<12, 10, false>(img, radius, improvedQuality, transposed); } diff --git a/ui/utils/exponentialblur.h b/ui/utils/exponentialblur.h new file mode 100644 index 0000000..0a5df8a --- /dev/null +++ b/ui/utils/exponentialblur.h @@ -0,0 +1,34 @@ +/* + * 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 EXPONENTIALBLUR_H +#define EXPONENTIALBLUR_H + +#include +#include +#include + +/** + * @todo write docs + */ + +namespace Utils { + void exponentialblur(QImage &img, qreal radius, bool improvedQuality = false, int transposed = 0); +}; + +#endif // EXPONENTIALBLUR_H diff --git a/ui/utils/shadowoverlay.cpp b/ui/utils/shadowoverlay.cpp new file mode 100644 index 0000000..3c28a15 --- /dev/null +++ b/ui/utils/shadowoverlay.cpp @@ -0,0 +1,91 @@ +/* + * 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 "shadowoverlay.h" + +ShadowOverlay::ShadowOverlay(unsigned int r, unsigned int t, const QColor& c, QWidget* parent): + QWidget(parent), + top(false), + right(false), + bottom(false), + left(false), + thickness(t), + radius(r), + color(c), + shadow(1, 1, QImage::Format_ARGB32_Premultiplied) +{ + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_TransparentForMouseEvents); +} + +void ShadowOverlay::paintEvent(QPaintEvent* event) +{ + QWidget::paintEvent(event); + + QPainter painter(this); + + painter.drawImage(0, 0, shadow); +} + +void ShadowOverlay::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + + updateImage(); +} + +void ShadowOverlay::updateImage() +{ + int w = width(); + int h = height(); + shadow = QImage({w, h + int(thickness)}, QImage::Format_ARGB32_Premultiplied); + shadow.fill(0); + + QPainter tmpPainter(&shadow); + tmpPainter.setCompositionMode(QPainter::CompositionMode_Source); + if (top) { + QRectF shadow(0, 0, w, thickness); + tmpPainter.fillRect(shadow, color); + } + if (right) { + QRectF shadow(w - thickness, 0, thickness, h); + tmpPainter.fillRect(shadow, color); + } + if (bottom) { + QRectF shadow(0, h - thickness, w, thickness * 2); //i have no idea why, but it leaves some unpainted stripe without some spare space + tmpPainter.fillRect(shadow, color); + } + if (left) { + QRectF shadow(0, 0, thickness, h); + tmpPainter.fillRect(shadow, color); + } + + Utils::exponentialblur(shadow, radius, false, 0); + tmpPainter.end(); +} + +void ShadowOverlay::setFrames(bool t, bool r, bool b, bool l) +{ + top = t; + right = r; + bottom = b; + left = l; + + updateImage(); + update(); +} diff --git a/ui/utils/shadowoverlay.h b/ui/utils/shadowoverlay.h new file mode 100644 index 0000000..524115a --- /dev/null +++ b/ui/utils/shadowoverlay.h @@ -0,0 +1,58 @@ +/* + * 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 SHADOWOVERLAY_H +#define SHADOWOVERLAY_H + +#include +#include +#include +#include +#include +#include + +#include + +/** + * @todo write docs + */ +class ShadowOverlay : public QWidget { + +public: + ShadowOverlay(unsigned int radius = 10, unsigned int thickness = 1, const QColor& color = Qt::black, QWidget* parent = nullptr); + + void setFrames(bool top, bool right, bool bottom, bool left); + +protected: + void updateImage(); + + void paintEvent(QPaintEvent * event) override; + void resizeEvent(QResizeEvent * event) override; + +private: + bool top; + bool right; + bool bottom; + bool left; + unsigned int thickness; + unsigned int radius; + QColor color; + QImage shadow; +}; + +#endif // SHADOWOVERLAY_H diff --git a/ui/widgets/CMakeLists.txt b/ui/widgets/CMakeLists.txt index 29e2dbc..c7e47e0 100644 --- a/ui/widgets/CMakeLists.txt +++ b/ui/widgets/CMakeLists.txt @@ -1,29 +1,24 @@ -cmake_minimum_required(VERSION 3.0) -project(squawkWidgets) - -# Instruct CMake to run moc automatically when needed. -set(CMAKE_AUTOMOC ON) -# Instruct CMake to create code from Qt designer ui files -set(CMAKE_AUTOUIC ON) - -# Find the QtWidgets library -find_package(Qt5Widgets CONFIG REQUIRED) +target_sources(squawk PRIVATE + account.cpp + account.h + account.ui + accounts.cpp + accounts.h + accounts.ui + chat.cpp + chat.h + conversation.cpp + conversation.h + conversation.ui + joinconference.cpp + joinconference.h + joinconference.ui + newcontact.cpp + newcontact.h + newcontact.ui + room.cpp + room.h + ) add_subdirectory(vcard) - -set(squawkWidgets_SRC - conversation.cpp - chat.cpp - room.cpp - newcontact.cpp - accounts.cpp - account.cpp - joinconference.cpp -) - -# Tell CMake to create the helloworld executable -add_library(squawkWidgets ${squawkWidgets_SRC}) - -# Use the Widgets module from Qt 5. -target_link_libraries(squawkWidgets vCardUI) -target_link_libraries(squawkWidgets Qt5::Widgets) +add_subdirectory(messageline) diff --git a/ui/widgets/chat.cpp b/ui/widgets/chat.cpp index acbcac1..052d83d 100644 --- a/ui/widgets/chat.cpp +++ b/ui/widgets/chat.cpp @@ -19,7 +19,7 @@ #include "chat.h" Chat::Chat(Models::Account* acc, Models::Contact* p_contact, QWidget* parent): - Conversation(false, acc, p_contact->getJid(), "", parent), + Conversation(false, acc, p_contact, p_contact->getJid(), "", parent), contact(p_contact) { setName(p_contact->getContactName()); @@ -71,31 +71,14 @@ Shared::Message Chat::createMessage() const return msg; } -void Chat::addMessage(const Shared::Message& data) +void Chat::onMessage(const Shared::Message& data) { - Conversation::addMessage(data); + Conversation::onMessage(data); - if (!data.getOutgoing()) { //TODO need to check if that was the last message + if (!data.getOutgoing()) { const QString& res = data.getPenPalResource(); if (res.size() > 0) { setPalResource(res); } } } - -void Chat::setName(const QString& name) -{ - Conversation::setName(name); - line->setPalName(getJid(), name); -} - -void Chat::setAvatar(const QString& path) -{ - Conversation::setAvatar(path); - - if (path.size() == 0) { - line->dropPalAvatar(contact->getJid()); - } else { - line->setPalAvatar(contact->getJid(), path); - } -} diff --git a/ui/widgets/chat.h b/ui/widgets/chat.h index f05b0fa..78e6bec 100644 --- a/ui/widgets/chat.h +++ b/ui/widgets/chat.h @@ -34,16 +34,13 @@ class Chat : public Conversation public: Chat(Models::Account* acc, Models::Contact* p_contact, QWidget* parent = 0); ~Chat(); - - void addMessage(const Shared::Message & data) override; - void setAvatar(const QString& path) override; protected slots: void onContactChanged(Models::Item* item, int row, int col); protected: - void setName(const QString & name) override; Shared::Message createMessage() const override; + void onMessage(const Shared::Message& msg) override; private: void updateState(); diff --git a/ui/widgets/conversation.cpp b/ui/widgets/conversation.cpp index fd87d9f..d003551 100644 --- a/ui/widgets/conversation.cpp +++ b/ui/widgets/conversation.cpp @@ -18,7 +18,6 @@ #include "conversation.h" #include "ui_conversation.h" -#include "ui/utils/dropshadoweffect.h" #include #include @@ -29,31 +28,44 @@ #include #include -Conversation::Conversation(bool muc, Models::Account* acc, const QString pJid, const QString pRes, QWidget* parent): +Conversation::Conversation(bool muc, Models::Account* acc, Models::Element* el, const QString pJid, const QString pRes, QWidget* parent): QWidget(parent), isMuc(muc), account(acc), + element(el), palJid(pJid), activePalResource(pRes), - line(new MessageLine(muc)), m_ui(new Ui::Conversation()), ker(), - scrollResizeCatcher(), - vis(), thread(), statusIcon(0), statusLabel(0), filesLayout(0), overlay(new QWidget()), filesToAttach(), - scroll(down), + feed(new FeedView()), + delegate(new MessageDelegate(this)), manualSliderChange(false), - requestingHistory(false), - everShown(false), - tsb(QApplication::style()->styleHint(QStyle::SH_ScrollBar_Transient) == 1) + tsb(QApplication::style()->styleHint(QStyle::SH_ScrollBar_Transient) == 1), + shadow(10, 1, Qt::black, this), + contextMenu(new QMenu()) { m_ui->setupUi(this); + shadow.setFrames(true, false, true, false); + + feed->setItemDelegate(delegate); + feed->setFrameShape(QFrame::NoFrame); + feed->setContextMenuPolicy(Qt::CustomContextMenu); + delegate->initializeFonts(feed->getFont()); + feed->setModel(el->feed); + el->feed->incrementObservers(); + m_ui->widget->layout()->addWidget(feed); + + connect(el->feed, &Models::MessageFeed::newMessage, this, &Conversation::onFeedMessage); + connect(feed, &FeedView::resized, this, &Conversation::positionShadow); + connect(feed, &FeedView::customContextMenuRequested, this, &Conversation::onFeedContext); + connect(acc, &Models::Account::childChanged, this, &Conversation::onAccountChanged); filesLayout = new FlowLayout(m_ui->filesPanel, 0); @@ -63,14 +75,7 @@ Conversation::Conversation(bool muc, Models::Account* acc, const QString pJid, c statusLabel = m_ui->statusLabel; connect(&ker, &KeyEnterReceiver::enterPressed, this, &Conversation::onEnterPressed); - connect(&scrollResizeCatcher, &Resizer::resized, this, &Conversation::onScrollResize); - connect(&vis, &VisibilityCatcher::shown, this, &Conversation::onScrollResize); - connect(&vis, &VisibilityCatcher::hidden, this, &Conversation::onScrollResize); connect(m_ui->sendButton, &QPushButton::clicked, this, &Conversation::onEnterPressed); - connect(line, &MessageLine::resize, this, &Conversation::onMessagesResize); - connect(line, &MessageLine::downloadFile, this, &Conversation::downloadFile); - connect(line, &MessageLine::uploadFile, this, qOverload(&Conversation::sendMessage)); - connect(line, &MessageLine::requestLocalFile, this, &Conversation::requestLocalFile); connect(m_ui->attachButton, &QPushButton::clicked, this, &Conversation::onAttach); connect(m_ui->clearButton, &QPushButton::clicked, this, &Conversation::onClearButton); connect(m_ui->messageEditor->document()->documentLayout(), &QAbstractTextDocumentLayout::documentSizeChanged, @@ -78,23 +83,43 @@ Conversation::Conversation(bool muc, Models::Account* acc, const QString pJid, c m_ui->messageEditor->installEventFilter(&ker); - QScrollBar* vs = m_ui->scrollArea->verticalScrollBar(); - m_ui->scrollArea->setWidget(line); - vs->installEventFilter(&vis); - line->setAutoFillBackground(false); - if (testAttribute(Qt::WA_TranslucentBackground)) { - m_ui->scrollArea->setAutoFillBackground(false); - } else { - m_ui->scrollArea->setBackgroundRole(QPalette::Base); + //line->setAutoFillBackground(false); + //if (testAttribute(Qt::WA_TranslucentBackground)) { + //m_ui->scrollArea->setAutoFillBackground(false); + //} else { + //m_ui->scrollArea->setBackgroundRole(QPalette::Base); + //} + + //line->setMyAvatarPath(acc->getAvatarPath()); + //line->setMyName(acc->getName()); + + initializeOverlay(); +} + +Conversation::~Conversation() +{ + delete contextMenu; + + element->feed->decrementObservers(); +} + +void Conversation::onAccountChanged(Models::Item* item, int row, int col) +{ + if (item == account) { + if (col == 2 && account->getState() == Shared::ConnectionState::connected) { //to request the history when we're back online after reconnect + //if (!requestingHistory) { + //requestingHistory = true; + //line->showBusyIndicator(); + //emit requestArchive(""); + //scroll = down; + //} + } } - - connect(vs, &QScrollBar::valueChanged, this, &Conversation::onSliderValueChanged); - m_ui->scrollArea->installEventFilter(&scrollResizeCatcher); - - line->setMyAvatarPath(acc->getAvatarPath()); - line->setMyName(acc->getName()); - +} + +void Conversation::initializeOverlay() +{ QGridLayout* gr = static_cast(layout()); QLabel* progressLabel = new QLabel(tr("Drop files here to attach them to your message")); gr->addWidget(overlay, 0, 0, 2, 1); @@ -115,36 +140,6 @@ Conversation::Conversation(bool muc, Models::Account* acc, const QString pJid, c nl->addWidget(progressLabel); nl->addStretch(); overlay->hide(); - - applyVisualEffects(); -} - -Conversation::~Conversation() -{ -} - -void Conversation::onAccountChanged(Models::Item* item, int row, int col) -{ - if (item == account) { - if (col == 2 && account->getState() == Shared::ConnectionState::connected) { - if (!requestingHistory) { - requestingHistory = true; - line->showBusyIndicator(); - emit requestArchive(""); - scroll = down; - } - } - } -} - -void Conversation::applyVisualEffects() -{ - DropShadowEffect *e1 = new DropShadowEffect; - e1->setBlurRadius(10); - e1->setColor(Qt::black); - e1->setThickness(1); - e1->setFrame(true, false, true, false); - m_ui->scrollArea->setGraphicsEffect(e1); } void Conversation::setName(const QString& name) @@ -163,22 +158,6 @@ QString Conversation::getJid() const return palJid; } -void Conversation::addMessage(const Shared::Message& data) -{ - int pos = m_ui->scrollArea->verticalScrollBar()->sliderPosition(); - int max = m_ui->scrollArea->verticalScrollBar()->maximum(); - - MessageLine::Position place = line->message(data); - if (place == MessageLine::invalid) { - return; - } -} - -void Conversation::changeMessage(const QString& id, const QMap& data) -{ - line->changeMessage(id, data); -} - KeyEnterReceiver::KeyEnterReceiver(QObject* parent): QObject(parent), ownEvent(false) {} bool KeyEnterReceiver::eventFilter(QObject* obj, QEvent* event) @@ -226,93 +205,19 @@ void Conversation::onEnterPressed() m_ui->messageEditor->clear(); Shared::Message msg = createMessage(); msg.setBody(body); - addMessage(msg); emit sendMessage(msg); } if (filesToAttach.size() > 0) { for (Badge* badge : filesToAttach) { Shared::Message msg = createMessage(); - line->appendMessageWithUpload(msg, badge->id); - usleep(1000); //this is required for the messages not to have equal time when appending into messageline + msg.setAttachPath(badge->id); + element->feed->registerUpload(msg.getId()); + emit sendMessage(msg); } - clearAttachedFiles(); + clearAttachedFiles(); } } -void Conversation::appendMessageWithUpload(const Shared::Message& data, const QString& path) -{ - line->appendMessageWithUploadNoSiganl(data, path); -} - -void Conversation::onMessagesResize(int amount) -{ - manualSliderChange = true; - switch (scroll) { - case down: - m_ui->scrollArea->verticalScrollBar()->setValue(m_ui->scrollArea->verticalScrollBar()->maximum()); - break; - case keep: { - int max = m_ui->scrollArea->verticalScrollBar()->maximum(); - int value = m_ui->scrollArea->verticalScrollBar()->value() + amount; - m_ui->scrollArea->verticalScrollBar()->setValue(value); - - if (value == max) { - scroll = down; - } else { - scroll = nothing; - } - } - break; - default: - break; - } - manualSliderChange = false; -} - -void Conversation::onSliderValueChanged(int value) -{ - if (!manualSliderChange) { - if (value == m_ui->scrollArea->verticalScrollBar()->maximum()) { - scroll = down; - } else { - if (!requestingHistory && value == 0) { - requestingHistory = true; - line->showBusyIndicator(); - emit requestArchive(line->firstMessageId()); - scroll = keep; - } else { - scroll = nothing; - } - } - } -} - -void Conversation::responseArchive(const std::list list) -{ - requestingHistory = false; - scroll = keep; - - line->hideBusyIndicator(); - for (std::list::const_iterator itr = list.begin(), end = list.end(); itr != end; ++itr) { - addMessage(*itr); - } -} - -void Conversation::showEvent(QShowEvent* event) -{ - if (!everShown) { - everShown = true; - line->showBusyIndicator(); - requestingHistory = true; - scroll = keep; - emit requestArchive(line->firstMessageId()); - } - emit shown(); - - QWidget::showEvent(event); - -} - void Conversation::onAttach() { QFileDialog* d = new QFileDialog(this, tr("Chose a file to send")); @@ -340,34 +245,6 @@ void Conversation::setStatus(const QString& status) statusLabel->setText(Shared::processMessageBody(status)); } -void Conversation::onScrollResize() -{ - if (everShown) { - int size = m_ui->scrollArea->width(); - QScrollBar* bar = m_ui->scrollArea->verticalScrollBar(); - if (bar->isVisible() && !tsb) { - size -= bar->width(); - - } - line->setMaximumWidth(size); - } -} - -void Conversation::responseFileProgress(const QString& messageId, qreal progress) -{ - line->fileProgress(messageId, progress); -} - -void Conversation::fileError(const QString& messageId, const QString& error) -{ - line->fileError(messageId, error); -} - -void Conversation::responseLocalFile(const QString& messageId, const QString& path) -{ - line->responseLocalFile(messageId, path); -} - Models::Roster::ElId Conversation::getId() const { return {getAccount(), getJid()}; @@ -444,7 +321,7 @@ void Conversation::onTextEditDocSizeChanged(const QSizeF& size) void Conversation::setFeedFrames(bool top, bool right, bool bottom, bool left) { - static_cast(m_ui->scrollArea->graphicsEffect())->setFrame(top, right, bottom, left); + shadow.setFrames(top, right, bottom, left); } void Conversation::dragEnterEvent(QDragEnterEvent* event) @@ -504,21 +381,65 @@ Shared::Message Conversation::createMessage() const return msg; } -bool VisibilityCatcher::eventFilter(QObject* obj, QEvent* event) +void Conversation::onFeedMessage(const Shared::Message& msg) { - if (event->type() == QEvent::Show) { - emit shown(); - } - - if (event->type() == QEvent::Hide) { - emit hidden(); - } - - return false; + this->onMessage(msg); } -VisibilityCatcher::VisibilityCatcher(QWidget* parent): -QObject(parent) +void Conversation::onMessage(const Shared::Message& msg) { + if (!msg.getForwarded()) { + QApplication::alert(this); + if (window()->windowState().testFlag(Qt::WindowMinimized)) { + emit notifyableMessage(getAccount(), msg); + } + } } +void Conversation::positionShadow() +{ + int w = width(); + int h = feed->height(); + + shadow.resize(w, h); + shadow.move(feed->pos()); + shadow.raise(); +} + +void Conversation::onFeedContext(const QPoint& pos) +{ + QModelIndex index = feed->indexAt(pos); + if (index.isValid()) { + Shared::Message* item = static_cast(index.internalPointer()); + + contextMenu->clear(); + bool showMenu = false; + if (item->getState() == Shared::Message::State::error) { + showMenu = true; + QString id = item->getId(); + QAction* resend = contextMenu->addAction(Shared::icon("view-refresh"), tr("Try sending again")); + connect(resend, &QAction::triggered, [this, id]() { + element->feed->registerUpload(id); + emit resendMessage(id); + }); + } + + QString path = item->getAttachPath(); + if (path.size() > 0) { + showMenu = true; + QAction* open = contextMenu->addAction(Shared::icon("document-preview"), tr("Open")); + connect(open, &QAction::triggered, [path]() { + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }); + + QAction* show = contextMenu->addAction(Shared::icon("folder"), tr("Show in folder")); + connect(show, &QAction::triggered, [path]() { + Shared::Global::highlightInFileManager(path); + }); + } + + if (showMenu) { + contextMenu->popup(feed->viewport()->mapToGlobal(pos)); + } + } +} diff --git a/ui/widgets/conversation.h b/ui/widgets/conversation.h index ea87607..b0eb745 100644 --- a/ui/widgets/conversation.h +++ b/ui/widgets/conversation.h @@ -24,18 +24,26 @@ #include #include #include +#include +#include +#include +#include #include "shared/message.h" -#include "order.h" -#include "ui/models/account.h" -#include "ui/models/roster.h" -#include "ui/utils/messageline.h" -#include "ui/utils/resizer.h" -#include "ui/utils/flowlayout.h" -#include "ui/utils/badge.h" +#include "shared/order.h" #include "shared/icons.h" #include "shared/utils.h" +#include "ui/models/account.h" +#include "ui/models/roster.h" + +#include "ui/utils/flowlayout.h" +#include "ui/utils/badge.h" +#include "ui/utils/shadowoverlay.h" + +#include "ui/widgets/messageline/feedview.h" +#include "ui/widgets/messageline/messagedelegate.h" + namespace Ui { class Conversation; @@ -54,54 +62,33 @@ signals: void enterPressed(); }; -class VisibilityCatcher : public QObject { - Q_OBJECT -public: - VisibilityCatcher(QWidget* parent = nullptr); - -protected: - bool eventFilter(QObject* obj, QEvent* event) override; - -signals: - void hidden(); - void shown(); -}; - class Conversation : public QWidget { Q_OBJECT public: - Conversation(bool muc, Models::Account* acc, const QString pJid, const QString pRes, QWidget* parent = 0); + Conversation(bool muc, Models::Account* acc, Models::Element* el, const QString pJid, const QString pRes, QWidget* parent = 0); ~Conversation(); QString getJid() const; QString getAccount() const; QString getPalResource() const; Models::Roster::ElId getId() const; - virtual void addMessage(const Shared::Message& data); void setPalResource(const QString& res); - void responseArchive(const std::list list); - void showEvent(QShowEvent * event) override; - void responseLocalFile(const QString& messageId, const QString& path); - void fileError(const QString& messageId, const QString& error); - void responseFileProgress(const QString& messageId, qreal progress); virtual void setAvatar(const QString& path); - void changeMessage(const QString& id, const QMap& data); void setFeedFrames(bool top, bool right, bool bottom, bool left); - virtual void appendMessageWithUpload(const Shared::Message& data, const QString& path); signals: void sendMessage(const Shared::Message& message); - void sendMessage(const Shared::Message& message, const QString& path); + void resendMessage(const QString& id); void requestArchive(const QString& before); void shown(); void requestLocalFile(const QString& messageId, const QString& url); void downloadFile(const QString& messageId, const QString& url); + void notifyableMessage(const QString& account, const Shared::Message& msg); protected: virtual void setName(const QString& name); - void applyVisualEffects(); virtual Shared::Message createMessage() const; void setStatus(const QString& status); void addAttachedFile(const QString& path); @@ -110,47 +97,44 @@ protected: void dragEnterEvent(QDragEnterEvent* event) override; void dragLeaveEvent(QDragLeaveEvent* event) override; void dropEvent(QDropEvent* event) override; + void initializeOverlay(); + virtual void onMessage(const Shared::Message& msg); protected slots: void onEnterPressed(); - void onMessagesResize(int amount); - void onSliderValueChanged(int value); void onAttach(); void onFileSelected(); - void onScrollResize(); void onBadgeClose(); void onClearButton(); void onTextEditDocSizeChanged(const QSizeF& size); void onAccountChanged(Models::Item* item, int row, int col); + void onFeedMessage(const Shared::Message& msg); + void positionShadow(); + void onFeedContext(const QPoint &pos); public: const bool isMuc; protected: - enum Scroll { - nothing, - keep, - down - }; Models::Account* account; + Models::Element* element; QString palJid; QString activePalResource; - MessageLine* line; QScopedPointer m_ui; KeyEnterReceiver ker; - Resizer scrollResizeCatcher; - VisibilityCatcher vis; QString thread; QLabel* statusIcon; QLabel* statusLabel; FlowLayout* filesLayout; QWidget* overlay; W::Order filesToAttach; - Scroll scroll; + FeedView* feed; + MessageDelegate* delegate; bool manualSliderChange; - bool requestingHistory; - bool everShown; bool tsb; //transient scroll bars + + ShadowOverlay shadow; + QMenu* contextMenu; }; #endif // CONVERSATION_H diff --git a/ui/widgets/conversation.ui b/ui/widgets/conversation.ui index 7093bcb..bb38666 100644 --- a/ui/widgets/conversation.ui +++ b/ui/widgets/conversation.ui @@ -214,61 +214,7 @@ - - - - true - - - QFrame::NoFrame - - - 0 - - - 0 - - - Qt::ScrollBarAlwaysOff - - - QAbstractScrollArea::AdjustIgnored - - - true - - - - - 0 - 0 - 520 - 385 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - scrollArea - widget_3 diff --git a/ui/widgets/messageline/CMakeLists.txt b/ui/widgets/messageline/CMakeLists.txt new file mode 100644 index 0000000..7cace9d --- /dev/null +++ b/ui/widgets/messageline/CMakeLists.txt @@ -0,0 +1,14 @@ +target_sources(squawk PRIVATE + messagedelegate.cpp + messagedelegate.h + #messageline.cpp + #messageline.h + preview.cpp + preview.h + messagefeed.cpp + messagefeed.h + feedview.cpp + feedview.h + #message.cpp + #message.h + ) diff --git a/ui/widgets/messageline/feedview.cpp b/ui/widgets/messageline/feedview.cpp new file mode 100644 index 0000000..6d8c180 --- /dev/null +++ b/ui/widgets/messageline/feedview.cpp @@ -0,0 +1,434 @@ +/* + * 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 "feedview.h" + +#include +#include +#include +#include + +#include "messagedelegate.h" +#include "messagefeed.h" + +constexpr int maxMessageHeight = 10000; +constexpr int approximateSingleMessageHeight = 20; +constexpr int progressSize = 70; + +const std::set FeedView::geometryChangingRoles = { + Models::MessageFeed::Attach, + Models::MessageFeed::Text, + Models::MessageFeed::Id, + Models::MessageFeed::Error, + Models::MessageFeed::Date +}; + +FeedView::FeedView(QWidget* parent): + QAbstractItemView(parent), + hints(), + vo(0), + specialDelegate(false), + specialModel(false), + clearWidgetsMode(false), + modelState(Models::MessageFeed::complete), + progress() +{ + horizontalScrollBar()->setRange(0, 0); + verticalScrollBar()->setSingleStep(approximateSingleMessageHeight); + setMouseTracking(true); + setSelectionBehavior(SelectItems); +// viewport()->setAttribute(Qt::WA_Hover, true); + + progress.setParent(viewport()); + progress.resize(progressSize, progressSize); +} + +FeedView::~FeedView() +{ +} + +QModelIndex FeedView::indexAt(const QPoint& point) const +{ + int32_t vh = viewport()->height(); + uint32_t y = vh - point.y() + vo; + + for (std::deque::size_type i = 0; i < hints.size(); ++i) { + const Hint& hint = hints[i]; + if (y <= hint.offset + hint.height) { + if (y > hint.offset) { + return model()->index(i, 0, rootIndex()); + } else { + break; + } + } + } + + return QModelIndex(); +} + +void FeedView::scrollTo(const QModelIndex& index, QAbstractItemView::ScrollHint hint) +{ +} + +QRect FeedView::visualRect(const QModelIndex& index) const +{ + unsigned int row = index.row(); + if (!index.isValid() || row >= hints.size()) { + qDebug() << "visualRect for" << row; + return QRect(); + } else { + const Hint& hint = hints.at(row); + const QWidget* vp = viewport(); + return QRect(0, vp->height() - hint.height - hint.offset + vo, vp->width(), hint.height); + } +} + +int FeedView::horizontalOffset() const +{ + return 0; +} + +bool FeedView::isIndexHidden(const QModelIndex& index) const +{ + return false; +} + +QModelIndex FeedView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) +{ + return QModelIndex(); +} + +void FeedView::setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags command) +{ +} + +int FeedView::verticalOffset() const +{ + return vo; +} + +QRegion FeedView::visualRegionForSelection(const QItemSelection& selection) const +{ + return QRegion(); +} + +void FeedView::rowsInserted(const QModelIndex& parent, int start, int end) +{ + QAbstractItemView::rowsInserted(parent, start, end); + + scheduleDelayedItemsLayout(); +} + +void FeedView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles) +{ + if (specialDelegate) { + for (int role : roles) { + if (geometryChangingRoles.count(role) != 0) { + scheduleDelayedItemsLayout(); //to recalculate layout only if there are some geometry changing modifications + break; + } + } + } + QAbstractItemView::dataChanged(topLeft, bottomRight, roles); +} + +void FeedView::updateGeometries() +{ + qDebug() << "updateGeometries"; + QScrollBar* bar = verticalScrollBar(); + + const QStyle* st = style(); + const QAbstractItemModel* m = model(); + QSize layoutBounds = maximumViewportSize(); + QStyleOptionViewItem option = viewOptions(); + option.rect.setHeight(maxMessageHeight); + option.rect.setWidth(layoutBounds.width()); + int frameAroundContents = 0; + int verticalScrollBarExtent = st->pixelMetric(QStyle::PM_ScrollBarExtent, 0, bar); + + bool layedOut = false; + if (verticalScrollBarExtent != 0 && verticalScrollBarPolicy() == Qt::ScrollBarAsNeeded && m->rowCount() * approximateSingleMessageHeight < layoutBounds.height()) { + hints.clear(); + layedOut = tryToCalculateGeometriesWithNoScrollbars(option, m, layoutBounds.height()); + } + + if (layedOut) { + bar->setRange(0, 0); + vo = 0; + } else { + int verticalMargin = 0; + if (st->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents)) { + frameAroundContents = st->pixelMetric(QStyle::PM_DefaultFrameWidth) * 2; + } + + if (verticalScrollBarPolicy() == Qt::ScrollBarAsNeeded) { + verticalMargin = verticalScrollBarExtent + frameAroundContents; + } + + layoutBounds.rwidth() -= verticalMargin; + + option.features |= QStyleOptionViewItem::WrapText; + option.rect.setWidth(layoutBounds.width()); + + hints.clear(); + uint32_t previousOffset = 0; + for (int i = 0, size = m->rowCount(); i < size; ++i) { + QModelIndex index = m->index(i, 0, rootIndex()); + int height = itemDelegate(index)->sizeHint(option, index).height(); + hints.emplace_back(Hint({ + false, + previousOffset, + static_cast(height) + })); + previousOffset += height; + } + + int totalHeight = previousOffset - layoutBounds.height(); + if (modelState != Models::MessageFeed::complete) { + totalHeight += progressSize; + } + vo = qMax(qMin(vo, totalHeight), 0); + bar->setRange(0, totalHeight); + bar->setPageStep(layoutBounds.height()); + bar->setValue(totalHeight - vo); + } + + positionProgress(); + + if (specialDelegate) { + clearWidgetsMode = true; + } + + + QAbstractItemView::updateGeometries(); +} + +bool FeedView::tryToCalculateGeometriesWithNoScrollbars(const QStyleOptionViewItem& option, const QAbstractItemModel* m, uint32_t totalHeight) +{ + uint32_t previousOffset = 0; + bool success = true; + for (int i = 0, size = m->rowCount(); i < size; ++i) { + QModelIndex index = m->index(i, 0, rootIndex()); + int height = itemDelegate(index)->sizeHint(option, index).height(); + + if (previousOffset + height > totalHeight) { + success = false; + break; + } + hints.emplace_back(Hint({ + false, + previousOffset, + static_cast(height) + })); + previousOffset += height; + } + + return success; +} + + +void FeedView::paintEvent(QPaintEvent* event) +{ + //qDebug() << "paint" << event->rect(); + const QAbstractItemModel* m = model(); + QWidget* vp = viewport(); + QRect zone = event->rect().translated(0, -vo); + uint32_t vph = vp->height(); + int32_t y1 = zone.y(); + int32_t y2 = y1 + zone.height(); + + bool inZone = false; + std::deque toRener; + for (std::deque::size_type i = 0; i < hints.size(); ++i) { + const Hint& hint = hints[i]; + int32_t relativeY1 = vph - hint.offset - hint.height; + if (!inZone) { + if (y2 > relativeY1) { + inZone = true; + } + } + if (inZone) { + toRener.emplace_back(m->index(i, 0, rootIndex())); + } + if (y1 > relativeY1) { + break; + } + } + + QPainter painter(vp); + QStyleOptionViewItem option = viewOptions(); + option.features = QStyleOptionViewItem::WrapText; + QPoint cursor = vp->mapFromGlobal(QCursor::pos()); + + if (specialDelegate) { + MessageDelegate* del = static_cast(itemDelegate()); + if (clearWidgetsMode) { + del->beginClearWidgets(); + } + } + + for (const QModelIndex& index : toRener) { + option.rect = visualRect(index); + bool mouseOver = option.rect.contains(cursor) && vp->rect().contains(cursor); + option.state.setFlag(QStyle::State_MouseOver, mouseOver); + itemDelegate(index)->paint(&painter, option, index); + } + + if (clearWidgetsMode && specialDelegate) { + MessageDelegate* del = static_cast(itemDelegate()); + del->endClearWidgets(); + clearWidgetsMode = false; + } + + if (event->rect().height() == vp->height()) { + // draw the blurred drop shadow... + } +} + +void FeedView::verticalScrollbarValueChanged(int value) +{ + vo = verticalScrollBar()->maximum() - value; + + positionProgress(); + + if (specialDelegate) { + clearWidgetsMode = true; + } + + if (modelState == Models::MessageFeed::incomplete && value < progressSize) { + model()->fetchMore(rootIndex()); + } + + QAbstractItemView::verticalScrollbarValueChanged(vo); +} + +void FeedView::mouseMoveEvent(QMouseEvent* event) +{ + if (!isVisible()) { + return; + } + + QAbstractItemView::mouseMoveEvent(event); +} + +void FeedView::resizeEvent(QResizeEvent* event) +{ + QAbstractItemView::resizeEvent(event); + + positionProgress(); + emit resized(); +} + +void FeedView::positionProgress() +{ + QSize layoutBounds = maximumViewportSize(); + int progressPosition = layoutBounds.height() - progressSize; + std::deque::size_type size = hints.size(); + if (size > 0) { + const Hint& hint = hints[size - 1]; + progressPosition -= hint.offset + hint.height; + } + progressPosition += vo; + progressPosition = qMin(progressPosition, 0); + + progress.move((width() - progressSize) / 2, progressPosition); +} + +QFont FeedView::getFont() const +{ + return viewOptions().font; +} + +void FeedView::setItemDelegate(QAbstractItemDelegate* delegate) +{ + if (specialDelegate) { + MessageDelegate* del = static_cast(itemDelegate()); + disconnect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed); + disconnect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath); + } + + QAbstractItemView::setItemDelegate(delegate); + + MessageDelegate* del = dynamic_cast(delegate); + if (del) { + specialDelegate = true; + connect(del, &MessageDelegate::buttonPushed, this, &FeedView::onMessageButtonPushed); + connect(del, &MessageDelegate::invalidPath, this, &FeedView::onMessageInvalidPath); + } else { + specialDelegate = false; + } +} + +void FeedView::setModel(QAbstractItemModel* p_model) +{ + if (specialModel) { + Models::MessageFeed* feed = static_cast(model()); + disconnect(feed, &Models::MessageFeed::syncStateChange, this, &FeedView::onModelSyncStateChange); + } + + QAbstractItemView::setModel(p_model); + + Models::MessageFeed* feed = dynamic_cast(p_model); + if (feed) { + onModelSyncStateChange(feed->getSyncState()); + specialModel = true; + connect(feed, &Models::MessageFeed::syncStateChange, this, &FeedView::onModelSyncStateChange); + } else { + onModelSyncStateChange(Models::MessageFeed::complete); + specialModel = false; + } +} + +void FeedView::onMessageButtonPushed(const QString& messageId) +{ + if (specialModel) { + Models::MessageFeed* feed = static_cast(model()); + feed->downloadAttachment(messageId); + } +} + +void FeedView::onMessageInvalidPath(const QString& messageId) +{ + if (specialModel) { + Models::MessageFeed* feed = static_cast(model()); + feed->reportLocalPathInvalid(messageId); + } +} + +void FeedView::onModelSyncStateChange(Models::MessageFeed::SyncState state) +{ + bool needToUpdateGeometry = false; + if (modelState != state) { + if (state == Models::MessageFeed::complete || modelState == Models::MessageFeed::complete) { + needToUpdateGeometry = true; + } + modelState = state; + + if (state == Models::MessageFeed::syncing) { + progress.show(); + progress.start(); + } else { + progress.stop(); + progress.hide(); + } + } + + if (needToUpdateGeometry) { + scheduleDelayedItemsLayout(); + } +} diff --git a/ui/widgets/messageline/feedview.h b/ui/widgets/messageline/feedview.h new file mode 100644 index 0000000..b20276c --- /dev/null +++ b/ui/widgets/messageline/feedview.h @@ -0,0 +1,95 @@ +/* + * 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 FEEDVIEW_H +#define FEEDVIEW_H + +#include + +#include +#include + +#include +#include + +/** + * @todo write docs + */ +class FeedView : public QAbstractItemView +{ + Q_OBJECT +public: + FeedView(QWidget* parent = nullptr); + ~FeedView(); + + QModelIndex indexAt(const QPoint & point) const override; + void scrollTo(const QModelIndex & index, QAbstractItemView::ScrollHint hint) override; + QRect visualRect(const QModelIndex & index) const override; + bool isIndexHidden(const QModelIndex & index) const override; + QModelIndex moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override; + void setSelection(const QRect & rect, QItemSelectionModel::SelectionFlags command) override; + QRegion visualRegionForSelection(const QItemSelection & selection) const override; + void setItemDelegate(QAbstractItemDelegate* delegate); + void setModel(QAbstractItemModel * model) override; + + QFont getFont() const; + +signals: + void resized(); + +public slots: + +protected slots: + void rowsInserted(const QModelIndex & parent, int start, int end) override; + void verticalScrollbarValueChanged(int value) override; + void dataChanged(const QModelIndex & topLeft, const QModelIndex & bottomRight, const QVector & roles) override; + void onMessageButtonPushed(const QString& messageId); + void onMessageInvalidPath(const QString& messageId); + void onModelSyncStateChange(Models::MessageFeed::SyncState state); + +protected: + int verticalOffset() const override; + int horizontalOffset() const override; + void paintEvent(QPaintEvent * event) override; + void updateGeometries() override; + void mouseMoveEvent(QMouseEvent * event) override; + void resizeEvent(QResizeEvent * event) override; + +private: + bool tryToCalculateGeometriesWithNoScrollbars(const QStyleOptionViewItem& option, const QAbstractItemModel* model, uint32_t totalHeight); + void positionProgress(); + +private: + struct Hint { + bool dirty; + uint32_t offset; + uint32_t height; + }; + std::deque hints; + int vo; + bool specialDelegate; + bool specialModel; + bool clearWidgetsMode; + Models::MessageFeed::SyncState modelState; + Progress progress; + + static const std::set geometryChangingRoles; + +}; + +#endif //FEEDVIEW_H diff --git a/ui/utils/message.cpp b/ui/widgets/messageline/message.cpp similarity index 100% rename from ui/utils/message.cpp rename to ui/widgets/messageline/message.cpp diff --git a/ui/utils/message.h b/ui/widgets/messageline/message.h similarity index 100% rename from ui/utils/message.h rename to ui/widgets/messageline/message.h diff --git a/ui/widgets/messageline/messagedelegate.cpp b/ui/widgets/messageline/messagedelegate.cpp new file mode 100644 index 0000000..8728ba3 --- /dev/null +++ b/ui/widgets/messageline/messagedelegate.cpp @@ -0,0 +1,579 @@ +/* + * 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 + +#include "messagedelegate.h" +#include "messagefeed.h" + +constexpr int avatarHeight = 50; +constexpr int margin = 6; +constexpr int textMargin = 2; +constexpr int statusIconSize = 16; + +MessageDelegate::MessageDelegate(QObject* parent): + QStyledItemDelegate(parent), + bodyFont(), + nickFont(), + dateFont(), + bodyMetrics(bodyFont), + nickMetrics(nickFont), + dateMetrics(dateFont), + buttonHeight(0), + barHeight(0), + buttons(new std::map()), + bars(new std::map()), + statusIcons(new std::map()), + pencilIcons(new std::map()), + bodies(new std::map()), + previews(new std::map()), + idsToKeep(new std::set()), + clearingWidgets(false) +{ + QPushButton btn; + buttonHeight = btn.sizeHint().height(); + + QProgressBar bar; + barHeight = bar.sizeHint().height(); +} + +MessageDelegate::~MessageDelegate() +{ + for (const std::pair& pair: *buttons){ + delete pair.second; + } + + for (const std::pair& pair: *bars){ + delete pair.second; + } + + for (const std::pair& pair: *statusIcons){ + delete pair.second; + } + + for (const std::pair& pair: *pencilIcons){ + delete pair.second; + } + + for (const std::pair& pair: *bodies){ + delete pair.second; + } + + for (const std::pair& pair: *previews){ + delete pair.second; + } + + delete statusIcons; + delete pencilIcons; + delete idsToKeep; + delete buttons; + delete bars; + delete bodies; + delete previews; +} + +void MessageDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + QVariant vi = index.data(Models::MessageFeed::Bulk); + if (!vi.isValid()) { + return; + } + Models::FeedItem data = qvariant_cast(vi); + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + + if (option.state & QStyle::State_MouseOver) { + painter->fillRect(option.rect, option.palette.brush(QPalette::Inactive, QPalette::Highlight)); + } + + QIcon icon(data.avatar); + + if (data.sentByMe) { + painter->drawPixmap(option.rect.width() - avatarHeight - margin, option.rect.y() + margin / 2, icon.pixmap(avatarHeight, avatarHeight)); + } else { + painter->drawPixmap(margin, option.rect.y() + margin / 2, icon.pixmap(avatarHeight, avatarHeight)); + } + + QStyleOptionViewItem opt = option; + QRect messageRect = option.rect.adjusted(margin, margin / 2, -(avatarHeight + 2 * margin), -margin / 2); + if (!data.sentByMe) { + opt.displayAlignment = Qt::AlignLeft | Qt::AlignTop; + messageRect.adjust(avatarHeight + margin, 0, avatarHeight + margin, 0); + } else { + opt.displayAlignment = Qt::AlignRight | Qt::AlignTop; + } + opt.rect = messageRect; + + QSize messageSize(0, 0); + QSize bodySize(0, 0); + if (data.text.size() > 0) { + messageSize = bodyMetrics.boundingRect(messageRect, Qt::TextWordWrap, data.text).size(); + bodySize = messageSize; + } + messageSize.rheight() += nickMetrics.lineSpacing(); + messageSize.rheight() += dateMetrics.height(); + if (messageSize.width() < opt.rect.width()) { + QSize senderSize = nickMetrics.boundingRect(messageRect, 0, data.sender).size(); + if (senderSize.width() > messageSize.width()) { + messageSize.setWidth(senderSize.width()); + } + QSize dateSize = dateMetrics.boundingRect(messageRect, 0, data.date.toLocalTime().toString()).size(); + int addition = 0; + + if (data.correction.corrected) { + addition += margin + statusIconSize; + } + if (data.sentByMe) { + addition += margin + statusIconSize; + } + if (dateSize.width() + addition > messageSize.width()) { + messageSize.setWidth(dateSize.width() + addition); + } + } else { + messageSize.setWidth(opt.rect.width()); + } + + QRect rect; + painter->setFont(nickFont); + painter->drawText(opt.rect, opt.displayAlignment, data.sender, &rect); + opt.rect.adjust(0, rect.height() + textMargin, 0, 0); + painter->save(); + switch (data.attach.state) { + case Models::none: + clearHelperWidget(data); //i can't imagine the situation where it's gonna be needed + break; //but it's a possible performance problem + case Models::uploading: + paintPreview(data, painter, opt); + case Models::downloading: + paintBar(getBar(data), painter, data.sentByMe, opt); + break; + case Models::remote: + paintButton(getButton(data), painter, data.sentByMe, opt); + break; + case Models::ready: + case Models::local: + clearHelperWidget(data); + paintPreview(data, painter, opt); + break; + case Models::errorDownload: { + paintButton(getButton(data), painter, data.sentByMe, opt); + paintComment(data, painter, opt); + } + + break; + case Models::errorUpload:{ + clearHelperWidget(data); + paintPreview(data, painter, opt); + paintComment(data, painter, opt); + } + break; + } + painter->restore(); + + int messageLeft = INT16_MAX; + int messageRight = opt.rect.x() + messageSize.width(); + QWidget* vp = static_cast(painter->device()); + if (data.text.size() > 0) { + QLabel* body = getBody(data); + body->setParent(vp); + body->setMaximumWidth(bodySize.width()); + body->setMinimumWidth(bodySize.width()); + body->setMinimumHeight(bodySize.height()); + body->setMaximumHeight(bodySize.height()); + body->setAlignment(opt.displayAlignment); + messageLeft = opt.rect.x(); + if (data.sentByMe) { + messageLeft = opt.rect.topRight().x() - bodySize.width(); + } + body->move(messageLeft, opt.rect.y()); + body->show(); + opt.rect.adjust(0, bodySize.height() + textMargin, 0, 0); + } + painter->setFont(dateFont); + QColor q = painter->pen().color(); + q.setAlpha(180); + painter->setPen(q); + painter->drawText(opt.rect, opt.displayAlignment, data.date.toLocalTime().toString(), &rect); + int currentY = opt.rect.y(); + if (data.sentByMe) { + QLabel* statusIcon = getStatusIcon(data); + + statusIcon->setParent(vp); + statusIcon->move(opt.rect.topRight().x() - messageSize.width(), currentY); + statusIcon->show(); + + opt.rect.adjust(0, statusIconSize + textMargin, 0, 0); + } + + if (data.correction.corrected) { + QLabel* pencilIcon = getPencilIcon(data); + + pencilIcon->setParent(vp); + if (data.sentByMe) { + pencilIcon->move(opt.rect.topRight().x() - messageSize.width() + statusIconSize + margin, currentY); + } else { + pencilIcon->move(messageRight - statusIconSize - margin, currentY); + } + pencilIcon->show(); + } else { + std::map::const_iterator itr = pencilIcons->find(data.id); + if (itr != pencilIcons->end()) { + delete itr->second; + pencilIcons->erase(itr); + } + } + + painter->restore(); + + if (clearingWidgets) { + idsToKeep->insert(data.id); + } +} + +QSize MessageDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + QRect messageRect = option.rect.adjusted(0, margin / 2, -(avatarHeight + 3 * margin), -margin / 2); + QStyleOptionViewItem opt = option; + opt.rect = messageRect; + QVariant va = index.data(Models::MessageFeed::Attach); + Models::Attachment attach = qvariant_cast(va); + QString body = index.data(Models::MessageFeed::Text).toString(); + QSize messageSize(0, 0); + if (body.size() > 0) { + messageSize = bodyMetrics.boundingRect(messageRect, Qt::TextWordWrap, body).size(); + messageSize.rheight() += textMargin; + } + + switch (attach.state) { + case Models::none: + break; + case Models::uploading: + messageSize.rheight() += Preview::calculateAttachSize(attach.localPath, messageRect).height() + textMargin; + case Models::downloading: + messageSize.rheight() += barHeight + textMargin; + break; + case Models::remote: + messageSize.rheight() += buttonHeight + textMargin; + break; + case Models::ready: + case Models::local: + messageSize.rheight() += Preview::calculateAttachSize(attach.localPath, messageRect).height() + textMargin; + break; + case Models::errorDownload: + messageSize.rheight() += buttonHeight + textMargin; + messageSize.rheight() += dateMetrics.boundingRect(messageRect, Qt::TextWordWrap, attach.error).size().height() + textMargin; + break; + case Models::errorUpload: + messageSize.rheight() += Preview::calculateAttachSize(attach.localPath, messageRect).height() + textMargin; + messageSize.rheight() += dateMetrics.boundingRect(messageRect, Qt::TextWordWrap, attach.error).size().height() + textMargin; + break; + } + + messageSize.rheight() += nickMetrics.lineSpacing(); + messageSize.rheight() += textMargin; + messageSize.rheight() += dateMetrics.height() > statusIconSize ? dateMetrics.height() : statusIconSize; + + if (messageSize.height() < avatarHeight) { + messageSize.setHeight(avatarHeight); + } + + messageSize.rheight() += margin; + + return messageSize; +} + +void MessageDelegate::initializeFonts(const QFont& font) +{ + bodyFont = font; + nickFont = font; + dateFont = font; + + nickFont.setBold(true); + + float ndps = nickFont.pointSizeF(); + if (ndps != -1) { + nickFont.setPointSizeF(ndps * 1.2); + } else { + nickFont.setPointSize(nickFont.pointSize() + 2); + } + + dateFont.setItalic(true); + float dps = dateFont.pointSizeF(); + if (dps != -1) { + dateFont.setPointSizeF(dps * 0.8); + } else { + dateFont.setPointSize(dateFont.pointSize() - 2); + } + + bodyMetrics = QFontMetrics(bodyFont); + nickMetrics = QFontMetrics(nickFont); + dateMetrics = QFontMetrics(dateFont); + + Preview::initializeFont(bodyFont); +} + +bool MessageDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) +{ + //qDebug() << event->type(); + + + return QStyledItemDelegate::editorEvent(event, model, option, index); +} + +void MessageDelegate::paintButton(QPushButton* btn, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const +{ + QPoint start; + if (sentByMe) { + start = {option.rect.width() - btn->width(), option.rect.top()}; + } else { + start = option.rect.topLeft(); + } + + QWidget* vp = static_cast(painter->device()); + btn->setParent(vp); + btn->move(start); + btn->show(); + + option.rect.adjust(0, buttonHeight + textMargin, 0, 0); +} + +void MessageDelegate::paintComment(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const +{ + painter->setFont(dateFont); + QColor q = painter->pen().color(); + q.setAlpha(180); + painter->setPen(q); + QRect rect; + painter->drawText(option.rect, option.displayAlignment, data.attach.error, &rect); + option.rect.adjust(0, rect.height() + textMargin, 0, 0); +} + +void MessageDelegate::paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const +{ + QPoint start = option.rect.topLeft(); + bar->resize(option.rect.width(), barHeight); + + painter->translate(start); + bar->render(painter, QPoint(), QRegion(), QWidget::DrawChildren); + + option.rect.adjust(0, barHeight + textMargin, 0, 0); +} + +void MessageDelegate::paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const +{ + Preview* preview = 0; + std::map::iterator itr = previews->find(data.id); + + QSize size = option.rect.size(); + if (itr != previews->end()) { + preview = itr->second; + preview->actualize(data.attach.localPath, size, option.rect.topLeft()); + } else { + QWidget* vp = static_cast(painter->device()); + preview = new Preview(data.attach.localPath, size, option.rect.topLeft(), data.sentByMe, vp); + previews->insert(std::make_pair(data.id, preview)); + } + + if (!preview->isFileReachable()) { //this is the situation when the file preview couldn't be painted because the file was moved + emit invalidPath(data.id); //or deleted. This signal notifies the model, and the model notifies the core, preview can + } //handle being invalid for as long as I need and can be even become valid again with a new path + + option.rect.adjust(0, preview->size().height() + textMargin, 0, 0); +} + +QPushButton * MessageDelegate::getButton(const Models::FeedItem& data) const +{ + std::map::const_iterator itr = buttons->find(data.id); + FeedButton* result = 0; + if (itr != buttons->end()) { + result = itr->second; + } else { + std::map::const_iterator barItr = bars->find(data.id); + if (barItr != bars->end()) { + delete barItr->second; + bars->erase(barItr); + } + } + + if (result == 0) { + result = new FeedButton(); + result->messageId = data.id; + result->setText(QCoreApplication::translate("MessageLine", "Download")); + buttons->insert(std::make_pair(data.id, result)); + connect(result, &QPushButton::clicked, this, &MessageDelegate::onButtonPushed); + } + + return result; +} + +QProgressBar * MessageDelegate::getBar(const Models::FeedItem& data) const +{ + std::map::const_iterator barItr = bars->find(data.id); + QProgressBar* result = 0; + if (barItr != bars->end()) { + result = barItr->second; + } else { + std::map::const_iterator itr = buttons->find(data.id); + if (itr != buttons->end()) { + delete itr->second; + buttons->erase(itr); + } + } + + if (result == 0) { + result = new QProgressBar(); + result->setRange(0, 100); + bars->insert(std::make_pair(data.id, result)); + } + + result->setValue(data.attach.progress * 100); + + return result; +} + +QLabel * MessageDelegate::getStatusIcon(const Models::FeedItem& data) const +{ + std::map::const_iterator itr = statusIcons->find(data.id); + QLabel* result = 0; + + if (itr != statusIcons->end()) { + result = itr->second; + } else { + result = new QLabel(); + statusIcons->insert(std::make_pair(data.id, result)); + } + + QIcon q(Shared::icon(Shared::messageStateThemeIcons[static_cast(data.state)])); + QString tt = Shared::Global::getName(data.state); + if (data.state == Shared::Message::State::error) { + if (data.error > 0) { + tt += ": " + data.error; + } + } + if (result->toolTip() != tt) { //If i just assign pixmap every time unconditionally + result->setPixmap(q.pixmap(statusIconSize)); //it invokes an infinite cycle of repaint + result->setToolTip(tt); //may be it's better to subclass and store last condition in int? + } + + return result; +} + +QLabel * MessageDelegate::getPencilIcon(const Models::FeedItem& data) const +{ + std::map::const_iterator itr = pencilIcons->find(data.id); + QLabel* result = 0; + + if (itr != pencilIcons->end()) { + result = itr->second; + } else { + result = new QLabel(); + QIcon icon = Shared::icon("edit-rename"); + result->setPixmap(icon.pixmap(statusIconSize)); + pencilIcons->insert(std::make_pair(data.id, result)); + } + + result->setToolTip("Last time edited: " + data.correction.lastCorrection.toLocalTime().toString() + + "\nOriginal message: " + data.correction.original); + + return result; +} + +QLabel * MessageDelegate::getBody(const Models::FeedItem& data) const +{ + std::map::const_iterator itr = bodies->find(data.id); + QLabel* result = 0; + + if (itr != bodies->end()) { + result = itr->second; + } else { + result = new QLabel(); + result->setFont(bodyFont); + result->setWordWrap(true); + result->setOpenExternalLinks(true); + result->setTextInteractionFlags(result->textInteractionFlags() | Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse); + bodies->insert(std::make_pair(data.id, result)); + } + + result->setText(Shared::processMessageBody(data.text)); + + return result; +} + +void MessageDelegate::beginClearWidgets() +{ + idsToKeep->clear(); + clearingWidgets = true; +} + +template +void removeElements(std::map* elements, std::set* idsToKeep) { + std::set toRemove; + for (const std::pair& pair: *elements) { + if (idsToKeep->find(pair.first) == idsToKeep->end()) { + delete pair.second; + toRemove.insert(pair.first); + } + } + for (const QString& key : toRemove) { + elements->erase(key); + } +} + +void MessageDelegate::endClearWidgets() +{ + if (clearingWidgets) { + removeElements(buttons, idsToKeep); + removeElements(bars, idsToKeep); + removeElements(statusIcons, idsToKeep); + removeElements(pencilIcons, idsToKeep); + removeElements(bodies, idsToKeep); + removeElements(previews, idsToKeep); + + idsToKeep->clear(); + clearingWidgets = false; + } +} + +void MessageDelegate::onButtonPushed() const +{ + FeedButton* btn = static_cast(sender()); + emit buttonPushed(btn->messageId); +} + +void MessageDelegate::clearHelperWidget(const Models::FeedItem& data) const +{ + std::map::const_iterator itr = buttons->find(data.id); + if (itr != buttons->end()) { + delete itr->second; + buttons->erase(itr); + } else { + std::map::const_iterator barItr = bars->find(data.id); + if (barItr != bars->end()) { + delete barItr->second; + bars->erase(barItr); + } + } +} + +// void MessageDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const +// { +// +// } diff --git a/ui/widgets/messageline/messagedelegate.h b/ui/widgets/messageline/messagedelegate.h new file mode 100644 index 0000000..7403285 --- /dev/null +++ b/ui/widgets/messageline/messagedelegate.h @@ -0,0 +1,105 @@ +/* + * 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 MESSAGEDELEGATE_H +#define MESSAGEDELEGATE_H + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "shared/icons.h" +#include "shared/global.h" +#include "shared/utils.h" + +#include "preview.h" + +namespace Models { + struct FeedItem; +}; + +class MessageDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + MessageDelegate(QObject *parent = nullptr); + ~MessageDelegate(); + + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; + //void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override; + + void initializeFonts(const QFont& font); + bool editorEvent(QEvent * event, QAbstractItemModel * model, const QStyleOptionViewItem & option, const QModelIndex & index) override; + void endClearWidgets(); + void beginClearWidgets(); + +signals: + void buttonPushed(const QString& messageId) const; + void invalidPath(const QString& messageId) const; + +protected: + void paintButton(QPushButton* btn, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const; + void paintBar(QProgressBar* bar, QPainter* painter, bool sentByMe, QStyleOptionViewItem& option) const; + void paintPreview(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const; + void paintComment(const Models::FeedItem& data, QPainter* painter, QStyleOptionViewItem& option) const; + QPushButton* getButton(const Models::FeedItem& data) const; + QProgressBar* getBar(const Models::FeedItem& data) const; + QLabel* getStatusIcon(const Models::FeedItem& data) const; + QLabel* getPencilIcon(const Models::FeedItem& data) const; + QLabel* getBody(const Models::FeedItem& data) const; + void clearHelperWidget(const Models::FeedItem& data) const; + +protected slots: + void onButtonPushed() const; + +private: + class FeedButton : public QPushButton { + public: + QString messageId; + }; + + QFont bodyFont; + QFont nickFont; + QFont dateFont; + QFontMetrics bodyMetrics; + QFontMetrics nickMetrics; + QFontMetrics dateMetrics; + + int buttonHeight; + int barHeight; + + std::map* buttons; + std::map* bars; + std::map* statusIcons; + std::map* pencilIcons; + std::map* bodies; + std::map* previews; + std::set* idsToKeep; + bool clearingWidgets; + +}; + +#endif // MESSAGEDELEGATE_H diff --git a/ui/widgets/messageline/messagefeed.cpp b/ui/widgets/messageline/messagefeed.cpp new file mode 100644 index 0000000..733cf1d --- /dev/null +++ b/ui/widgets/messageline/messagefeed.cpp @@ -0,0 +1,674 @@ +/* + * 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 +#include + +#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