diff --git a/build_scripts/OBS b/build_scripts/OBS index c064abd74..879d5dfe8 160000 --- a/build_scripts/OBS +++ b/build_scripts/OBS @@ -1 +1 @@ -Subproject commit c064abd74b27e1cc440917e9dbac800316bb8470 +Subproject commit 879d5dfe8dcd8995be753120cf1b8bab4dd2ec82 diff --git a/libretroshare/src/jsonapi/jsonapi.cpp b/libretroshare/src/jsonapi/jsonapi.cpp index 3888117a5..0c180ce52 100644 --- a/libretroshare/src/jsonapi/jsonapi.cpp +++ b/libretroshare/src/jsonapi/jsonapi.cpp @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see * - * SPDX-FileCopyrightText: 2004-2019 RetroShare Team + * SPDX-FileCopyrightText: 2004-2020 RetroShare Team * SPDX-License-Identifier: AGPL-3.0-only */ @@ -73,6 +73,8 @@ JsonApiServer::corsOptionsHeaders = { "Content-Length", "0" } }; +/* static */ const RsJsonApiErrorCategory RsJsonApiErrorCategory::instance; + #define INITIALIZE_API_CALL_JSON_CONTEXT \ RsGenericSerializer::SerializeContext cReq( \ nullptr, 0, \ @@ -124,32 +126,45 @@ JsonApiServer::corsOptionsHeaders = return false; } +void JsonApiServer::unProtectedRestart() +{ + /* Extremely sensitive stuff! + * Make sure you read documentation in header before changing or use!! */ + + fullstop(); + RsThread::start("JSON API Server"); +} + bool RsJsonApi::parseToken( const std::string& clear_token, std::string& user,std::string& passwd ) { - uint32_t colonIndex = 0; - uint32_t colonCounter = 0; + auto colonIndex = std::string::npos; const auto tkLen = clear_token.length(); - for(uint32_t i=0; i < tkLen && colonCounter < 2; ++i) - if(clear_token[i] == ':') { ++colonCounter; colonIndex = i; } - else if(!librs::util::is_alphanumeric(clear_token[i])) return false; + for(uint32_t i=0; i < tkLen; ++i) + if(clear_token[i] == ':') { colonIndex = i; break; } - if(colonCounter != 1) return false; + user = clear_token.substr(0, colonIndex); - user = clear_token.substr(0, colonIndex); - passwd = clear_token.substr(colonIndex + 1); + if(colonIndex < tkLen) + passwd = clear_token.substr(colonIndex + 1); return true; } - JsonApiServer::JsonApiServer(): configMutex("JsonApiServer config"), - mService(std::make_shared()), - mServiceMutex("JsonApiServer restbed ptr"), + mService(nullptr), mListeningPort(RsJsonApi::DEFAULT_PORT), - mBindingAddress(RsJsonApi::DEFAULT_BINDING_ADDRESS) + mBindingAddress(RsJsonApi::DEFAULT_BINDING_ADDRESS), + mRestartReqTS(0) { +#if defined(RS_THREAD_FORCE_STOP) && defined(RS_JSONAPI_DEBUG_SERVICE_STOP) + /* When called in bursts it seems that Restbed::Service::stop() doesn't + * always does the job, to debug those cases it has been useful to ask + * RsThread to force it to stop for us. */ + RsThread::setStopTimeout(10); +#endif + registerHandler("/rsLoginHelper/createLocation", [this](const std::shared_ptr session) { @@ -248,7 +263,13 @@ JsonApiServer::JsonApiServer(): configMutex("JsonApiServer config"), { INITIALIZE_API_CALL_JSON_CONTEXT; DEFAULT_API_CALL_JSON_RETURN(rb::OK); - rsControl->rsGlobalShutDown(); + + /* Wrap inside RsThread::async because this call + * RsThread::fullstop() also on JSON API server thread. + * Calling RsThread::fullstop() from it's own thread should never + * happen and if it happens an error message is printed + * accordingly by RsThread::fullstop() */ + RsThread::async([](){ rsControl->rsGlobalShutDown(); }); } ); }, true); @@ -388,6 +409,41 @@ JsonApiServer::JsonApiServer(): configMutex("JsonApiServer config"), session->yield(message.str()); } ); }, true); + + registerHandler("/rsJsonApi/restart", + [this](const std::shared_ptr session) + { + auto reqSize = session->get_request()->get_header("Content-Length", 0); + session->fetch( static_cast(reqSize), [this]( + const std::shared_ptr session, + const rb::Bytes& body ) + { + INITIALIZE_API_CALL_JSON_CONTEXT; + + std::error_condition retval; + + const auto now = time(nullptr); + if(mRestartReqTS.exchange(now) + RESTART_BURST_PROTECTION > now) + retval = RsJsonApiErrorNum::NOT_A_MACHINE_GUN; + + // serialize out parameters and return value to JSON + { + RsGenericSerializer::SerializeContext& ctx(cAns); + RsGenericSerializer::SerializeJob j(RsGenericSerializer::TO_JSON); + RS_SERIAL_PROCESS(retval); + } + + DEFAULT_API_CALL_JSON_RETURN(rb::OK); + + /* Wrap inside RsThread::async because this call fullstop() on + * JSON API server thread. + * Calling RsThread::fullstop() from it's own thread should never + * happen and if it happens an error message is printed + * accordingly by RsThread::fullstop() */ + if(!retval) RsThread::async([this](){ unProtectedRestart(); }); + } ); + }, true); + // Generated at compile time #include "jsonapi-wrappers.inl" } @@ -405,10 +461,22 @@ void JsonApiServer::registerHandler( if(requiresAutentication) resource->set_authentication_handler( - [this]( + [this, path]( const std::shared_ptr session, const std::function)>& callback ) { + const auto authFail = + [&path, &session](int status) -> RsWarn::stream_type& + { + /* Capture session by reference as it is cheaper then copying + * shared_ptr by value which is not needed in this case */ + + session->close(status, corsOptionsHeaders); + return RsWarn() << "JsonApiServer authentication handler " + "blocked an attempt to call JSON API " + "authenticated method: " << path; + }; + if(session->get_request()->get_method() == "OPTIONS") { callback(session); @@ -417,7 +485,8 @@ void JsonApiServer::registerHandler( if(!rsLoginHelper->isLoggedIn()) { - session->close(rb::CONFLICT, corsOptionsHeaders); + authFail(rb::CONFLICT) << " before RetroShare login" + << std::endl; return; } @@ -429,15 +498,24 @@ void JsonApiServer::registerHandler( if(authToken != "Basic") { - session->close(rb::UNAUTHORIZED, corsOptionsHeaders); + authFail(rb::UNAUTHORIZED) + << " with wrong Authorization header: " + << authHeader.str() << std::endl; return; } std::getline(authHeader, authToken, ' '); authToken = decodeToken(authToken); - if(isAuthTokenValid(authToken)) callback(session); - else session->close(rb::UNAUTHORIZED, corsOptionsHeaders); + std::error_condition ec; + if(isAuthTokenValid(authToken, ec)) callback(session); + else + { + std::string tUser; + parseToken(authToken, tUser, RS_DEFAULT_STORAGE_PARAM(std::string)); + authFail(rb::UNAUTHORIZED) + << " user: " << tUser << ec << std::endl; + } } ); mResources.push_back(resource); @@ -447,24 +525,62 @@ void JsonApiServer::setNewAccessRequestCallback( const std::function& callback ) { mNewAccessRequestCallback = callback; } -bool JsonApiServer::requestNewTokenAutorization( +/*static*/ std::error_condition JsonApiServer::badApiCredientalsFormat( const std::string& user, const std::string& passwd ) { - if(rsLoginHelper->isLoggedIn() && mNewAccessRequestCallback(user, passwd)) - return authorizeUser(user, passwd); + if(user.find(':') < std::string::npos) + return RsJsonApiErrorNum::API_USER_CONTAIN_COLON; - return false; + if(user.empty()) + RsWarn() << __PRETTY_FUNCTION__ << " User is empty, are you sure " + << "this what you wanted?" << std::endl; + + if(passwd.empty()) + RsWarn() << __PRETTY_FUNCTION__ << " Password is empty, are you sure " + << "this what you wanted?" << std::endl; + + return std::error_condition(); } -bool JsonApiServer::isAuthTokenValid(const std::string& token) +std::error_condition JsonApiServer::requestNewTokenAutorization( + const std::string& user, const std::string& passwd ) +{ + auto ec = badApiCredientalsFormat(user, passwd); + if(ec) return ec; + + if(!rsLoginHelper->isLoggedIn()) + return RsJsonApiErrorNum::CANNOT_EXECUTE_BEFORE_RS_LOGIN; + + if(mNewAccessRequestCallback(user, passwd)) + return authorizeUser(user, passwd); + + return RsJsonApiErrorNum::AUTHORIZATION_REQUEST_DENIED; +} + +bool JsonApiServer::isAuthTokenValid( + const std::string& token, std::error_condition& error ) { RS_STACK_MUTEX(configMutex); + const auto failure = [&error](RsJsonApiErrorNum e) -> bool + { + error = e; + return false; + }; + + const auto success = [&error]() + { + error.clear(); + return true; + }; + std::string user,passwd; - if(!parseToken(token,user,passwd)) return false; + if(!parseToken(token, user, passwd)) + return failure(RsJsonApiErrorNum::TOKEN_FORMAT_INVALID); auto it = mAuthTokenStorage.mAuthorizedTokens.find(user); - if(it == mAuthTokenStorage.mAuthorizedTokens.end()) return false; + if(it == mAuthTokenStorage.mAuthorizedTokens.end()) + return failure(RsJsonApiErrorNum::UNKNOWN_API_USER); // attempt avoiding +else CRYPTO_memcmp+ being optimized away int noOptimiz = 1; @@ -476,12 +592,16 @@ bool JsonApiServer::isAuthTokenValid(const std::string& token) if( passwd.size() == it->second.size() && ( noOptimiz = CRYPTO_memcmp( passwd.data(), it->second.data(), it->second.size() ) ) == 0 ) - return true; + return success(); // Make token size guessing harder else noOptimiz = CRYPTO_memcmp(passwd.data(), passwd.data(), passwd.size()); - // attempt avoiding +else CRYPTO_memcmp+ being optimized away - return static_cast(noOptimiz) + 1 == 0; + /* At this point we are sure password is wrong, and one could think to + * plainly `return false` still this ugly and apparently unuseful extra + * calculation is here to avoid `else CRYPTO_memcmp` being optimized away, + * so a pontential attacker cannot guess password size based on timing */ + return static_cast(noOptimiz) + 1 == 0 ? + success() : failure(RsJsonApiErrorNum::WRONG_API_PASSWORD); } std::map JsonApiServer::getAuthorizedTokens() @@ -509,22 +629,11 @@ void JsonApiServer::connectToConfigManager(p3ConfigMgr& cfgmgr) loadConfiguration(hash); } -bool JsonApiServer::authorizeUser( +std::error_condition JsonApiServer::authorizeUser( const std::string& user, const std::string& passwd ) { - if(!librs::util::is_alphanumeric(user)) - { - RsErr() << __PRETTY_FUNCTION__ << " User name is not alphanumeric" - << std::endl; - return false; - } - - if(passwd.empty()) - { - RsWarn() << __PRETTY_FUNCTION__ << " Password is empty, are you sure " - << "this what you wanted?" << std::endl; - return false; - } + auto ec = badApiCredientalsFormat(user, passwd); + if(ec) return ec; RS_STACK_MUTEX(configMutex); @@ -534,7 +643,7 @@ bool JsonApiServer::authorizeUser( p = passwd; IndicateConfigChanged(); } - return true; + return ec; } /*static*/ std::string JsonApiServer::decodeToken(const std::string& radix64_token) @@ -600,21 +709,21 @@ std::vector > JsonApiServer::getResources() const return tab; } -void JsonApiServer::restart() +std::error_condition JsonApiServer::restart() { - /* It is important to wrap into async(...) because fullstop() method can't - * be called from same thread of execution hence from JSON API thread! */ - RsThread::async([this]() - { - fullstop(); - RsThread::start("JSON API Server"); - }); + const auto now = time(nullptr); + if(mRestartReqTS.exchange(now) + RESTART_BURST_PROTECTION > now) + return RsJsonApiErrorNum::NOT_A_MACHINE_GUN; + + unProtectedRestart(); + return std::error_condition(); } void JsonApiServer::onStopRequested() { - RS_STACK_MUTEX(mServiceMutex); - mService->stop(); + auto tService = std::atomic_exchange( + &mService, std::shared_ptr(nullptr) ); + if(tService) tService->stop(); } uint16_t JsonApiServer::listeningPort() const { return mListeningPort; } @@ -630,14 +739,9 @@ void JsonApiServer::run() settings->set_bind_address(mBindingAddress); settings->set_default_header("Connection", "close"); - /* re-allocating mService is important because it deletes the existing - * service and therefore leaves the listening port open */ - { - RS_STACK_MUTEX(mServiceMutex); - mService = std::make_shared(); - } + auto tService = std::make_shared(); - for(auto& r: getResources()) mService->publish(r); + for(auto& r: getResources()) tService->publish(r); try { @@ -645,7 +749,20 @@ void JsonApiServer::run() .setPort(mListeningPort); RsInfo() << __PRETTY_FUNCTION__ << " JSON API server listening on " << apiUrl.toString() << std::endl; - mService->start(settings); + + /* re-allocating mService is important because it deletes the existing + * service and therefore leaves the listening port open */ + auto tExpected = std::shared_ptr(nullptr); + if(atomic_compare_exchange_strong(&mService, &tExpected, tService)) + tService->start(settings); + else + { + RsErr() << __PRETTY_FUNCTION__ << " mService was expected to be " + << " null, instead we got: " << tExpected + << " something wrong happened JsonApiServer won't start" + << std::endl; + print_stacktrace(); + } } catch(std::exception& e) { @@ -668,3 +785,19 @@ void JsonApiServer::run() extra = RS_EXTRA_VERSION; human = RS_HUMAN_READABLE_VERSION; } + +std::error_condition RsJsonApiErrorCategory::default_error_condition(int ev) const noexcept +{ + switch(static_cast(ev)) + { + case RsJsonApiErrorNum::TOKEN_FORMAT_INVALID: // fallthrough + case RsJsonApiErrorNum::UNKNOWN_API_USER: // fallthrough + case RsJsonApiErrorNum::WRONG_API_PASSWORD: // fallthrough + case RsJsonApiErrorNum::AUTHORIZATION_REQUEST_DENIED: + return std::errc::permission_denied; + case RsJsonApiErrorNum::API_USER_CONTAIN_COLON: + return std::errc::invalid_argument; + default: + return std::error_condition(ev, *this); + } +} diff --git a/libretroshare/src/jsonapi/jsonapi.h b/libretroshare/src/jsonapi/jsonapi.h index 803be8459..70cc5e309 100644 --- a/libretroshare/src/jsonapi/jsonapi.h +++ b/libretroshare/src/jsonapi/jsonapi.h @@ -30,6 +30,7 @@ #include #include #include +#include #include "util/rsthreads.h" #include "pqi/p3cfgmgr.h" @@ -40,6 +41,7 @@ namespace rb = restbed; +/** Interface to provide addotional resources to JsonApiServer */ class JsonApiResourceProvider { public: @@ -66,7 +68,7 @@ public: void fullstop() override { RsThread::fullstop(); } /// @see RsJsonApi - void restart() override; + std::error_condition restart() override; /// @see RsJsonApi void askForStop() override { RsThread::askForStop(); } @@ -90,9 +92,8 @@ public: void connectToConfigManager(p3ConfigMgr& cfgmgr) override; /// @see RsJsonApi - virtual bool authorizeUser( - const std::string& alphanumeric_user, - const std::string& alphanumeric_passwd ) override; + virtual std::error_condition authorizeUser( + const std::string& user, const std::string& passwd ) override; /// @see RsJsonApi std::map getAuthorizedTokens() override; @@ -101,10 +102,13 @@ public: bool revokeAuthToken(const std::string& user) override; /// @see RsJsonApi - bool isAuthTokenValid(const std::string& token) override; + bool isAuthTokenValid( + const std::string& token, + std::error_condition& error = RS_DEFAULT_STORAGE_PARAM(std::error_condition) + ) override; /// @see RsJsonAPI - bool requestNewTokenAutorization( + std::error_condition requestNewTokenAutorization( const std::string& user, const std::string& password ) override; /// @see RsJsonApi @@ -147,6 +151,11 @@ protected: /// @see RsThread void onStopRequested() override; + static const RsJsonApiErrorCategory sErrorCategory; + + static std::error_condition badApiCredientalsFormat( + const std::string& user, const std::string& passwd ); + private: /// @see RsThread void run() override; @@ -195,13 +204,33 @@ private: std::reference_wrapper, std::less > mResourceProviders; + /** + * This pointer should be accessed via std::atomic_* operations, up until + * now only very critical operations like reallocation, are done that way, + * but this is not still 100% thread safe, but seems to handle all of the + * test cases (no crash, no deadlock), once we switch to C++20 we shoud + * change this into std::atomic> which + * will automatically handle atomic access properly all the times + */ std::shared_ptr mService; - /** Protect service only during very critical operation like resetting the - * pointer, still not 100% thread safe, but hopefully we can avoid - * crashes/freeze with this */ - RsMutex mServiceMutex; uint16_t mListeningPort; std::string mBindingAddress; + + /// @see unProtectedRestart() + std::atomic mRestartReqTS; + + /// @see unProtectedRestart() + constexpr static rstime_t RESTART_BURST_PROTECTION = 7; + + /** It is very important to protect this method from being called in bursts, + * because Restbed::Service::stop() together with + * Restbed::Service::start(...), which are called internally, silently fails + * if combined in bursts, probably because they have to deal with + * listening/releasing TCP port. + * @see JsonApiServer::restart() and @see JsonApiServer::JsonApiServer() + * implementation to understand how correctly use this. + */ + void unProtectedRestart(); }; diff --git a/libretroshare/src/retroshare/rsjsonapi.h b/libretroshare/src/retroshare/rsjsonapi.h index 251310aaa..98b052d58 100644 --- a/libretroshare/src/retroshare/rsjsonapi.h +++ b/libretroshare/src/retroshare/rsjsonapi.h @@ -1,8 +1,9 @@ /* * RetroShare JSON API public header * - * Copyright (C) 2018-2019 Gioacchino Mazzurco + * Copyright (C) 2018-2020 Gioacchino Mazzurco * Copyright (C) 2019 Cyril Soler + * Copyright (C) 2020 Asociación Civil Altermundi * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free @@ -25,9 +26,11 @@ #include #include +#include +#include + +#include "util/rsmemory.h" -class p3ConfigMgr; -class JsonApiResourceProvider; class RsJsonApi; /** @@ -36,6 +39,71 @@ class RsJsonApi; */ extern RsJsonApi* rsJsonApi; +enum class RsJsonApiErrorNum : int32_t +{ + TOKEN_FORMAT_INVALID = 2004, + UNKNOWN_API_USER = 2005, + WRONG_API_PASSWORD = 2006, + API_USER_CONTAIN_COLON = 2007, + AUTHORIZATION_REQUEST_DENIED = 2008, + CANNOT_EXECUTE_BEFORE_RS_LOGIN = 2009, + NOT_A_MACHINE_GUN = 2010 +}; + +struct RsJsonApiErrorCategory: std::error_category +{ + const char* name() const noexcept override + { return "RetroShare JSON API"; } + + std::string message(int ev) const override + { + switch (static_cast(ev)) + { + case RsJsonApiErrorNum::TOKEN_FORMAT_INVALID: + return "Invalid token format, must be alphanumeric_user:password"; + case RsJsonApiErrorNum::UNKNOWN_API_USER: + return "Unknown API user"; + case RsJsonApiErrorNum::WRONG_API_PASSWORD: + return "Wrong API password"; + case RsJsonApiErrorNum::API_USER_CONTAIN_COLON: + return "API user cannot contain colon character"; + case RsJsonApiErrorNum::AUTHORIZATION_REQUEST_DENIED: + return "User denied new token autorization"; + case RsJsonApiErrorNum::CANNOT_EXECUTE_BEFORE_RS_LOGIN: + return "This operation cannot be executed bedore RetroShare login"; + case RsJsonApiErrorNum::NOT_A_MACHINE_GUN: + return "Method must not be called in burst"; + default: + return "Error message for error: " + std::to_string(ev) + + " not available in category: " + name(); + } + } + + std::error_condition default_error_condition(int ev) const noexcept override; + + const static RsJsonApiErrorCategory instance; +}; + + +namespace std +{ +/** Register RsJsonApiErrorNum as an error condition enum, must be in std + * namespace */ +template<> struct is_error_condition_enum : true_type {}; +} + +/** Provide RsJsonApiErrorNum conversion to std::error_condition, must be in + * same namespace of RsJsonApiErrorNum */ +inline std::error_condition make_error_condition(RsJsonApiErrorNum e) noexcept +{ + return std::error_condition( + static_cast(e), RsJsonApiErrorCategory::instance ); +}; + + +class p3ConfigMgr; +class JsonApiResourceProvider; + class RsJsonApi { public: @@ -43,10 +111,12 @@ public: static const std::string DEFAULT_BINDING_ADDRESS; // 127.0.0.1 /** - * @brief Restart RsJsonApi server asynchronously. - * @jsonapi{development} + * @brief Restart RsJsonApi server. + * This method is asyncronous when called from JSON API. + * @jsonapi{development,manualwrapper} + * @return Success or error details */ - virtual void restart() = 0; + virtual std::error_condition restart() = 0; /** @brief Request RsJsonApi to stop and wait until it has stopped. * Do not expose this method to JSON API as fullstop must not be called from @@ -124,9 +194,9 @@ public: * @jsonapi{development,unauthenticated} * @param[in] user user name to authorize * @param[in] password password for the new user - * @return true if authorization succeded, false otherwise. + * @return if an error occurred details about it. */ - virtual bool requestNewTokenAutorization( + virtual std::error_condition requestNewTokenAutorization( const std::string& user, const std::string& password) = 0; /** Split a token in USER:PASSWORD format into user and password */ @@ -135,14 +205,13 @@ public: std::string& user, std::string& passwd ); /** - * Add new API auth (user,passwd) token to the authorized set. + * Add new API auth user, passwd to the authorized set. * @jsonapi{development} - * @param[in] user user name to autorize, must be alphanumerinc - * @param[in] password password for the user, must be alphanumerinc - * @return true if the token has been added to authorized, false if error - * occurred + * @param[in] user user name to autorize, must not contain ':' + * @param[in] password password for the user + * @return if some error occurred return details about it */ - virtual bool authorizeUser( + virtual std::error_condition authorizeUser( const std::string& user, const std::string& password ) = 0; /** @@ -164,9 +233,13 @@ public: * @brief Check if given JSON API auth token is authorized * @jsonapi{development,unauthenticated} * @param[in] token decoded - * @return tru if authorized, false otherwise + * @param[out] error optional storage for error details + * @return true if authorized, false otherwise */ - virtual bool isAuthTokenValid(const std::string& token) = 0; + virtual bool isAuthTokenValid( + const std::string& token, + std::error_condition& error = RS_DEFAULT_STORAGE_PARAM(std::error_condition) + ) = 0; /** * @brief Write version information to given paramethers @@ -182,3 +255,4 @@ public: virtual ~RsJsonApi() = default; }; + diff --git a/libretroshare/src/rsserver/rsinit.cc b/libretroshare/src/rsserver/rsinit.cc index db290ea63..2de431589 100644 --- a/libretroshare/src/rsserver/rsinit.cc +++ b/libretroshare/src/rsserver/rsinit.cc @@ -397,8 +397,8 @@ int RsInit::InitRetroShare(const RsConfigOptions& conf) #ifdef RS_JSONAPI // We create the JsonApiServer this early, because it is needed *before* login - RsInfo() << __PRETTY_FUNCTION__ - << "Allocating jsonAPI server (not launched yet)" << std::endl; + RsDbg() << __PRETTY_FUNCTION__ + << " Allocating JSON API server (not launched yet)" << std::endl; JsonApiServer* jas = new JsonApiServer(); jas->setListeningPort(conf.jsonApiPort); jas->setBindingAddress(conf.jsonApiBindAddress); diff --git a/libretroshare/src/serialiser/rstypeserializer.cc b/libretroshare/src/serialiser/rstypeserializer.cc index 47fd99e45..1b086576a 100644 --- a/libretroshare/src/serialiser/rstypeserializer.cc +++ b/libretroshare/src/serialiser/rstypeserializer.cc @@ -768,3 +768,37 @@ bool RsTypeSerializer::from_JSON( const std::string& memberName, } return ret; } + +void RsTypeSerializer::ErrConditionWrapper::serial_process( + RsGenericSerializer::SerializeJob j, + RsGenericSerializer::SerializeContext& ctx ) +{ + switch(j) + { + case RsGenericSerializer::SIZE_ESTIMATE: // fallthrough + case RsGenericSerializer::DESERIALIZE: // fallthrough + case RsGenericSerializer::SERIALIZE: // fallthrough + case RsGenericSerializer::FROM_JSON: + RsFatal() << __PRETTY_FUNCTION__ << " SerializeJob: " << j + << "is not supported on std::error_condition " << std::endl; + print_stacktrace(); + exit(-2); + case RsGenericSerializer::PRINT: // fallthrough + case RsGenericSerializer::TO_JSON: + { + constexpr RsGenericSerializer::SerializeJob rj = + RsGenericSerializer::TO_JSON; + + int32_t tNum = mec.value(); + RsTypeSerializer::serial_process(rj, ctx, tNum, "errorNumber"); + + std::string tStr = mec.category().name(); + RsTypeSerializer::serial_process(rj, ctx, tStr, "errorCategory"); + + tStr = mec.message(); + RsTypeSerializer::serial_process(rj, ctx, tStr, "errorMessage"); + break; + } + default: RsTypeSerializer::fatalUnknownSerialJob(j); + } +} diff --git a/libretroshare/src/serialiser/rstypeserializer.h b/libretroshare/src/serialiser/rstypeserializer.h index 550b89f07..55568632a 100644 --- a/libretroshare/src/serialiser/rstypeserializer.h +++ b/libretroshare/src/serialiser/rstypeserializer.h @@ -22,6 +22,12 @@ *******************************************************************************/ #pragma once +#include // for typeid +#include +#include +#include + + #include "serialiser/rsserial.h" #include "serialiser/rstlvbase.h" #include "serialiser/rstlvlist.h" @@ -34,11 +40,6 @@ #include "util/rsjson.h" #include "util/rsdebug.h" -#include // for typeid -#include -#include - - /* INTERNAL ONLY helper to avoid copy paste code for std::{vector,list,set} * Can't use a template function because T is needed for const_cast */ #define RsTypeSerializer_PRIVATE_TO_JSON_ARRAY() do \ @@ -132,7 +133,12 @@ struct RsTypeSerializer /// Generic types template - typename std::enable_if::value || !(std::is_base_of::value || std::is_enum::value || std::is_base_of::value )>::type + typename + std::enable_if< std::is_same::value || !( + std::is_base_of::value || + std::is_enum::value || + std::is_base_of::value || + std::is_same::value ) >::type static /*void*/ serial_process( RsGenericSerializer::SerializeJob j, RsGenericSerializer::SerializeContext& ctx, T& member, const std::string& member_name ) @@ -160,11 +166,7 @@ struct RsTypeSerializer ctx.mOk &= (ctx.mOk || ctx.mFlags & RsGenericSerializer::SERIALIZATION_FLAG_YIELDING) && from_JSON(member_name, member, ctx.mJson); break; - default: - std::cerr << __PRETTY_FUNCTION__ << " Unknown serial job: " - << static_cast::type>(j) - << std::endl; - exit(EINVAL); + default: fatalUnknownSerialJob(j); } } @@ -200,11 +202,7 @@ struct RsTypeSerializer (ctx.mOk || ctx.mFlags & RsGenericSerializer::SERIALIZATION_FLAG_YIELDING) && from_JSON(member_name, type_id, member, ctx.mJson); break; - default: - std::cerr << __PRETTY_FUNCTION__ << " Unknown serial job: " - << static_cast::type>(j) - << std::endl; - exit(EINVAL); + default: fatalUnknownSerialJob(j); } } @@ -362,11 +360,7 @@ struct RsTypeSerializer } break; } - default: - std::cerr << __PRETTY_FUNCTION__ << " Unknown serial job: " - << static_cast::type>(j) - << std::endl; - exit(EINVAL); + default: fatalUnknownSerialJob(j); } } @@ -441,12 +435,7 @@ struct RsTypeSerializer break; } - default: - std::cerr << __PRETTY_FUNCTION__ << " Unknown serial job: " - << static_cast::type>(j) - << std::endl; - exit(EINVAL); - break; + default: fatalUnknownSerialJob(j); } } @@ -505,7 +494,7 @@ struct RsTypeSerializer case RsGenericSerializer::FROM_JSON: RsTypeSerializer_PRIVATE_FROM_JSON_ARRAY(push_back); break; - default: break; + default: fatalUnknownSerialJob(j); } } @@ -564,7 +553,7 @@ struct RsTypeSerializer case RsGenericSerializer::FROM_JSON: RsTypeSerializer_PRIVATE_FROM_JSON_ARRAY(insert); break; - default: break; + default: fatalUnknownSerialJob(j); } } @@ -620,7 +609,7 @@ struct RsTypeSerializer case RsGenericSerializer::FROM_JSON: RsTypeSerializer_PRIVATE_FROM_JSON_ARRAY(push_back); break; - default: break; + default: fatalUnknownSerialJob(j); } } @@ -663,7 +652,7 @@ struct RsTypeSerializer && (v = t_RsFlags32(f), true); break; } - default: break; + default: fatalUnknownSerialJob(j); } } @@ -772,18 +761,15 @@ struct RsTypeSerializer break; } - default: - std::cerr << __PRETTY_FUNCTION__ << " Unknown serial job: " - << static_cast::type>(j) - << std::endl; - exit(EINVAL); - break; + default: fatalUnknownSerialJob(j); } } /// RsTlvItem derivatives only template - typename std::enable_if::value && !std::is_same::value>::type + typename std::enable_if< + std::is_base_of::value && !std::is_same::value + >::type static /*void*/ serial_process( RsGenericSerializer::SerializeJob j, RsGenericSerializer::SerializeContext& ctx, T& member, @@ -792,6 +778,21 @@ struct RsTypeSerializer serial_process(j, ctx, static_cast(member), memberName); } + /** std::error_condition + * supports only TO_JSON ErrConditionWrapper::serial_process will explode + * at runtime if a different SerializeJob is passed down */ + template + typename std::enable_if< std::is_base_of::value >::type + static /*void*/ serial_process( + RsGenericSerializer::SerializeJob j, + RsGenericSerializer::SerializeContext& ctx, + const T& cond, + const std::string& member_name ) + { + ErrConditionWrapper ew(cond); + serial_process(j, ctx, ew, member_name); + } + protected: //============================================================================// @@ -909,6 +910,27 @@ protected: t_RsTlvList& member, RsJson& jDoc ); + [[noreturn]] static void fatalUnknownSerialJob(int j) + { + RsFatal() << " Unknown serial job: " << j << std::endl; + print_stacktrace(); + exit(EINVAL); + } + + struct ErrConditionWrapper : RsSerializable + { + ErrConditionWrapper(const std::error_condition& ec): mec(ec) {} + + /** supports only TO_JSON if a different SerializeJob is passed it will + * explode at runtime */ + void serial_process( + RsGenericSerializer::SerializeJob j, + RsGenericSerializer::SerializeContext& ctx ) override; + + private: + const std::error_condition& mec; + }; + RS_SET_CONTEXT_DEBUG_LEVEL(1) }; diff --git a/libretroshare/src/util/rsdebug.cc b/libretroshare/src/util/rsdebug.cc index 4c126b656..93049e1b9 100644 --- a/libretroshare/src/util/rsdebug.cc +++ b/libretroshare/src/util/rsdebug.cc @@ -3,7 +3,9 @@ * * * libretroshare: retroshare core library * * * - * Copyright 2004-2008 by Robert Fernie * + * Copyright (C) 2004-2008 by Robert Fernie * + * Copyright (C) 2020 Gioacchino Mazzurco * + * Copyright (C) 2020 Asociación Civil Altermundi * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU Lesser General Public License as * @@ -21,11 +23,25 @@ *******************************************************************************/ #include "util/rsdebug.h" -#include "util/rsthreads.h" -#include "util/rsdir.h" + +std::ostream &operator<<(std::ostream& out, const std::error_condition& err) +{ + return out << " error: " << err.value() << " " << err.message() + << " category: " << err.category().name(); +} + + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +/// All the following lines are DEPRECATED!! #include -#include +#include + +#include "util/rsthreads.h" +#include "util/rsdir.h" #include "util/rstime.h" const int RS_DEBUG_STDERR = 1; /* stuff goes to stderr */ @@ -186,6 +202,3 @@ void rslog(const RsLog::logLvl lvl, RsLog::logInfo *info, const std::string &msg lineCount++; } } - - - diff --git a/libretroshare/src/util/rsdebug.h b/libretroshare/src/util/rsdebug.h index 227d5f8b7..1148f74f2 100644 --- a/libretroshare/src/util/rsdebug.h +++ b/libretroshare/src/util/rsdebug.h @@ -2,7 +2,8 @@ * RetroShare debugging utilities * * * * Copyright (C) 2004-2008 Robert Fernie * - * Copyright (C) 2019 Gioacchino Mazzurco * + * Copyright (C) 2019-2020 Gioacchino Mazzurco * + * Copyright (C) 2020 Asociación Civil Altermundi * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU Lesser General Public License as * @@ -21,6 +22,10 @@ #pragma once #include +#include + +/** Stream helper for std::error_condition */ +std::ostream &operator<<(std::ostream& out, const std::error_condition& err); #ifdef __ANDROID__ # include @@ -41,12 +46,14 @@ struct t_RsLogger { inline t_RsLogger() = default; + typedef t_RsLogger stream_type; + template - inline t_RsLogger& operator<<(const T& val) + inline stream_type& operator<<(const T& val) { ostr << val; return *this; } /// needed for manipulators and things like std::endl - t_RsLogger& operator<<(std::ostream& (*pf)(std::ostream&)) + stream_type& operator<<(std::ostream& (*pf)(std::ostream&)) { if(pf == static_cast( &std::endl< char, std::char_traits > )) @@ -84,8 +91,10 @@ struct t_RsLogger { inline t_RsLogger() = default; + typedef decltype(std::cerr) stream_type; + template - inline std::ostream& operator<<(const T& val) + inline stream_type& operator<<(const T& val) { return std::cerr << static_cast(CATEGORY) << " " << time(nullptr) << " " << val; @@ -97,7 +106,7 @@ struct t_RsLogger /** * Comfortable debug message loggin, supports chaining like std::cerr but can * be easly and selectively disabled at compile time to reduce generated binary - * size and performance impact without too many #ifdef around. + * size and performance impact without too many \#ifdef around. * * To selectively debug your context you can just add something like this in * in that context, as an example for a class you can just add a line like this diff --git a/libretroshare/src/util/rsdir.cc b/libretroshare/src/util/rsdir.cc index a890ed8f1..1b794f982 100644 --- a/libretroshare/src/util/rsdir.cc +++ b/libretroshare/src/util/rsdir.cc @@ -254,7 +254,13 @@ int RsDirUtil::breakupDirList(const std::string& path, /**** Copied and Tweaked from ftcontroller ***/ bool RsDirUtil::fileExists(const std::string& filename) { +#ifdef WINDOWS_SYS + std::wstring wfilename; + librs::util::ConvertUtf8ToUtf16(filename, wfilename); + return ( _waccess( wfilename.c_str(), F_OK ) != -1 ); +#else return ( access( filename.c_str(), F_OK ) != -1 ); +#endif } bool RsDirUtil::moveFile(const std::string& source,const std::string& dest) diff --git a/libretroshare/src/util/rsstring.cc b/libretroshare/src/util/rsstring.cc index 7b540156a..6dcb8bd21 100644 --- a/libretroshare/src/util/rsstring.cc +++ b/libretroshare/src/util/rsstring.cc @@ -205,6 +205,7 @@ bool ConvertUtf16ToUtf8(const std::wstring& source, std::string& dest) return true; } +#if 0 bool is_alphanumeric(char c) { return (c>='0' && c<='9') || (c>='a' && c<='z') || (c>='A' && c<='Z'); @@ -216,6 +217,7 @@ bool is_alphanumeric(const std::string& s) if(!is_alphanumeric(s[i])) return false; return true; } +#endif } } // librs::util diff --git a/libretroshare/src/util/rsstring.h b/libretroshare/src/util/rsstring.h index e2aceac68..b3fd2553c 100644 --- a/libretroshare/src/util/rsstring.h +++ b/libretroshare/src/util/rsstring.h @@ -30,10 +30,10 @@ namespace librs { bool ConvertUtf8ToUtf16(const std::string& source, std::wstring& dest); bool ConvertUtf16ToUtf8(const std::wstring& source, std::string& dest); - +#if 0 bool is_alphanumeric(char c) ; bool is_alphanumeric(const std::string& s); - +#endif } } // librs::util #ifdef WIN32 diff --git a/libretroshare/src/util/rsthreads.cc b/libretroshare/src/util/rsthreads.cc index c2b84ff70..e4c4866a3 100644 --- a/libretroshare/src/util/rsthreads.cc +++ b/libretroshare/src/util/rsthreads.cc @@ -4,7 +4,7 @@ * libretroshare: retroshare core library * * * * Copyright (C) 2004-2007 Robert Fernie * - * Copyright (C) 2016-2019 Gioacchino Mazzurco * + * Copyright (C) 2016-2020 Gioacchino Mazzurco * * Copyright (C) 2019-2020 Asociación Civil Altermundi * * * * This program is free software: you can redistribute it and/or modify * @@ -23,7 +23,7 @@ *******************************************************************************/ #include -#include +#include #include #include @@ -95,6 +95,9 @@ void RsThread::resetTid() } RsThread::RsThread() : mHasStopped(true), mShouldStop(false), mLastTid() +#ifdef RS_THREAD_FORCE_STOP + , mStopTimeout(0) +#endif { resetTid(); } bool RsThread::isRunning() { return !mHasStopped; } @@ -117,6 +120,10 @@ void RsThread::wrapRun() void RsThread::fullstop() { +#ifdef RS_THREAD_FORCE_STOP + const rstime_t stopRequTS = time(nullptr); +#endif + askForStop(); const pthread_t callerTid = pthread_self(); @@ -141,6 +148,32 @@ void RsThread::fullstop() RsDbg() << __PRETTY_FUNCTION__ << " " << i*0.2 << " seconds passed" << " waiting for thread: " << std::hex << mLastTid << std::dec << " " << mFullName << " to stop" << std::endl; + +#ifdef RS_THREAD_FORCE_STOP + if(mStopTimeout && time(nullptr) > stopRequTS + mStopTimeout) + { + RsErr() << __PRETTY_FUNCTION__ << " thread mLastTid: " << std::hex + << mLastTid << " mTid: " << mTid << std::dec << " " + << mFullName + << " ignored our nice stop request for more then " + << mStopTimeout + << " seconds, will be forcefully stopped. " + << "Please submit a report to RetroShare developers" + << std::endl; + + const auto terr = pthread_cancel(mTid); + if(terr == 0) mHasStopped = true; + else + { + RsErr() << __PRETTY_FUNCTION__ << " pthread_cancel(" + << std::hex << mTid << std::dec <<") returned " + << terr << " " << rsErrnoName(terr) << std::endl; + print_stacktrace(); + } + + break; + } +#endif // def RS_THREAD_FORCE_STOP } } diff --git a/libretroshare/src/util/rsthreads.h b/libretroshare/src/util/rsthreads.h index 5692bb256..3ff803f99 100644 --- a/libretroshare/src/util/rsthreads.h +++ b/libretroshare/src/util/rsthreads.h @@ -35,6 +35,9 @@ #include "util/rsmemory.h" #include "util/rsdeprecate.h" +#ifdef RS_THREAD_FORCE_STOP +# include "util/rstime.h" +#endif //#define RSMUTEX_DEBUG @@ -249,6 +252,13 @@ protected: * of this method, @see JsonApiServer for an usage example. */ virtual void onStopRequested() {} +#ifdef RS_THREAD_FORCE_STOP + /** Set last resort timeout to forcefully kill thread if it didn't stop + * nicely, one should never use this, still we needed to introduce this + * to investigate some bugs in external libraries */ + void setStopTimeout(rstime_t timeout) { mStopTimeout = timeout; } +#endif + private: /** Call @see run() setting the appropriate flags around it*/ void wrapRun(); @@ -277,6 +287,11 @@ private: * and that might happens concurrently (or just before) a debug message * being printed, thus causing the debug message to print a mangled value.*/ pthread_t mLastTid; + +#ifdef RS_THREAD_FORCE_STOP + /// @see setStopTimeout + rstime_t mStopTimeout; +#endif }; /** diff --git a/retroshare-gui/src/gui/Posted/PhotoView.cpp b/retroshare-gui/src/gui/Posted/PhotoView.cpp new file mode 100644 index 000000000..839e5f4d3 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/PhotoView.cpp @@ -0,0 +1,103 @@ +/******************************************************************************* + * retroshare-gui/src/gui/Posted/PhotoView.cpp * + * * + * Copyright (C) 2020 by RetroShare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "PhotoView.h" + +#include +#include +#include + +#include "gui/gxs/GxsIdDetails.h" +#include "gui/RetroShareLink.h" + +#include +#include + +/** Constructor */ +PhotoView::PhotoView(QWidget *parent) +: QDialog(parent, Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint), + ui(new Ui::PhotoView) +{ + /* Invoke the Qt Designer generated object setup routine */ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + + connect(ui->shareButton, SIGNAL(clicked()), this, SLOT(copyMessageLink())); +} + +/** Destructor */ +PhotoView::~PhotoView() +{ + delete ui; +} + +void PhotoView::setPixmap(const QPixmap& pixmap) +{ + ui->photoLabel->setPixmap(pixmap); + this->adjustSize(); +} + +void PhotoView::setTitle(const QString& text) +{ + ui->titleLabel->setText(text); +} + +void PhotoView::setName(const RsGxsId& authorID) +{ + ui->nameLabel->setId(authorID); + + RsIdentityDetails idDetails ; + rsIdentity->getIdDetails(authorID,idDetails); + + QPixmap pixmap ; + + if(idDetails.mAvatar.mSize == 0 || !GxsIdDetails::loadPixmapFromData(idDetails.mAvatar.mData, idDetails.mAvatar.mSize, pixmap,GxsIdDetails::SMALL)) + pixmap = GxsIdDetails::makeDefaultIcon(authorID,GxsIdDetails::SMALL); + + ui->avatarWidget->setPixmap(pixmap); +} + +void PhotoView::setTime(const QString& text) +{ + ui->timeLabel->setText(text); +} + +void PhotoView::setGroupId(const RsGxsGroupId &groupId) +{ + mGroupId = groupId; +} + +void PhotoView::setMessageId(const RsGxsMessageId& messageId) +{ + mMessageId = messageId ; +} + +void PhotoView::copyMessageLink() +{ + RetroShareLink link = RetroShareLink::createGxsMessageLink(RetroShareLink::TYPE_POSTED, mGroupId, mMessageId, ui->titleLabel->text()); + + if (link.valid()) { + QList urls; + urls.push_back(link); + RSLinkClipboard::copyLinks(urls); + QMessageBox::information(NULL,tr("information"),tr("The Retrohare link was copied to your clipboard.")) ; + } +} diff --git a/retroshare-gui/src/gui/Posted/PhotoView.h b/retroshare-gui/src/gui/Posted/PhotoView.h new file mode 100644 index 000000000..d44406af3 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/PhotoView.h @@ -0,0 +1,65 @@ +/******************************************************************************* + * retroshare-gui/src/gui/Posted/PhotoView.h * + * * + * Copyright (C) 2020 by RetroShare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef _PHOTO_VIEW_H +#define _PHOTO_VIEW_H + +#include "ui_PhotoView.h" + +#include + +namespace Ui { + class PhotoView; +} + +class PhotoView : public QDialog +{ + Q_OBJECT + +public: + /** Default Constructor */ + PhotoView(QWidget *parent = 0); + + /** Default Destructor */ + ~PhotoView(); + + +public slots: + void setPixmap(const QPixmap& pixmap); + void setTitle (const QString &text); + void setName(const RsGxsId& authorID); + void setTime(const QString& text); + void setGroupId(const RsGxsGroupId &groupId); + void setMessageId(const RsGxsMessageId& messageId); + +private slots: + void copyMessageLink(); + +private: + RsGxsMessageId mMessageId; + RsGxsGroupId mGroupId; + + /** Qt Designer generated object */ + Ui::PhotoView *ui; + +}; + +#endif + diff --git a/retroshare-gui/src/gui/Posted/PhotoView.ui b/retroshare-gui/src/gui/Posted/PhotoView.ui new file mode 100644 index 000000000..e0d4dce64 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/PhotoView.ui @@ -0,0 +1,229 @@ + + + PhotoView + + + + 0 + 0 + 490 + 316 + + + + Photo View + + + Qt::LeftToRight + + + + + + + MS Sans Serif + 11 + 75 + true + + + + TextLabel + + + true + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::AlignCenter + + + + + + + + + + + + + MS Sans Serif + 9 + 50 + false + + + + Posted by + + + + + + + + 24 + 24 + + + + + 24 + 24 + + + + + + + + + MS Sans Serif + 9 + + + + TextLabel + + + + + + + + MS Sans Serif + 9 + + + + TextLabel + + + + + + + + MS Sans Serif + 9 + + + + ago + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + Copy RetroShare link + + + Share + + + + :/images/share.png:/images/share.png + + + Qt::ToolButtonTextBesideIcon + + + true + + + + + + + + + + GxsIdLabel + QLabel +
gui/gxs/GxsIdLabel.h
+
+ + AvatarWidget + QWidget +
gui/common/AvatarWidget.h
+ 1 +
+ + AspectRatioPixmapLabel + QLabel +
util/AspectRatioPixmapLabel.h
+
+
+ + + + +
diff --git a/retroshare-gui/src/gui/Posted/PostedCardView.cpp b/retroshare-gui/src/gui/Posted/PostedCardView.cpp new file mode 100644 index 000000000..1fef4f160 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/PostedCardView.cpp @@ -0,0 +1,553 @@ +/******************************************************************************* + * retroshare-gui/src/gui/Posted/PostedCardView.cpp * + * * + * Copyright (C) 2019 Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include +#include +#include +#include + +#include "rshare.h" +#include "PostedCardView.h" +#include "gui/feeds/FeedHolder.h" +#include "gui/gxs/GxsIdDetails.h" +#include "util/misc.h" +#include "util/HandleRichText.h" + +#include "ui_PostedCardView.h" + +#include +#include + +#define LINK_IMAGE ":/images/thumb-link.png" + +/** Constructor */ + +PostedCardView::PostedCardView(FeedHolder *feedHolder, uint32_t feedId, const RsGxsGroupId &groupId, const RsGxsMessageId &messageId, bool isHome, bool autoUpdate) : + GxsFeedItem(feedHolder, feedId, groupId, messageId, isHome, rsPosted, autoUpdate) +{ + setup(); + + requestGroup(); + requestMessage(); + requestComment(); +} + +PostedCardView::PostedCardView(FeedHolder *feedHolder, uint32_t feedId, const RsPostedGroup &group, const RsPostedPost &post, bool isHome, bool autoUpdate) : + GxsFeedItem(feedHolder, feedId, post.mMeta.mGroupId, post.mMeta.mMsgId, isHome, rsPosted, autoUpdate) +{ + setup(); + + mMessageId = post.mMeta.mMsgId; + + + setGroup(group, false); + setPost(post); + requestComment(); +} + +PostedCardView::PostedCardView(FeedHolder *feedHolder, uint32_t feedId, const RsPostedPost &post, bool isHome, bool autoUpdate) : + GxsFeedItem(feedHolder, feedId, post.mMeta.mGroupId, post.mMeta.mMsgId, isHome, rsPosted, autoUpdate) +{ + setup(); + + requestGroup(); + setPost(post); + requestComment(); +} + +PostedCardView::~PostedCardView() +{ + delete(ui); +} + +void PostedCardView::setup() +{ + /* Invoke the Qt Designer generated object setup routine */ + ui = new Ui::PostedCardView; + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + + mInFill = false; + + /* clear ui */ + ui->titleLabel->setText(tr("Loading")); + ui->dateLabel->clear(); + ui->fromLabel->clear(); + ui->siteLabel->clear(); + + /* general ones */ + connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(removeItem())); + + /* specific */ + connect(ui->readAndClearButton, SIGNAL(clicked()), this, SLOT(readAndClearItem())); + + connect(ui->commentButton, SIGNAL( clicked()), this, SLOT(loadComments())); + connect(ui->voteUpButton, SIGNAL(clicked()), this, SLOT(makeUpVote())); + connect(ui->voteDownButton, SIGNAL(clicked()), this, SLOT( makeDownVote())); + connect(ui->readButton, SIGNAL(toggled(bool)), this, SLOT(readToggled(bool))); + + QAction *CopyLinkAction = new QAction(QIcon(""),tr("Copy RetroShare Link"), this); + connect(CopyLinkAction, SIGNAL(triggered()), this, SLOT(copyMessageLink())); + + + int S = QFontMetricsF(font()).height() ; + + ui->voteUpButton->setIconSize(QSize(S*1.5,S*1.5)); + ui->voteDownButton->setIconSize(QSize(S*1.5,S*1.5)); + ui->commentButton->setIconSize(QSize(S*1.5,S*1.5)); + ui->readButton->setIconSize(QSize(S*1.5,S*1.5)); + ui->shareButton->setIconSize(QSize(S*1.5,S*1.5)); + + QMenu *menu = new QMenu(); + menu->addAction(CopyLinkAction); + ui->shareButton->setMenu(menu); + + ui->clearButton->hide(); + ui->readAndClearButton->hide(); +} + +bool PostedCardView::setGroup(const RsPostedGroup &group, bool doFill) +{ + if (groupId() != group.mMeta.mGroupId) { + std::cerr << "PostedCardView::setGroup() - Wrong id, cannot set post"; + std::cerr << std::endl; + return false; + } + + mGroup = group; + + if (doFill) { + fill(); + } + + return true; +} + +bool PostedCardView::setPost(const RsPostedPost &post, bool doFill) +{ + if (groupId() != post.mMeta.mGroupId || messageId() != post.mMeta.mMsgId) { + std::cerr << "PostedCardView::setPost() - Wrong id, cannot set post"; + std::cerr << std::endl; + return false; + } + + mPost = post; + + if (doFill) { + fill(); + } + + return true; +} + +void PostedCardView::loadGroup(const uint32_t &token) +{ + std::vector groups; + if (!rsPosted->getGroupData(token, groups)) + { + std::cerr << "PostedCardView::loadGroup() ERROR getting data"; + std::cerr << std::endl; + return; + } + + if (groups.size() != 1) + { + std::cerr << "PostedCardView::loadGroup() Wrong number of Items"; + std::cerr << std::endl; + return; + } + + setGroup(groups[0]); +} + +void PostedCardView::loadMessage(const uint32_t &token) +{ + std::vector posts; + std::vector cmts; + if (!rsPosted->getPostData(token, posts, cmts)) + { + std::cerr << "GxsChannelPostItem::loadMessage() ERROR getting data"; + std::cerr << std::endl; + return; + } + + if (posts.size() == 1) + { + setPost(posts[0]); + } + else if (cmts.size() == 1) + { + RsGxsComment cmt = cmts[0]; + + //ui->newCommentLabel->show(); + //ui->commLabel->show(); + //ui->commLabel->setText(QString::fromUtf8(cmt.mComment.c_str())); + + //Change this item to be uploaded with thread element. + setMessageId(cmt.mMeta.mThreadId); + requestMessage(); + } + else + { + std::cerr << "GxsChannelPostItem::loadMessage() Wrong number of Items. Remove It."; + std::cerr << std::endl; + removeItem(); + return; + } +} + +void PostedCardView::loadComment(const uint32_t &token) +{ + std::vector cmts; + if (!rsPosted->getRelatedComments(token, cmts)) + { + std::cerr << "GxsChannelPostItem::loadComment() ERROR getting data"; + std::cerr << std::endl; + return; + } + + size_t comNb = cmts.size(); + QString sComButText = tr("Comment"); + if (comNb == 1) { + sComButText = sComButText.append("(1)"); + } else if (comNb > 1) { + sComButText = " " + tr("Comments").append(" (%1)").arg(comNb); + } + ui->commentButton->setText(sComButText); +} + +void PostedCardView::fill() +{ + if (isLoading()) { + /* Wait for all requests */ + return; + } + + QPixmap sqpixmap2 = QPixmap(":/images/thumb-default.png"); + + mInFill = true; + int desired_height = 1.5*(ui->voteDownButton->height() + ui->voteUpButton->height() + ui->scoreLabel->height()); + int desired_width = sqpixmap2.width()*desired_height/(float)sqpixmap2.height(); + + QDateTime qtime; + qtime.setTime_t(mPost.mMeta.mPublishTs); + QString timestamp = qtime.toString("hh:mm dd-MMM-yyyy"); + QString timestamp2 = misc::timeRelativeToNow(mPost.mMeta.mPublishTs); + ui->dateLabel->setText(timestamp2); + ui->dateLabel->setToolTip(timestamp); + + ui->fromLabel->setId(mPost.mMeta.mAuthorId); + + // Use QUrl to check/parse our URL + // The only combination that seems to work: load as EncodedUrl, extract toEncoded(). + QByteArray urlarray(mPost.mLink.c_str()); + QUrl url = QUrl::fromEncoded(urlarray.trimmed()); + QString urlstr = "Invalid Link"; + QString sitestr = "Invalid Link"; + + bool urlOkay = url.isValid(); + if (urlOkay) + { + QString scheme = url.scheme(); + if ((scheme != "https") + && (scheme != "http") + && (scheme != "ftp") + && (scheme != "retroshare")) + { + urlOkay = false; + sitestr = "Invalid Link Scheme"; + } + } + + if (urlOkay) + { + urlstr = QString(" "); + urlstr += messageName(); + urlstr += QString(" "); + + QString siteurl = url.toEncoded(); + sitestr = QString(" %2 ").arg(siteurl).arg(siteurl); + + ui->titleLabel->setText(urlstr); + }else + { + ui->titleLabel->setText(messageName()); + + } + + if (urlarray.isEmpty()) + { + ui->siteLabel->hide(); + } + + ui->siteLabel->setText(sitestr); + + if(mPost.mImage.mData != NULL) + { + QPixmap pixmap; + GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); + // Wiping data - as its been passed to thumbnail. + + QPixmap scaledpixmap; + if(pixmap.width() > 800){ + QPixmap scaledpixmap = pixmap.scaledToWidth(800, Qt::SmoothTransformation); + ui->pictureLabel->setPixmap(scaledpixmap); + }else{ + ui->pictureLabel->setPixmap(pixmap); + } + } + else if (mPost.mImage.mData == NULL) + { + ui->picture_frame->hide(); + } + else + { + ui->picture_frame->show(); + } + + + //QString score = "Hot" + QString::number(post.mHotScore); + //score += " Top" + QString::number(post.mTopScore); + //score += " New" + QString::number(post.mNewScore); + + QString score = QString::number(mPost.mTopScore); + + ui->scoreLabel->setText(score); + + // FIX THIS UP LATER. + ui->notes->setText(RsHtml().formatText(NULL, QString::fromUtf8(mPost.mNotes.c_str()), RSHTML_FORMATTEXT_EMBED_SMILEYS | RSHTML_FORMATTEXT_EMBED_LINKS)); + + QTextDocument doc; + doc.setHtml(ui->notes->text()); + + if(doc.toPlainText().trimmed().isEmpty()) + ui->notes->hide(); + // differences between Feed or Top of Comment. + if (mFeedHolder) + { + // feed. + //frame_comment->show(); + ui->commentButton->show(); + + if (mPost.mComments) + { + QString commentText = QString::number(mPost.mComments); + commentText += " "; + commentText += tr("Comments"); + ui->commentButton->setText(commentText); + } + else + { + ui->commentButton->setText(tr("Comment")); + } + + setReadStatus(IS_MSG_NEW(mPost.mMeta.mMsgStatus), IS_MSG_UNREAD(mPost.mMeta.mMsgStatus) || IS_MSG_NEW(mPost.mMeta.mMsgStatus)); + } + else + { + // no feed. + //frame_comment->hide(); + ui->commentButton->hide(); + + ui->readButton->hide(); + ui->newLabel->hide(); + } + + if (mIsHome) + { + ui->clearButton->hide(); + ui->readAndClearButton->hide(); + } + else + { + ui->clearButton->show(); + ui->readAndClearButton->show(); + } + + // disable voting buttons - if they have already voted. + if (mPost.mMeta.mMsgStatus & GXS_SERV::GXS_MSG_STATUS_VOTE_MASK) + { + ui->voteUpButton->setEnabled(false); + ui->voteDownButton->setEnabled(false); + } + +#if 0 + uint32_t up, down, nComments; + + bool ok = rsPosted->retrieveScores(mPost.mMeta.mServiceString, up, down, nComments); + + if(ok) + { + int32_t vote = up - down; + scoreLabel->setText(QString::number(vote)); + + numCommentsLabel->setText("

# Comments: " + + QString::number(nComments) + "

"); + } +#endif + + mInFill = false; + + emit sizeChanged(this); +} + +const RsPostedPost &PostedCardView::getPost() const +{ + return mPost; +} + +RsPostedPost &PostedCardView::post() +{ + return mPost; +} + +QString PostedCardView::groupName() +{ + return QString::fromUtf8(mGroup.mMeta.mGroupName.c_str()); +} + +QString PostedCardView::messageName() +{ + return QString::fromUtf8(mPost.mMeta.mMsgName.c_str()); +} + +void PostedCardView::makeDownVote() +{ + RsGxsGrpMsgIdPair msgId; + msgId.first = mPost.mMeta.mGroupId; + msgId.second = mPost.mMeta.mMsgId; + + ui->voteUpButton->setEnabled(false); + ui->voteDownButton->setEnabled(false); + + emit vote(msgId, false); +} + +void PostedCardView::makeUpVote() +{ + RsGxsGrpMsgIdPair msgId; + msgId.first = mPost.mMeta.mGroupId; + msgId.second = mPost.mMeta.mMsgId; + + ui->voteUpButton->setEnabled(false); + ui->voteDownButton->setEnabled(false); + + emit vote(msgId, true); +} + +void PostedCardView::loadComments() +{ + std::cerr << "PostedCardView::loadComments()"; + std::cerr << std::endl; + + if (mFeedHolder) + { + QString title = QString::fromUtf8(mPost.mMeta.mMsgName.c_str()); + +#warning (csoler) Posted item versions not handled yet. When it is the case, start here. + + QVector post_versions ; + post_versions.push_back(mPost.mMeta.mMsgId) ; + + mFeedHolder->openComments(0, mPost.mMeta.mGroupId, post_versions,mPost.mMeta.mMsgId, title); + } +} + +void PostedCardView::setReadStatus(bool isNew, bool isUnread) +{ + if (isUnread) + { + ui->readButton->setChecked(true); + ui->readButton->setIcon(QIcon(":/images/message-state-unread.png")); + } + else + { + ui->readButton->setChecked(false); + ui->readButton->setIcon(QIcon(":/images/message-state-read.png")); + } + + ui->newLabel->setVisible(isNew); + + ui->mainFrame->setProperty("new", isNew); + ui->mainFrame->style()->unpolish(ui->mainFrame); + ui->mainFrame->style()->polish( ui->mainFrame); +} + +void PostedCardView::readToggled(bool checked) +{ + if (mInFill) { + return; + } + + RsGxsGrpMsgIdPair msgPair = std::make_pair(groupId(), messageId()); + + uint32_t token; + rsPosted->setMessageReadStatus(token, msgPair, !checked); + + setReadStatus(false, checked); +} + +void PostedCardView::readAndClearItem() +{ +#ifdef DEBUG_ITEM + std::cerr << "PostedCardView::readAndClearItem()"; + std::cerr << std::endl; +#endif + + readToggled(false); + removeItem(); +} + + +void PostedCardView::doExpand(bool open) +{ + /*if (open) + { + + } + else + { + + } + + emit sizeChanged(this);*/ + +} + +void PostedCardView::copyMessageLink() +{ + if (groupId().isNull() || mMessageId.isNull()) { + return; + } + + RetroShareLink link = RetroShareLink::createGxsMessageLink(RetroShareLink::TYPE_POSTED, groupId(), mMessageId, messageName()); + + if (link.valid()) { + QList urls; + urls.push_back(link); + RSLinkClipboard::copyLinks(urls); + } +} diff --git a/retroshare-gui/src/gui/Posted/PostedCardView.h b/retroshare-gui/src/gui/Posted/PostedCardView.h new file mode 100644 index 000000000..7fc303680 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/PostedCardView.h @@ -0,0 +1,98 @@ +/******************************************************************************* + * retroshare-gui/src/gui/Posted/PostedCardView.h * + * * + * Copyright (C) 2019 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef _POSTED_CARDVIEW_H +#define _POSTED_CARDVIEW_H + +#include + +#include +#include "gui/gxs/GxsFeedItem.h" + +namespace Ui { +class PostedCardView; +} + +class FeedHolder; +class RsPostedPost; + +class PostedCardView : public GxsFeedItem +{ + Q_OBJECT + +public: + PostedCardView(FeedHolder *parent, uint32_t feedId, const RsGxsGroupId &groupId, const RsGxsMessageId &messageId, bool isHome, bool autoUpdate); + PostedCardView(FeedHolder *parent, uint32_t feedId, const RsPostedGroup &group, const RsPostedPost &post, bool isHome, bool autoUpdate); + PostedCardView(FeedHolder *parent, uint32_t feedId, const RsPostedPost &post, bool isHome, bool autoUpdate); + virtual ~PostedCardView(); + + bool setGroup(const RsPostedGroup& group, bool doFill = true); + bool setPost(const RsPostedPost& post, bool doFill = true); + + const RsPostedPost &getPost() const; + RsPostedPost &post(); + + uint64_t uniqueIdentifier() const override { return hash_64bits("PostedItem " + mMessageId.toStdString()); } + +protected: + /* FeedItem */ + virtual void doExpand(bool open); + +private slots: + void loadComments(); + void makeUpVote(); + void makeDownVote(); + void readToggled(bool checked); + void readAndClearItem(); + void copyMessageLink(); + +signals: + void vote(const RsGxsGrpMsgIdPair& msgId, bool up); + +protected: + /* GxsGroupFeedItem */ + virtual QString groupName(); + virtual void loadGroup(const uint32_t &token); + virtual RetroShareLink::enumType getLinkType() { return RetroShareLink::TYPE_UNKNOWN; } + + /* GxsFeedItem */ + virtual QString messageName(); + virtual void loadMessage(const uint32_t &token); + virtual void loadComment(const uint32_t &token); + +private: + void setup(); + void fill(); + void setReadStatus(bool isNew, bool isUnread); + +private: + bool mInFill; + + RsPostedGroup mGroup; + RsPostedPost mPost; + RsGxsMessageId mMessageId; + + /** Qt Designer generated object */ + Ui::PostedCardView *ui; +}; + +//Q_DECLARE_METATYPE(RsPostedPost) + +#endif diff --git a/retroshare-gui/src/gui/Posted/PostedCardView.ui b/retroshare-gui/src/gui/Posted/PostedCardView.ui new file mode 100644 index 000000000..8cbbd4d56 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/PostedCardView.ui @@ -0,0 +1,526 @@ + + + PostedCardView + + + + 0 + 0 + 614 + 182 + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + false + + + QFrame::Box + + + QFrame::Sunken + + + + 0 + + + 0 + + + 0 + + + 2 + + + 0 + + + 6 + + + + + + 0 + 0 + + + + + Arial + 10 + 75 + true + + + + This is a very very very very loooooooooooooooonnnnnnnnnnnnnnnnng title don't you think? Yes it is and should wrap around I hope + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + site + + + true + + + + + + + 5 + + + 0 + + + 6 + + + + + + 0 + 0 + + + + + 50 + false + + + + Posted by + + + + + + + + 0 + 0 + + + + Signed by + + + true + + + + + + + + 0 + 0 + + + + You eyes only + + + true + + + + + + + + 24 + 16777215 + + + + Qt::NoFocus + + + Toggle Message Read Status + + + + :/images/message-state-unread.png:/images/message-state-unread.png + + + true + + + false + + + true + + + + + + + New + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 70 + 20 + + + + + + + + + + + 37 + 0 + + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 0 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 0 + 0 + + + + Vote up + + + + + + + :/images/up-arrow.png:/images/up-arrow.png + + + true + + + + + + + + 9 + + + + 0 + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Vote down + + + + + + \/ + + + + :/images/down-arrow.png:/images/down-arrow.png + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 5 + + + + + + + + + + + + + Comments + + + + :/images/comments.png:/images/comments.png + + + Qt::ToolButtonTextBesideIcon + + + true + + + + + + + Share + + + + :/images/share.png:/images/share.png + + + true + + + + + + + Qt::Horizontal + + + + 308 + 20 + + + + + + + + + 24 + 16777215 + + + + Qt::NoFocus + + + Set as read and remove item + + + + :/icons/png/correct.png:/icons/png/correct.png + + + + + + + + 24 + 16777215 + + + + Qt::NoFocus + + + Remove Item + + + + :/icons/png/exit2.png:/icons/png/exit2.png + + + + + + + + + + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + PictureLabel + + + true + + + + + + + Qt::Horizontal + + + + 268 + 17 + + + + + + + + + + + + + + + StyledLabel + QLabel +
gui/common/StyledLabel.h
+
+ + GxsIdLabel + QLabel +
gui/gxs/GxsIdLabel.h
+
+
+ + + + + + +
diff --git a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp index c822bb492..ddacd4895 100644 --- a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp +++ b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp @@ -20,6 +20,8 @@ #include #include +#include +#include #include "PostedCreatePostDialog.h" #include "ui_PostedCreatePostDialog.h" @@ -34,6 +36,10 @@ #include +#include + +#include + PostedCreatePostDialog::PostedCreatePostDialog(TokenQueue* tokenQ, RsPosted *posted, const RsGxsGroupId& grpId, QWidget *parent): QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint), mTokenQueue(tokenQ), mPosted(posted), mGrpId(grpId), @@ -44,7 +50,7 @@ PostedCreatePostDialog::PostedCreatePostDialog(TokenQueue* tokenQ, RsPosted *pos connect(ui->submitButton, SIGNAL(clicked()), this, SLOT(createPost())); connect(ui->buttonBox, SIGNAL(rejected()), this, SLOT(close())); - connect(ui->pushButton, SIGNAL(clicked() ), this , SLOT(addPicture())); + connect(ui->addPicButton, SIGNAL(clicked() ), this , SLOT(addPicture())); ui->headerFrame->setHeaderImage(QPixmap(":/icons/png/postedlinks.png")); ui->headerFrame->setHeaderText(tr("Create a new Post")); @@ -52,19 +58,59 @@ PostedCreatePostDialog::PostedCreatePostDialog(TokenQueue* tokenQ, RsPosted *pos setAttribute ( Qt::WA_DeleteOnClose, true ); ui->RichTextEditWidget->setPlaceHolderTextPosted(); + + ui->hashBox->setAutoHide(true); + ui->hashBox->setDefaultTransferRequestFlags(RS_FILE_REQ_ANONYMOUS_ROUTING); + connect(ui->hashBox, SIGNAL(fileHashingFinished(QList)), this, SLOT(fileHashingFinished(QList))); + ui->sizeWarningLabel->setText(QString("Post size is limited to %1 KB, pictures will be downscaled.").arg(MAXMESSAGESIZE / 1024)); /* fill in the available OwnIds for signing */ ui->idChooser->loadIds(IDCHOOSER_ID_REQUIRED, RsGxsId()); + + ui->removeButton->hide(); + + /* load settings */ + processSettings(true); } PostedCreatePostDialog::~PostedCreatePostDialog() { Settings->saveWidgetInformation(this); + + // save settings + processSettings(false); + delete ui; } +void PostedCreatePostDialog::processSettings(bool load) +{ + Settings->beginGroup(QString("PostedCreatePostDialog")); + + if (load) { + // load settings + + // state of ID Chooser combobox + int index = Settings->value("IDChooser", 0).toInt(); + ui->idChooser->setCurrentIndex(index); + } else { + // save settings + + // state of ID Chooser combobox + Settings->setValue("IDChooser", ui->idChooser->currentIndex()); + } + + Settings->endGroup(); +} + void PostedCreatePostDialog::createPost() { + if(ui->titleEdit->text().isEmpty()) { + /* error message */ + QMessageBox::warning(this, "RetroShare", tr("Please add a Title"), QMessageBox::Ok, QMessageBox::Ok); + return; //Don't add a empty title!! + } + RsGxsId authorId; switch (ui->idChooser->getChosenId(authorId)) { case GxsIdChooser::KnowId: @@ -85,37 +131,27 @@ void PostedCreatePostDialog::createPost() post.mMeta.mGroupId = mGrpId; post.mLink = std::string(ui->linkEdit->text().toUtf8()); - QString text; - text = ui->RichTextEditWidget->toHtml(); - post.mNotes = std::string(text.toUtf8()); + if(!ui->RichTextEditWidget->toPlainText().trimmed().isEmpty()) { + QString text; + text = ui->RichTextEditWidget->toHtml(); + post.mNotes = std::string(text.toUtf8()); + } post.mMeta.mAuthorId = authorId; - - if(!ui->titleEdit->text().isEmpty()) - { - post.mMeta.mMsgName = std::string(ui->titleEdit->text().toUtf8()); - }else - { - post.mMeta.mMsgName = std::string(ui->titleEditLink->text().toUtf8()); - } - - QByteArray ba; - QBuffer buffer(&ba); + post.mMeta.mMsgName = std::string(ui->titleEdit->text().toUtf8()); - if(!picture.isNull()) + if(imagebytes.size() > 0) { // send posted image + post.mImage.copy((uint8_t *) imagebytes.data(), imagebytes.size()); + } - buffer.open(QIODevice::WriteOnly); - picture.save(&buffer, "PNG"); // writes image into ba in PNG format - post.mImage.copy((uint8_t *) ba.data(), ba.size()); + int msgsize = post.mLink.length() + post.mMeta.mMsgName.length() + post.mNotes.length() + imagebytes.size(); + if(msgsize > MAXMESSAGESIZE) { + QString errormessage = QString(tr("Message is too large.
actual size: %1 bytes, maximum size: %2 bytes.")).arg(msgsize).arg(MAXMESSAGESIZE); + QMessageBox::warning(this, "RetroShare", errormessage, QMessageBox::Ok, QMessageBox::Ok); + return; } - - if(ui->titleEdit->text().isEmpty()&& ui->titleEditLink->text().isEmpty()) { - /* error message */ - QMessageBox::warning(this, "RetroShare", tr("Please add a Title"), QMessageBox::Ok, QMessageBox::Ok); - return; //Don't add a empty title!! - }//if(ui->titleEdit->text().isEmpty()) uint32_t token; mPosted->createPost(token, post); @@ -124,17 +160,66 @@ void PostedCreatePostDialog::createPost() accept(); } -void PostedCreatePostDialog::addPicture() +void PostedCreatePostDialog::fileHashingFinished(QList hashedFiles) { - QPixmap img = misc::getOpenThumbnailedPicture(this, tr("Load thumbnail picture"), 800, 600); + if(hashedFiles.length() > 0) { //It seems like it returns 0 if hashing cancelled + HashedFile hashedFile = hashedFiles[0]; //Should be exactly one file + RetroShareLink link; + link = RetroShareLink::createFile(hashedFile.filename, hashedFile.size, QString::fromStdString(hashedFile.hash.toStdString())); + ui->linkEdit->setText(link.toString()); + } + ui->submitButton->setEnabled(true); + ui->addPicButton->setEnabled(true); +} - if (img.isNull()) - return; +void PostedCreatePostDialog::addPicture() +{ + imagefilename = ""; + imagebytes.clear(); + QPixmap empty; + ui->imageLabel->setPixmap(empty); - picture = img; + // select a picture file + if (misc::getOpenFileName(window(), RshareSettings::LASTDIR_IMAGES, tr("Load Picture File"), "Pictures (*.png *.xpm *.jpg *.jpeg *.gif *.webp )", imagefilename)) { + QString encodedImage; + QImage image; + if (image.load(imagefilename) == false) { + fprintf (stderr, "RsHtml::makeEmbeddedImage() - image \"%s\" can't be load\n", imagefilename.toLatin1().constData()); + imagefilename = ""; + return; + } + + QImage opt; + if(ImageUtil::optimizeSizeBytes(imagebytes, image, opt, 640*480, MAXMESSAGESIZE - 2000)) { //Leave space for other stuff + ui->imageLabel->setPixmap(QPixmap::fromImage(opt)); + ui->stackedWidgetPicture->setCurrentIndex(1); + ui->removeButton->show(); + } else { + imagefilename = ""; + imagebytes.clear(); + return; + } + } + + //Do we need to hash the image? + QMessageBox::StandardButton answer; + answer = QMessageBox::question(this, tr("Post image"), tr("Do you want to share and link the original image?"), QMessageBox::Yes|QMessageBox::No); + if (answer == QMessageBox::Yes) { + if(!ui->linkEdit->text().trimmed().isEmpty()) { + answer = QMessageBox::question(this, tr("Post image"), tr("You already added a link.
Do you want to replace it?"), QMessageBox::Yes|QMessageBox::No); + } + } + + //If still yes then link it + if(answer == QMessageBox::Yes) { + ui->submitButton->setEnabled(false); + ui->addPicButton->setEnabled(false); + QStringList files; + files.append(imagefilename); + ui->hashBox->addAttachments(files,RS_FILE_REQ_ANONYMOUS_ROUTING); + } + - // to show the selected - ui->imageLabel->setPixmap(picture); } void PostedCreatePostDialog::on_postButton_clicked() @@ -151,3 +236,13 @@ void PostedCreatePostDialog::on_linkButton_clicked() { ui->stackedWidget->setCurrentIndex(2); } + +void PostedCreatePostDialog::on_removeButton_clicked() +{ + imagefilename = ""; + imagebytes.clear(); + QPixmap empty; + ui->imageLabel->setPixmap(empty); + ui->removeButton->hide(); + ui->stackedWidgetPicture->setCurrentIndex(0); +} diff --git a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h index de13358fb..5aed59950 100644 --- a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h +++ b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h @@ -22,6 +22,7 @@ #define POSTEDCREATEPOSTDIALOG_H #include +#include #include "retroshare/rsposted.h" #include "util/RichTextEdit.h" @@ -43,7 +44,10 @@ public: explicit PostedCreatePostDialog(TokenQueue* tokenQ, RsPosted* posted, const RsGxsGroupId& grpId, QWidget *parent = 0); ~PostedCreatePostDialog(); - QPixmap picture; +private: + QString imagefilename; + QByteArray imagebytes; + const int MAXMESSAGESIZE = 199000; private slots: void createPost(); @@ -51,8 +55,12 @@ private slots: void on_postButton_clicked(); void on_imageButton_clicked(); void on_linkButton_clicked(); + void on_removeButton_clicked(); + void fileHashingFinished(QList hashedFiles); private: + void processSettings(bool load); + QString mLink; QString mNotes; TokenQueue* mTokenQueue; diff --git a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.ui b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.ui index d64c3ad01..50abd48a2 100644 --- a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.ui +++ b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.ui @@ -7,7 +7,7 @@ 0 0 575 - 429 + 518 @@ -48,6 +48,427 @@ QFrame::Raised + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + true + + + + + + + Preview + + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 0 + + + + + + + 9 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Add Picture + + + + :/icons/png/add-image.png:/icons/png/add-image.png + + + + 24 + 24 + + + + + + + + Post size is limited to 32 KB, pictures will be downscaled. + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + + 267 + 138 + + + + + + + + + + 2 + + + 2 + + + 0 + + + 2 + + + + + + 800 + 200 + + + + + + + true + + + + + + + Qt::Horizontal + + + + 188 + 17 + + + + + + + + Remove image + + + + :/images/trashcan.png:/images/trashcan.png + + + + 24 + 24 + + + + true + + + + + + + + + + + + + + + + 0 + + + 6 + + + 0 + + + + + Url + + + + + + + Qt::Vertical + + + + 20 + 248 + + + + + + + + + + + + + + + 0 + 0 + + + + Post as + + + + + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + + + + Post + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + + + + 2 + + + + + Post + + + + :/images/post.png:/images/post.png + + + + 24 + 24 + + + + + + + + Image + + + + :/images/photo.png:/images/photo.png + + + + 24 + 24 + + + + + + + + Link + + + + :/images/link.png:/images/link.png + + + + 24 + 24 + + + + + + + + Qt::Horizontal + + + + 298 + 20 + + + + + + @@ -128,105 +549,7 @@ - - - - 2 - - - - - Post - - - - :/images/post.png:/images/post.png - - - - 24 - 24 - - - - - - - - Image - - - - :/images/photo.png:/images/photo.png - - - - 24 - 24 - - - - - - - - Link - - - - :/images/link.png:/images/link.png - - - - 24 - 24 - - - - - - - - Qt::Horizontal - - - - 298 - 20 - - - - - - - - - - - - - 0 - 0 - - - - Post as - - - - - - - - 0 - 0 - - - - - - - + Qt::Horizontal @@ -242,207 +565,28 @@ - - - - - 0 - 0 - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel - - - - - - 0 + + + 6 - - - - 0 + + + + + 0 + 0 + - - 6 + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - 0 + + Title - - 0 - - - - - - - - Title - - - - - - - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 447 - 20 - - - - - - - - Preview - - - - - - - 250 - 200 - - - - - 800 - 200 - - - - - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - Add Picture - - - - - - - Picture size is limited to 34 KB - - - - - - - - - 0 - - - 6 - - - 0 - - - - - Url - - - - - - - Qt::Vertical - - - - 20 - 248 - - - - - - - - Title - - - - - - - - - - - - 0 - 0 - - - - - - - Post - - + + + @@ -455,6 +599,12 @@ QLabel
gui/common/StyledLabel.h
+ + HashBox + QScrollArea +
gui/common/HashBox.h
+ 1 +
HeaderFrame QFrame @@ -475,7 +625,6 @@ - diff --git a/retroshare-gui/src/gui/Posted/PostedItem.cpp b/retroshare-gui/src/gui/Posted/PostedItem.cpp index 5d4e00a93..b09452a8e 100644 --- a/retroshare-gui/src/gui/Posted/PostedItem.cpp +++ b/retroshare-gui/src/gui/Posted/PostedItem.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "rshare.h" #include "PostedItem.h" @@ -28,7 +29,7 @@ #include "gui/gxs/GxsIdDetails.h" #include "util/misc.h" #include "util/HandleRichText.h" - +#include "PhotoView.h" #include "ui_PostedItem.h" #include @@ -109,11 +110,12 @@ void PostedItem::setup() connect(ui->notesButton, SIGNAL(clicked()), this, SLOT( toggleNotes())); connect(ui->readButton, SIGNAL(toggled(bool)), this, SLOT(readToggled(bool))); - + connect(ui->thumbnailLabel, SIGNAL(clicked()), this, SLOT(viewPicture())); + QAction *CopyLinkAction = new QAction(QIcon(""),tr("Copy RetroShare Link"), this); connect(CopyLinkAction, SIGNAL(triggered()), this, SLOT(copyMessageLink())); - - + + int S = QFontMetricsF(font()).height() ; ui->voteUpButton->setIconSize(QSize(S*1.5,S*1.5)); @@ -302,6 +304,11 @@ void PostedItem::fill() } + if (urlarray.isEmpty()) + { + ui->siteLabel->hide(); + } + ui->siteLabel->setText(sitestr); if(mPost.mImage.mData != NULL) @@ -312,7 +319,15 @@ void PostedItem::fill() QPixmap sqpixmap = pixmap.scaled(desired_width,desired_height, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); ui->thumbnailLabel->setPixmap(sqpixmap); - ui->pictureLabel->setPixmap(pixmap); + ui->thumbnailLabel->setToolTip(tr("Click to view Picture")); + + QPixmap scaledpixmap; + if(pixmap.width() > 800){ + QPixmap scaledpixmap = pixmap.scaledToWidth(800, Qt::SmoothTransformation); + ui->pictureLabel->setPixmap(scaledpixmap); + }else{ + ui->pictureLabel->setPixmap(pixmap); + } } else if (urlOkay && (mPost.mImage.mData == NULL)) { @@ -337,7 +352,10 @@ void PostedItem::fill() // FIX THIS UP LATER. ui->notes->setText(RsHtml().formatText(NULL, QString::fromUtf8(mPost.mNotes.c_str()), RSHTML_FORMATTEXT_EMBED_SMILEYS | RSHTML_FORMATTEXT_EMBED_LINKS)); - if(ui->notes->text().isEmpty()) + QTextDocument doc; + doc.setHtml(ui->notes->text()); + + if(doc.toPlainText().trimmed().isEmpty()) ui->notesButton->hide(); // differences between Feed or Top of Comment. if (mFeedHolder) @@ -569,3 +587,28 @@ void PostedItem::toggleNotes() } } + +void PostedItem::viewPicture() +{ + if(mPost.mImage.mData == NULL) { + return; + } + + QString timestamp = misc::timeRelativeToNow(mPost.mMeta.mPublishTs); + QPixmap pixmap; + GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); + RsGxsId authorID = mPost.mMeta.mAuthorId; + + PhotoView *PView = new PhotoView(this); + + PView->setPixmap(pixmap); + PView->setTitle(messageName()); + PView->setName(authorID); + PView->setTime(timestamp); + PView->setGroupId(groupId()); + PView->setMessageId(mMessageId); + + PView->show(); + + /* window will destroy itself! */ +} diff --git a/retroshare-gui/src/gui/Posted/PostedItem.h b/retroshare-gui/src/gui/Posted/PostedItem.h index aa855d72b..97b6974d0 100644 --- a/retroshare-gui/src/gui/Posted/PostedItem.h +++ b/retroshare-gui/src/gui/Posted/PostedItem.h @@ -63,6 +63,7 @@ private slots: void toggle() override; void copyMessageLink(); void toggleNotes(); + void viewPicture(); signals: void vote(const RsGxsGrpMsgIdPair& msgId, bool up); diff --git a/retroshare-gui/src/gui/Posted/PostedItem.ui b/retroshare-gui/src/gui/Posted/PostedItem.ui index 164ba1a89..f470fea00 100644 --- a/retroshare-gui/src/gui/Posted/PostedItem.ui +++ b/retroshare-gui/src/gui/Posted/PostedItem.ui @@ -7,7 +7,7 @@ 0 0 825 - 339 + 337 @@ -109,6 +109,9 @@ Vote up + + + @@ -148,8 +151,8 @@ Vote down - - \/ + + @@ -185,7 +188,7 @@ 6 - + 0 @@ -267,6 +270,9 @@ true + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + @@ -415,6 +421,9 @@ + + Expand + @@ -428,7 +437,7 @@ - + 24 @@ -451,7 +460,7 @@ false - + true @@ -482,6 +491,18 @@ + + + 0 + 0 + + + + + 0 + 22 + + Share @@ -489,6 +510,9 @@ :/images/share.png:/images/share.png + + false + true @@ -529,24 +553,6 @@ - - - 0 - 0 - - - - - 50 - 44 - - - - - 50 - 44 - - Qt::NoFocus @@ -561,24 +567,6 @@ - - - 0 - 0 - - - - - 50 - 44 - - - - - 50 - 44 - - Qt::NoFocus @@ -634,6 +622,9 @@ true + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + @@ -714,11 +705,16 @@ QLabel
gui/gxs/GxsIdLabel.h
+ + ClickableLabel + QLabel +
util/ClickableLabel.h
+
- - + + diff --git a/retroshare-gui/src/gui/Posted/PostedListWidget.cpp b/retroshare-gui/src/gui/Posted/PostedListWidget.cpp index 1373c6d9b..0d7960804 100644 --- a/retroshare-gui/src/gui/Posted/PostedListWidget.cpp +++ b/retroshare-gui/src/gui/Posted/PostedListWidget.cpp @@ -19,6 +19,7 @@ *******************************************************************************/ #include +#include #include "PostedListWidget.h" #include "ui_PostedListWidget.h" @@ -27,10 +28,12 @@ #include "gui/gxs/GxsIdDetails.h" #include "PostedCreatePostDialog.h" #include "PostedItem.h" +#include "PostedCardView.h" #include "gui/common/UIStateHelper.h" #include "gui/RetroShareLink.h" #include "util/HandleRichText.h" #include "util/DateTime.h" +#include "gui/settings/rsharesettings.h" #include #include "retroshare/rsgxscircles.h" @@ -42,6 +45,10 @@ #define TOPIC_DEFAULT_IMAGE ":/icons/png/posted.png" +/* View mode */ +#define VIEW_MODE_CLASSIC 1 +#define VIEW_MODE_CARD 2 + /** Constructor */ PostedListWidget::PostedListWidget(const RsGxsGroupId &postedId, QWidget *parent) : GxsMessageFramePostWidget(rsPosted, parent), @@ -64,6 +71,12 @@ PostedListWidget::PostedListWidget(const RsGxsGroupId &postedId, QWidget *parent connect(ui->comboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(getRankings(int))); + QSignalMapper *signalMapper = new QSignalMapper(this); + connect(ui->classicViewButton, SIGNAL(clicked()), signalMapper, SLOT(map())); + connect(ui->cardViewButton, SIGNAL(clicked()), signalMapper, SLOT(map())); + signalMapper->setMapping(ui->classicViewButton, VIEW_MODE_CLASSIC); + signalMapper->setMapping(ui->cardViewButton, VIEW_MODE_CARD); + connect(signalMapper, SIGNAL(mapped(int)), this, SLOT(setViewMode(int))); // default sort method. mSortMethod = RsPosted::HotRankType; @@ -103,17 +116,23 @@ PostedListWidget::~PostedListWidget() delete(ui); } -void PostedListWidget::processSettings(bool /*load*/) +void PostedListWidget::processSettings(bool load) { -// Settings->beginGroup(QString("PostedListWidget")); -// -// if (load) { -// // load settings -// } else { -// // save settings -// } -// -// Settings->endGroup(); + Settings->beginGroup(QString("PostedListWidget")); + + if (load) { + // load settings + + /* View mode */ + setViewMode(Settings->value("viewMode", VIEW_MODE_CLASSIC).toInt()); + } else { + // save settings + + /* View mode */ + Settings->setValue("viewMode", viewMode()); + } + + Settings->endGroup(); } QIcon PostedListWidget::groupIcon() @@ -143,6 +162,12 @@ QScrollArea *PostedListWidget::getScrollArea() return ui->scrollArea; } +// Overloaded from FeedHolder. +/*QScrollArea *PostedListWidget::getScrollArea() +{ + return ui->scrollAreaCardView; +}*/ + void PostedListWidget::deleteFeedItem(FeedItem *, uint32_t /*type*/) { #ifdef DEBUG_POSTED_LIST_WIDGET @@ -414,11 +439,23 @@ void PostedListWidget::loadPost(const RsPostedPost &post) PostedItem *item = new PostedItem(this, 0, dummyGroup, post, true, false); connect(item, SIGNAL(vote(RsGxsGrpMsgIdPair,bool)), this, SLOT(submitVote(RsGxsGrpMsgIdPair,bool))); mPosts.insert(post.mMeta.mMsgId, item); - //QLayout *alayout = ui.scrollAreaWidgetContents->layout(); - //alayout->addWidget(item); + mPostItems.push_back(item); } +void PostedListWidget::loadPostCardView(const RsPostedPost &post) +{ + /* Group is not always available because of the TokenQueue */ + RsPostedGroup dummyGroup; + dummyGroup.mMeta.mGroupId = groupId(); + + PostedCardView *cvitem = new PostedCardView(this, 0, dummyGroup, post, true, false); + connect(cvitem, SIGNAL(vote(RsGxsGrpMsgIdPair,bool)), this, SLOT(submitVote(RsGxsGrpMsgIdPair,bool))); + mCVPosts.insert(post.mMeta.mMsgId, cvitem); + + mPostCardView.push_back(cvitem); +} + static bool CmpPIHot(const GxsFeedItem *a, const GxsFeedItem *b) { const PostedItem *aa = dynamic_cast(a); @@ -471,6 +508,58 @@ static bool CmpPINew(const GxsFeedItem *a, const GxsFeedItem *b) return (aa->getPost().mNewScore > bb->getPost().mNewScore); } +static bool CVHot(const GxsFeedItem *a, const GxsFeedItem *b) +{ + const PostedCardView *aa = dynamic_cast(a); + const PostedCardView *bb = dynamic_cast(b); + + if (!aa || !bb) { + return true; + } + + const RsPostedPost &postA = aa->getPost(); + const RsPostedPost &postB = bb->getPost(); + + if (postA.mHotScore == postB.mHotScore) + { + return (postA.mNewScore > postB.mNewScore); + } + + return (postA.mHotScore > postB.mHotScore); +} + +static bool CVTop(const GxsFeedItem *a, const GxsFeedItem *b) +{ + const PostedCardView *aa = dynamic_cast(a); + const PostedCardView *bb = dynamic_cast(b); + + if (!aa || !bb) { + return true; + } + + const RsPostedPost &postA = aa->getPost(); + const RsPostedPost &postB = bb->getPost(); + + if (postA.mTopScore == postB.mTopScore) + { + return (postA.mNewScore > postB.mNewScore); + } + + return (postA.mTopScore > postB.mTopScore); +} + +static bool CVNew(const GxsFeedItem *a, const GxsFeedItem *b) +{ + const PostedCardView *aa = dynamic_cast(a); + const PostedCardView *bb = dynamic_cast(b); + + if (!aa || !bb) { + return true; + } + + return (aa->getPost().mNewScore > bb->getPost().mNewScore); +} + void PostedListWidget::applyRanking() { /* uses current settings to sort posts, then add to layout */ @@ -491,6 +580,7 @@ void PostedListWidget::applyRanking() std::cerr << std::endl; #endif qSort(mPostItems.begin(), mPostItems.end(), CmpPIHot); + qSort(mPostCardView.begin(), mPostCardView.end(), CVHot); break; case RsPosted::NewRankType: #ifdef DEBUG_POSTED_LIST_WIDGET @@ -498,6 +588,7 @@ void PostedListWidget::applyRanking() std::cerr << std::endl; #endif qSort(mPostItems.begin(), mPostItems.end(), CmpPINew); + qSort(mPostCardView.begin(), mPostCardView.end(), CVNew); break; case RsPosted::TopRankType: #ifdef DEBUG_POSTED_LIST_WIDGET @@ -505,6 +596,7 @@ void PostedListWidget::applyRanking() std::cerr << std::endl; #endif qSort(mPostItems.begin(), mPostItems.end(), CmpPITop); + qSort(mPostCardView.begin(), mPostCardView.end(), CVTop); break; } mLastSortMethod = mSortMethod; @@ -516,8 +608,11 @@ void PostedListWidget::applyRanking() /* go through list (skipping out-of-date items) to get */ QLayout *alayout = ui->scrollAreaWidgetContents->layout(); + + int counter = 0; time_t min_ts = 0; + foreach (PostedItem *item, mPostItems) { #ifdef DEBUG_POSTED_LIST_WIDGET @@ -564,6 +659,46 @@ void PostedListWidget::applyRanking() ++counter; } + // Card View + counter = 0; + QLayout *cviewlayout = ui->scrollAreaWidgetContentsCardView->layout(); + + foreach (PostedCardView *item, mPostCardView) + { + std::cerr << "PostedListWidget::applyRanking() Item: " << item; + std::cerr << std::endl; + + if (item->getPost().mMeta.mPublishTs < min_ts) + { + std::cerr << "\t Skipping OLD"; + std::cerr << std::endl; + item->hide(); + continue; + } + + if (counter >= mPostIndex + mPostShow) + { + std::cerr << "\t END - Counter too high"; + std::cerr << std::endl; + item->hide(); + } + else if (counter >= mPostIndex) + { + std::cerr << "\t Adding to Layout"; + std::cerr << std::endl; + /* add it in! */ + cviewlayout->addWidget(item); + item->show(); + } + else + { + std::cerr << "\t Skipping to Low"; + std::cerr << std::endl; + item->hide(); + } + ++counter; + } + #ifdef DEBUG_POSTED_LIST_WIDGET std::cerr << "PostedListWidget::applyRanking() Loaded New Order"; std::cerr << std::endl; @@ -571,6 +706,8 @@ void PostedListWidget::applyRanking() // trigger a redraw. ui->scrollAreaWidgetContents->update(); + ui->scrollAreaWidgetContentsCardView->update(); + } void PostedListWidget::blank() @@ -580,10 +717,17 @@ void PostedListWidget::blank() } void PostedListWidget::clearPosts() { - /* clear all messages */ + /* clear all classic view messages */ foreach (PostedItem *item, mPostItems) { delete(item); } + + /* clear all card view messages */ + foreach (PostedCardView *item, mPostCardView) { + delete(item); + } + + mPostCardView.clear(); mPostItems.clear(); mPosts.clear(); } @@ -641,6 +785,45 @@ void PostedListWidget::shallowClearPosts() PostedItem *item = *pit; alayout->removeWidget(item); } + + + //Posted Card view + + std::list postedCardViewItems; + std::list::iterator pcvit; + + QLayout *cviewlayout = ui->scrollAreaWidgetContentsCardView->layout(); + int countcv = cviewlayout->count(); + for(int i = 0; i < countcv; ++i) + { + QLayoutItem *litem = cviewlayout->itemAt(i); + if (!litem) + { + std::cerr << "PostedListWidget::shallowClearPosts() missing litem"; + std::cerr << std::endl; + continue; + } + + PostedCardView *item = dynamic_cast(litem->widget()); + if (item) + { + std::cerr << "PostedListWidget::shallowClearPosts() item: " << item; + std::cerr << std::endl; + + postedCardViewItems.push_back(item); + } + else + { + std::cerr << "PostedListWidget::shallowClearPosts() Found Child, which is not a PostedItem???"; + std::cerr << std::endl; + } + } + + for(pcvit = postedCardViewItems.begin(); pcvit != postedCardViewItems.end(); ++pcvit) + { + PostedCardView *item = *pcvit; + cviewlayout->removeWidget(item); + } } bool PostedListWidget::insertGroupData(const uint32_t &token, RsGroupMetaData &metaData) @@ -668,6 +851,7 @@ void PostedListWidget::insertAllPosts(const uint32_t &token, GxsMessageFramePost { RsPostedPost& p = *vit; loadPost(p); + loadPostCardView(p); } applyRanking(); @@ -701,6 +885,7 @@ void PostedListWidget::insertPosts(const uint32_t &token) #endif /* insert new entry */ loadPost(p); + loadPostCardView(p); } } @@ -767,3 +952,39 @@ void PostedListWidget::loadRequest(const TokenQueue *queue, const TokenRequest & GxsMessageFramePostWidget::loadRequest(queue, req); } + +int PostedListWidget::viewMode() +{ + if (ui->classicViewButton->isChecked()) { + return VIEW_MODE_CLASSIC; + } else if (ui->cardViewButton->isChecked()) { + return VIEW_MODE_CARD; + } + + /* Default */ + return VIEW_MODE_CLASSIC; +} + +void PostedListWidget::setViewMode(int viewMode) +{ + switch (viewMode) { + case VIEW_MODE_CLASSIC: + ui->stackedWidget->setCurrentIndex(0); + + + ui->classicViewButton->setChecked(true); + ui->cardViewButton->setChecked(false); + + break; + case VIEW_MODE_CARD: + ui->stackedWidget->setCurrentIndex(1); + + ui->cardViewButton->setChecked(true); + ui->classicViewButton->setChecked(false); + + break; + default: + setViewMode(VIEW_MODE_CLASSIC); + return; + } +} diff --git a/retroshare-gui/src/gui/Posted/PostedListWidget.h b/retroshare-gui/src/gui/Posted/PostedListWidget.h index cf05e9d4c..fa9f25c8e 100644 --- a/retroshare-gui/src/gui/Posted/PostedListWidget.h +++ b/retroshare-gui/src/gui/Posted/PostedListWidget.h @@ -29,6 +29,7 @@ class RsPostedGroup; class RsPostedPost; class PostedItem; +class PostedCardView; namespace Ui { class PostedListWidget; @@ -79,16 +80,21 @@ private slots: void showNext(); void showPrev(); + void setViewMode(int viewMode); + private: void processSettings(bool load); void updateShowText(); + int viewMode(); + /*! * Only removes it from layout */ void shallowClearPosts(); void loadPost(const RsPostedPost &post); + void loadPostCardView(const RsPostedPost &post); void insertPostedDetails(const RsPostedGroup &group); @@ -115,6 +121,9 @@ private: QMap mPosts; QList mPostItems; + QMap mCVPosts; + QList mPostCardView; + /* UI - from Designer */ Ui::PostedListWidget *ui; }; diff --git a/retroshare-gui/src/gui/Posted/PostedListWidget.ui b/retroshare-gui/src/gui/Posted/PostedListWidget.ui index cb9b906ef..304a79c76 100644 --- a/retroshare-gui/src/gui/Posted/PostedListWidget.ui +++ b/retroshare-gui/src/gui/Posted/PostedListWidget.ui @@ -7,16 +7,13 @@ 0 0 616 - 428 + 595 Form - - - 3 - + 0 @@ -29,7 +26,10 @@ 0 - + + 0 + + QFrame::Box @@ -37,12 +37,9 @@ QFrame::Sunken - - - 6 - + - 4 + 6 2 @@ -164,6 +161,59 @@ + + + + 0 + + + + + Classic view + + + + :/images/classic.png:/images/classic.png + + + + 24 + 24 + + + + true + + + true + + + + + + + Card View + + + + :/images/card.png:/images/card.png + + + + 24 + 24 + + + + true + + + true + + + + + @@ -205,7 +255,7 @@ - + QFrame::StyledPanel @@ -478,30 +528,13 @@ p, li { white-space: pre-wrap; } - - - - true + + + + 1 - - - - 0 - 0 - 614 - 16 - - - - - 0 - 0 - - - - - 0 - + + 0 @@ -514,6 +547,102 @@ p, li { white-space: pre-wrap; } 0 + + + + true + + + + + 0 + 0 + 614 + 16 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + + 0 + 0 + 614 + 16 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + diff --git a/retroshare-gui/src/gui/Posted/Posted_images.qrc b/retroshare-gui/src/gui/Posted/Posted_images.qrc index 8c713b53f..9c2be0db4 100644 --- a/retroshare-gui/src/gui/Posted/Posted_images.qrc +++ b/retroshare-gui/src/gui/Posted/Posted_images.qrc @@ -1,6 +1,6 @@ - images/posted_16.png + images/posted_16.png images/posted_24.png images/posted_32.png images/posted_48.png @@ -21,7 +21,13 @@ images/share.png images/notes.png images/link.png + images/linkext.png images/post.png images/photo.png + images/classic.png + images/card.png + images/down-hover.png + images/up-hover.png + images/trashcan.png diff --git a/retroshare-gui/src/gui/Posted/images/card.png b/retroshare-gui/src/gui/Posted/images/card.png new file mode 100644 index 000000000..47cc72d0a Binary files /dev/null and b/retroshare-gui/src/gui/Posted/images/card.png differ diff --git a/retroshare-gui/src/gui/Posted/images/classic.png b/retroshare-gui/src/gui/Posted/images/classic.png new file mode 100644 index 000000000..b85e0a119 Binary files /dev/null and b/retroshare-gui/src/gui/Posted/images/classic.png differ diff --git a/retroshare-gui/src/gui/Posted/images/down-arrow.png b/retroshare-gui/src/gui/Posted/images/down-arrow.png index b6c7fcc06..1e531f03d 100644 Binary files a/retroshare-gui/src/gui/Posted/images/down-arrow.png and b/retroshare-gui/src/gui/Posted/images/down-arrow.png differ diff --git a/retroshare-gui/src/gui/Posted/images/down-hover.png b/retroshare-gui/src/gui/Posted/images/down-hover.png new file mode 100644 index 000000000..b822a0d87 Binary files /dev/null and b/retroshare-gui/src/gui/Posted/images/down-hover.png differ diff --git a/retroshare-gui/src/gui/Posted/images/linkext.png b/retroshare-gui/src/gui/Posted/images/linkext.png new file mode 100644 index 000000000..8fa5953a0 Binary files /dev/null and b/retroshare-gui/src/gui/Posted/images/linkext.png differ diff --git a/retroshare-gui/src/gui/Posted/images/notes.png b/retroshare-gui/src/gui/Posted/images/notes.png index a73a03968..800eec492 100644 Binary files a/retroshare-gui/src/gui/Posted/images/notes.png and b/retroshare-gui/src/gui/Posted/images/notes.png differ diff --git a/retroshare-gui/src/gui/Posted/images/trashcan.png b/retroshare-gui/src/gui/Posted/images/trashcan.png new file mode 100644 index 000000000..d812bcfe5 Binary files /dev/null and b/retroshare-gui/src/gui/Posted/images/trashcan.png differ diff --git a/retroshare-gui/src/gui/Posted/images/up-arrow.png b/retroshare-gui/src/gui/Posted/images/up-arrow.png index 728ccf068..7f276af8f 100644 Binary files a/retroshare-gui/src/gui/Posted/images/up-arrow.png and b/retroshare-gui/src/gui/Posted/images/up-arrow.png differ diff --git a/retroshare-gui/src/gui/Posted/images/up-hover.png b/retroshare-gui/src/gui/Posted/images/up-hover.png new file mode 100644 index 000000000..6e1144f96 Binary files /dev/null and b/retroshare-gui/src/gui/Posted/images/up-hover.png differ diff --git a/retroshare-gui/src/gui/qss/stylesheet/Standard.qss b/retroshare-gui/src/gui/qss/stylesheet/Standard.qss index 282f4917f..3e9a063cf 100644 --- a/retroshare-gui/src/gui/qss/stylesheet/Standard.qss +++ b/retroshare-gui/src/gui/qss/stylesheet/Standard.qss @@ -396,11 +396,6 @@ GxsChannelPostItem QLabel#newLabel { border-radius: 3px; } -GxsChannelPostItem QFrame#msgFrame { - border: 2px solid #238; - border-radius: 10px; -} - GxsChannelPostItem QLabel#logoLabel { border: 2px solid #D3D3D3; } @@ -430,12 +425,6 @@ ForumMsgItem QFrame#prevFrame { border-radius: 10px; } -SubFileItem > QFrame#frame { - border: 2px solid #238; - background: white; - border-radius: 10px; -} - SubFileItem QProgressBar#progressBar { border: 1px solid black; text-align: center; @@ -857,6 +846,22 @@ PostedItem QLabel#fromBoldLabel, QLabel#fromLabel, QLabel#dateLabel, QLabel#site color: #787c7e; } +PostedItem QLabel#newLabel { + border: 1px solid #167BE7; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #2291E0, stop: 1 #3EB3FF); + border-radius: 3px; +} + +PostedCardView QLabel#newLabel { + border: 1px solid #167BE7; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #2291E0, stop: 1 #3EB3FF); + border-radius: 3px; +} + +PostedCardView QFrame#voteFrame { + background: #f8f9fa; +} + GxsCommentDialog QComboBox#sortBox { font: bold; color: #0099cc; diff --git a/retroshare-gui/src/gui/settings/JsonApiPage.cc b/retroshare-gui/src/gui/settings/JsonApiPage.cc index 6c6dee018..5f1bb225f 100644 --- a/retroshare-gui/src/gui/settings/JsonApiPage.cc +++ b/retroshare-gui/src/gui/settings/JsonApiPage.cc @@ -112,28 +112,41 @@ QString JsonApiPage::helpText() const { return ""; } bool JsonApiPage::checkStartJsonApi() { - if(!Settings->getJsonApiEnabled()) - return false; + if(!Settings->getJsonApiEnabled()) return false; rsJsonApi->setListeningPort(Settings->getJsonApiPort()); rsJsonApi->setBindingAddress(Settings->getJsonApiListenAddress().toStdString()); - rsJsonApi->restart(); + + const auto rErr = rsJsonApi->restart(); + if(rErr == RsJsonApiErrorNum::NOT_A_MACHINE_GUN) + { + RsDbg() << __PRETTY_FUNCTION__ << " apparently the user is attempting " + << "to restart JSON API service in a burst. Re-scheduling " + << "restart in a while..." << std::endl; + RsThread::async([]() + { + std::this_thread::sleep_for(std::chrono::seconds(10)); + rsJsonApi->restart(); + }); + } + else if(rErr) + { + RsErr() << __PRETTY_FUNCTION__ << rErr << std::endl; + return false; + } return true; } /*static*/ void JsonApiPage::checkShutdownJsonApi() { - if(!rsJsonApi->isRunning()) return; rsJsonApi->fullstop(); // this is a blocks until the thread is terminated. } void JsonApiPage::onApplyClicked() { - // restart - - checkShutdownJsonApi(); - checkStartJsonApi(); + // restart + checkStartJsonApi(); } void JsonApiPage::checkToken(QString s) diff --git a/retroshare-gui/src/qss/qdarkstyle-v2.qss b/retroshare-gui/src/qss/qdarkstyle-v2.qss index 90324f9ab..c9e951921 100644 --- a/retroshare-gui/src/qss/qdarkstyle-v2.qss +++ b/retroshare-gui/src/qss/qdarkstyle-v2.qss @@ -934,6 +934,7 @@ QPushButton::menu-indicator { subcontrol-origin: padding; subcontrol-position: bottom right; bottom: 4px; + } QPushButton:pressed { @@ -1836,6 +1837,7 @@ QToolBox QScrollArea QWidget QWidget { QFrame { border-radius: 4px; border: 1px solid #32414B; + } QFrame[frameShape="0"] { @@ -2045,4 +2047,95 @@ GxsChannelPostsWidget QToolButton#subscribeToolButton::menu-arrow { GxsChannelPostsWidget QToolButton#subscribeToolButton::menu-button { image: none; -} \ No newline at end of file +} + +QTabBar#smTab::tab{ + height: 32px; + width: 32px; +} + +PostedCreatePostDialog QPushButton#submitButton { + font: bold; + font-size: 15px; + color: white; + background: #0099cc; + border-radius: 4px; + min-width: 2em; + +} + +PostedCreatePostDialog QPushButton#submitButton:hover { + background: #03b1f3; + border-radius: 4px; + min-width: 2em; + +} + +PostedItem QFrame#mainFrame { + border-radius: 4px; + border: 1px solid #32414B; + background-color: #19232D; + +} + +GxsChannelPostItem QFrame#mainFrame { + border-radius: 4px; + border: 1px solid #32414B; + background-color: #19232D; + +} + +PostedItem QPushButton#shareButton +{ + background-color: transparent; + min-width: 80px; + max-height: 22px; + +} + +PostedItem QLabel#scoreLabel +{ + background-color: transparent; + +} + +PostedItem QFrame#voteFrame { + background: #141415; +} + +PostedItem QToolButton#voteDownButton, QToolButton#voteUpButton +{ + border: none; + +} + +PostedItem QLabel#thumbnailLabel{ + border: 2px solid #CCCCCC; + border-radius: 3px; +} + +PostedCardView QPushButton#shareButton +{ + background-color: transparent; + min-width: 80px; + max-height: 22px; + +} + +PostedCardView QFrame#voteFrame { + background: #141415; +} + +PostedCardView QFrame#mainFrame { + + background-color: #19232D; + +} + +PostedCardView QFrame#mainFrame [new=false]{ + background: #19232D; +} + +PostedCardView > QFrame#mainFrame[new=true] { + background-color: #005000; +} diff --git a/retroshare-gui/src/qss/qdarkstyle.qss b/retroshare-gui/src/qss/qdarkstyle.qss index 3e0927975..ef8495956 100644 --- a/retroshare-gui/src/qss/qdarkstyle.qss +++ b/retroshare-gui/src/qss/qdarkstyle.qss @@ -996,7 +996,7 @@ QToolButton::menu-arrow:open { QPushButton::menu-indicator { subcontrol-origin: padding; subcontrol-position: bottom right; - left: 8px; + } QTableView @@ -1209,3 +1209,85 @@ GxsChannelPostsWidget QToolButton#subscribeToolButton::menu-button { image: none; } + +QTabBar#smTab::tab{ + height: 32px; + width: 32px; +} + +PostedCreatePostDialog QPushButton#submitButton { + font: bold; + font-size: 15px; + color: white; + background: #0099cc; + border-radius: 4px; + min-width: 2em; + +} + +PostedCreatePostDialog QPushButton#submitButton:hover { + background: #03b1f3; + border-radius: 4px; + min-width: 2em; + +} + +GxsForumThreadWidget QLabel#forumName +{ + qproperty-fontSizeFactor: 140; + color: #0099cc; + font-size: 15px; + font: bold; +} + +PostedItem QPushButton#shareButton +{ + background-color: transparent; + border: none; + min-width: 75px; + max-height: 22px; +} + +PostedCardView QPushButton#shareButton +{ + background-color: transparent; + border: none; + min-width: 75px; +} + +PostedItem QFrame#voteFrame { + background: #141415; +} + +PostedCardView QFrame#voteFrame { + background: #141415; +} + +QPushButton#shareButton:hover, QPushButton#shareButton::menu-button:hover { + background-color: #4A4949; + border: 1px solid gray; +} + +PostedItem QToolButton#voteDownButton, QToolButton#voteUpButton, QToolButton#expandButton, QToolButton#readButton, +QToolButton#commentButton, QToolButton#notesButton +{ + border: none; +} + +PostedItem QLabel#thumbnailLabel{ + border: 2px solid #CCCCCC; + border-radius: 3px; +} + +PostedCardView QToolButton#voteDownButton, QToolButton#voteUpButton +{ + border: none; +} + +PostedCardView QFrame#mainFrame [new=false]{ + background: #302F2F; +} + +PostedCardView > QFrame#mainFrame[new=true] { + background-color: #005000; +} diff --git a/retroshare-gui/src/retroshare-gui.pro b/retroshare-gui/src/retroshare-gui.pro index 8f49a1525..59f1cb458 100644 --- a/retroshare-gui/src/retroshare-gui.pro +++ b/retroshare-gui/src/retroshare-gui.pro @@ -469,6 +469,8 @@ HEADERS += rshare.h \ util/QtVersion.h \ util/RsFile.h \ util/qtthreadsutils.h \ + util/ClickableLabel.h \ + util/AspectRatioPixmapLabel.h \ gui/profile/ProfileWidget.h \ gui/profile/ProfileManager.h \ gui/profile/StatusMessage.h \ @@ -828,6 +830,8 @@ SOURCES += main.cpp \ util/ObjectPainter.cpp \ util/RsFile.cpp \ util/RichTextEdit.cpp \ + util/ClickableLabel.cpp \ + util/AspectRatioPixmapLabel.cpp \ gui/profile/ProfileWidget.cpp \ gui/profile/StatusMessage.cpp \ gui/profile/ProfileManager.cpp \ @@ -1344,9 +1348,11 @@ posted { HEADERS += gui/Posted/PostedDialog.h \ gui/Posted/PostedListWidget.h \ gui/Posted/PostedItem.h \ + gui/Posted/PostedCardView.h \ gui/Posted/PostedGroupDialog.h \ gui/feeds/PostedGroupItem.h \ gui/Posted/PostedCreatePostDialog.h \ + gui/Posted/PhotoView.h \ gui/Posted/PostedUserNotify.h #gui/Posted/PostedCreateCommentDialog.h \ @@ -1355,8 +1361,9 @@ posted { FORMS += gui/Posted/PostedListWidget.ui \ gui/feeds/PostedGroupItem.ui \ gui/Posted/PostedItem.ui \ + gui/Posted/PostedCardView.ui \ gui/Posted/PostedCreatePostDialog.ui \ - + gui/Posted/PhotoView.ui #gui/Posted/PostedDialog.ui \ #gui/Posted/PostedComments.ui \ #gui/Posted/PostedCreateCommentDialog.ui @@ -1365,8 +1372,10 @@ posted { gui/Posted/PostedListWidget.cpp \ gui/feeds/PostedGroupItem.cpp \ gui/Posted/PostedItem.cpp \ + gui/Posted/PostedCardView.cpp \ gui/Posted/PostedGroupDialog.cpp \ gui/Posted/PostedCreatePostDialog.cpp \ + gui/Posted/PhotoView.cpp \ gui/Posted/PostedUserNotify.cpp #gui/Posted/PostedDialog.cpp \ diff --git a/retroshare-gui/src/util/AspectRatioPixmapLabel.cpp b/retroshare-gui/src/util/AspectRatioPixmapLabel.cpp new file mode 100644 index 000000000..08ecf18c6 --- /dev/null +++ b/retroshare-gui/src/util/AspectRatioPixmapLabel.cpp @@ -0,0 +1,59 @@ +/******************************************************************************* + * retroshare-gui/src/util/AspectRatioPixmapLabel.cpp * + * * + * Copyright (C) 2019 Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "AspectRatioPixmapLabel.h" +#include + +AspectRatioPixmapLabel::AspectRatioPixmapLabel(QWidget *parent) : + QLabel(parent) +{ + this->setMinimumSize(1,1); + setScaledContents(false); +} + +void AspectRatioPixmapLabel::setPixmap ( const QPixmap & p) +{ + pix = p; + QLabel::setPixmap(pix); + //std::cout << "Information size: " << pix.width() << 'x' << pix.height() << std::endl; +} + +int AspectRatioPixmapLabel::heightForWidth( int width ) const +{ + return pix.isNull() ? this->height() : ((qreal)pix.height()*width)/pix.width(); +} + +QSize AspectRatioPixmapLabel::sizeHint() const +{ + return QSize(pix.width(), pix.height()); +} + +QPixmap AspectRatioPixmapLabel::scaledPixmap() const +{ + return pix.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); +} + +void AspectRatioPixmapLabel::resizeEvent(QResizeEvent * e) +{ + if(!pix.isNull()) + QLabel::setPixmap(scaledPixmap()); + QLabel::resizeEvent(e); + //std::cout << "Information resized: " << e->oldSize().width() << 'x' << e->oldSize().height() << " to " << e->size().width() << 'x' << e->size().height() << std::endl; +} diff --git a/retroshare-gui/src/util/AspectRatioPixmapLabel.h b/retroshare-gui/src/util/AspectRatioPixmapLabel.h new file mode 100644 index 000000000..ad7c40e4c --- /dev/null +++ b/retroshare-gui/src/util/AspectRatioPixmapLabel.h @@ -0,0 +1,44 @@ +/******************************************************************************* + * retroshare-gui/src/util/AspectRatioPixmapLabel.h * + * * + * Copyright (C) 2019 Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef ASPECTRATIOPIXMAPLABEL_H +#define ASPECTRATIOPIXMAPLABEL_H + +#include +#include +#include + +class AspectRatioPixmapLabel : public QLabel +{ + Q_OBJECT +public: + explicit AspectRatioPixmapLabel(QWidget *parent = nullptr); + virtual int heightForWidth( int width ) const override; + virtual QSize sizeHint() const override; + QPixmap scaledPixmap() const; +public slots: + void setPixmap ( const QPixmap & ); +protected: + void resizeEvent(QResizeEvent *event) override; +private: + QPixmap pix; +}; + +#endif // ASPECTRATIOPIXMAPLABEL_H diff --git a/retroshare-gui/src/util/ClickableLabel.cpp b/retroshare-gui/src/util/ClickableLabel.cpp new file mode 100644 index 000000000..7b7ccb5fb --- /dev/null +++ b/retroshare-gui/src/util/ClickableLabel.cpp @@ -0,0 +1,35 @@ +/******************************************************************************* + * retroshare-gui/src/util/ClickableLabel.cpp * + * * + * Copyright (C) 2020 by RetroShare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include "ClickableLabel.h" + +/** Constructor */ +ClickableLabel::ClickableLabel(QWidget* parent, Qt::WindowFlags f) + : QLabel(parent) { + +} + +ClickableLabel::~ClickableLabel() { + +} + +void ClickableLabel::mousePressEvent(QMouseEvent* event) { + emit clicked(); +} \ No newline at end of file diff --git a/retroshare-gui/src/util/ClickableLabel.h b/retroshare-gui/src/util/ClickableLabel.h new file mode 100644 index 000000000..ec1c66b8d --- /dev/null +++ b/retroshare-gui/src/util/ClickableLabel.h @@ -0,0 +1,47 @@ +/******************************************************************************* + * retroshare-gui/src/util/ClickableLabel.h * + * * + * Copyright (C) 2020 by RetroShare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#ifndef CLICKABLELABEL_H +#define CLICKABLELABEL_H + +#include +#include +#include + +class ClickableLabel : public QLabel { + Q_OBJECT + +public: + explicit ClickableLabel(QWidget* parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); + ~ClickableLabel(); + +signals: + void clicked(); + +protected: + void mousePressEvent(QMouseEvent* event); + + void enterEvent(QEvent *ev) override { setStyleSheet("QLabel { border: 1px solid #3A3939; }");} + + void leaveEvent(QEvent *ev) override { setStyleSheet("QLabel { border: 2px solid #CCCCCC; border-radius: 3px; }");} + +}; + +#endif // CLICKABLELABEL_H \ No newline at end of file diff --git a/retroshare-gui/src/util/HandleRichText.cpp b/retroshare-gui/src/util/HandleRichText.cpp index 7818c984d..c500a6bb9 100644 --- a/retroshare-gui/src/util/HandleRichText.cpp +++ b/retroshare-gui/src/util/HandleRichText.cpp @@ -1185,7 +1185,7 @@ bool RsHtml::makeEmbeddedImage(const QImage &originalImage, QString &embeddedIma { rstime::RsScopeTimer s("Embed image"); QImage opt; - return ImageUtil::optimizeSize(embeddedImage, originalImage, opt, maxPixels, maxBytes); + return ImageUtil::optimizeSizeHtml(embeddedImage, originalImage, opt, maxPixels, maxBytes); } QString RsHtml::plainText(const QString &text) diff --git a/retroshare-gui/src/util/RichTextEdit.cpp b/retroshare-gui/src/util/RichTextEdit.cpp index 541e6c3eb..613aa1c19 100644 --- a/retroshare-gui/src/util/RichTextEdit.cpp +++ b/retroshare-gui/src/util/RichTextEdit.cpp @@ -585,7 +585,7 @@ void RichTextEdit::setText(const QString& text) { void RichTextEdit::insertImage() { QString file; - if (misc::getOpenFileName(window(), RshareSettings::LASTDIR_IMAGES, tr("Load Picture File"), "Pictures (*.png *.xpm *.jpg *.jpeg)", file)) { + if (misc::getOpenFileName(window(), RshareSettings::LASTDIR_IMAGES, tr("Load Picture File"), "Pictures (*.png *.xpm *.jpg *.jpeg *.gif *.webp)", file)) { QString encodedImage; if (RsHtml::makeEmbeddedImage(file, encodedImage, 640*480, MAX_ALLOWED_GXS_MESSAGE_SIZE - 200)) { QTextDocumentFragment fragment = QTextDocumentFragment::fromHtml(encodedImage); diff --git a/retroshare-gui/src/util/imageutil.cpp b/retroshare-gui/src/util/imageutil.cpp index c507bfa05..47029505e 100644 --- a/retroshare-gui/src/util/imageutil.cpp +++ b/retroshare-gui/src/util/imageutil.cpp @@ -68,12 +68,12 @@ void ImageUtil::extractImage(QWidget *window, QTextCursor cursor, QString file) } } -bool ImageUtil::optimizeSize(QString &html, const QImage& original, QImage &optimized, int maxPixels, int maxBytes) +bool ImageUtil::optimizeSizeBytes(QByteArray &bytearray, const QImage &original, QImage &optimized, int maxPixels, int maxBytes) { //nothing to do if it fits into the limits optimized = original; if ((maxPixels <= 0) || (optimized.width()*optimized.height() <= maxPixels)) { - int s = checkSize(html, optimized, maxBytes); + int s = checkSize(bytearray, optimized); if((maxBytes <= 0) || (s <= maxBytes)) { return true; } @@ -92,7 +92,7 @@ bool ImageUtil::optimizeSize(QString &html, const QImage& original, QImage &opti //if maxBytes not defined, do not reduce color space, just downscale if(maxBytes <= 0) { - checkSize(html, optimized = original.scaledToWidth(maxwidth, Qt::SmoothTransformation), maxBytes); + checkSize(bytearray, optimized = original.scaledToWidth(maxwidth, Qt::SmoothTransformation)); return true; } @@ -100,9 +100,9 @@ bool ImageUtil::optimizeSize(QString &html, const QImage& original, QImage &opti quantization(original, ct); //Use binary search to find a suitable image size + linear regression to guess the file size - double maxsize = (double)checkSize(html, optimized = original.scaledToWidth(maxwidth, Qt::SmoothTransformation).convertToFormat(QImage::Format_Indexed8, ct, Qt::ThresholdDither), maxBytes); + double maxsize = (double)checkSize(bytearray, optimized = original.scaledToWidth(maxwidth, Qt::SmoothTransformation).convertToFormat(QImage::Format_Indexed8, ct, Qt::ThresholdDither)); if(maxsize <= maxBytes) return true; //success - double minsize = (double)checkSize(html, optimized = original.scaledToWidth(minwidth, Qt::SmoothTransformation).convertToFormat(QImage::Format_Indexed8, ct, Qt::ThresholdDither), maxBytes); + double minsize = (double)checkSize(bytearray, optimized = original.scaledToWidth(minwidth, Qt::SmoothTransformation).convertToFormat(QImage::Format_Indexed8, ct, Qt::ThresholdDither)); if(minsize > maxBytes) return false; //impossible // std::cout << "maxS: " << maxsize << " minS: " << minsize << std::endl; @@ -115,7 +115,7 @@ bool ImageUtil::optimizeSize(QString &html, const QImage& original, QImage &opti double b = maxsize - m * ((double)maxwidth * (double)maxwidth / whratio); double a = ((double)(maxBytes - region/2) - b) / m; //maxBytes - region/2 target the center of the accepted region int nextwidth = (int)sqrt(a * whratio); - int nextsize = checkSize(html, optimized = original.scaledToWidth(nextwidth, Qt::SmoothTransformation).convertToFormat(QImage::Format_Indexed8, ct, Qt::ThresholdDither), maxBytes); + int nextsize = checkSize(bytearray, optimized = original.scaledToWidth(nextwidth, Qt::SmoothTransformation).convertToFormat(QImage::Format_Indexed8, ct, Qt::ThresholdDither)); if(nextsize <= maxBytes) { minsize = nextsize; minwidth = nextwidth; @@ -137,34 +137,41 @@ bool ImageUtil::optimizeSize(QString &html, const QImage& original, QImage &opti //std::cout << html.toStdString() << std::endl; } -int ImageUtil::checkSize(QString &embeddedImage, const QImage &img, int maxBytes) +bool ImageUtil::optimizeSizeHtml(QString &html, const QImage& original, QImage &optimized, int maxPixels, int maxBytes) +{ + QByteArray bytearray; + if(maxBytes > 0){ + maxBytes = maxBytes * 3/4 - 50; //base64 and html stuff + if(maxBytes < 1) maxBytes = 1; + } + + if(optimizeSizeBytes(bytearray, original, optimized, maxPixels, maxBytes)) + { + QByteArray encodedByteArray = bytearray.toBase64(); + html = ""); + return true; + } + return false; +} + +int ImageUtil::checkSize(QByteArray &bytearray, const QImage &img) { rstime::RsScopeTimer st("Check size"); - QByteArray bytearray; + bytearray.clear(); QBuffer buffer(&bytearray); int size = 0; //std::cout << QString("Trying image: format PNG, size %1x%2, colors %3\n").arg(img.width()).arg(img.height()).arg(img.colorCount()).toStdString(); if (buffer.open(QIODevice::WriteOnly)) { if (img.save(&buffer, "PNG", 0)) { - size = bytearray.length() * 4/3; - if((maxBytes > 0) && (size > maxBytes)) // *4/3 for base64 - { - //std::cout << QString("\tToo large, size: %1, limit: %2 bytes\n").arg(bytearray.length() * 4/3).arg(maxBytes).toStdString(); - }else{ - //std::cout << QString("\tOK, size: %1, limit: %2 bytes\n").arg(bytearray.length() * 4/3).arg(maxBytes).toStdString(); - QByteArray encodedByteArray = bytearray.toBase64(); - //embeddedImage = ""); - } + size = bytearray.length(); } else { std::cerr << "ImageUtil: image can't be saved to buffer" << std::endl; } buffer.close(); - bytearray.clear(); } else { std::cerr << "ImageUtil: buffer can't be opened" << std::endl; } diff --git a/retroshare-gui/src/util/imageutil.h b/retroshare-gui/src/util/imageutil.h index a6ff19b88..1a9658e13 100644 --- a/retroshare-gui/src/util/imageutil.h +++ b/retroshare-gui/src/util/imageutil.h @@ -23,6 +23,7 @@ #include #include +#include #include class ImageUtil @@ -31,10 +32,11 @@ public: ImageUtil(); static void extractImage(QWidget *window, QTextCursor cursor, QString file = ""); - static bool optimizeSize(QString &html, const QImage& original, QImage &optimized, int maxPixels = -1, int maxBytes = -1); + static bool optimizeSizeHtml(QString &html, const QImage& original, QImage &optimized, int maxPixels = -1, int maxBytes = -1); + static bool optimizeSizeBytes(QByteArray &bytearray, const QImage& original, QImage &optimized, int maxPixels = -1, int maxBytes = -1); private: - static int checkSize(QString& embeddedImage, const QImage& img, int maxBytes = -1); + static int checkSize(QByteArray& embeddedImage, const QImage& img); static void quantization(const QImage& img, QVector& palette); static void quantization(QList::iterator begin, QList::iterator end, int depth, QVector& palette); static void avgbucket(QList::iterator begin, QList::iterator end, QVector& palette);