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..8632b38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,117 +1,143 @@ -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) - -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Debug) +set(WIN32_FLAG "") +set(MACOSX_BUNDLE_FLAG "") +if (CMAKE_BUILD_TYPE STREQUAL "Release") + if (WIN32) + set(WIN32_FLAG WIN32) + endif(WIN32) + if (APPLE) + set(MACOSX_BUNDLE_FLAG MACOSX_BUNDLE) + endif(APPLE) endif() -set(CMAKE_CXX_FLAGS_DEBUG "-g -Wall -Wextra") -set(CMAKE_CXX_FLAGS_RELEASE "-O3") +add_executable(squawk ${WIN32_FLAG} ${MACOSX_BUNDLE_FLAG}) +target_include_directories(squawk PRIVATE ${CMAKE_SOURCE_DIR}) + +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) +find_package(Boost COMPONENTS) + +target_include_directories(squawk PRIVATE ${Boost_INCLUDE_DIRS}) +target_include_directories(squawk PRIVATE ${Qt5_INCLUDE_DIRS}) +target_include_directories(squawk PRIVATE ${Qt5Widgets_INCLUDE_DIRS}) +target_include_directories(squawk PRIVATE ${Qt5DBus_INCLUDE_DIRS}) +target_include_directories(squawk PRIVATE ${Qt5Gui_INCLUDE_DIRS}) +target_include_directories(squawk PRIVATE ${Qt5Xml_INCLUDE_DIRS}) +target_include_directories(squawk PRIVATE ${Qt5Network_INCLUDE_DIRS}) +target_include_directories(squawk PRIVATE ${Qt5Core_INCLUDE_DIRS}) + +## 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) +# Link thread libraries on Linux +if(UNIX AND NOT APPLE) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) + target_link_libraries(squawk PRIVATE Threads::Threads) +endif() + +# Build type +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Debug) +endif () + message("Build type: ${CMAKE_BUILD_TYPE}") +if(CMAKE_COMPILER_IS_GNUCXX) +target_compile_options(squawk PRIVATE + "-Wall;-Wextra" + "$<$:-g>" + "$<$:-O3>" + ) +endif(CMAKE_COMPILER_IS_GNUCXX) -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) + +if (CMAKE_BUILD_TYPE STREQUAL "Release") + if (APPLE) + add_custom_command(TARGET squawk POST_BUILD COMMENT "Running macdeployqt..." + COMMAND "${Qt5Widgets_DIR}/../../../bin/macdeployqt" "${CMAKE_CURRENT_BINARY_DIR}/squawk.app" + ) + endif(APPLE) +endif() + diff --git a/README.md b/README.md index 30c6473..e94972f 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,12 @@ ### Prerequisites - QT 5.12 *(lower versions might work but it wasn't tested)* -- uuid _(usually included in some other package, for example it's ***libutil-linux*** in archlinux)_ - lmdb - CMake 3.0 or higher - qxmpp 1.1.0 or higher -- kwallet (optional) +- KDE Frameworks: kwallet (optional) +- KDE Frameworks: KIO (optional) +- Boost ### Getting @@ -33,6 +34,26 @@ You can also clone the repo and build it from source Squawk requires Qt with SSL enabled. It uses CMake as build system. +Please check the prerequisites and install them before installation. + +#### For Windows (Mingw-w64) build + +You need Qt for mingw64 (MinGW 64-bit) platform when installing Qt. + +The best way to acquire library `lmdb` and `boost` is through Msys2. + +First install Msys2, and then install `mingw-w64-x86_64-lmdb` and `mingw-w64-x86_64-boost` by pacman. + +Then you need to provide the cmake cache entry when calling cmake for configuration: + +``` +cmake .. -D LMDB_ROOT_DIR:PATH= -D BOOST_ROOT:PATH= +``` + +``: e.g. `C:/msys64/mingw64`. + +--- + There are two ways to build, it depends whether you have qxmpp installed in your system #### Building with system qxmpp @@ -44,7 +65,7 @@ $ git clone https://git.macaw.me/blue/squawk $ cd squawk $ mkdir build $ cd build -$ cmake .. +$ cmake .. [-D LMDB_ROOT_DIR:PATH=...] [-D BOOST_ROOT:PATH=...] $ cmake --build . ``` @@ -57,16 +78,19 @@ $ git clone --recurse-submodules https://git.macaw.me/blue/squawk $ cd squawk $ mkdir build $ cd build -$ cmake .. -D SYSTEM_QXMPP=False +$ cmake .. -D SYSTEM_QXMPP=False [-D LMDB_ROOT_DIR:PATH=...] [-D BOOST_ROOT:PATH=...] $ cmake --build . ``` +You can always refer to `appveyor.yml` to see how AppVeyor build squawk. + ### List of keys 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/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..9b20f3b --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,117 @@ +image: + - Visual Studio 2019 + - "Previous Ubuntu1804" + - macOS-Mojave + +branches: + except: + - gh-pages + +for: +- + matrix: + only: + - image: Visual Studio 2019 + + environment: + QTDIR: C:\Qt\5.15.2\mingw81_64 + QTTOOLDIR: C:\Qt\Tools\mingw810_64\bin + QTNINJADIR: C:\Qt\Tools\Ninja + + install: + - set PATH=%QTTOOLDIR%;%QTNINJADIR%;%QTDIR%\bin;%PATH% + - git submodule update --init --recursive + + before_build: + - choco install --yes zstandard + - choco install --yes --version=7.1.0.2 imagemagick.app + - set PATH=C:\Program Files\ImageMagick-7.1.0-Q16-HDRI;%PATH% + + - mkdir lmdb + - cd lmdb + - curl -OL https://mirror.msys2.org/mingw/mingw64/mingw-w64-x86_64-lmdb-0.9.27-1-any.pkg.tar.zst + - zstd -d ./mingw-w64-x86_64-lmdb-0.9.27-1-any.pkg.tar.zst + - tar -xvf ./mingw-w64-x86_64-lmdb-0.9.27-1-any.pkg.tar + - cd .. + + - mkdir boost + - cd boost + - curl -OL https://repo.msys2.org/mingw/mingw64/mingw-w64-x86_64-boost-1.77.0-2-any.pkg.tar.zst + - zstd -d ./mingw-w64-x86_64-boost-1.77.0-2-any.pkg.tar.zst + - tar -xvf ./mingw-w64-x86_64-boost-1.77.0-2-any.pkg.tar + - cd .. + + - mkdir build + - cd build + - cmake -GNinja -DCMAKE_BUILD_TYPE:String=Release -DCMAKE_PREFIX_PATH:STRING=%QTDIR% -DLMDB_ROOT_DIR:PATH=C:/projects/squawk/lmdb/mingw64 -DBOOST_ROOT:PATH=C:/projects/squawk/boost/mingw64 .. + + build_script: + - cmake --build . + - mkdir deploy + - cd deploy + - copy ..\squawk.exe .\ + - copy ..\external\qxmpp\src\libqxmpp.dll .\ + - windeployqt .\squawk.exe + - windeployqt .\libqxmpp.dll + - cd ..\.. + + artifacts: + - path: build/deploy/squawk.exe + name: Squawk executable (Qt 5.15.2) + + - path: build/deploy + name: Squawk deployment with Qt Framework + +- + matrix: + only: + - image: macOS + + install: + - brew install lmdb imagemagick boost + - git submodule update --init --recursive + + before_build: + - mkdir build + - cd build + - cmake -DCMAKE_BUILD_TYPE:String=Release -DCMAKE_PREFIX_PATH:STRING=$HOME/Qt/5.15.2/clang_64 .. + + build_script: + - cmake --build . + + after_build: + - zip -r squawk.app.zip squawk.app + + artifacts: + - path: build/squawk.app/Contents/MacOS/squawk + name: Squawk executable (Qt 5.15.2) + - path: build/external/qxmpp/src/ + name: QXMPP + - path: build/squawk.app.zip + name: Squawk Bundle with Qt Framework (Qt 5.15.2) + +- + matrix: + only: + - image: "Previous Ubuntu1804" + + install: + - ls $HOME/Qt + - sudo apt update + - sudo apt install -y liblmdb-dev liblmdb0 imagemagick mesa-common-dev libglu1-mesa-dev libboost-all-dev + - git submodule update --init --recursive + + before_build: + - mkdir build + - cd build + - cmake -DCMAKE_BUILD_TYPE:String=Release -DCMAKE_PREFIX_PATH:STRING=$HOME/Qt/5.12.10/gcc_64 -DCMAKE_BUILD_RPATH="\$ORIGIN" .. + + build_script: + - cmake --build . + + after_build: + - zip -r squawk.zip squawk -j external/qxmpp/src/libqxmpp* + + artifacts: + - path: build/squawk.zip + name: Squawk executable and libraries (Qt 5.12) diff --git a/cmake/FindLMDB.cmake b/cmake/FindLMDB.cmake new file mode 100644 index 0000000..d6f2cd3 --- /dev/null +++ b/cmake/FindLMDB.cmake @@ -0,0 +1,52 @@ +#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 liblmdb.a liblmdb.so liblmdb.so.a liblmdb.dll.a # We want lmdb to be static, if possible + HINTS ${LMDB_ROOT_DIR}/lib + ) + +add_library(lmdb UNKNOWN IMPORTED) +set_target_properties(lmdb PROPERTIES + IMPORTED_LOCATION ${LMDB_LIBRARIES} + ) + +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/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in new file mode 100644 index 0000000..ac4bbec --- /dev/null +++ b/cmake/MacOSXBundleInfo.plist.in @@ -0,0 +1,41 @@ + + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + NSPrincipalClass + NSApplication + NSHighResolutionCapable + True + + + diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index b74a055..9369cb7 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,46 +1,34 @@ -cmake_minimum_required(VERSION 3.0) -project(squawkCORE) +set(SIGNALCATCHER_SOURCE signalcatcher.cpp) +if(WIN32) + set(SIGNALCATCHER_SOURCE signalcatcher_win32.cpp) +endif(WIN32) -set(CMAKE_AUTOMOC ON) +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_SOURCE} + signalcatcher.h + squawk.cpp + squawk.h + storage.cpp + storage.h + urlstorage.cpp + urlstorage.h + ) -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_include_directories(squawk PRIVATE ${LMDB_INCLUDE_DIRS}) +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..66b31f6 100644 --- a/core/account.cpp +++ b/core/account.cpp @@ -19,6 +19,7 @@ #include "account.h" #include #include +#include "shared/utils.h" using namespace Core; @@ -84,8 +85,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 +157,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,12 +405,13 @@ 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) +void Core::Account::onMamMessageReceived(const QString& queryId, const QXmppMessage& msgConst) { - if (msg.id().size() > 0 && (msg.body().size() > 0 || msg.outOfBandUrl().size() > 0)) { + QXmppMessage msg(msgConst); + if (msg.id().size() == 0) { + msg.setId(Shared::generateUUID() + QStringLiteral("-squawkgenerated")); + } + if ((msg.body().size() > 0 || msg.outOfBandUrl().size() > 0)) { std::map::const_iterator itr = archiveQueries.find(queryId); if (itr != archiveQueries.end()) { QString jid = itr->second; @@ -434,13 +438,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); @@ -461,6 +465,11 @@ void Core::Account::onContactNeedHistory(const QString& before, const QString& a qDebug() << "Requesting remote history from empty for" << contact->jid; } else { if (before.size() > 0) { + if (before.endsWith("-squawkgenerated")) { + qDebug() << "Can't query history before an squawk-generated ID, making fake empty result"; + contact->flushMessagesToArchive(true, before, before); + return; + } query.setBefore(before); } if (after.size() > 0) { //there is some strange behavior of ejabberd server returning empty result set @@ -469,6 +478,12 @@ void Core::Account::onContactNeedHistory(const QString& before, const QString& a } else { query.setAfter(after); } + + if (after.endsWith("-squawkgenerated")) { + qDebug() << "Can't query history after an squawk-generated ID, making fake empty result"; + contact->flushMessagesToArchive(true, after, after); + return; + } } qDebug() << "Remote query for" << contact->jid << "from" << after << ", to" << before; } @@ -552,9 +567,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 +926,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/signalcatcher_win32.cpp b/core/signalcatcher_win32.cpp new file mode 100644 index 0000000..ca7b5a2 --- /dev/null +++ b/core/signalcatcher_win32.cpp @@ -0,0 +1,42 @@ +/* + * Squawk messenger. + * Copyright (C) 2021 Shunf4 + * + * 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 "signalcatcher.h" +#include + +SignalCatcher::SignalCatcher(QCoreApplication *p_app, QObject *parent): + QObject(parent), + app(p_app) +{ +} + +SignalCatcher::~SignalCatcher() +{} + +void SignalCatcher::handleSigInt() +{ +} + +void SignalCatcher::intSignalHandler(int unused) +{ +} + +int SignalCatcher::setup_unix_signal_handlers() +{ + return 0; +} 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/packaging/squawk.desktop b/packaging/squawk.desktop index 0395af1..ba0f13c 100644 --- a/packaging/squawk.desktop +++ b/packaging/squawk.desktop @@ -5,8 +5,10 @@ Version=1.0 Name=Squawk GenericName=Instant Messenger GenericName[ru]=Мгновенные сообщения +GenericName[pt_BR]=Mensageiro instantâneo Comment=XMPP (Jabber) instant messenger client Comment[ru]=XMPP (Jabber) клиент обмена мгновенными сообщениями +Comment[pt_BR]=Cliente de mensagem instantânea XMPP (Jabber) Exec=squawk %u Icon=squawk StartupNotify=true 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..9288650 --- /dev/null +++ b/resources/CMakeLists.txt @@ -0,0 +1,59 @@ +target_sources(squawk PRIVATE resources.qrc) + +configure_file(images/logo.svg squawk.svg COPYONLY) +configure_file(squawk.rc squawk.rc COPYONLY) + +if(WIN32) + set(CONVERT_BIN magick convert) +else(WIN32) + set(CONVERT_BIN convert) +endif(WIN32) +execute_process(COMMAND ${CONVERT_BIN} -background none -size 48x48 squawk.svg squawk48.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND ${CONVERT_BIN} -background none -size 64x64 squawk.svg squawk64.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND ${CONVERT_BIN} -background none -size 128x128 squawk.svg squawk128.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +execute_process(COMMAND ${CONVERT_BIN} -background none -size 256x256 squawk.svg squawk256.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +if (WIN32) + execute_process(COMMAND ${CONVERT_BIN} squawk48.png squawk64.png squawk256.png squawk.ico WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + set(SQUAWK_WIN_RC "${CMAKE_CURRENT_BINARY_DIR}/squawk.rc") + set(SQUAWK_WIN_RC "${SQUAWK_WIN_RC}" PARENT_SCOPE) + target_sources(squawk PRIVATE ${SQUAWK_WIN_RC}) +endif(WIN32) + +if (APPLE) + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/icns.iconset") + execute_process(COMMAND convert -background none -size 16x16 squawk.svg icns.iconset/icon_16x16.png WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + execute_process(COMMAND convert -background none -resize !32x32 squawk.svg "icns.iconset/icon_16x16@2x.png" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + execute_process(COMMAND convert -background none -resize !32x32 squawk.svg "icns.iconset/icon_32x32.png" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + execute_process(COMMAND convert -background none -resize !64x64 squawk.svg "icns.iconset/icon_32x32@2x.png" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + execute_process(COMMAND convert -background none -resize !128x128 squawk.svg "icns.iconset/icon_128x128.png" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + execute_process(COMMAND convert -background none -resize !256x256 squawk.svg "icns.iconset/icon_128x128@2x.png" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + execute_process(COMMAND convert -background none -resize !256x256 squawk.svg "icns.iconset/icon_256x256.png" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + execute_process(COMMAND convert -background none -resize !512x512 squawk.svg "icns.iconset/icon_256x256@2x.png" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + execute_process(COMMAND convert -background none -resize !512x512 squawk.svg "icns.iconset/icon_512x512.png" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + execute_process(COMMAND convert -background none -resize !1024x1024 squawk.svg "icns.iconset/icon_512x512@2x.png" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + execute_process(COMMAND iconutil -c icns "icns.iconset" -o "squawk.icns" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + set(MACOSX_BUNDLE_ICON_FILE squawk.icns) + set(MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE} PARENT_SCOPE) + set(APP_ICON_MACOSX ${CMAKE_CURRENT_BINARY_DIR}/squawk.icns) + set(APP_ICON_MACOSX ${APP_ICON_MACOSX} PARENT_SCOPE) + target_sources(squawk PRIVATE ${APP_ICON_MACOSX}) + set_source_files_properties(${APP_ICON_MACOSX} TARGET_DIRECTORY squawk PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources") + if (CMAKE_BUILD_TYPE STREQUAL "Release") + if (APPLE) + set_target_properties(squawk PROPERTIES + MACOSX_BUNDLE_EXECUTABLE_NAME "Squawk" + MACOSX_BUNDLE_ICON_FILE "${MACOSX_BUNDLE_ICON_FILE}" # TODO + MACOSX_BUNDLE_BUNDLE_NAME "Squawk" + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/cmake/MacOSXBundleInfo.plist.in) + endif(APPLE) + endif() +endif (APPLE) + +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/resources/squawk.rc b/resources/squawk.rc new file mode 100644 index 0000000..6061f20 --- /dev/null +++ b/resources/squawk.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON "squawk.ico" 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..d6f2169 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.cpp b/shared/utils.cpp index 924be85..a7a4ecb 100644 --- a/shared/utils.cpp +++ b/shared/utils.cpp @@ -17,15 +17,11 @@ */ #include "utils.h" +#include QString Shared::generateUUID() { - uuid_t uuid; - uuid_generate(uuid); - - char uuid_str[36]; - uuid_unparse_lower(uuid, uuid_str); - return uuid_str; + return QUuid::createUuid().toString(); } diff --git a/shared/utils.h b/shared/utils.h index e9e3d29..6dcb141 100644 --- a/shared/utils.h +++ b/shared/utils.h @@ -20,11 +20,13 @@ #define SHARED_UTILS_H #include +#include #include #include -#include -#include +// #include "KIO/OpenFileManagerWindowJob" + +#include namespace Shared { diff --git a/signalcatcher_win32.cpp b/signalcatcher_win32.cpp new file mode 100644 index 0000000..ca7b5a2 --- /dev/null +++ b/signalcatcher_win32.cpp @@ -0,0 +1,42 @@ +/* + * Squawk messenger. + * Copyright (C) 2021 Shunf4 + * + * 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 "signalcatcher.h" +#include + +SignalCatcher::SignalCatcher(QCoreApplication *p_app, QObject *parent): + QObject(parent), + app(p_app) +{ +} + +SignalCatcher::~SignalCatcher() +{} + +void SignalCatcher::handleSigInt() +{ +} + +void SignalCatcher::intSignalHandler(int unused) +{ +} + +int SignalCatcher::setup_unix_signal_handlers() +{ + return 0; +} diff --git a/translations/CMakeLists.txt b/translations/CMakeLists.txt new file mode 100644 index 0000000..86d2a8c --- /dev/null +++ b/translations/CMakeLists.txt @@ -0,0 +1,11 @@ +find_package(Qt5LinguistTools) + +set(TS_FILES + squawk.ru.ts + squawk.pt_BR.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) diff --git a/translations/squawk.pt_BR.ts b/translations/squawk.pt_BR.ts new file mode 100644 index 0000000..4330979 --- /dev/null +++ b/translations/squawk.pt_BR.ts @@ -0,0 +1,800 @@ + + + + + Account + + Account + Window header + Conta + + + Your account login + Suas informações de login + + + john_smith1987 + josé_silva1987 + + + Server + Servidor + + + A server address of your account. Like 404.city or macaw.me + O endereço do servidor da sua conta, como o 404.city ou o macaw.me + + + macaw.me + macaw.me + + + Login + Usuário + + + Password + Senha + + + Password of your account + Senha da sua conta + + + Name + Nome + + + Just a name how would you call this account, doesn't affect anything + Apenas um nome para identificar esta conta. Não influencia em nada + + + John + José + + + Resource + Recurso + + + A resource name like "Home" or "Work" + Um nome de recurso como "Casa" ou "Trabalho" + + + QXmpp + Default resource + QXmpp + + + Password storage + Armazenamento de senha + + + + Accounts + + Accounts + Contas + + + Delete + Apagar + + + Add + Adicionar + + + Edit + Editar + + + Change password + Alterar senha + + + Connect + Conectar + + + Disconnect + Desconectar + + + + Conversation + + Type your message here... + Digite sua mensagem aqui... + + + Chose a file to send + Escolha um arquivo para enviar + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Liberation Sans'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> + + + + Drop files here to attach them to your message + Arraste seus arquivos aqui para anexá-los a sua mensagem + + + + Global + + Online + Availability + Conectado + + + Away + Availability + Distante + + + Absent + Availability + Ausente + + + Busy + Availability + Ocupado + + + Chatty + Availability + Tagarela + + + Invisible + Availability + Invisível + + + Offline + Availability + Desconectado + + + Disconnected + ConnectionState + Desconectado + + + Connecting + ConnectionState + Connectando + + + Connected + ConnectionState + Conectado + + + Error + ConnectionState + Erro + + + None + SubscriptionState + Nenhum + + + From + SubscriptionState + De + + + To + SubscriptionState + Para + + + Both + SubscriptionState + Ambos + + + Unknown + SubscriptionState + Desconhecido + + + Unspecified + Affiliation + Não especificado + + + Outcast + Affiliation + Rejeitado + + + Nobody + Affiliation + Ninguém + + + Member + Affiliation + Membro + + + Admin + Affiliation + Administrador + + + Owner + Affiliation + Dono + + + Unspecified + Role + Não especificado + + + Nobody + Role + Ninguém + + + Visitor + Role + Visitante + + + Participant + Role + Participante + + + Moderator + Role + Moderador + + + Pending + MessageState + Aguardando + + + Sent + MessageState + Enviada + + + Delivered + MessageState + Entregue + + + Error + MessageState + Erro + + + Plain + AccountPassword + Texto simples + + + Jammed + AccountPassword + Embaralhado + + + Always Ask + AccountPassword + Sempre perguntar + + + KWallet + AccountPassword + KWallet + + + Your password is going to be stored in config file but jammed with constant encryption key you can find in program source code. It might look like encryption but it's not + AccountPasswordDescription + Sua senha será armazenada em um arquivo de configurações, porém embaralhada com uma chave criptográfica constante que você pode encontrar no código fonte do programa. Parece criptografado, mas não é + + + Squawk is going to query you for the password on every start of the program + AccountPasswordDescription + O Squark vai requisitar sua senha a cada vez que você abrir o programa + + + Your password is going to be stored in config file in plain text + AccountPasswordDescription + Sua senha será armazenada em um arquivo de configurações em texto simples + + + Your password is going to be stored in KDE wallet storage (KWallet). You're going to be queried for permissions + AccountPasswordDescription + Sua senha será armazenada no KDE wallet (KWallet). Sua autorização será requerida + + + + JoinConference + + Join new conference + Заголовок окна + Entrar em uma nova conferência + + + JID + JID + + + Room JID + Sala JID + + + identifier@conference.server.org + identifier@conference.server.org + + + Account + Conta + + + Join on login + Entrar ao se conectar + + + If checked Squawk will try to join this conference on login + Se marcado, o Squawk tentará entrar nesta conferência automaticamente durante o login + + + Nick name + Apelido + + + Your nick name for that conference. If you leave this field empty your account name will be used as a nick name + Seu apelido para essa conferência. Se você deixar este campo em branco, seu nome de usuário será usado como apelido + + + John + José + + + + Message + + Open + Abrir + + + + MessageLine + + Downloading... + Baixando... + + + Download + Baixar + + + Error uploading file: %1 +You can try again + Error fazendo upload do arquivo: +%1 +Você pode tentar novamente + + + Upload + Upload + + + Error downloading file: %1 +You can try again + Erro baixando arquivo: +%1 +Você pode tentar novamente + + + Uploading... + Fazendo upload... + + + Push the button to download the file + Pressione o botão para baixar o arquivo + + + + Models::Room + + Subscribed + Inscrito + + + Temporarily unsubscribed + Inscrição temporariamente cancelada + + + Temporarily subscribed + Temporariamente inscrito + + + Unsubscribed + Não inscrito + + + + Models::Roster + + New messages + Novas mensagens + + + New messages: + Novas mensagens: + + + Jabber ID: + ID Jabber: + + + Availability: + Disponibilidade: + + + Status: + Status: + + + Subscription: + Inscrição: + + + Affiliation: + Я правда не знаю, как это объяснить, не то что перевести + Afiliação: + + + Role: + Papel: + + + Online contacts: + Contatos online: + + + Total contacts: + Contatos totais: + + + Members: + Membros: + + + + NewContact + + Add new contact + Заголовок окна + Adicionar novo contato + + + Account + Conta + + + An account that is going to have new contact + A conta que terá um novo contato + + + JID + JID + + + Jabber id of your new contact + ID Jabber do seu novo contato + + + name@server.dmn + Placeholder поля ввода JID + nome@servidor.com.br + + + Name + Nome + + + The way this new contact will be labeled in your roster (optional) + A forma com que o novo contato será classificado em sua lista (opcional) + + + John Smith + José Silva + + + + Squawk + + squawk + Squawk + + + Settings + Configurações + + + Squawk + Squawk + + + Accounts + Contas + + + Quit + Sair + + + Add contact + Adicionar contato + + + Add conference + Adicionar conferência + + + Disconnect + Desconectar + + + Connect + Conectar + + + VCard + VCard + + + Remove + Remover + + + Open dialog + Abrir caixa de diálogo + + + Unsubscribe + Cancelar inscrição + + + Subscribe + Inscrever-se + + + Rename + Renomear + + + Input new name for %1 +or leave it empty for the contact +to be displayed as %1 + Digite um novo nome para %1 +ou o deixe em branco para o contato +ser exibido com +%1 + + + Renaming %1 + Renomeando %1 + + + Groups + Grupos + + + New group + Novo grupo + + + New group name + Novo nome do grupo + + + Add %1 to a new group + Adicionar %1 a um novo grupo + + + Open conversation + Abrir conversa + + + %1 account card + cartão da conta %1 + + + %1 contact card + cartão de contato %1 + + + Downloading vCard + Baixando vCard + + + Attached file + Arquivo anexado + + + Input the password for account %1 + Digite a senha para a conta %1 + + + Password for account %1 + Senha para a conta %1 + + + Please select a contact to start chatting + Por favor selecione um contato para começar a conversar + + + + VCard + + Received 12.07.2007 at 17.35 + Recebido 12/07/2007 às 17:35 + + + General + Geral + + + Organization + Empresa + + + Middle name + Nome do meio + + + First name + Primeiro nome + + + Last name + Sobrenome + + + Nick name + Apelido + + + Birthday + Data de aniversário + + + Organization name + Nome da empresa + + + Unit / Department + Unidade/Departamento + + + Role / Profession + Profissão + + + Job title + Cargo + + + Full name + Nome completo + + + Personal information + Informações pessoais + + + Addresses + Endereços + + + E-Mail addresses + Endereços de e-mail + + + Phone numbers + Números de telefone + + + Contact + Contato + + + Jabber ID + ID Jabber + + + Web site + Site web + + + Description + Descrição + + + Set avatar + Definir avatar + + + Clear avatar + Apagar avatar + + + Account %1 card + Cartão da conta %1 + + + Contact %1 card + Cartão do contato %1 + + + Received %1 at %2 + Recebido %1 em %2 + + + Chose your new avatar + Escolha um novo avatar + + + Images (*.png *.jpg *.jpeg) + Imagens (*.png *.jpg *.jpeg) + + + Add email address + Adicionar endereço de e-mail + + + Unset this email as preferred + Desmarcar este e-mail como preferido + + + Set this email as preferred + Marcar este e-mail como preferido + + + Remove selected email addresses + Remover endereço de e-mail selecionado + + + Copy selected emails to clipboard + Copiar endereços de e-mails selecionados para a área de transferência + + + Add phone number + Adicionar número de telefone + + + Unset this phone as preferred + Desmarcar este número de telefone como preferido + + + Set this phone as preferred + Marcar este número de telefone como preferido + + + Remove selected phone numbers + Remover os números de telefones selecionados + + + Copy selected phones to clipboard + Copiar os números de telefone selecionados para a área de transferência + + + 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..7bdfb9e --- /dev/null +++ b/ui/widgets/messageline/feedview.cpp @@ -0,0 +1,494 @@ +/* + * 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; +constexpr int dateDeviderMargin = 10; + +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(), + dividerFont(), + dividerMetrics(dividerFont) +{ + horizontalScrollBar()->setRange(0, 0); + verticalScrollBar()->setSingleStep(approximateSingleMessageHeight); + setMouseTracking(true); + setSelectionBehavior(SelectItems); +// viewport()->setAttribute(Qt::WA_Hover, true); + + progress.setParent(viewport()); + progress.resize(progressSize, progressSize); + + dividerFont = getFont(); + dividerFont.setBold(true); + float ndps = dividerFont.pointSizeF(); + if (ndps != -1) { + dividerFont.setPointSizeF(ndps * 1.2); + } else { + dividerFont.setPointSize(dividerFont.pointSize() + 2); + } +} + +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; + QDateTime lastDate; + for (int i = 0, size = m->rowCount(); i < size; ++i) { + QModelIndex index = m->index(i, 0, rootIndex()); + QDateTime currentDate = index.data(Models::MessageFeed::Date).toDateTime(); + if (i > 0) { + if (currentDate.daysTo(lastDate) > 0) { + previousOffset += dividerMetrics.height() + dateDeviderMargin * 2; + } + } + lastDate = currentDate; + 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; + QDateTime lastDate; + for (int i = 0, size = m->rowCount(); i < size; ++i) { + QModelIndex index = m->index(i, 0, rootIndex()); + QDateTime currentDate = index.data(Models::MessageFeed::Date).toDateTime(); + if (i > 0) { + if (currentDate.daysTo(lastDate) > 0) { + previousOffset += dateDeviderMargin * 2 + dividerMetrics.height(); + } + } + lastDate = currentDate; + 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) { + inZone = false; + 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(); + } + } + + QDateTime lastDate; + bool first = true; + for (const QModelIndex& index : toRener) { + QDateTime currentDate = index.data(Models::MessageFeed::Date).toDateTime(); + option.rect = visualRect(index); + if (first) { + int ind = index.row() - 1; + if (ind > 0) { + QDateTime underDate = m->index(ind, 0, rootIndex()).data(Models::MessageFeed::Date).toDateTime(); + if (currentDate.daysTo(underDate) > 0) { + drawDateDevider(option.rect.bottom(), underDate, painter); + } + } + first = false; + } + bool mouseOver = option.rect.contains(cursor) && vp->rect().contains(cursor); + option.state.setFlag(QStyle::State_MouseOver, mouseOver); + itemDelegate(index)->paint(&painter, option, index); + + if (!lastDate.isNull() && currentDate.daysTo(lastDate) > 0) { + drawDateDevider(option.rect.bottom(), lastDate, painter); + } + lastDate = currentDate; + } + if (!lastDate.isNull() && inZone) { //if after drawing all messages there is still space + drawDateDevider(option.rect.bottom(), lastDate, painter); + } + + 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::drawDateDevider(int top, const QDateTime& date, QPainter& painter) +{ + int divisionHeight = dateDeviderMargin * 2 + dividerMetrics.height(); + QRect r(QPoint(0, top), QSize(viewport()->width(), divisionHeight)); + painter.save(); + painter.setFont(dividerFont); + painter.drawText(r, Qt::AlignCenter, date.toString("d MMMM")); + painter.restore(); +} + +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..5e08946 --- /dev/null +++ b/ui/widgets/messageline/feedview.h @@ -0,0 +1,98 @@ +/* + * 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(); + void drawDateDevider(int top, const QDateTime& date, QPainter& painter); + +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; + QFont dividerFont; + QFontMetrics dividerMetrics; + + 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..649230e --- /dev/null +++ b/ui/widgets/messageline/messagedelegate.cpp @@ -0,0 +1,580 @@ +/* + * 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(); + QString dateString = data.date.toLocalTime().toString("hh:mm"); + 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, dateString).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, dateString, &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