diff --git a/CMakeLists.txt b/CMakeLists.txt index a476d3338ab55ee8781c601bf90a5cf94248f71e..196012a773d0becc10c0b5864e0eb3fc7ccd0c46 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -399,6 +399,7 @@ SET(libringclient_interface_LIB_HDRS SET( libringclient_extra_LIB_HDRS src/typedefs.h + src/containerview.h ) IF(${ENABLE_LIBWRAP} MATCHES true OR ${ENABLE_TEST} MATCHES true) diff --git a/src/api/conversation.h b/src/api/conversation.h index f667e8c2a0f26faae21a79d1059fb03f044a8e17..4db77d12dd0004ae7af8b5762d361371d1bd7526 100644 --- a/src/api/conversation.h +++ b/src/api/conversation.h @@ -32,6 +32,12 @@ namespace conversation { struct Info { + Info() = default; + Info(const Info& other) = delete; + Info(Info&& other) = default; + Info& operator=(const Info& other) = delete; + Info& operator=(Info&& other) = default; + QString uid = ""; QString accountId; VectorString participants; diff --git a/src/api/conversationmodel.h b/src/api/conversationmodel.h index 4d3e114e91bd3fb99c1cba32cfd9aae039d9c972..9f02730f87f729938aee70a127a86f9902371486 100644 --- a/src/api/conversationmodel.h +++ b/src/api/conversationmodel.h @@ -23,6 +23,7 @@ #include "api/conversation.h" #include "api/profile.h" #include "api/datatransfer.h" +#include "containerview.h" #include <QObject> #include <QVector> @@ -30,7 +31,6 @@ #include <memory> #include <deque> -#include <optional> namespace lrc { @@ -56,9 +56,6 @@ class NewAccountModel; enum class ConferenceableItem { CALL, CONTACT }; Q_ENUM_NS(ConferenceableItem) -template<typename T> -using OptRef = std::optional<std::reference_wrapper<T>>; - struct AccountConversation { QString convId; @@ -70,7 +67,6 @@ struct AccountConversation * for calls and contacts contain only one element * for conferences contains multiple entries */ - typedef QVector<QVector<AccountConversation>> ConferenceableValue; /** @@ -81,6 +77,7 @@ class LIB_EXPORT ConversationModel : public QObject Q_OBJECT public: using ConversationQueue = std::deque<conversation::Info>; + using ConversationQueueProxy = ContainerView<ConversationQueue>; const account::Info& owner; @@ -95,7 +92,7 @@ public: * Get conversations which should be shown client side * @return conversations filtered with the current filter */ - const ConversationQueue& allFilteredConversations() const; + const ConversationQueueProxy& allFilteredConversations() const; /** * Get conversation for a given uid @@ -130,7 +127,7 @@ public: * Get a custom filtered set of conversations * @return conversations filtered */ - const ConversationQueue& getFilteredConversations( + const ConversationQueueProxy& getFilteredConversations( const profile::Type& filter = profile::Type::INVALID, bool forceUpdate = false, const bool includeBanned = false) const; @@ -139,7 +136,7 @@ public: * @param row * @return a copy of the conversation */ - conversation::Info filteredConversation(unsigned int row) const; + OptRef<conversation::Info> filteredConversation(unsigned row) const; /** * Get the search results @@ -152,7 +149,7 @@ public: * @param row * @return a copy of the conversation */ - conversation::Info searchResultForRow(unsigned int row) const; + OptRef<conversation::Info> searchResultForRow(unsigned row) const; /** * Update the searchResults @@ -322,9 +319,9 @@ Q_SIGNALS: */ void conversationUpdated(const QString& uid) const; /** - * Emitted when conversations are sorted by last interaction + * Emitted when the conversations list is modified */ - void modelSorted() const; + void modelChanged() const; /** * Emitted when filter has changed */ diff --git a/src/containerview.h b/src/containerview.h new file mode 100644 index 0000000000000000000000000000000000000000..3ce32bb2acf0c292bd1d317fb44a1788772d0e82 --- /dev/null +++ b/src/containerview.h @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2020 by Savoir-faire Linux + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "typedefs.h" + +#include <algorithm> +#include <functional> +#include <iostream> +#include <iterator> +#include <optional> +#include <type_traits> + +template<class T> +using OptRef = typename std::optional<std::reference_wrapper<T>>; + +// Some SFINAE helpers to clarify what happened when someone tries +// to make a ContainerView<float> or something wacky like that. +namespace detail { +template<typename... Ts> +struct has_defs +{}; +template<typename T, typename _ = void> +struct is_container : std::false_type +{}; +template<typename T> +struct is_container<T, + std::conditional_t<false, + has_defs<decltype(std::declval<T>().front()), + decltype(std::declval<T>().begin()), + decltype(std::declval<T>().end())>, + void>> : public std::true_type +{}; +} // namespace detail + +// Extra compile-time check to clarify why compilation has failed. +// Otherwise, the error message will be "'x' uses undefined struct ContainerView<decltype(x), void>" +template<class BaseType, class Enable = void> +struct ContainerView +{ + static_assert(detail::is_container<BaseType>::value == true, + "Template parameter is probably not a container!"); +}; + +template<class BaseType> +struct ContainerView<BaseType, std::enable_if_t<detail::is_container<BaseType>::value>> +{ +private: + // Form a type by rebinding the underlying container type to a reference + // wrapped value type. + template<class ContainerType, class NewType> + struct rebind; + + template<class ValueType, class... Args, template<class...> class ContainerType, class NewType> + struct rebind<ContainerType<ValueType, Args...>, NewType> + { + using type = ContainerType<NewType, typename rebind<Args, NewType>::type...>; + }; + + using value_type = std::remove_reference_t<decltype(std::declval<BaseType>().front())>; + using const_reference = const value_type&; + using view_type = typename rebind<BaseType, std::reference_wrapper<value_type>>::type; + +public: + using FilterCallback = std::function<bool(const_reference)>; + using SortCallback = std::function<bool(const_reference, const_reference)>; + using OnEntryCallback = std::function<void(const_reference)>; + + ContainerView() = default; + ContainerView(const BaseType& container) + { + data_ = std::make_optional(std::ref(container)); + auto& dataSource = std::remove_const_t<BaseType&>(data()); + view_.assign(dataSource.begin(), dataSource.end()); + }; + ContainerView(const ContainerView& other) + : data_(std::nullopt) + , view_(other().begin(), other().end()) + , dirty_(other.dirty_) + , sortCallback_(other.sortCallback_) + , filterCallback_(other.filterCallback_) {}; + ContainerView& operator=(const ContainerView& other) = default; + ContainerView& operator=(ContainerView&& other) = default; + ContainerView(ContainerView&& other) noexcept = default; + + // Allow concatenation of views. + ContainerView& operator+=(const ContainerView& rhs) + { + view_.insert(view_.cend(), rhs.get().cbegin(), rhs.get().cend()); + return *this; + } + friend ContainerView operator+(ContainerView lhs, const ContainerView& rhs) + { + lhs += rhs; + return lhs; + } + + // Reset the underlying container and initialize the view. + ContainerView& reset(const BaseType& container) + { + data_ = std::make_optional(std::ref(container)); + auto& dataSource = std::remove_const_t<BaseType&>(data()); + view_.assign(dataSource.begin(), dataSource.end()); + invalidate(); + return *this; + } + + // Alternately, reset the view to another view and disregard underlying data. + ContainerView& reset(const ContainerView& other) + { + data_ = std::nullopt; + auto& dataSource = std::remove_const_t<ContainerView&>(other); + view_.assign(dataSource.get().begin(), dataSource.get().end()); + invalidate(); + return *this; + } + + // Sort the reference wrapped elements of the view container with the + // given predicate or the stored one. + ContainerView& sort(SortCallback&& pred = {}) + { + if (!dirty_) { + std::cout << "view not dirty, no-op sort" << std::endl; + return *this; + } + if (auto&& sortCallback = pred ? pred : sortCallback_) + std::sort(view_.begin(), view_.end(), sortCallback); + else + std::cout << "no sort function specified or bound" << std::endl; + return *this; + } + + // Filter the reference wrapped elements of the view container with the + // given predicate or the stored one. + // Only done if the view has been invalidated(e.g. the underlying container + // has been updated) + ContainerView& filter(FilterCallback&& pred = {}) + { + if (!dirty_) { + std::cout << "view not dirty, no-op filter" << std::endl; + return *this; + } + if (auto&& filterCallback = pred ? pred : filterCallback_) { + if (data_.has_value()) { + auto& dataSource = std::remove_const_t<BaseType&>(data()); + applyFilter(dataSource, filterCallback); + } else { + auto viewSource = view_; + applyFilter(viewSource, filterCallback); + } + } else + std::cout << "no filter function specified or bound" << std::endl; + return *this; + } + + // Iterate over the the reference wrapped elements of the view container + // and execute a callback on each element. + ContainerView& for_each(OnEntryCallback&& pred) + { + for (const auto& e : view_) + pred(e); + return *this; + } + + // Store a non-static member function as a SortCallback. + // The member function must match that of a binary predicate. + template<typename T, typename... Args> + void bindSortCallback(T* inst, bool (T::*func)(Args...)) + { + bindCallback(sortCallback_, inst, func); + } + // Overload for function objects. + template<typename Func = SortCallback> + void bindSortCallback(Func&& func) + { + sortCallback_ = func; + } + + // Store a non-static member function as a FilterCallback. + // The member function must match that of a unary predicate. + template<typename T, typename... Args> + void bindFilterCallback(T* inst, bool (T::*func)(Args...)) + { + bindCallback(filterCallback_, inst, func); + } + // Overload for function objects. + template<typename Func = FilterCallback> + void bindFilterCallback(Func&& func) + { + filterCallback_ = func; + } + + // Basic container operations should be avoided. + size_t size() const { return view_.size(); } + const_reference at(size_t pos) const { return view_.at(pos); } + void clear() { view_.clear(); } + + // TODO: re-filtering ?? should maybe observe underlying data in order to + // not track this manually. + bool isDirty() const noexcept { return dirty_; }; + ContainerView& invalidate() noexcept + { + dirty_ = true; + return *this; + }; + ContainerView& validate() noexcept + { + dirty_ = false; + return *this; + }; + + // Returns whether or not this view has a concrete data source + // or is just a view of a view. + constexpr bool hasUnderlyingData() const noexcept { return data_.has_value(); } + + // Access the view. + constexpr const view_type& operator()() const noexcept { return view_; } + constexpr const view_type& get() const noexcept { return view_; } + +private: + // A reference to the optional underlying container data source. + // If not used, the view can be constructed from another proxy + // container view, and refiltered and sorted. + OptRef<const BaseType> data_; + + // The 'view' is a container of reference wrapped values suitable + // for container operations like sorting and filtering. + view_type view_; + + // TODO: remove this invalidation flag if possible. + bool dirty_ {true}; + + // Stores the sorting/filtering predicates that can be re-applied + // instead of passing a lambda as a parameter to sort(). + FilterCallback filterCallback_ {}; + + // Same as above but for sorting. + SortCallback sortCallback_ {}; + + // A generic non-static member function storing function. + template<typename C, typename T, typename... Args> + void bindCallback(C& callback, T* inst, bool (T::*func)(Args...)) + { + // Using a lambda instead of std::bind works better for functions with an + // unknown number of parameters. e.g. callback = std::bind(func, inst, _1, ???); + callback = [=](Args... args) -> bool { + return (inst->*func)(args...); + }; + } + + // Hard unbox and unwrap underlying data container. + const BaseType& data() { return data_.value().get(); } + + // Actually filter the view. + template<typename T, typename Func = FilterCallback> + ContainerView& applyFilter(T& source, Func&& pred) + { + view_.clear(); + std::copy_if(source.begin(), source.end(), std::back_inserter(view_), pred); + return *this; + } +}; diff --git a/src/conversationmodel.cpp b/src/conversationmodel.cpp index b2c073117939b7621aa4d212af74897e013b3aa2..26a69144845a9becd0cb4f100030624426079dfb 100644 --- a/src/conversationmodel.cpp +++ b/src/conversationmodel.cpp @@ -31,6 +31,7 @@ #include "api/datatransfer.h" #include "api/datatransfermodel.h" #include "callbackshandler.h" +#include "containerview.h" #include "authority/storagehelper.h" #include "uri.h" @@ -116,10 +117,14 @@ public: * Initialize conversations_ and filteredConversations_ */ void initConversations(); + /** + * Filter all conversations + */ + bool filter(const conversation::Info& conv); /** * Sort conversation by last action */ - void sortConversations(); + bool sort(const conversation::Info& convA, const conversation::Info& convB); /** * Call contactModel.addContact if necessary * @param contactUri @@ -196,6 +201,8 @@ public: */ void acceptTransfer(const QString& convUid, uint64_t interactionId, const QString& path); + void invalidateModel(); + const ConversationModel& linked; Lrc& lrc; Database& db; @@ -203,15 +210,15 @@ public: const BehaviorController& behaviorController; ConversationModel::ConversationQueue conversations; ///< non-filtered conversations - ConversationModel::ConversationQueue filteredConversations; ConversationModel::ConversationQueue searchResults; - ConversationModel::ConversationQueue customFilteredConversations; - QString filter; + + ConversationModel::ConversationQueueProxy filteredConversations; + ConversationModel::ConversationQueueProxy customFilteredConversations; + + QString currentFilter; profile::Type typeFilter; profile::Type customTypeFilter; - std::pair<bool, bool> dirtyConversations { - true, - true}; ///< true if filteredConversations/customFilteredConversations must be regenerated + std::map<QString, std::mutex> interactionsLocks; ///< {convId, mutex} public Q_SLOTS: @@ -324,104 +331,13 @@ ConversationModel::ConversationModel(const account::Info& owner, ConversationModel::~ConversationModel() {} -const ConversationModel::ConversationQueue& +const ConversationModel::ConversationQueueProxy& ConversationModel::allFilteredConversations() const { - if (!pimpl_->dirtyConversations.first) + if (!pimpl_->filteredConversations.isDirty()) return pimpl_->filteredConversations; - pimpl_->filteredConversations = pimpl_->conversations; - - auto it = std::copy_if( - pimpl_->conversations.begin(), - pimpl_->conversations.end(), - pimpl_->filteredConversations.begin(), - [this](const conversation::Info& entry) { - try { - auto contactInfo = owner.contactModel->getContact(entry.participants.front()); - - auto filter = pimpl_->filter; - auto uri = URI(filter); - bool stripScheme = (uri.schemeType() < URI::SchemeType::COUNT__); - FlagPack<URI::Section> flags = URI::Section::USER_INFO | URI::Section::HOSTNAME - | URI::Section::PORT; - if (!stripScheme) { - flags |= URI::Section::SCHEME; - } - - filter = uri.format(flags); - - /* Check contact */ - // If contact is banned, only match if filter is a perfect match - if (contactInfo.isBanned) { - if (filter == "") - return false; - return contactInfo.profileInfo.uri == filter - || contactInfo.profileInfo.alias == filter - || contactInfo.registeredName == filter; - } - - std::regex regexFilter; - auto isValidReFilter = true; - try { - regexFilter = std::regex(filter.toStdString(), std::regex_constants::icase); - } catch (std::regex_error&) { - isValidReFilter = false; - } - - auto filterUriAndReg = [regexFilter, isValidReFilter](auto contact, auto filter) { - auto result = contact.profileInfo.uri.contains(filter) - || contact.registeredName.contains(filter); - if (!result) { - auto regexFound - = isValidReFilter - ? (!contact.profileInfo.uri.isEmpty() - && std::regex_search(contact.profileInfo.uri.toStdString(), - regexFilter)) - || std::regex_search(contact.registeredName.toStdString(), - regexFilter) - : false; - result |= regexFound; - } - return result; - }; - - /* Check type */ - if (pimpl_->typeFilter != profile::Type::PENDING) { - // Remove pending contacts and get the temporary item if filter is not empty - switch (contactInfo.profileInfo.type) { - case profile::Type::COUNT__: - case profile::Type::INVALID: - case profile::Type::PENDING: - return false; - case profile::Type::TEMPORARY: - return filterUriAndReg(contactInfo, filter); - case profile::Type::SIP: - case profile::Type::RING: - break; - } - } else { - // We only want pending requests matching with the filter - if (contactInfo.profileInfo.type != profile::Type::PENDING) - return false; - } - - // Otherwise perform usual regex search - bool result = contactInfo.profileInfo.alias.contains(filter); - if (!result && isValidReFilter) - result |= std::regex_search(contactInfo.profileInfo.alias.toStdString(), - regexFilter); - if (!result) - result |= filterUriAndReg(contactInfo, filter); - return result; - } catch (std::out_of_range&) { - // getContact() failed - return false; - } - }); - pimpl_->filteredConversations.resize(std::distance(pimpl_->filteredConversations.begin(), it)); - pimpl_->dirtyConversations.first = false; - return pimpl_->filteredConversations; + return pimpl_->filteredConversations.filter().sort().validate(); } QMap<ConferenceableItem, ConferenceableValue> @@ -438,7 +354,7 @@ ConversationModel::getConferenceableConversations(const QString& convId, const Q auto currentCallId = pimpl_->conversations.at(conversationIdx).callId; auto calls = pimpl_->lrc.getCalls(); auto conferences = pimpl_->lrc.getConferences(); - auto conversations = pimpl_->conversations; + auto& conversations = pimpl_->conversations; auto currentAccountID = pimpl_->linked.owner.id; // add contacts for current account for (const auto& conv : conversations) { @@ -487,7 +403,15 @@ ConversationModel::getConferenceableConversations(const QString& convId, const Q auto& accountInfo = pimpl_->lrc.getAccountModel().getAccountInfo(account_id); auto accountConv = accountInfo.conversationModel->getFilteredConversations( accountInfo.profileInfo.type); - for (const auto& conv : accountConv) { + accountConv.for_each([filter, + &accountInfo, + account_id, + currentCallId, + currentConfId, + &conferences, + &calls, + &tempConferences, + &callsVector](const conversation::Info& conv) { bool confFilterPredicate = !conv.confId.isEmpty() && conv.confId != currentConfId && std::find(conferences.begin(), conferences.end(), @@ -498,7 +422,7 @@ ConversationModel::getConferenceableConversations(const QString& convId, const Q != calls.end(); if (!confFilterPredicate && !callFilterPredicate) { - continue; + return; } // vector of conversationID accountID pair @@ -524,7 +448,7 @@ ConversationModel::getConferenceableConversations(const QString& convId, const Q || contact.profileInfo.uri.contains(filter) || contact.registeredName.contains(filter)); if (!result) { - continue; + return; } if (isConference && tempConferences.count(conv.confId)) { tempConferences.find(conv.confId).value().push_back(accConv); @@ -533,7 +457,7 @@ ConversationModel::getConferenceableConversations(const QString& convId, const Q } else if (shouldAddCall) { callsVector.push_back(cv); } - } + }); } catch (...) { } } @@ -545,7 +469,7 @@ ConversationModel::getConferenceableConversations(const QString& convId, const Q for (AccountConversation accConv : it.second) { try { auto& account = pimpl_->lrc.getAccountModel().getAccountInfo(accConv.accountId); - auto conv = account.conversationModel->getConversationForUid(accConv.convId)->get(); + auto& conv = account.conversationModel->getConversationForUid(accConv.convId)->get(); auto cont = account.contactModel->getContact(conv.participants.front()); if (cont.profileInfo.alias.contains(filter) || cont.profileInfo.uri.contains(filter) || cont.registeredName.contains(filter)) { @@ -567,31 +491,24 @@ ConversationModel::getAllSearchResults() const return pimpl_->searchResults; } -const ConversationModel::ConversationQueue& +const ConversationModel::ConversationQueueProxy& ConversationModel::getFilteredConversations(const profile::Type& filter, bool forceUpdate, const bool includeBanned) const { - if (pimpl_->customTypeFilter == filter && !pimpl_->dirtyConversations.second && !forceUpdate) + if (pimpl_->customTypeFilter == filter && !pimpl_->customFilteredConversations.isDirty() + && !forceUpdate) return pimpl_->customFilteredConversations; pimpl_->customTypeFilter = filter; - pimpl_->customFilteredConversations = pimpl_->conversations; - - auto it = std::copy_if(pimpl_->conversations.begin(), - pimpl_->conversations.end(), - pimpl_->customFilteredConversations.begin(), - [this, &includeBanned](const conversation::Info& entry) { - auto contactInfo = owner.contactModel->getContact( - entry.participants.front()); - if (!includeBanned && contactInfo.isBanned) - return false; - return (contactInfo.profileInfo.type == pimpl_->customTypeFilter); - }); - pimpl_->customFilteredConversations.resize( - std::distance(pimpl_->customFilteredConversations.begin(), it)); - pimpl_->dirtyConversations.second = false; - return pimpl_->customFilteredConversations; + return pimpl_->customFilteredConversations.reset(pimpl_->conversations) + .filter([this, &includeBanned](const conversation::Info& entry) { + auto contactInfo = owner.contactModel->getContact(entry.participants.front()); + if (!includeBanned && contactInfo.isBanned) + return false; + return (contactInfo.profileInfo.type == pimpl_->customTypeFilter); + }) + .validate(); } OptRef<conversation::Info> @@ -631,26 +548,24 @@ ConversationModel::getConversationForCallId(const QString& callId) } } -conversation::Info -ConversationModel::filteredConversation(const unsigned int row) const +OptRef<conversation::Info> +ConversationModel::filteredConversation(unsigned row) const { - const auto& conversations = allFilteredConversations(); - if (row >= conversations.size()) - return conversation::Info(); - - auto conversationInfo = conversations.at(row); + auto conversations = allFilteredConversations(); + if (row >= conversations.get().size()) + return std::nullopt; - return conversationInfo; + return std::make_optional(conversations.get().at(row)); } -conversation::Info -ConversationModel::searchResultForRow(const unsigned int row) const +OptRef<conversation::Info> +ConversationModel::searchResultForRow(unsigned row) const { - const auto& results = pimpl_->searchResults; + auto& results = pimpl_->searchResults; if (row >= results.size()) - return conversation::Info(); + return std::nullopt; - return results.at(row); + return std::make_optional(std::ref(results.at(row))); } void @@ -849,7 +764,8 @@ ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly) return; } - dirtyConversations = {true, true}; + invalidateModel(); + emit behaviorController.showIncomingCallView(linked.owner.id, newConv.uid); }); @@ -976,13 +892,12 @@ ConversationModel::sendMessage(const QString& uid, const QString& body) } newConv.lastMessageUid = msgId; - pimpl_->dirtyConversations = {true, true}; // Emit this signal for chatview in the client emit newInteraction(convId, msgId, msg); // This conversation is now at the top of the list - pimpl_->sortConversations(); // The order has changed, informs the client to redraw the list - emit modelSorted(); + pimpl_->invalidateModel(); + emit modelChanged(); }); if (isTemporary) { @@ -1022,7 +937,7 @@ ConversationModel::sendMessage(const QString& uid, const QString& body) void ConversationModel::refreshFilter() { - pimpl_->dirtyConversations = {true, true}; + pimpl_->invalidateModel(); emit filterChanged(); } @@ -1035,8 +950,8 @@ ConversationModel::updateSearchStatus(const QString& status) const void ConversationModel::setFilter(const QString& filter) { - pimpl_->filter = filter; - pimpl_->dirtyConversations = {true, true}; + pimpl_->currentFilter = filter; + pimpl_->invalidateModel(); pimpl_->searchResults.clear(); emit searchResultUpdated(); owner.contactModel->searchContact(filter); @@ -1048,7 +963,7 @@ ConversationModel::setFilter(const profile::Type& filter) { // Switch between PENDING, RING and SIP contacts. pimpl_->typeFilter = filter; - pimpl_->dirtyConversations = {true, true}; + pimpl_->invalidateModel(); emit filterChanged(); } @@ -1099,8 +1014,8 @@ ConversationModel::clearHistory(const QString& uid) conversation.interactions.clear(); } storage::getHistory(pimpl_->db, conversation); // will contains "Conversation started" - pimpl_->sortConversations(); - emit modelSorted(); + + emit modelChanged(); emit conversationCleared(uid); } @@ -1160,13 +1075,12 @@ ConversationModel::clearInteractionFromConversation(const QString& convId, emit displayedInteractionChanged(convId, participantURI, interactionId, newDisplayedUid); } if (erased_keys > 0) { - pimpl_->dirtyConversations.first = true; + pimpl_->filteredConversations.invalidate(); emit interactionRemoved(convId, interactionId); } if (lastInteractionUpdated) { // last interaction as changed, so the order can changes. - pimpl_->sortConversations(); - emit modelSorted(); + emit modelChanged(); } } @@ -1247,8 +1161,7 @@ ConversationModel::clearAllHistory() } storage::getHistory(pimpl_->db, conversation); } - pimpl_->sortConversations(); - emit modelSorted(); + emit modelChanged(); } void @@ -1276,7 +1189,7 @@ ConversationModel::setInteractionRead(const QString& convId, const uint64_t& int } } if (emitUpdated) { - pimpl_->dirtyConversations = {true, true}; + pimpl_->invalidateModel(); auto daemonId = storage::getDaemonIdByInteractionId(pimpl_->db, QString::number(interactionId)); if (owner.profileInfo.type != profile::Type::SIP) { @@ -1327,7 +1240,7 @@ ConversationModel::clearUnreadInteractions(const QString& convId) 3); if (emitUpdated) { pimpl_->conversations[conversationIdx].unreadMessages = 0; - pimpl_->dirtyConversations = {true, true}; + pimpl_->invalidateModel(); emit conversationUpdated(convId); } } @@ -1345,6 +1258,9 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked, , customTypeFilter(profile::Type::INVALID) , behaviorController(behaviorController) { + filteredConversations.bindSortCallback(this, &ConversationModelPimpl::sort); + filteredConversations.bindFilterCallback(this, &ConversationModelPimpl::filter); + initConversations(); // Contact related @@ -1593,9 +1509,7 @@ ConversationModelPimpl::initConversations() } } - sortConversations(); - filteredConversations = conversations; - dirtyConversations.first = false; + filteredConversations.reset(conversations).sort(); // Load all non treated messages for this account QVector<Message> messages = ConfigurationManager::instance() @@ -1610,54 +1524,131 @@ ConversationModelPimpl::initConversations() } } -void -ConversationModelPimpl::sortConversations() -{ - std::sort(conversations.begin(), - conversations.end(), - [this](const auto& conversationA, const auto& conversationB) { - // A or B is a temporary contact - if (conversationA.participants.isEmpty()) - return true; - if (conversationB.participants.isEmpty()) - return false; - - if (conversationA.uid == conversationB.uid) - return false; - - auto& mtxA = interactionsLocks[conversationA.uid]; - auto& mtxB = interactionsLocks[conversationB.uid]; - std::lock(mtxA, mtxB); - std::lock_guard<std::mutex> lockConvA(mtxA, std::adopt_lock); - std::lock_guard<std::mutex> lockConvB(mtxB, std::adopt_lock); - - auto historyA = conversationA.interactions; - auto historyB = conversationB.interactions; - - // A or B is a new conversation (without CONTACT interaction) - if (conversationA.uid.isEmpty() || conversationB.uid.isEmpty()) - return conversationA.uid.isEmpty(); - - if (historyA.empty() && historyB.empty()) { - // If no information to compare, sort by Ring ID - return conversationA.participants.front() - > conversationB.participants.front(); - } - if (historyA.empty()) - return false; - if (historyB.empty()) - return true; - // Sort by last Interaction - try { - auto lastMessageA = historyA.at(conversationA.lastMessageUid); - auto lastMessageB = historyB.at(conversationB.lastMessageUid); - return lastMessageA.timestamp > lastMessageB.timestamp; - } catch (const std::exception& e) { - qDebug() << "ConversationModel::sortConversations(), can't get lastMessage"; - return false; - } - }); - dirtyConversations = {true, true}; +bool +ConversationModelPimpl::filter(const conversation::Info& entry) +{ + try { + auto contactInfo = linked.owner.contactModel->getContact(entry.participants.front()); + + auto uri = URI(currentFilter); + bool stripScheme = (uri.schemeType() < URI::SchemeType::COUNT__); + FlagPack<URI::Section> flags = URI::Section::USER_INFO | URI::Section::HOSTNAME + | URI::Section::PORT; + if (!stripScheme) { + flags |= URI::Section::SCHEME; + } + + currentFilter = uri.format(flags); + + // Check contact + // If contact is banned, only match if filter is a perfect match + if (contactInfo.isBanned) { + if (currentFilter == "") + return false; + return contactInfo.profileInfo.uri == currentFilter + || contactInfo.profileInfo.alias == currentFilter + || contactInfo.registeredName == currentFilter; + } + + std::regex regexFilter; + auto isValidReFilter = true; + try { + regexFilter = std::regex(currentFilter.toStdString(), std::regex_constants::icase); + } catch (std::regex_error&) { + isValidReFilter = false; + } + + auto filterUriAndReg = [regexFilter, isValidReFilter](auto contact, auto filter) { + auto result = contact.profileInfo.uri.contains(filter) + || contact.registeredName.contains(filter); + if (!result) { + auto regexFound = isValidReFilter + ? (!contact.profileInfo.uri.isEmpty() + && std::regex_search(contact.profileInfo.uri.toStdString(), + regexFilter)) + || std::regex_search(contact.registeredName.toStdString(), + regexFilter) + : false; + result |= regexFound; + } + return result; + }; + + // Check type + if (typeFilter != profile::Type::PENDING) { + // Remove pending contacts and get the temporary item if filter is not empty + switch (contactInfo.profileInfo.type) { + case profile::Type::COUNT__: + case profile::Type::INVALID: + case profile::Type::PENDING: + return false; + case profile::Type::TEMPORARY: + return filterUriAndReg(contactInfo, currentFilter); + case profile::Type::SIP: + case profile::Type::RING: + break; + } + } else { + // We only want pending requests matching with the filter + if (contactInfo.profileInfo.type != profile::Type::PENDING) + return false; + } + + // Otherwise perform usual regex search + bool result = contactInfo.profileInfo.alias.contains(currentFilter); + if (!result && isValidReFilter) + result |= std::regex_search(contactInfo.profileInfo.alias.toStdString(), regexFilter); + if (!result) + result |= filterUriAndReg(contactInfo, currentFilter); + return result; + } catch (std::out_of_range&) { + // getContact() failed + return false; + } +} + +bool +ConversationModelPimpl::sort(const conversation::Info& convA, const conversation::Info& convB) +{ + // A or B is a temporary contact + if (convA.participants.isEmpty()) + return true; + if (convB.participants.isEmpty()) + return false; + + if (convA.uid == convB.uid) + return false; + + auto& mtxA = interactionsLocks[convA.uid]; + auto& mtxB = interactionsLocks[convB.uid]; + std::lock(mtxA, mtxB); + std::lock_guard<std::mutex> lockConvA(mtxA, std::adopt_lock); + std::lock_guard<std::mutex> lockConvB(mtxB, std::adopt_lock); + + auto historyA = convA.interactions; + auto historyB = convB.interactions; + + // A or B is a new conversation (without CONTACT interaction) + if (convA.uid.isEmpty() || convB.uid.isEmpty()) + return convA.uid.isEmpty(); + + if (historyA.empty() && historyB.empty()) { + // If no information to compare, sort by Ring ID + return convA.participants.front() > convB.participants.front(); + } + if (historyA.empty()) + return false; + if (historyB.empty()) + return true; + // Sort by last Interaction + try { + auto lastMessageA = historyA.at(convA.lastMessageUid); + auto lastMessageB = historyB.at(convB.lastMessageUid); + return lastMessageA.timestamp > lastMessageB.timestamp; + } catch (const std::exception& e) { + qDebug() << "ConversationModel::sortConversations(), can't get lastMessage"; + return false; + } } void @@ -1702,9 +1693,8 @@ ConversationModelPimpl::slotContactAdded(const QString& contactUri) searchResults.erase(searchResults.begin() + i); } - sortConversations(); emit linked.conversationReady(profileInfo.uri); - emit linked.modelSorted(); + emit linked.modelChanged(); } void @@ -1738,7 +1728,7 @@ ConversationModelPimpl::slotPendingContactAccepted(const QString& uri) std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]); conversations[convIdx].interactions.emplace(msgId, interaction); } - dirtyConversations = {true, true}; + filteredConversations.invalidate(); emit linked.newInteraction(convs[0], msgId, interaction); } catch (std::out_of_range& e) { qDebug() << "ConversationModelPimpl::slotContactAdded can't find contact"; @@ -1756,9 +1746,10 @@ ConversationModelPimpl::slotContactRemoved(const QString& uri) } auto& conversationUid = conversations[conversationIdx].uid; conversations.erase(conversations.begin() + conversationIdx); - dirtyConversations = {true, true}; emit linked.conversationRemoved(conversationUid); - emit linked.modelSorted(); + + invalidateModel(); + emit linked.modelChanged(); } void @@ -1768,7 +1759,7 @@ ConversationModelPimpl::slotContactModelUpdated(const QString& uri, bool needsSo if (!needsSorted) { try { auto& conversation = getConversationForPeerUri(uri, true).get(); - dirtyConversations = {true, true}; + invalidateModel(); emit linked.conversationUpdated(conversation.uid); } catch (std::out_of_range&) { qDebug() << "contact updated for not existing conversation"; @@ -1776,9 +1767,9 @@ ConversationModelPimpl::slotContactModelUpdated(const QString& uri, bool needsSo return; } - if (filter.isEmpty()) { + if (currentFilter.isEmpty()) { if (searchResults.empty()) { - emit linked.modelSorted(); + emit linked.modelChanged(); return; } searchResults.clear(); @@ -1792,7 +1783,7 @@ ConversationModelPimpl::slotContactModelUpdated(const QString& uri, bool needsSo conversationInfo.uid = user.profileInfo.uri; conversationInfo.participants.push_back(user.profileInfo.uri); conversationInfo.accountId = linked.owner.id; - searchResults.emplace_front(conversationInfo); + searchResults.emplace_front(std::move(conversationInfo)); } emit linked.searchResultUpdated(); } @@ -1845,8 +1836,8 @@ ConversationModelPimpl::addConversationWith(const QString& convId, const QString } conversation.unreadMessages = getNumberOfUnreadMessagesFor(convId); - conversations.emplace_back(conversation); - dirtyConversations = {true, true}; + conversations.emplace_back(std::move(conversation)); + invalidateModel(); } int @@ -1917,7 +1908,7 @@ ConversationModelPimpl::slotIncomingCall(const QString& fromId, const QString& c qDebug() << "Add call to conversation with " << fromId; conversation.callId = callId; - dirtyConversations = {true, true}; + invalidateModel(); emit behaviorController.showIncomingCallView(linked.owner.id, conversation.uid); } @@ -1942,7 +1933,7 @@ ConversationModelPimpl::slotCallStatusChanged(const QString& callId, int code) for (auto& conversation : conversations) { if (conversation.participants.front() == call.peerUri) { conversation.callId = callId; - dirtyConversations = {true, true}; + invalidateModel(); emit linked.conversationUpdated(conversation.uid); } } @@ -1984,7 +1975,7 @@ ConversationModelPimpl::slotCallEnded(const QString& callId) if (conversation.callId == callId) { conversation.callId = ""; conversation.confId = ""; // The participant is detached - dirtyConversations = {true, true}; + invalidateModel(); emit linked.conversationUpdated(conversation.uid); } } catch (std::out_of_range& e) { @@ -2028,13 +2019,14 @@ ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId, std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]); conv_it->interactions[msgId] = msg; } - dirtyConversations = {true, true}; + if (newInteraction) emit linked.newInteraction(conv_it->uid, msgId, msg); else emit linked.interactionStatusUpdated(conv_it->uid, msgId, msg); - sortConversations(); - emit linked.modelSorted(); + + invalidateModel(); + emit linked.modelChanged(); } void @@ -2112,11 +2104,13 @@ ConversationModelPimpl::addIncomingMessage(const QString& from, conversations[conversationIdx].lastMessageUid = msgId; conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convIds[0]); } - dirtyConversations = {true, true}; + emit behaviorController.newUnreadInteraction(linked.owner.id, convIds[0], msgId, msg); emit linked.newInteraction(convIds[0], msgId, msg); - sortConversations(); - emit linked.modelSorted(); + + invalidateModel(); + emit linked.modelChanged(); + return msgId; } @@ -2126,8 +2120,7 @@ ConversationModelPimpl::slotCallAddedToConference(const QString& callId, const Q for (auto& conversation : conversations) { if (conversation.callId == callId && conversation.confId != confId) { conversation.confId = confId; - dirtyConversations = {true, true}; - + invalidateModel(); // Refresh the conference status only if attached MapStringString confDetails = CallManager::instance().getConferenceDetails(confId); if (confDetails["STATE"] == "ACTIVE_ATTACHED") @@ -2211,7 +2204,7 @@ ConversationModelPimpl::slotUpdateInteractionStatus(const QString& accountId, emit linked.displayedInteractionChanged(convIds[0], peer_uri, oldDisplayedUid, msgId); } if (emitUpdated) { - dirtyConversations = {true, true}; + invalidateModel(); emit linked.interactionStatusUpdated(convIds[0], msgId, itCopy); } } @@ -2360,7 +2353,7 @@ ConversationModel::cancelTransfer(const QString& convUid, uint64_t interactionId if (emitUpdated) { // Forward cancel action to daemon (will invoke slotTransferStatusCanceled) pimpl_->lrc.getDataTransferModel().cancel(interactionId); - pimpl_->dirtyConversations = {true, true}; + pimpl_->invalidateModel(); emit interactionStatusUpdated(convUid, interactionId, itCopy); emit pimpl_->behaviorController.newReadInteraction(owner.id, convUid, interactionId); } @@ -2447,14 +2440,14 @@ ConversationModelPimpl::slotTransferStatusCreated(long long dringId, datatransfe conversations[conversationIdx].lastMessageUid = interactionId; conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convId); } - dirtyConversations = {true, true}; emit behaviorController.newUnreadInteraction(linked.owner.id, convId, interactionId, interaction); emit linked.newInteraction(convId, interactionId, interaction); - sortConversations(); - emit linked.modelSorted(); + + invalidateModel(); + emit linked.modelChanged(); } void @@ -2489,7 +2482,7 @@ ConversationModelPimpl::slotTransferStatusAwaitingHost(long long dringId, datatr } } if (emitUpdated) { - dirtyConversations = {true, true}; + invalidateModel(); emit linked.interactionStatusUpdated(convId, interactionId, itCopy); // Only accept if contact is added if (!lrc.getDataTransferModel().acceptFromUnstrusted) { @@ -2552,12 +2545,19 @@ ConversationModelPimpl::acceptTransfer(const QString& convUid, } if (emitUpdated) { sendContactRequest(conversations[conversationIdx].participants.front()); - dirtyConversations = {true, true}; + invalidateModel(); emit linked.interactionStatusUpdated(convUid, interactionId, itCopy); emit behaviorController.newReadInteraction(linked.owner.id, convUid, interactionId); } } +void +ConversationModelPimpl::invalidateModel() +{ + filteredConversations.invalidate(); + customFilteredConversations.invalidate(); +} + void ConversationModelPimpl::slotTransferStatusOngoing(long long dringId, datatransfer::Info info) { @@ -2589,7 +2589,7 @@ ConversationModelPimpl::slotTransferStatusOngoing(long long dringId, datatransfe updateTransfer(timer, convId, conversationIdx, interactionId); }); timer->start(1000); - dirtyConversations = {true, true}; + invalidateModel(); emit linked.interactionStatusUpdated(convId, interactionId, itCopy); } } @@ -2624,7 +2624,7 @@ ConversationModelPimpl::slotTransferStatusFinished(long long dringId, datatransf } } if (emitUpdated) { - dirtyConversations = {true, true}; + invalidateModel(); storage::updateInteractionStatus(db, interactionId, newStatus); emit linked.interactionStatusUpdated(convId, interactionId, itCopy); } @@ -2684,7 +2684,7 @@ ConversationModelPimpl::updateTransferStatus(long long dringId, } } if (emitUpdated) { - dirtyConversations = {true, true}; + invalidateModel(); emit linked.interactionStatusUpdated(convId, interactionId, itCopy); } } @@ -2717,7 +2717,7 @@ ConversationModelPimpl::updateTransfer(QTimer* timer, } timer->stop(); - delete timer; + timer->deleteLater(); } } // namespace lrc