diff --git a/CHANGELOG.md b/CHANGELOG.md index 8105d437a..2286aea46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.2.0 - undefined + +### Fixed +- Download path and emojis size settings +- Mac emoji font. + +### Added +- Chat reactions ## 5.1.2 - 2023-08-25 diff --git a/linphone-app/CMakeLists.txt b/linphone-app/CMakeLists.txt index 96f83e7c7..fb89135ec 100644 --- a/linphone-app/CMakeLists.txt +++ b/linphone-app/CMakeLists.txt @@ -242,6 +242,9 @@ set(SOURCES src/components/chat-events/ChatMessageListener.cpp src/components/chat-events/ChatMessageModel.cpp src/components/chat-events/ChatNoticeModel.cpp + src/components/chat-reaction/ChatReactionModel.cpp + src/components/chat-reaction/ChatReactionListModel.cpp + src/components/chat-reaction/ChatReactionProxyModel.cpp src/components/chat-room/ChatRoomInitializer.cpp src/components/chat-room/ChatRoomListener.cpp src/components/chat-room/ChatRoomModel.cpp @@ -384,6 +387,9 @@ set(HEADERS src/components/chat-events/ChatMessageListener.hpp src/components/chat-events/ChatMessageModel.hpp src/components/chat-events/ChatNoticeModel.hpp + src/components/chat-reaction/ChatReactionModel.hpp + src/components/chat-reaction/ChatReactionListModel.hpp + src/components/chat-reaction/ChatReactionProxyModel.hpp src/components/chat-room/ChatRoomInitializer.hpp src/components/chat-room/ChatRoomListener.hpp src/components/chat-room/ChatRoomModel.hpp diff --git a/linphone-app/assets/languages/cs.ts b/linphone-app/assets/languages/cs.ts index f2a2d0b48..863e7a246 100644 --- a/linphone-app/assets/languages/cs.ts +++ b/linphone-app/assets/languages/cs.ts @@ -758,6 +758,18 @@ Adresa URL není nakonfigurována. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/da.ts b/linphone-app/assets/languages/da.ts index e2a07bd4b..60a0e490a 100644 --- a/linphone-app/assets/languages/da.ts +++ b/linphone-app/assets/languages/da.ts @@ -752,6 +752,17 @@ Server url ikke konfigureret. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/de.ts b/linphone-app/assets/languages/de.ts index 87a4ac1c8..30c79fca1 100644 --- a/linphone-app/assets/languages/de.ts +++ b/linphone-app/assets/languages/de.ts @@ -752,6 +752,17 @@ Server URL ist nicht konfiguriert. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + + ChatReplyMessage @@ -1596,7 +1607,7 @@ Server URL ist nicht konfiguriert. incallPauseWarning 'You are currently out of the conference.' : Pause message in video conference. - + Sie haben den Anruf unterbrochen. incallPauseHint diff --git a/linphone-app/assets/languages/en.ts b/linphone-app/assets/languages/en.ts index fa04c0f46..c34a73c56 100644 --- a/linphone-app/assets/languages/en.ts +++ b/linphone-app/assets/languages/en.ts @@ -752,6 +752,17 @@ Server URL not configured. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + %1<br>reaction + %1<br>reactions + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/es.ts b/linphone-app/assets/languages/es.ts index 36182ccea..2a06a48e9 100644 --- a/linphone-app/assets/languages/es.ts +++ b/linphone-app/assets/languages/es.ts @@ -752,6 +752,17 @@ URL del servidor no configurada. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/fr_FR.ts b/linphone-app/assets/languages/fr_FR.ts index 9a77aa04a..72a46c6f3 100644 --- a/linphone-app/assets/languages/fr_FR.ts +++ b/linphone-app/assets/languages/fr_FR.ts @@ -752,6 +752,17 @@ URL du serveur non configurée. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + %1<br>réaction + %1<br>réactions + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/hu.ts b/linphone-app/assets/languages/hu.ts index cc3b1fd78..ed92ff8cf 100644 --- a/linphone-app/assets/languages/hu.ts +++ b/linphone-app/assets/languages/hu.ts @@ -746,6 +746,16 @@ A kiszolgáló URL-je nincs konfigurálva. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/it.ts b/linphone-app/assets/languages/it.ts index f1c502500..dd74dafea 100644 --- a/linphone-app/assets/languages/it.ts +++ b/linphone-app/assets/languages/it.ts @@ -752,6 +752,17 @@ URL del server non configurato. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/ja.ts b/linphone-app/assets/languages/ja.ts index d02d4b0c1..80ba15599 100644 --- a/linphone-app/assets/languages/ja.ts +++ b/linphone-app/assets/languages/ja.ts @@ -746,6 +746,16 @@ + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/lt.ts b/linphone-app/assets/languages/lt.ts index 5d5184fb5..6d7d9e0ac 100644 --- a/linphone-app/assets/languages/lt.ts +++ b/linphone-app/assets/languages/lt.ts @@ -758,6 +758,18 @@ Nesukonfigūruotas serverio url. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/pt_BR.ts b/linphone-app/assets/languages/pt_BR.ts index c6948365b..4d674c8b6 100644 --- a/linphone-app/assets/languages/pt_BR.ts +++ b/linphone-app/assets/languages/pt_BR.ts @@ -752,6 +752,17 @@ URL do servidor não configurado. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/ru.ts b/linphone-app/assets/languages/ru.ts index 4de374f52..c0ee72c9b 100644 --- a/linphone-app/assets/languages/ru.ts +++ b/linphone-app/assets/languages/ru.ts @@ -758,6 +758,18 @@ + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/sv.ts b/linphone-app/assets/languages/sv.ts index 7afff6c73..0b3f819f2 100644 --- a/linphone-app/assets/languages/sv.ts +++ b/linphone-app/assets/languages/sv.ts @@ -752,6 +752,17 @@ Serverwebbadressen är inte konfigurerad. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/tr.ts b/linphone-app/assets/languages/tr.ts index 432d87649..5af771560 100644 --- a/linphone-app/assets/languages/tr.ts +++ b/linphone-app/assets/languages/tr.ts @@ -746,6 +746,16 @@ Sunucu url'si yapılandırılmadı. + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/uk.ts b/linphone-app/assets/languages/uk.ts index be6b43132..080872db0 100644 --- a/linphone-app/assets/languages/uk.ts +++ b/linphone-app/assets/languages/uk.ts @@ -758,6 +758,18 @@ + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + + + ChatReplyMessage diff --git a/linphone-app/assets/languages/zh_CN.ts b/linphone-app/assets/languages/zh_CN.ts index 1de29a6ac..be907c51a 100644 --- a/linphone-app/assets/languages/zh_CN.ts +++ b/linphone-app/assets/languages/zh_CN.ts @@ -746,6 +746,16 @@ + + ChatReactionsDetails + + reactionsCount + "%1<br>reactions" : count of all chat reactions with a jump line between count and text. + + + + + ChatReplyMessage diff --git a/linphone-app/resources.qrc b/linphone-app/resources.qrc index 74f8f8b24..282f0bab3 100644 --- a/linphone-app/resources.qrc +++ b/linphone-app/resources.qrc @@ -346,6 +346,7 @@ ui/modules/Linphone/Chat/ChatFilePreview.qml ui/modules/Linphone/Chat/ChatForwardMessage.qml ui/modules/Linphone/Chat/ChatMessagePreview.qml + ui/modules/Linphone/Chat/ChatReactionsDetails.qml ui/modules/Linphone/Chat/ChatReplyMessage.qml ui/modules/Linphone/Chat/ChatReplyPreview.qml ui/modules/Linphone/Chat/ChatTextMessage.qml @@ -403,6 +404,7 @@ ui/modules/Linphone/Styles/Chat/ChatFilePreviewStyle.qml ui/modules/Linphone/Styles/Chat/ChatCalendarMessageStyle.qml ui/modules/Linphone/Styles/Chat/ChatForwardMessageStyle.qml + ui/modules/Linphone/Styles/Chat/ChatReactionsDetailsStyle.qml ui/modules/Linphone/Styles/Chat/ChatReplyMessageStyle.qml ui/modules/Linphone/Styles/Codecs/CodecsViewerStyle.qml ui/modules/Linphone/Styles/Contact/AvatarStyle.qml diff --git a/linphone-app/src/app/App.cpp b/linphone-app/src/app/App.cpp index 0289ce755..9dbe87445 100644 --- a/linphone-app/src/app/App.cpp +++ b/linphone-app/src/app/App.cpp @@ -722,6 +722,7 @@ void App::registerTypes () { registerType("CallsListProxyModel"); registerType("Camera"); registerType("ChatRoomProxyModel"); + registerType("ChatReactionProxyModel"); registerType("ConferenceHelperModel"); registerType("ConferenceProxyModel"); registerType("ConferenceInfoModel"); @@ -762,6 +763,7 @@ void App::registerTypes () { registerUncreatableType("ChatCallModel"); registerUncreatableType("ChatMessageModel"); registerUncreatableType("ChatNoticeModel"); + registerUncreatableType("ChatReactionListModel"); registerUncreatableType("ChatRoomModel"); registerUncreatableType("ColorModel"); registerUncreatableType("ImageModel"); diff --git a/linphone-app/src/app/proxyModel/ProxyAbstractListModel.hpp b/linphone-app/src/app/proxyModel/ProxyAbstractListModel.hpp index 22254aea7..f490478a4 100644 --- a/linphone-app/src/app/proxyModel/ProxyAbstractListModel.hpp +++ b/linphone-app/src/app/proxyModel/ProxyAbstractListModel.hpp @@ -112,11 +112,11 @@ public: return true; } - virtual void clearData(){ + virtual void clearData() override{ mList.clear(); } - virtual void resetData(){ + virtual void resetData() override{ beginResetModel(); clearData(); endResetModel(); diff --git a/linphone-app/src/components/Components.hpp b/linphone-app/src/components/Components.hpp index 68810b554..41c82275f 100644 --- a/linphone-app/src/components/Components.hpp +++ b/linphone-app/src/components/Components.hpp @@ -30,6 +30,7 @@ #include "components/chat-events/ChatCallModel.hpp" #include "components/chat-events/ChatMessageModel.hpp" #include "components/chat-events/ChatNoticeModel.hpp" +#include "components/chat-reaction/ChatReactionProxyModel.hpp" #include "chat-room/ChatRoomProxyModel.hpp" #include "codecs/AudioCodecsModel.hpp" #include "codecs/VideoCodecsModel.hpp" diff --git a/linphone-app/src/components/chat-events/ChatMessageListener.cpp b/linphone-app/src/components/chat-events/ChatMessageListener.cpp index 148205dfe..77ae497a6 100644 --- a/linphone-app/src/components/chat-events/ChatMessageListener.cpp +++ b/linphone-app/src/components/chat-events/ChatMessageListener.cpp @@ -49,6 +49,9 @@ void ChatMessageListener::onFileTransferProgressIndication (const std::shared_pt void ChatMessageListener::onMsgStateChanged (const std::shared_ptr &message, linphone::ChatMessage::State state){ emit msgStateChanged(message, state); } +void ChatMessageListener::onNewMessageReaction(const std::shared_ptr & message, const std::shared_ptr & reaction) { + emit newMessageReaction(message, reaction); +} void ChatMessageListener::onParticipantImdnStateChanged(const std::shared_ptr & message, const std::shared_ptr & state){ emit participantImdnStateChanged(message, state); } diff --git a/linphone-app/src/components/chat-events/ChatMessageListener.hpp b/linphone-app/src/components/chat-events/ChatMessageListener.hpp index f2c1520b0..29f40d149 100644 --- a/linphone-app/src/components/chat-events/ChatMessageListener.hpp +++ b/linphone-app/src/components/chat-events/ChatMessageListener.hpp @@ -41,6 +41,7 @@ public: virtual std::shared_ptr onFileTransferSend(const std::shared_ptr & message, const std::shared_ptr & content, size_t offset, size_t size) override; virtual void onFileTransferProgressIndication (const std::shared_ptr &message, const std::shared_ptr &, size_t offset, size_t) override; virtual void onMsgStateChanged (const std::shared_ptr &message, linphone::ChatMessage::State state) override; + virtual void onNewMessageReaction(const std::shared_ptr & message, const std::shared_ptr & reaction) override; virtual void onParticipantImdnStateChanged(const std::shared_ptr & message, const std::shared_ptr & state) override; virtual void onEphemeralMessageTimerStarted(const std::shared_ptr & message) override; virtual void onEphemeralMessageDeleted(const std::shared_ptr & message) override; @@ -50,6 +51,7 @@ signals: std::shared_ptr fileTransferSend (const std::shared_ptr &,const std::shared_ptr &,size_t,size_t); void fileTransferProgressIndication (const std::shared_ptr &message, const std::shared_ptr &, size_t offset, size_t); void msgStateChanged (const std::shared_ptr &message, linphone::ChatMessage::State state); + void newMessageReaction(const std::shared_ptr & message, const std::shared_ptr & reaction); void participantImdnStateChanged(const std::shared_ptr & message, const std::shared_ptr & state); void ephemeralMessageTimerStarted(const std::shared_ptr & message); void ephemeralMessageDeleted(const std::shared_ptr & message); diff --git a/linphone-app/src/components/chat-events/ChatMessageModel.cpp b/linphone-app/src/components/chat-events/ChatMessageModel.cpp index 1f9a34636..2348b5740 100644 --- a/linphone-app/src/components/chat-events/ChatMessageModel.cpp +++ b/linphone-app/src/components/chat-events/ChatMessageModel.cpp @@ -38,6 +38,8 @@ #include "app/App.hpp" #include "app/paths/Paths.hpp" +#include "components/chat-reaction/ChatReactionModel.hpp" +#include "components/chat-reaction/ChatReactionListModel.hpp" #include "components/contact/ContactModel.hpp" #include "components/contacts/ContactsListModel.hpp" #include "components/content/ContentListModel.hpp" @@ -61,6 +63,7 @@ void ChatMessageModel::connectTo(ChatMessageListener * listener){ connect(listener, &ChatMessageListener::fileTransferSend, this, &ChatMessageModel::onFileTransferSend); connect(listener, &ChatMessageListener::fileTransferProgressIndication, this, &ChatMessageModel::onFileTransferProgressIndication); connect(listener, &ChatMessageListener::msgStateChanged, this, &ChatMessageModel::onMsgStateChanged); + connect(listener, &ChatMessageListener::newMessageReaction, this, &ChatMessageModel::onNewMessageReaction); connect(listener, &ChatMessageListener::participantImdnStateChanged, this, &ChatMessageModel::onParticipantImdnStateChanged); connect(listener, &ChatMessageListener::ephemeralMessageTimerStarted, this, &ChatMessageModel::onEphemeralMessageTimerStarted); connect(listener, &ChatMessageListener::ephemeralMessageDeleted, this, &ChatMessageModel::onEphemeralMessageDeleted); @@ -95,6 +98,7 @@ ChatMessageModel::ChatMessageModel ( std::shared_ptr chat mWasDownloaded = false; mContentListModel = QSharedPointer::create(this); + mChatReactionListModel = QSharedPointer::create(this); } ChatMessageModel::~ChatMessageModel(){ @@ -188,6 +192,10 @@ QSharedPointer ChatMessageModel::getContents() const{ return mContentListModel; } +QSharedPointer ChatMessageModel::getChatReactions() const { + return mChatReactionListModel; +} + bool ChatMessageModel::isReply() const{ return mChatMessage && mChatMessage->isReply(); } @@ -247,6 +255,16 @@ void ChatMessageModel::resendMessage (){ } } +void ChatMessageModel::sendChatReaction(const QString& reaction){ + auto chatReaction = mChatMessage->createReaction(Utils::appStringToCoreString(reaction)); + if( mChatReactionListModel->exists(chatReaction)) { + chatReaction = mChatMessage->createReaction(""); + return; // TODO : remove return when sending empty emoji will be supported. + } + chatReaction->send(); + mChatReactionListModel->updateChatReaction(chatReaction); +} + void ChatMessageModel::deleteEvent(){ if (mChatMessage && mChatMessage->getFileTransferInformation()) { mChatMessage->cancelFileTransfer(); @@ -307,6 +325,11 @@ void ChatMessageModel::onMsgStateChanged (const std::shared_ptr & message, const std::shared_ptr & reaction){ + mChatReactionListModel->updateChatReaction(reaction); +} + void ChatMessageModel::onParticipantImdnStateChanged(const std::shared_ptr & message, const std::shared_ptr & state){ } diff --git a/linphone-app/src/components/chat-events/ChatMessageModel.hpp b/linphone-app/src/components/chat-events/ChatMessageModel.hpp index 88a61c1d0..bf51db599 100644 --- a/linphone-app/src/components/chat-events/ChatMessageModel.hpp +++ b/linphone-app/src/components/chat-events/ChatMessageModel.hpp @@ -33,6 +33,8 @@ class ChatMessageModel; class ChatMessageListener; +class ChatReactionModel; +class ChatReactionListModel; class ParticipantImdnStateProxyModel; class ParticipantImdnStateListModel; class ContentModel; @@ -93,6 +95,7 @@ public: Q_INVOKABLE ParticipantImdnStateProxyModel * getProxyImdnStates(); QSharedPointer getParticipantImdnStates() const; QSharedPointer getContents() const; + QSharedPointer getChatReactions() const; bool isReply() const; ChatMessageModel * getReplyChatMessageModel() const; @@ -110,6 +113,7 @@ public: //---------------------------------------------------------------------------- Q_INVOKABLE void resendMessage (); + Q_INVOKABLE void sendChatReaction(const QString& reaction); virtual void deleteEvent() override; void updateFileTransferInformation(); @@ -121,6 +125,7 @@ public: std::shared_ptr onFileTransferSend (const std::shared_ptr &,const std::shared_ptr &,size_t,size_t); void onFileTransferProgressIndication (const std::shared_ptr &message, const std::shared_ptr &, size_t offset, size_t); void onMsgStateChanged (const std::shared_ptr &message, linphone::ChatMessage::State state); + void onNewMessageReaction(const std::shared_ptr & message, const std::shared_ptr & reaction); void onParticipantImdnStateChanged(const std::shared_ptr & message, const std::shared_ptr & state); void onEphemeralMessageTimerStarted(const std::shared_ptr & message); void onEphemeralMessageDeleted(const std::shared_ptr & message); @@ -152,6 +157,7 @@ private: QSharedPointer mFileTransfertContent; QSharedPointer mParticipantImdnStateListModel; QSharedPointer mReplyChatMessageModel; + QSharedPointer mChatReactionListModel; QString mFromDisplayNameCache; QString fromDisplayNameReplyMessage; diff --git a/linphone-app/src/components/chat-reaction/ChatReactionListModel.cpp b/linphone-app/src/components/chat-reaction/ChatReactionListModel.cpp new file mode 100644 index 000000000..e35d25e0d --- /dev/null +++ b/linphone-app/src/components/chat-reaction/ChatReactionListModel.cpp @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ChatReactionListModel.hpp" + +#include "ChatReactionModel.hpp" +#include "components/chat-events/ChatMessageModel.hpp" +#include "utils/Utils.hpp" + +// ============================================================================= + +ChatReactionListModel::ChatReactionListModel (ChatMessageModel * message, QObject* parent) : ProxyAbstractListModel(parent) { + mParent = message; + setChatMessageModel(message); +} + +void ChatReactionListModel::setChatMessageModel(ChatMessageModel * message) { + if(message){ + auto reactions = message->getChatMessage()->getReactions(); + mReactions.clear(); + mBodies.clear(); + for(auto reaction : reactions){ + auto reactionModel = QSharedPointer::create(reaction); + auto body = reactionModel->getBody(); + if(!body.isEmpty()) { + mReactions[reactionModel->getFromAddress()] = reactionModel; + mBodies[reactionModel->getBody()].push_back(reactionModel); + } + } + updateList(); + } +} + +int ChatReactionListModel::count(){ + return mList.count(); +} + +int ChatReactionListModel::getChatReactionCount(const QString& emoji) const { + if(emoji.isEmpty()) + return mReactions.size(); + else if(mBodies.contains(emoji)) + return mBodies[emoji].size(); + else + return 0; +} + +QSharedPointer ChatReactionListModel::add(std::shared_ptr reaction){ + auto reactionModel = QSharedPointer::create(reaction); + //ProxyListModel::add(reactionModel); + emit chatReactionsChanged(); + return reactionModel; +} + + +void ChatReactionListModel::remove(ChatReactionModel * model){/* + int count = 0; + for(auto it = mList.begin() ; it != mList.end() ; ++count, ++it) { + if( it->get() == model) { + removeRow(count, QModelIndex()); + return; + } + }*/ +} + +void ChatReactionListModel::clear(){ + resetData(); +} + +/* +QSharedPointer ChatReactionListModel::getChatReactionModel(const std::shared_ptr& reaction){ + for(auto item : mList){ + auto c = item.objectCast(); + if(c->get() == content) + return c; + } + if(content->isFileTransfer() || content->isFile() || content->isFileEncrypted()){ + for(auto item : mList){// Content object can be different for file (like while data transfer) + auto c = item.objectCast(); + if(c->getContent()->getFilePath() == content->getFilePath()) + return c; + } + } + return nullptr; +} +*/ +void ChatReactionListModel::updateChatReaction(const std::shared_ptr& reaction) { + QString address = Utils::coreStringToAppString(reaction->getFromAddress()->asStringUriOnly()); + auto itReaction = mReactions.find(address); + int oldReactionCount = mReactions.size(); + if( itReaction == mReactions.end()) {// New + auto reactionModel = QSharedPointer::create(reaction); + auto body = reactionModel->getBody(); + if(body.isEmpty()) { + mReactions.remove(reactionModel->getFromAddress()); + // TODO: optimize remove + mBodies.clear(); + for(auto it : mReactions) + mBodies[it->getBody()].push_back(it); + }else{ + mReactions[reactionModel->getFromAddress()] = reactionModel; + mBodies[reactionModel->getBody()].push_back(reactionModel); + } + }else{// Update + (*itReaction)->setBody(Utils::coreStringToAppString(reaction->getBody())); + // TODO: optimize update with a swap + mBodies.clear(); + for(auto it : mReactions) + mBodies[it->getBody()].push_back(it); + } + updateList(); + if(oldReactionCount != mReactions.size()) + emit chatReactionCountChanged(); +} +void ChatReactionListModel::updateList(){ + QList data; + if(mGroupBy == EMOJIES){ + for(auto it = mBodies.begin() ; it != mBodies.end() ; ++it) { + QVariantMap emoji; + emoji["body"] = it.key(); + emoji["reactionsCount"] = it->size(); + data << emoji; + } + }else{ + for(auto reaction : mReactions){ + QVariantMap react; + react["reaction"] = QVariant::fromValue(reaction.get()); + data << react; + } + } + resetData(); + ProxyAbstractListModel::add(data); + emit chatReactionsChanged(); +} + +bool ChatReactionListModel::exists(std::shared_ptr reaction) const { + QString address = Utils::coreStringToAppString(reaction->getFromAddress()->asStringUriOnly()); + auto itReaction = mReactions.find(address); + if(itReaction != mReactions.end()) + return (*itReaction)->getBody() == Utils::coreStringToAppString(reaction->getBody()); + return false; +} + +void ChatReactionListModel::updateChatReaction(std::shared_ptr oldReaction, std::shared_ptr newReaction) { + + +} +void ChatReactionListModel::updateChatReaction(ChatMessageModel * messageModel) { + +} + +ChatReactionListModel::GROUP_BY_TYPE ChatReactionListModel::getGroupBy() const { + return mGroupBy; +} + +void ChatReactionListModel::setGroupBy(ChatReactionListModel::GROUP_BY_TYPE mode) { + if( mGroupBy != mode ) { + mGroupBy = mode; + updateList(); + emit groupByChanged(); + } +} + + +/* + +void ContentListModel::updateContent(std::shared_ptr oldContent, std::shared_ptr newContent){ + int row = 0; + for(auto content = mList.begin() ; content != mList.end() ; ++content, ++row){ + auto contentModel = content->objectCast(); + if( contentModel->getContent() == oldContent){ + mList.replace(row, QSharedPointer::create(newContent, contentModel->getChatMessageModel())); + emit dataChanged(index(row,0), index(row,0)); + return; + } + } +} + +void ContentListModel::updateContents(ChatMessageModel * messageModel){ + std::list> contents = messageModel->getChatMessage()->getContents() ; + int count = 0; + beginResetModel(); + for(auto content : contents){ + if( count >= mList.size()){// New content + mList.insert(count, QSharedPointer::create(content, messageModel)); + }else if(mList.at(count).objectCast()->getContent() != content){ // This content is not at its place + int c = count + 1; + while( c < mList.size() && mList.at(c).objectCast()->getContent() != content) + ++c; + if( c < mList.size()){// Found => swap position +#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) + mList.swap(count, c); +#else + mList.swapItemsAt(count, c); +#endif + }else{// content is new + mList.insert(count, QSharedPointer::create(content, messageModel)); + } + } + ++count; + } + if(count < mList.size())// Remove all old contents + mList.erase(mList.begin()+count, mList.end()); + endResetModel(); +} + +void ContentListModel::updateAllTransferData(){ + emit updateTransferDataRequested(); +} + +void ContentListModel::downloaded(){ + for(auto content : mList) + content.objectCast()->createThumbnail(); +}*/ diff --git a/linphone-app/src/components/chat-reaction/ChatReactionListModel.hpp b/linphone-app/src/components/chat-reaction/ChatReactionListModel.hpp new file mode 100644 index 000000000..e94f28b4d --- /dev/null +++ b/linphone-app/src/components/chat-reaction/ChatReactionListModel.hpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CHAT_REACTION_LIST_MODEL_H_ +#define CHAT_REACTION_LIST_MODEL_H_ + +#include +#include "app/proxyModel/ProxyAbstractListModel.hpp" +#include + +class ChatReactionModel; +class ChatMessageModel; + +class ChatReactionListModel : public ProxyAbstractListModel { + Q_OBJECT + +public: + typedef enum{ + EMOJIES, + REACTIONS + }GROUP_BY_TYPE; + Q_ENUM(GROUP_BY_TYPE) + + ChatReactionListModel (ChatMessageModel * message = nullptr, QObject * parent = nullptr); + void setChatMessageModel(ChatMessageModel * message); + + int count(); + int getChatReactionCount(const QString& emoji = "")const; + + QSharedPointer add(std::shared_ptr reaction); + Q_INVOKABLE void remove(ChatReactionModel * model); + + void clear(); + + ChatReactionListModel::GROUP_BY_TYPE getGroupBy() const; + void setGroupBy(ChatReactionListModel::GROUP_BY_TYPE mode); + + //QSharedPointer getChatReactionModel(const std::shared_ptr& reaction); + + bool exists(std::shared_ptr reaction) const; + + void updateChatReaction(std::shared_ptr oldReaction, std::shared_ptr newReaction); + void updateChatReaction(const std::shared_ptr& reaction); + void updateChatReaction(ChatMessageModel * messageModel); + + void updateList(); +signals: + void chatReactionsChanged(); + void chatReactionCountChanged(); + void groupByChanged(); + +private: + ChatMessageModel * mParent; + QMap> mReactions; + QMap>> mBodies; + GROUP_BY_TYPE mGroupBy = EMOJIES; + +}; + +Q_DECLARE_METATYPE(std::shared_ptr) + +#endif diff --git a/linphone-app/src/components/chat-reaction/ChatReactionModel.cpp b/linphone-app/src/components/chat-reaction/ChatReactionModel.cpp new file mode 100644 index 000000000..6d6150e33 --- /dev/null +++ b/linphone-app/src/components/chat-reaction/ChatReactionModel.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ChatReactionModel.hpp" + +#include "app/App.hpp" +#include "utils/Utils.hpp" + +#include + +ChatReactionModel::ChatReactionModel(const std::shared_ptr& reaction) { + App::getInstance()->getEngine()->setObjectOwnership(this, QQmlEngine::CppOwnership);// Avoid QML to destroy it when passing by Q_INVOKABLE + mBody = Utils::coreStringToAppString(reaction->getBody()); + mFromAddress = Utils::coreStringToAppString(reaction->getFromAddress()->asStringUriOnly()); +} + +QString ChatReactionModel::getBody() const { + return mBody; +} + +void ChatReactionModel::setBody(const QString& body) { + mBody = body; + emit bodyChanged(); +} + +QString ChatReactionModel::getFromAddress() const { + return mFromAddress; +} diff --git a/linphone-app/src/components/chat-reaction/ChatReactionModel.hpp b/linphone-app/src/components/chat-reaction/ChatReactionModel.hpp new file mode 100644 index 000000000..4135446e4 --- /dev/null +++ b/linphone-app/src/components/chat-reaction/ChatReactionModel.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CHAT_REACTION_MODEL_H_ +#define CHAT_REACTION_MODEL_H_ + +#include +#include +#include + +class ChatReactionModel : public QObject { + + Q_OBJECT + +public: + ChatReactionModel(const std::shared_ptr& reaction); + + Q_PROPERTY(QString body READ getBody WRITE setBody NOTIFY bodyChanged) + Q_PROPERTY(QString fromAddress READ getFromAddress CONSTANT) + + QString getBody() const; + void setBody(const QString& body); + + QString getFromAddress() const; + +signals: + void bodyChanged(); + +private: + QString mBody; + QString mFromAddress; +}; + +Q_DECLARE_METATYPE(QSharedPointer) + +#endif diff --git a/linphone-app/src/components/chat-reaction/ChatReactionProxyModel.cpp b/linphone-app/src/components/chat-reaction/ChatReactionProxyModel.cpp new file mode 100644 index 000000000..f001f09f7 --- /dev/null +++ b/linphone-app/src/components/chat-reaction/ChatReactionProxyModel.cpp @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ChatReactionProxyModel.hpp" + +#include "ChatReactionListModel.hpp" +#include "ChatReactionModel.hpp" + +// ============================================================================= + +ChatReactionProxyModel::ChatReactionProxyModel (QObject * parent) : SortFilterProxyModel(parent){ + mContents = QSharedPointer::create(); + connect(mContents.get(), &ChatReactionListModel::chatReactionCountChanged, this, &ChatReactionProxyModel::chatReactionCountChanged); + connect(mContents.get(), &ChatReactionListModel::groupByChanged, this, &ChatReactionProxyModel::groupByChanged); + setSourceModel(mContents.get()); + sort(0); +} + +ChatMessageModel * ChatReactionProxyModel::getChatMessageModel() const{ + return nullptr; +} + +void ChatReactionProxyModel::setChatMessageModel(ChatMessageModel * message){ + setChatMessageModel(message, ChatReactionListModel::GROUP_BY_TYPE::EMOJIES); +} + +void ChatReactionProxyModel::setChatMessageModel(ChatMessageModel * message, ChatReactionListModel::GROUP_BY_TYPE groupByMode) { + if(message){ + auto model = qobject_cast(sourceModel()); + model->setChatMessageModel(message); + model->setGroupBy(groupByMode); + } + emit chatMessageModelChanged(); + emit chatReactionCountChanged(); +} + +int ChatReactionProxyModel::getChatReactionCount() const { + auto model = qobject_cast(sourceModel()); + if(model) + return model->getChatReactionCount(); + else + return 0; +} + +int ChatReactionProxyModel::getChatReactionCount(const QString& emoji) const { + auto model = qobject_cast(sourceModel()); + if(model) + return model->getChatReactionCount(emoji); + else + return 0; +} + +ChatReactionListModel::GROUP_BY_TYPE ChatReactionProxyModel::getGroupBy() const { + auto model = qobject_cast(sourceModel()); + return model ? model->getGroupBy() : ChatReactionListModel::GROUP_BY_TYPE::EMOJIES; +} + +void ChatReactionProxyModel::setGroupBy(ChatReactionListModel::GROUP_BY_TYPE mode) { + auto model = qobject_cast(sourceModel()); + if(model) + model->setGroupBy(mode); +} + +QString ChatReactionProxyModel::getFilter() const { + return mFilter; +} + +void ChatReactionProxyModel::setFilter(const QString& filter) { + if(mFilter != filter) { + mFilter = filter; + emit filterChanged(); + invalidate(); + } +} + +/* +void ChatReactionProxyModel::setContentListModel(ContentListModel * model){ + setSourceModel(model); + sort(0); + emit chatMessageModelChanged(); +} +*/ + +bool ChatReactionProxyModel::filterAcceptsRow ( + int sourceRow, + const QModelIndex &sourceParent + ) const { + + bool show = false; + + if (mFilter.isEmpty()) + show = true; + else{ + auto model = qobject_cast(sourceModel()); + QModelIndex index = sourceModel()->index(sourceRow, 0, QModelIndex()); + auto reaction = sourceModel()->data(index).value(); + + if( model->getGroupBy() == ChatReactionListModel::GROUP_BY_TYPE::REACTIONS) { + if( mFilter == reaction["reaction"].value()->getBody()) + show = true; + } + } + return show; +} + +/* +bool ChatReactionProxyModel::lessThan (const QModelIndex &left, const QModelIndex &right) const { + const ContentModel *contentA = sourceModel()->data(left).value(); + const ContentModel *contentB = sourceModel()->data(right).value(); + bool aIsForward = contentA->getChatMessageModel() && contentA->getChatMessageModel()->isForward(); + bool aIsReply = contentA->getChatMessageModel() && contentA->getChatMessageModel()->isReply(); + bool aIsVoiceRecording = contentA->isVoiceRecording(); + bool aIsFile = contentA->isFile() || contentA->isFileEncrypted() || contentA->isFileTransfer(); + bool aIsText = contentA->isText() ; + bool bIsForward = contentB->getChatMessageModel() && contentB->getChatMessageModel()->isForward(); + bool bIsReply = contentB->getChatMessageModel() && contentB->getChatMessageModel()->isReply(); + bool bIsVoiceRecording = contentB->isVoiceRecording(); + bool bIsFile = contentB->isFile() || contentB->isFileEncrypted() || contentB->isFileTransfer(); + bool bIsText = contentB->isText() ; + + return !bIsForward && (aIsForward + || !bIsReply && (aIsReply + || !bIsVoiceRecording && (aIsVoiceRecording + || !bIsFile && (aIsFile + || aIsText && !bIsText + ) + ) + ) + ); +} +*/ +/* +void ChatReactionProxyModel::remove(ContentModel * model){ + qobject_cast(sourceModel())->remove(model); +} + +void ChatReactionProxyModel::clear(){ + qobject_cast(sourceModel())->clear(); +} + +ContentProxyModel::FilterContentType ChatReactionProxyModel::getFilter() const{ + return mFilter; +} +void ChatReactionProxyModel::setFilter(const FilterContentType& contentType){ + if(contentType != mFilter){ + mFilter = contentType; + emit filterChanged(); + invalidate(); + } +}*/ \ No newline at end of file diff --git a/linphone-app/src/components/chat-reaction/ChatReactionProxyModel.hpp b/linphone-app/src/components/chat-reaction/ChatReactionProxyModel.hpp new file mode 100644 index 000000000..76e3a001b --- /dev/null +++ b/linphone-app/src/components/chat-reaction/ChatReactionProxyModel.hpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CHAT_REACTION_PROXY_MODEL_H_ +#define CHAT_REACTION_PROXY_MODEL_H_ + +#include "app/proxyModel/SortFilterProxyModel.hpp" +#include "components/chat-events/ChatMessageModel.hpp" +#include "ChatReactionListModel.hpp" + +// ============================================================================= + +class ChatReactionProxyModel : public SortFilterProxyModel { + Q_OBJECT + +public: + ChatReactionProxyModel (QObject *parent = nullptr); + Q_PROPERTY(ChatMessageModel * chatMessageModel READ getChatMessageModel WRITE setChatMessageModel NOTIFY chatMessageModelChanged) + Q_PROPERTY(int reactionCount READ getChatReactionCount NOTIFY chatReactionCountChanged) + Q_PROPERTY(ChatReactionListModel::GROUP_BY_TYPE groupBy READ getGroupBy WRITE setGroupBy NOTIFY groupByChanged) + Q_PROPERTY(QString filter READ getFilter WRITE setFilter NOTIFY filterChanged) + /* + Q_PROPERTY(FilterContentType filter READ getFilter WRITE setFilter NOTIFY filterChanged) + + enum FilterContentType { + All, + File, + Text, + Voice, + Conference, + Unknown + }; + Q_ENUM(FilterContentType) + */ + ChatMessageModel * getChatMessageModel() const; + void setChatMessageModel(ChatMessageModel * message); + Q_INVOKABLE void setChatMessageModel(ChatMessageModel * message, ChatReactionListModel::GROUP_BY_TYPE groupByMode); + + int getChatReactionCount() const; + Q_INVOKABLE int getChatReactionCount(const QString& emoji) const; + + ChatReactionListModel::GROUP_BY_TYPE getGroupBy() const; + void setGroupBy(ChatReactionListModel::GROUP_BY_TYPE mode); + + QString getFilter() const; + void setFilter(const QString& filter); + //Q_INVOKABLE void setContentListModel(ContentListModel * model); + //Q_INVOKABLE void addFile(const QString& path); + //Q_INVOKABLE void remove(ContentModel * model); + //Q_INVOKABLE void clear(); + +signals: + void chatMessageModelChanged(); + void chatReactionCountChanged(); + void groupByChanged(); + void filterChanged(); + + +protected: + QSharedPointer mContents; + virtual bool filterAcceptsRow (int sourceRow, const QModelIndex &sourceParent) const override; + //virtual bool lessThan (const QModelIndex &left, const QModelIndex &right) const override; + + //std::shared_ptr mContents; + QString mFilter = ""; +}; + + +#endif diff --git a/linphone-app/src/components/chat-room/ChatRoomModel.cpp b/linphone-app/src/components/chat-room/ChatRoomModel.cpp index 09d8163f0..46b4c02ee 100644 --- a/linphone-app/src/components/chat-room/ChatRoomModel.cpp +++ b/linphone-app/src/components/chat-room/ChatRoomModel.cpp @@ -867,24 +867,39 @@ void ChatRoomModel::updateNewMessageNotice(const int& count){ int ChatRoomModel::loadTillMessage(ChatMessageModel * message){ if( message){ - qDebug() << "Load history till message : " << message->getChatMessage()->getMessageId().c_str(); auto linphoneMessage = message->getChatMessage(); + return loadTillMessage(message); + }else + return -1; +} + +int ChatRoomModel::loadTillMessage(std::shared_ptr linphoneMessage){ + if(linphoneMessage) + return loadTillMessageId(Utils::coreStringToAppString(linphoneMessage->getMessageId())); + else + return -1; +} + +int ChatRoomModel::loadTillMessageId(const QString& messageId){ + if(!messageId.isEmpty()){ + std::string lMessageId = Utils::appStringToCoreString(messageId); + qDebug() << "Load history till message : " << messageId; // First find on current list - auto entry = std::find_if(mList.begin(), mList.end(), [linphoneMessage](const QSharedPointer& entry ){ + auto entry = std::find_if(mList.begin(), mList.end(), [lMessageId](const QSharedPointer& entry ){ auto chatEventEntry = entry.objectCast(); - return chatEventEntry->mType == ChatRoomModel::EntryType::MessageEntry && chatEventEntry.objectCast()->getChatMessage() == linphoneMessage; + return chatEventEntry->mType == ChatRoomModel::EntryType::MessageEntry && chatEventEntry.objectCast()->getChatMessage()->getMessageId() == lMessageId; }); // if not find, load more entries and find it in new entries. if( entry == mList.end()){ mPostModelChangedEvents = false; beginResetModel(); int newEntries = loadMoreEntries(); - while( newEntries > 0){// no more new entries + while( newEntries > 0){// at 0 = no more new entries int entryCount = 0; entry = mList.begin(); auto chatEventEntry = entry->objectCast(); while(entryCount < newEntries && - (chatEventEntry->mType != ChatRoomModel::EntryType::MessageEntry || chatEventEntry.objectCast()->getChatMessage() != linphoneMessage) + (chatEventEntry->mType != ChatRoomModel::EntryType::MessageEntry || chatEventEntry.objectCast()->getChatMessage()->getMessageId() != lMessageId) ){ ++entryCount; ++entry; @@ -892,36 +907,43 @@ int ChatRoomModel::loadTillMessage(ChatMessageModel * message){ chatEventEntry = entry->objectCast(); } if( entryCount < newEntries){// We got it - qDebug() << "Find message at " << entryCount << " after loading new entries"; + qDebug() << "Find message at " << entryCount << " after loading new entries " << mEntriesLoading; mPostModelChangedEvents = true; endResetModel(); + emit tillMessagesLoaded(entryCount); return entryCount; }else newEntries = loadMoreEntries();// continue } mPostModelChangedEvents = true; endResetModel(); + emit tillMessagesLoaded(newEntries); }else{ int entryCount = entry - mList.begin(); qDebug() << "Find message at " << entryCount; + emit tillMessagesLoaded(entryCount); return entryCount; } - qWarning() << "Message has not been found in history"; } + qWarning() << "Message has not been found in history"; return -1; } +QSharedPointer ChatRoomModel::getChatMessageModel(const std::shared_ptr message) const { + auto entry = std::find_if(mList.begin(), mList.end(), [message](const QSharedPointer& entry ){ + auto chatEventEntry = entry.objectCast(); + return chatEventEntry->mType == ChatRoomModel::EntryType::MessageEntry && chatEventEntry.objectCast()->getChatMessage() == message; + }); + return entry != mList.end() ? entry->objectCast(): nullptr; +} + bool ChatRoomModel::isTerminated(const std::shared_ptr& chatRoom){ return chatRoom->getState() == linphone::ChatRoom::State::Terminated || chatRoom->getState() == linphone::ChatRoom::State::Deleted; } bool ChatRoomModel::exists(const std::shared_ptr message) const{ - auto entry = std::find_if(mList.begin(), mList.end(), [message](const QSharedPointer& entry ){ - auto chatEventEntry = entry.objectCast(); - return chatEventEntry->mType == ChatRoomModel::EntryType::MessageEntry && chatEventEntry.objectCast()->getChatMessage() == message; - }); // if not find, load more entries and find it in new entries. - return entry != mList.end(); + return getChatMessageModel(message) != nullptr; } void ChatRoomModel::addBindingCall(){ // If a call is binding to this chat room, we avoid cleaning data (Add=+1, remove=-1) diff --git a/linphone-app/src/components/chat-room/ChatRoomModel.hpp b/linphone-app/src/components/chat-room/ChatRoomModel.hpp index c24f0a96c..7b8286b8f 100644 --- a/linphone-app/src/components/chat-room/ChatRoomModel.hpp +++ b/linphone-app/src/components/chat-room/ChatRoomModel.hpp @@ -184,7 +184,10 @@ public: Q_INVOKABLE int loadMoreEntries(); // return new entries count void onCallEnded(std::shared_ptr call); void updateNewMessageNotice(const int& count); + QSharedPointer getChatMessageModel(const std::shared_ptr message) const; Q_INVOKABLE int loadTillMessage(ChatMessageModel * message);// Load all entries till message and return its index. -1 if not found. + Q_INVOKABLE int loadTillMessageId(const QString& messageId); + int loadTillMessage(std::shared_ptr linphoneMessage); static bool isTerminated(const std::shared_ptr& chatRoom); bool exists(const std::shared_ptr message) const; @@ -256,9 +259,11 @@ signals: bool isRemoteComposingChanged (); void entriesLoadingChanged(const bool& loading); void moreEntriesLoaded(const int& count); + void tillMessagesLoaded(int messageIndex); void allEntriesRemoved (QSharedPointer model); void lastEntryRemoved (); + void displayMessageIdRequested(const QString& messageId); void messageSent (const std::shared_ptr &message); void messageReceived (const std::shared_ptr &message); diff --git a/linphone-app/src/components/chat-room/ChatRoomProxyModel.cpp b/linphone-app/src/components/chat-room/ChatRoomProxyModel.cpp index 47c7b8c5e..602961b7d 100644 --- a/linphone-app/src/components/chat-room/ChatRoomProxyModel.cpp +++ b/linphone-app/src/components/chat-room/ChatRoomProxyModel.cpp @@ -126,6 +126,11 @@ void ChatRoomProxyModel::loadMoreEntriesAsync(){ void ChatRoomProxyModel::onMoreEntriesLoaded(const int& count){ emit moreEntriesLoaded(count); } + +void ChatRoomProxyModel::onTillMessagesLoaded(const int& index){ + int messageIndex = mapFromSource(static_cast(sourceModel())->index(index, 0)).row(); + emit moreEntriesLoaded(messageIndex); +} void ChatRoomProxyModel::loadMoreEntries() { if(mChatRoomModel ) { mChatRoomModel->loadMoreEntries(); @@ -283,7 +288,9 @@ void ChatRoomProxyModel::reload (ChatRoomModel *chatRoomModel) { QObject::disconnect(ChatRoomModel, &ChatRoomModel::messageSent, this, &ChatRoomProxyModel::handleMessageSent); QObject::disconnect(ChatRoomModel, &ChatRoomModel::markAsReadEnabledChanged, this, &ChatRoomProxyModel::markAsReadEnabledChanged); QObject::disconnect(ChatRoomModel, &ChatRoomModel::moreEntriesLoaded, this, &ChatRoomProxyModel::onMoreEntriesLoaded); + QObject::disconnect(ChatRoomModel, &ChatRoomModel::tillMessagesLoaded, this, &ChatRoomProxyModel::onTillMessagesLoaded); QObject::disconnect(ChatRoomModel, &ChatRoomModel::chatRoomDeleted, this, &ChatRoomProxyModel::chatRoomDeleted); + QObject::disconnect(ChatRoomModel, &ChatRoomModel::displayMessageIdRequested, this, &ChatRoomProxyModel::displayMessageIdRequested); if(mIsCall) mChatRoomModel->removeBindingCall(); } @@ -300,7 +307,9 @@ void ChatRoomProxyModel::reload (ChatRoomModel *chatRoomModel) { QObject::connect(ChatRoomModel, &ChatRoomModel::messageSent, this, &ChatRoomProxyModel::handleMessageSent); QObject::connect(ChatRoomModel, &ChatRoomModel::markAsReadEnabledChanged, this, &ChatRoomProxyModel::markAsReadEnabledChanged); QObject::connect(ChatRoomModel, &ChatRoomModel::moreEntriesLoaded, this, &ChatRoomProxyModel::onMoreEntriesLoaded); + QObject::connect(ChatRoomModel, &ChatRoomModel::tillMessagesLoaded, this, &ChatRoomProxyModel::onTillMessagesLoaded); QObject::connect(ChatRoomModel, &ChatRoomModel::chatRoomDeleted, this, &ChatRoomProxyModel::chatRoomDeleted); + QObject::connect(ChatRoomModel, &ChatRoomModel::displayMessageIdRequested, this, &ChatRoomProxyModel::displayMessageIdRequested); mChatRoomModel->initEntries();// This way, we don't load huge chat rooms (that lead to freeze GUI) } setSourceModel(mChatRoomModel.get()); @@ -336,9 +345,17 @@ int ChatRoomProxyModel::loadTillMessage(ChatMessageModel * message){ return messageIndex; } +int ChatRoomProxyModel::loadTillMessageId(const QString& messageId){ + int messageIndex = mChatRoomModel->loadTillMessageId(messageId); + if( messageIndex>= 0 ) { + messageIndex = mapFromSource(static_cast(sourceModel())->index(messageIndex, 0)).row(); + } + qDebug() << "Message index from chat room proxy : " << messageIndex; + return messageIndex; +} + ChatRoomModel *ChatRoomProxyModel::getChatRoomModel () const{ return mChatRoomModel.get(); - } void ChatRoomProxyModel::setChatRoomModel (ChatRoomModel *chatRoomModel){ diff --git a/linphone-app/src/components/chat-room/ChatRoomProxyModel.hpp b/linphone-app/src/components/chat-room/ChatRoomProxyModel.hpp index d5e3f647a..7fe6f9c59 100644 --- a/linphone-app/src/components/chat-room/ChatRoomProxyModel.hpp +++ b/linphone-app/src/components/chat-room/ChatRoomProxyModel.hpp @@ -74,9 +74,11 @@ public: Q_INVOKABLE void resetMessageCount(); Q_INVOKABLE int loadTillMessage(ChatMessageModel * message);// Load all entries till message and return its index in displayed list (-1 if not found) + Q_INVOKABLE int loadTillMessageId(const QString& messageId); public slots: void onMoreEntriesLoaded(const int& count); + void onTillMessagesLoaded(const int& messageIndex); signals: void peerAddressChanged (const QString &peerAddress); @@ -91,6 +93,8 @@ signals: void chatRoomDeleted(); void moreEntriesLoaded (int n); + void tillMessagesLoaded(int messageIndex); + void displayMessageIdRequested(const QString& messageId); void entryTypeFilterChanged (int type); void filterTextChanged(); diff --git a/linphone-app/src/components/core/CoreHandlers.cpp b/linphone-app/src/components/core/CoreHandlers.cpp index 14068247b..357a68a42 100644 --- a/linphone-app/src/components/core/CoreHandlers.cpp +++ b/linphone-app/src/components/core/CoreHandlers.cpp @@ -62,6 +62,7 @@ void CoreHandlers::connectTo(CoreListener * listener){ connect(listener, &CoreListener::logCollectionUploadProgressIndication, this, &CoreHandlers::onLogCollectionUploadProgressIndication); connect(listener, &CoreListener::messageReceived, this, &CoreHandlers::onMessageReceived); connect(listener, &CoreListener::messagesReceived, this, &CoreHandlers::onMessagesReceived); + connect(listener, &CoreListener::newMessageReaction, this, &CoreHandlers::onNewMessageReaction); connect(listener, &CoreListener::notifyPresenceReceivedForUriOrTel, this, &CoreHandlers::onNotifyPresenceReceivedForUriOrTel); connect(listener, &CoreListener::notifyPresenceReceived, this, &CoreHandlers::onNotifyPresenceReceived); connect(listener, &CoreListener::qrcodeFound, this, &CoreHandlers::onQrcodeFound); @@ -318,6 +319,56 @@ void CoreHandlers::onMessagesReceived ( } } +void CoreHandlers::onNewMessageReaction(const std::shared_ptr & core, const std::shared_ptr & chatRoom, const std::shared_ptr & message, const std::shared_ptr & reaction){ + QList, std::shared_ptr >> reactionsToNotify; + CoreManager *coreManager = CoreManager::getInstance(); + SettingsModel *settingsModel = coreManager->getSettingsModel(); + const App *app = App::getInstance(); + QStringList notNotifyReasons; + QSettings appSettings; + + appSettings.beginGroup("chatrooms"); + + if( !message || CoreManager::getInstance()->getAccountSettingsModel()->findAccount(reaction->getFromAddress())) + return; + // 1. Do not notify if chat is not activated. + if (chatRoom->getCurrentParams()->getEncryptionBackend() == linphone::ChatRoom::EncryptionBackend::None && !settingsModel->getStandardChatEnabled() + || chatRoom->getCurrentParams()->getEncryptionBackend() != linphone::ChatRoom::EncryptionBackend::None && !settingsModel->getSecureChatEnabled()) + return; + + // 2. Do not notify if the chatroom's notification has been deactivated. + appSettings.beginGroup(ChatRoomModel::getChatRoomId(chatRoom)); + if(!appSettings.value("notifications", true).toBool()){ + appSettings.endGroup(); + return; + }else{ + appSettings.endGroup(); + } + // 3. Notify with Notification popup. + if (coreManager->getSettingsModel()->getChatNotificationsEnabled() + && (!app->hasFocus() || !Utils::isMe(chatRoom->getLocalAddress()))) + reactionsToNotify.push_back({message, reaction}); + else{ + notNotifyReasons.push_back( + "NotifEnabled=" + QString::number(coreManager->getSettingsModel()->getChatNotificationsEnabled()) + +" focus=" +QString::number(app->hasFocus()) + +" isMe=" +QString::number(Utils::isMe(chatRoom->getLocalAddress())) + ); + } + if( reactionsToNotify.size() > 0) + app->getNotifier()->notifyReceivedReactions(reactionsToNotify); + else if( notNotifyReasons.size() > 0) + qInfo() << "Notification received but was not selected to popup. Reasons : \n" << notNotifyReasons.join("\n"); + // 3. Notify with sound. + if( reactionsToNotify.size() > 0) { + if (!coreManager->getSettingsModel()->getChatNotificationsEnabled() || !settingsModel->getChatNotificationSoundEnabled()) + return; + + if ( !app->hasFocus() || !CoreManager::getInstance()->getTimelineListModel()->getChatRoomModel(chatRoom, false) ) + core->playLocal(Utils::appStringToCoreString(settingsModel->getChatNotificationSoundPath())); + } +} + void CoreHandlers::onNotifyPresenceReceivedForUriOrTel ( const shared_ptr &, const shared_ptr &, diff --git a/linphone-app/src/components/core/CoreHandlers.hpp b/linphone-app/src/components/core/CoreHandlers.hpp index ca131d689..90f3be1d0 100644 --- a/linphone-app/src/components/core/CoreHandlers.hpp +++ b/linphone-app/src/components/core/CoreHandlers.hpp @@ -53,6 +53,7 @@ signals: void isComposingChanged (const std::shared_ptr &chatRoom); void logsUploadStateChanged (linphone::Core::LogCollectionUploadState state, const std::string &info); void messagesReceived (const std::list> &messages); + void newMessageReaction(const std::shared_ptr & chatRoom, const std::shared_ptr & message, const std::shared_ptr & reaction); void presenceReceived (const QString &sipAddress, const std::shared_ptr &presenceModel); void presenceStatusReceived(std::shared_ptr contact); void registrationStateChanged (const std::shared_ptr &account, linphone::RegistrationState state); @@ -82,6 +83,7 @@ public slots: void onLogCollectionUploadProgressIndication (const std::shared_ptr &lc,size_t offset,size_t total); void onMessageReceived (const std::shared_ptr &core,const std::shared_ptr &room,const std::shared_ptr &message); void onMessagesReceived (const std::shared_ptr &core,const std::shared_ptr &room,const std::list> &messages); + void onNewMessageReaction(const std::shared_ptr & core, const std::shared_ptr & chatRoom, const std::shared_ptr & message, const std::shared_ptr & reaction); void onNotifyPresenceReceivedForUriOrTel (const std::shared_ptr &core,const std::shared_ptr &linphoneFriend,const std::string &uriOrTel,const std::shared_ptr &presenceModel); void onNotifyPresenceReceived (const std::shared_ptr &core,const std::shared_ptr &linphoneFriend); void onQrcodeFound(const std::shared_ptr & core, const std::string & result); diff --git a/linphone-app/src/components/core/CoreListener.cpp b/linphone-app/src/components/core/CoreListener.cpp index 5a7154635..87ad06770 100644 --- a/linphone-app/src/components/core/CoreListener.cpp +++ b/linphone-app/src/components/core/CoreListener.cpp @@ -81,6 +81,9 @@ void CoreListener::onMessageReceived (const std::shared_ptr &cor void CoreListener::onMessagesReceived (const std::shared_ptr &core,const std::shared_ptr &room,const std::list> &messages){ emit messagesReceived (core,room,messages); } +void CoreListener::onNewMessageReaction(const std::shared_ptr & core, const std::shared_ptr & chatRoom, const std::shared_ptr & message, const std::shared_ptr & reaction){ + emit newMessageReaction (core,chatRoom,message, reaction); +} void CoreListener::onNotifyPresenceReceivedForUriOrTel (const std::shared_ptr &core,const std::shared_ptr &linphoneFriend,const std::string &uriOrTel,const std::shared_ptr &presenceModel){ emit notifyPresenceReceivedForUriOrTel (core,linphoneFriend,uriOrTel,presenceModel); } diff --git a/linphone-app/src/components/core/CoreListener.hpp b/linphone-app/src/components/core/CoreListener.hpp index 6bc8c6db4..705c17c4a 100644 --- a/linphone-app/src/components/core/CoreListener.hpp +++ b/linphone-app/src/components/core/CoreListener.hpp @@ -52,6 +52,7 @@ public: virtual void onLogCollectionUploadProgressIndication (const std::shared_ptr &lc,size_t offset,size_t total) override; virtual void onMessageReceived (const std::shared_ptr &core,const std::shared_ptr &room,const std::shared_ptr &message) override; virtual void onMessagesReceived (const std::shared_ptr &core,const std::shared_ptr &room,const std::list> &messages) override; + virtual void onNewMessageReaction(const std::shared_ptr & core, const std::shared_ptr & chatRoom, const std::shared_ptr & message, const std::shared_ptr & reaction) override; virtual void onNotifyPresenceReceivedForUriOrTel (const std::shared_ptr &core,const std::shared_ptr &linphoneFriend,const std::string &uriOrTel,const std::shared_ptr &presenceModel) override; virtual void onNotifyPresenceReceived (const std::shared_ptr &core,const std::shared_ptr &linphoneFriend) override; virtual void onQrcodeFound(const std::shared_ptr & core, const std::string & result) override; @@ -78,6 +79,7 @@ signals: void logCollectionUploadProgressIndication (const std::shared_ptr &lc,size_t offset,size_t total); void messageReceived (const std::shared_ptr &core,const std::shared_ptr &room,const std::shared_ptr &message); void messagesReceived (const std::shared_ptr &core,const std::shared_ptr &room,const std::list> &messages); + void newMessageReaction(const std::shared_ptr & core, const std::shared_ptr & chatRoom, const std::shared_ptr & message, const std::shared_ptr & reaction); void notifyPresenceReceivedForUriOrTel (const std::shared_ptr &core,const std::shared_ptr &linphoneFriend,const std::string &uriOrTel,const std::shared_ptr &presenceModel); void notifyPresenceReceived (const std::shared_ptr &core,const std::shared_ptr &linphoneFriend); void qrcodeFound(const std::shared_ptr & core, const std::string & result); diff --git a/linphone-app/src/components/notifier/Notifier.cpp b/linphone-app/src/components/notifier/Notifier.cpp index ab8856624..7937f7dfa 100644 --- a/linphone-app/src/components/notifier/Notifier.cpp +++ b/linphone-app/src/components/notifier/Notifier.cpp @@ -28,6 +28,7 @@ #include "app/App.hpp" #include "components/call/CallModel.hpp" +#include "components/chat-events/ChatMessageModel.hpp" #include "components/core/CoreManager.hpp" #include "components/timeline/TimelineModel.hpp" #include "components/timeline/TimelineListModel.hpp" @@ -312,6 +313,49 @@ void Notifier::notifyReceivedMessages (const list, std::shared_ptr>> &reactions) { + QVariantMap map; + QString txt; + + if( reactions.size() > 0){ + ChatMessageModel *redirection = nullptr; + QPair,std::shared_ptr> reaction = reactions.front(); + shared_ptr message = reaction.first; + shared_ptr chatRoom(message->getChatRoom()); + auto timelineModel = CoreManager::getInstance()->getTimelineListModel()->getTimeline(chatRoom, true); + map["messageId"] = Utils::coreStringToAppString(message->getMessageId()); + if( reactions.size() == 1){ + QString messageTxt; + auto fileContent = message->getFileTransferInformation(); + if(!fileContent ){ + foreach(auto content, message->getContents()){ + if(content->isText()) + messageTxt += content->getUtf8Text().c_str(); + } + }else if( fileContent->isVoiceRecording()) + messageTxt += "Voice message"; + else + messageTxt += "File"; + if(messageTxt.isEmpty() && message->hasConferenceInvitationContent()) + messageTxt += "Conference invitation"; + txt = QString("Has reacted by %2 to: %3").arg(Utils::coreStringToAppString(reaction.second->getBody())).arg(messageTxt); + + }else + txt = "New message reactions received"; + map["message"] = txt; + + map["timelineModel"].setValue(timelineModel.get()); + if( reactions.size() == 1) {// Display only sender on mono message. + map["peerAddress"] = Utils::coreStringToAppString(reaction.second->getFromAddress()->asStringUriOnly()); + map["fullPeerAddress"] = Utils::coreStringToAppString(reaction.second->getFromAddress()->asString()); + } + map["localAddress"] = Utils::coreStringToAppString(chatRoom->getLocalAddress()->asStringUriOnly()); + map["fullLocalAddress"] = Utils::coreStringToAppString(chatRoom->getLocalAddress()->asString()); + map["window"].setValue(App::getInstance()->getMainWindow()); + CREATE_NOTIFICATION(Notifier::ReceivedMessage, map) + } +} + void Notifier::notifyReceivedFileMessage (const shared_ptr &message, const shared_ptr &content) { QVariantMap map; shared_ptr chatRoom(message->getChatRoom()); diff --git a/linphone-app/src/components/notifier/Notifier.hpp b/linphone-app/src/components/notifier/Notifier.hpp index 3748accd7..655c41ef8 100644 --- a/linphone-app/src/components/notifier/Notifier.hpp +++ b/linphone-app/src/components/notifier/Notifier.hpp @@ -55,6 +55,7 @@ public: }; void notifyReceivedMessages (const std::list> &messages); + void notifyReceivedReactions(const QList, std::shared_ptr>> &reactions); void notifyReceivedFileMessage (const std::shared_ptr &message, const std::shared_ptr &content); void notifyReceivedCall (const std::shared_ptr &call); void notifyNewVersionAvailable (const QString &version, const QString &url); diff --git a/linphone-app/src/components/other/colors/ColorListModel.hpp b/linphone-app/src/components/other/colors/ColorListModel.hpp index 3ca9d7bdf..eb6612290 100644 --- a/linphone-app/src/components/other/colors/ColorListModel.hpp +++ b/linphone-app/src/components/other/colors/ColorListModel.hpp @@ -309,6 +309,9 @@ class ColorListModel : public ProxyListModel { ADD_COLOR_WITH_ALPHA("i", 30, "") ADD_COLOR_WITH_ALPHA("j", 50, "") ADD_COLOR_WITH_ALPHA("j", 90, "") + ADD_COLOR_WITH_ALPHA("l", 10, "") + ADD_COLOR_WITH_ALPHA("l", 20, "") + ADD_COLOR_WITH_ALPHA("l", 30, "") ADD_COLOR_WITH_ALPHA("l", 50, "") ADD_COLOR_WITH_ALPHA("l", 80, "") ADD_COLOR_WITH_ALPHA("q", 50, "") diff --git a/linphone-app/src/components/timeline/TimelineModel.hpp b/linphone-app/src/components/timeline/TimelineModel.hpp index 5999ceeb4..8b58bc8ac 100644 --- a/linphone-app/src/components/timeline/TimelineModel.hpp +++ b/linphone-app/src/components/timeline/TimelineModel.hpp @@ -30,8 +30,7 @@ #include -#include "../contact/ContactModel.hpp" - +class ChatMessageModel; class ChatRoomModel; class ChatRoomListener; class TimelineListModel; diff --git a/linphone-app/ui/modules/Common/Form/Tab/TabButton.qml b/linphone-app/ui/modules/Common/Form/Tab/TabButton.qml index e6a0510b3..587485ecb 100644 --- a/linphone-app/ui/modules/Common/Form/Tab/TabButton.qml +++ b/linphone-app/ui/modules/Common/Form/Tab/TabButton.qml @@ -14,45 +14,53 @@ Controls.TabButton { property int iconSize: TabButtonStyle.icon.size property string iconName + property alias textFont: textItem.font - readonly property bool _isSelected: parent.parent.currentItem === button + readonly property bool isSelected: parent.parent.currentItem === button + property bool displaySelector: false + property bool stretchContent: true + property QtObject style: TabButtonStyle.menu // --------------------------------------------------------------------------- function _getBackgroundColor () { - if (_isSelected) { - return TabButtonStyle.backgroundColor.selected.color + if(!button.style) + return '' + if (isSelected) { + return button.style.backgroundColor.selected.color } return button.enabled ? ( button.down - ? TabButtonStyle.backgroundColor.pressed.color + ? button.style.backgroundColor.pressed.color : ( button.hovered - ? TabButtonStyle.backgroundColor.hovered.color - : TabButtonStyle.backgroundColor.normal.color + ? button.style.backgroundColor.hovered.color + : button.style.backgroundColor.normal.color ) ) - : TabButtonStyle.backgroundColor.disabled.color + : button.style.backgroundColor.disabled.color } function _getTextColor () { - if (_isSelected) { - return TabButtonStyle.text.color.selected.color + if(!button.style) + return '' + if (isSelected) { + return button.style.text.color.selected.color } return button.enabled ? ( button.down - ? TabButtonStyle.text.color.pressed.color + ? button.style.text.color.pressed.color : ( button.hovered - ? TabButtonStyle.text.color.hovered.color - : TabButtonStyle.text.color.normal.color + ? button.style.text.color.hovered.color + : button.style.text.color.normal.color ) ) - : TabButtonStyle.text.color.disabled.color + : button.style.text.color.disabled.color } // --------------------------------------------------------------------------- @@ -60,22 +68,35 @@ Controls.TabButton { background: Rectangle { color: _getBackgroundColor() implicitHeight: TabButtonStyle.text.height + Rectangle{ + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 2 + visible: button.displaySelector && button.isSelected + color: button.style ? button.style.selector.color : '' + } } contentItem: RowLayout { height: button.height width: button.width spacing: TabButtonStyle.spacing - + Item{ + Layout.fillWidth: visible + Layout.fillHeight: true + visible: !button.stretchContent + } Icon { id: icon Layout.fillHeight: true - Layout.leftMargin: TabButtonStyle.text.leftPadding + Layout.leftMargin: visible ? TabButtonStyle.text.leftPadding : 0 overwriteColor: textItem.color icon: button.iconName - iconSize: button.iconSize + iconSize: visible ? button.iconSize : 0 + visible: button.iconName } Text { @@ -93,11 +114,18 @@ Controls.TabButton { elide: Text.ElideRight height: parent.height + horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter rightPadding: TabButtonStyle.text.rightPadding text: button.text + textFormat: Text.RichText + } + Item{ + Layout.fillWidth: visible + Layout.fillHeight: true + visible: !button.stretchContent } } diff --git a/linphone-app/ui/modules/Common/Styles/Form/Tab/TabButtonStyle.qml b/linphone-app/ui/modules/Common/Styles/Form/Tab/TabButtonStyle.qml index 9936ec716..37c400789 100644 --- a/linphone-app/ui/modules/Common/Styles/Form/Tab/TabButtonStyle.qml +++ b/linphone-app/ui/modules/Common/Styles/Form/Tab/TabButtonStyle.qml @@ -7,17 +7,7 @@ import ColorsList 1.0 // ============================================================================= QtObject { - property string sectionName: 'TabButton' property int spacing: 8 - - property QtObject backgroundColor: QtObject { - property var disabled: ColorsList.add(sectionName+'_bg_d', 'i30') - property var hovered: ColorsList.add(sectionName+'_bg_h', 'b') - property var normal: ColorsList.add(sectionName+'_bg_n', 'i') - property var pressed: ColorsList.add(sectionName+'_bg_p', 'm') - property var selected: ColorsList.add(sectionName+'_bg_c', 'k') - } - property QtObject icon: QtObject { property int size: 20 property string sipAccountsIcon: 'settings_sip_accounts_custom' @@ -27,19 +17,58 @@ QtObject { property string networkIcon: 'settings_network_custom' property string advancedIcon: 'settings_advanced_custom' } - property QtObject text: QtObject { property int pointSize: Units.dp * 9 property int height: 40 property int leftPadding: 10 property int rightPadding: 10 + } + + property QtObject menu: QtObject{ + property string sectionName: 'TabButtonMenu' - property QtObject color: QtObject { - property var disabled: ColorsList.add(sectionName+'_text_d', 'q') - property var hovered: ColorsList.add(sectionName+'_text_h', 'q') - property var normal: ColorsList.add(sectionName+'_text_n', 'q') - property var pressed: ColorsList.add(sectionName+'_text_p', 'q') - property var selected: ColorsList.add(sectionName+'_text_c', 'i') + property QtObject backgroundColor: QtObject { + property var disabled: ColorsList.add(menu.sectionName+'_bg_d', 'i30') + property var hovered: ColorsList.add(menu.sectionName+'_bg_h', 'b') + property var normal: ColorsList.add(menu.sectionName+'_bg_n', 'i') + property var pressed: ColorsList.add(menu.sectionName+'_bg_p', 'm') + property var selected: ColorsList.add(menu.sectionName+'_bg_c', 'k') } + + property QtObject text: QtObject { + + property QtObject color: QtObject { + property var disabled: ColorsList.add(menu.sectionName+'_text_d', 'q') + property var hovered: ColorsList.add(menu.sectionName+'_text_h', 'q') + property var normal: ColorsList.add(menu.sectionName+'_text_n', 'q') + property var pressed: ColorsList.add(menu.sectionName+'_text_p', 'q') + property var selected: ColorsList.add(menu.sectionName+'_text_c', 'i') + } + } + property var selector: ColorsList.add(menu.sectionName+'_selector', 'i') + } + property QtObject popup: QtObject{ + property string sectionName: 'TabButtonPopup' + + property QtObject backgroundColor: QtObject { + property var disabled: ColorsList.add(popup.sectionName+'_bg_d', 'l10') + property var hovered: ColorsList.add(popup.sectionName+'_bg_h', 'l20') + property var normal: ColorsList.add(popup.sectionName+'_bg_n', 'a') + property var pressed: ColorsList.add(popup.sectionName+'_bg_p', 'l30') + property var selected: ColorsList.add(popup.sectionName+'_bg_c', 'a') + } + + + property QtObject text: QtObject { + + property QtObject color: QtObject { + property var disabled: ColorsList.add(popup.sectionName+'_text_d', 'g') + property var hovered: ColorsList.add(popup.sectionName+'_text_h', 'g') + property var normal: ColorsList.add(popup.sectionName+'_text_n', 'g') + property var pressed: ColorsList.add(popup.sectionName+'_text_p', 'g') + property var selected: ColorsList.add(popup.sectionName+'_text_c', 'i') + } + } + property var selector: ColorsList.add(popup.sectionName+'_selector', 'i') } } diff --git a/linphone-app/ui/modules/Linphone/Chat/Chat.js b/linphone-app/ui/modules/Linphone/Chat/Chat.js index 82f4c5b19..cebffbe97 100644 --- a/linphone-app/ui/modules/Linphone/Chat/Chat.js +++ b/linphone-app/ui/modules/Linphone/Chat/Chat.js @@ -55,6 +55,7 @@ function handleFilesDropped (files) { function handleMoreEntriesLoaded (n) { chat.positionViewAtIndex(n - 1, QtQuick.ListView.Beginning) + //moveToEvent.indexToMove = n > 0 ? n - 1 : 0 } function handleMovementEnded () { diff --git a/linphone-app/ui/modules/Linphone/Chat/Chat.qml b/linphone-app/ui/modules/Linphone/Chat/Chat.qml index b545f23c9..57eba4315 100644 --- a/linphone-app/ui/modules/Linphone/Chat/Chat.qml +++ b/linphone-app/ui/modules/Linphone/Chat/Chat.qml @@ -36,22 +36,46 @@ Rectangle { color: ChatStyle.colorModel.color clip: true + + Item{// Let some time to have a better cell sizes + id: moveToEvent + property int indexToMove: -1 + property bool toReposition: indexToMove>=0 && !container.tryingToLoadMoreEntries + + function reposition(){ + chat.positionViewAtIndex(indexToMove, ListView.Center) + repositionerDelay.indexToMove = indexToMove + indexToMove = -1 + repositionerDelay.restart() + } + onToRepositionChanged: if(toReposition){ + console.debug('Moving to ' + indexToMove) + Qt.callLater(reposition); + } + } Timer{// Let some time to have a better cell sizes id: repositionerDelay - property int indexToMove + property int indexToMove: -1 interval: 100 - onTriggered: chat.positionViewAtIndex(indexToMove, ListView.Center) + onTriggered: { + chat.positionViewAtIndex(indexToMove, ListView.Center) + } } + function positionViewAtIndex(index){ - chat.bindToEnd = false - chat.positionViewAtIndex(index, ListView.Center) - repositionerDelay.indexToMove = index - repositionerDelay.restart() + if(index>=0) { + chat.bindToEnd = false + chat.positionViewAtIndex(index, ListView.Center) + moveToEvent.indexToMove = index + } } function goToMessage(message){ positionViewAtIndex(container.proxyModel.loadTillMessage(message)) } + function goToMessageId(messageId){ + positionViewAtIndex(container.proxyModel.loadTillMessageId(messageId)) + } ColumnLayout { anchors.fill: parent @@ -61,10 +85,11 @@ Rectangle { id: chat // ----------------------------------------------------------------------- property bool displaying: false + property bool entriesLoading: container.proxyModel.chatRoomModel && container.proxyModel.chatRoomModel.entriesLoading + onEntriesLoadingChanged: console.log("entriesLoading="+entriesLoading) property bool loadingEntries: (container.proxyModel.chatRoomModel && container.proxyModel.chatRoomModel.entriesLoading) || displaying property bool tryToLoadMoreEntries: loadingEntries || remainingLoadersCount>0 property bool isMoving : false // replace moving read-only property to allow using movement signals. - // Load optimizations property int remainingLoadersCount: 0 property int visibleItemsEstimation: chat.height / (2 * textMetrics.height) // Title + body @@ -74,8 +99,9 @@ Rectangle { signal refreshContents() onLoadingEntriesChanged: { - if( loadingEntries && !displaying) + if( loadingEntries && !displaying && container.proxyModel.chatRoomModel.entriesLoading) { displaying = true + } } onBindToEndChanged: if( bindToEnd){ markAsReadTimer.start() @@ -116,7 +142,7 @@ Rectangle { Component.onCompleted: { Logic.initView() refreshContentsTimer.start() - console.debug("Chat loading with "+chat.visibleItemsEstimation+" visible items. "+chat.count) + console.debug("Chat loading with "+chat.visibleItemsEstimation+" visible items. Count="+chat.count) if(chat.visibleItemsEstimation >= chat.count) Qt.callLater(container.proxyModel.loadMoreEntriesAsync) } @@ -137,6 +163,11 @@ Rectangle { Logic.handleMoreEntriesLoaded(n)// move view to n - 1 item chat.displaying = false } + onTillMessagesLoaded: positionViewAtIndex(messageIndex) + onDisplayMessageIdRequested: { + console.log("Display Message requested "+messageId) + container.goToMessageId(messageId) + } } // ----------------------------------------------------------------------- @@ -303,6 +334,7 @@ Rectangle { onConferenceIcsCopied: container.noticeBannerText = qsTr('conferencesCopiedICS') onAddContactClicked: container.addContactClicked(contactAddress) onViewContactClicked: container.viewContactClicked(contactAddress) + onReactionsClicked: chatReactionsDetails.show(message) } } } @@ -465,5 +497,10 @@ Rectangle { }// ColumnLayout }// Bottom background } + ChatReactionsDetails { + id: chatReactionsDetails + anchors.fill: parent + visible: false + } } diff --git a/linphone-app/ui/modules/Linphone/Chat/ChatMenu.qml b/linphone-app/ui/modules/Linphone/Chat/ChatMenu.qml index b0e190925..81806d136 100644 --- a/linphone-app/ui/modules/Linphone/Chat/ChatMenu.qml +++ b/linphone-app/ui/modules/Linphone/Chat/ChatMenu.qml @@ -45,6 +45,33 @@ Item { Menu { id: messageMenu menuStyle : MenuStyle.aux + MenuItem { + id: reactionBar + property font customFont : SettingsModel.emojiFont + menuItemStyle : MenuItemStyle.aux + contentItem: RowLayout{ + Layout.fillWidth: true + spacing:0 + Repeater{ + model: ['❤️','👍','😂','😮','😢'] + delegate: Text{ + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: modelData + font.family: reactionBar.customFont.family + font.pointSize: Units.dp * reactionBar.customFont.pointSize * 2 + MouseArea{ + anchors.fill: parent + onClicked: { + chatMessageModel.sendChatReaction(modelData) + messageMenu.close() + } + } + } + } + } + } MenuItem { //: 'Copy all' : Text menu to copy all message text into clipboard text: (container.lastTextSelected == '' ? qsTr('menuCopyAll') diff --git a/linphone-app/ui/modules/Linphone/Chat/ChatReactionsDetails.qml b/linphone-app/ui/modules/Linphone/Chat/ChatReactionsDetails.qml new file mode 100644 index 000000000..38eb7de94 --- /dev/null +++ b/linphone-app/ui/modules/Linphone/Chat/ChatReactionsDetails.qml @@ -0,0 +1,121 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.3 + +import Common 1.0 +import Common.Styles 1.0 +import Linphone 1.0 +import Linphone.Styles 1.0 +import Utils 1.0 +import UtilsCpp 1.0 +import LinphoneEnums 1.0 +import Units 1.0 +import ColorsList 1.0 + +// ============================================================================= + +Rectangle{ + id: mainItem + property string sectionName: 'ChatReactions' + property font emojiFont : SettingsModel.emojiFont + property font textFont : SettingsModel.textMessageFont + property alias chatMessageModel: chatReactionsList.chatMessageModel + + + function show(message){ + chatReactionsList.setChatMessageModel(message, ChatReactionListModel.REACTIONS) + visible = true + } + + color: ChatReactionsDetailsStyle.backgroundColorModel.color + onVisibleChanged: if(visible){ + tabBar.currentIndex = 0 + } + MouseArea{ + anchors.fill: parent + onClicked: mainItem.visible = false + } + Rectangle{ + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: parent.height / 2 + color: ChatReactionsDetailsStyle.stickerColorModel.color + + ColumnLayout{ + anchors.fill: parent + spacing: 0 + TabBar { + Layout.fillWidth: true + id: tabBar + TabButton { + Layout.fillWidth: true + //: "%1
reactions" : count of all chat reactions with a jump line between count and text. + text: UtilsCpp.encodeTextToQmlRichFormat(qsTr('reactionsCount', '', chatReactionsList.reactionCount).arg(chatReactionsList.reactionCount), {noLink:1}).toUpperCase() + // noLink=1 to avoid
convertion + textFont: mainItem.textFont + onIsSelectedChanged: if(isSelected) chatReactionsList.filter = '' + displaySelector: true + stretchContent: false + style: TabButtonStyle.popup + } + Repeater{ + model: ['❤️','👍','😂','😮','😢'] + delegate: TabButton { + width: visible ? undefined : 0 + property int reactionCount: 0 + visible: reactionCount > 0 + text: UtilsCpp.encodeTextToQmlRichFormat(modelData + ' '+reactionCount) + textFont.family: mainItem.textFont.family + textFont.pointSize: ChatReactionsDetailsStyle.tabBar.pointSize + + onIsSelectedChanged: if(isSelected) chatReactionsList.filter = modelData + displaySelector: true + stretchContent: false + style: TabButtonStyle.popup + + Connections{ + target: chatReactionsList + onChatMessageModelChanged: reactionCount = chatReactionsList.getChatReactionCount(modelData) + } + } + } + } + Rectangle{ + id: separator + Layout.fillWidth: true + Layout.preferredHeight: 2 + color: ChatReactionsDetailsStyle.separatorColorModel.color + } + Item{ + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 10 + ScrollableListView{ + id: listView + anchors.fill: parent + model: ChatReactionProxyModel{ + id: chatReactionsList + groupBy: ChatReactionListModel.REACTIONS + } + delegate: RowLayout{ + width: listView.width + Contact { + Layout.fillWidth: true + showSubtitle: false + property var sipObserver: SipAddressesModel.getSipAddressObserver($modelData.reaction.fromAddress, $modelData.reaction.fromAddress) + entry: sipObserver + Component.onDestruction: sipObserver=null// Need to set it to null because of not calling destructor if not. + } + Text{ + Layout.rightMargin: 20 + text: $modelData.reaction.body + font.family: mainItem.emojiFont.family + font.pointSize: mainItem.emojiFont.pointSize * 2 + } + } + } + } + } + } +} diff --git a/linphone-app/ui/modules/Linphone/Chat/IncomingMessage.qml b/linphone-app/ui/modules/Linphone/Chat/IncomingMessage.qml index eea41c58d..726be4f23 100644 --- a/linphone-app/ui/modules/Linphone/Chat/IncomingMessage.qml +++ b/linphone-app/ui/modules/Linphone/Chat/IncomingMessage.qml @@ -9,7 +9,7 @@ import UtilsCpp 1.0 // ============================================================================= RowLayout { - id:mainRow + id: mainRow Layout.fillWidth: true @@ -25,6 +25,7 @@ RowLayout { signal conferenceIcsCopied() signal addContactClicked(string contactAddress) signal viewContactClicked(string contactAddress) + signal reactionsClicked(ChatMessageModel message) implicitHeight: message.height spacing: 0 @@ -70,6 +71,7 @@ RowLayout { onConferenceIcsCopied: mainRow.conferenceIcsCopied() onAddContactClicked: mainRow.addContactClicked(contactAddress) onViewContactClicked: mainRow.viewContactClicked(contactAddress) + onReactionsClicked: mainRow.reactionsClicked(message) Layout.fillWidth: true Layout.rightMargin: 10 diff --git a/linphone-app/ui/modules/Linphone/Chat/Message.qml b/linphone-app/ui/modules/Linphone/Chat/Message.qml index 1b7f3f3f9..17209565f 100644 --- a/linphone-app/ui/modules/Linphone/Chat/Message.qml +++ b/linphone-app/ui/modules/Linphone/Chat/Message.qml @@ -42,10 +42,11 @@ Item { signal conferenceIcsCopied() signal addContactClicked(string contactAddress) signal viewContactClicked(string contactAddress) + signal reactionsClicked(ChatMessageModel message); // --------------------------------------------------------------------------- property string lastTextSelected - implicitHeight: (deliveryLayout.visible? deliveryLayout.height : 0) +(ephemeralTimerRow.visible? 16 : 0) + chatContent.height + implicitHeight: (deliveryLayout.visible? deliveryLayout.height : 0) +(ephemeralTimerRow.visible? 16 : 0) + chatContent.height + reactionItem.height-10 Rectangle { id: rectangle property int availableWidth: parent.width @@ -55,7 +56,7 @@ Item { anchors.left: !$chatEntry.isOutgoing ? parent.left : undefined anchors.right: $chatEntry.isOutgoing ? parent.right : undefined - height: parent.height - (deliveryLayout.visible? deliveryLayout.height : 0) + height: parent.height - (deliveryLayout.visible? deliveryLayout.height : 0) - (reactionItem.height-10) radius: ChatStyle.entry.message.radius clip: false color: colorModel.color @@ -143,6 +144,71 @@ Item { chatMessageModel: $chatEntry } + Rectangle{ + id: reactionItem + anchors.top: rectangle.bottom + anchors.left: !$chatEntry.isOutgoing ? rectangle.left : undefined + anchors.right: $chatEntry.isOutgoing ? rectangle.right : undefined + anchors.topMargin: -10 + anchors.leftMargin: 5 + anchors.rightMargin: 5 + height: visible ? reactionList.height +10 : 0 + width: visible ? reactionLayout.width + 10 : 0 + color: rectangle.color + radius: rectangle.radius + visible: reactionList.count > 0 + property font customFont : SettingsModel.textMessageFont + property font customEmojiFont : SettingsModel.emojiFont + + RowLayout{ + id: reactionLayout + anchors.centerIn: parent + ListView{ + id: reactionList + Layout.preferredWidth: contentItem.childrenRect.width + Layout.preferredHeight: reactionMetrics.height + TextMetrics{ + id: reactionMetrics + text: '😮' + font.family: reactionItem.customEmojiFont.family + font.pointSize: Units.dp * reactionItem.customEmojiFont.pointSize * 2 + } + orientation: ListView.Horizontal + model: ChatReactionProxyModel{ + id: chatReactionProxyModel + chatMessageModel: $chatEntry + } + spacing: 10 + delegate: + Text{ + width: reactionMetrics.width + height: reactionList.height + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.family: reactionItem.customEmojiFont.family + font.pointSize: Units.dp * reactionItem.customEmojiFont.pointSize * 2 + text: $modelData.body || '' + } + } + + Text{ + Layout.preferredWidth: contentWidth + Layout.preferredHeight: reactionList.height + Layout.alignment: Qt.AlignVCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.family: reactionItem.customFont.family + font.pointSize: Units.dp * reactionItem.customFont.pointSize + visible: chatReactionProxyModel.count != chatReactionProxyModel.reactionCount + + text: chatReactionProxyModel.reactionCount + } + } + MouseArea{ + anchors.fill: parent + onClicked: container.reactionsClicked($chatEntry) + } + } ActionButton { id: menuButton diff --git a/linphone-app/ui/modules/Linphone/Chat/OutgoingMessage.qml b/linphone-app/ui/modules/Linphone/Chat/OutgoingMessage.qml index 22d36354f..380f5a0fe 100644 --- a/linphone-app/ui/modules/Linphone/Chat/OutgoingMessage.qml +++ b/linphone-app/ui/modules/Linphone/Chat/OutgoingMessage.qml @@ -27,6 +27,7 @@ Item { signal conferenceIcsCopied() signal addContactClicked(string contactAddress) signal viewContactClicked(string contactAddress) + signal reactionsClicked(ChatMessageModel message) implicitHeight: message.height RowLayout{ @@ -43,6 +44,7 @@ Item { onConferenceIcsCopied: mainItem.conferenceIcsCopied() onAddContactClicked: mainItem.addContactClicked(contactAddress) onViewContactClicked: mainItem.viewContactClicked(contactAddress) + onReactionsClicked: mainItem.reactionsClicked(message) backgroundColorModel: ChatStyle.entry.message.outgoing.backgroundColor Layout.fillWidth: true diff --git a/linphone-app/ui/modules/Linphone/Notifications/NotificationReceivedMessage.qml b/linphone-app/ui/modules/Linphone/Notifications/NotificationReceivedMessage.qml index 6c0d1e2b5..8f184ef09 100644 --- a/linphone-app/ui/modules/Linphone/Notifications/NotificationReceivedMessage.qml +++ b/linphone-app/ui/modules/Linphone/Notifications/NotificationReceivedMessage.qml @@ -23,6 +23,7 @@ Notification { readonly property string localAddress: notificationData && notificationData.localAddress || '' readonly property string fullPeerAddress: notificationData && notificationData.fullPeerAddress || '' readonly property string fullLocalAddress: notificationData && notificationData.fullLocalAddress || '' + readonly property string messageId: notificationData && notificationData.messageId || '' // --------------------------------------------------------------------------- @@ -99,10 +100,10 @@ Notification { AccountSettingsModel.setDefaultAccountFromSipAddress(notification.localAddress) var chatroom = notification.timelineModel.getChatRoomModel() console.debug("Load conversation from notification: "+chatroom) - //notification.notificationData.window.setView('Conversation', { - // chatRoomModel: chatroom - // }) - notification.timelineModel.selected = true + if(!notification.timelineModel.selected)// Check to avoid reloading + notification.timelineModel.selected = true + if(chatroom && notification.messageId) + chatroom.displayMessageIdRequested(notification.messageId) App.smartShowWindow(notification.notificationData.window) }) } diff --git a/linphone-app/ui/modules/Linphone/Styles/Chat/ChatReactionsDetailsStyle.qml b/linphone-app/ui/modules/Linphone/Styles/Chat/ChatReactionsDetailsStyle.qml new file mode 100644 index 000000000..8d283d528 --- /dev/null +++ b/linphone-app/ui/modules/Linphone/Styles/Chat/ChatReactionsDetailsStyle.qml @@ -0,0 +1,18 @@ +pragma Singleton +import QtQml 2.2 + +import Units 1.0 +import ColorsList 1.0 + +// ============================================================================= + +QtObject { + property string sectionName : 'ChatReactionsDetails' + property var backgroundColorModel: ColorsList.add(sectionName+'_bg', 'l50') + property var stickerColorModel: ColorsList.add(sectionName+'_sticker_bg', 'k') + property var separatorColorModel: ColorsList.add(sectionName+'_separator', 'f') + + property QtObject tabBar: QtObject{ + property int pointSize: Units.dp * 11 + } +} diff --git a/linphone-app/ui/modules/Linphone/Styles/qmldir b/linphone-app/ui/modules/Linphone/Styles/qmldir index 6e41d0f42..e2deecd4a 100644 --- a/linphone-app/ui/modules/Linphone/Styles/qmldir +++ b/linphone-app/ui/modules/Linphone/Styles/qmldir @@ -18,6 +18,7 @@ singleton ChatEmojisStyle 1.0 Chat/ChatEmojisStyle.qml singleton ChatFilePreviewStyle 1.0 Chat/ChatFilePreviewStyle.qml singleton ChatCalendarMessageStyle 1.0 Chat/ChatCalendarMessageStyle.qml singleton ChatForwardMessageStyle 1.0 Chat/ChatForwardMessageStyle.qml +singleton ChatReactionsDetailsStyle 1.0 Chat/ChatReactionsDetailsStyle.qml singleton ChatReplyMessageStyle 1.0 Chat/ChatReplyMessageStyle.qml singleton CallControlsStyle 1.0 Calls/CallControlsStyle.qml diff --git a/linphone-app/ui/modules/Linphone/qmldir b/linphone-app/ui/modules/Linphone/qmldir index dd4a0b2a4..c277080dc 100644 --- a/linphone-app/ui/modules/Linphone/qmldir +++ b/linphone-app/ui/modules/Linphone/qmldir @@ -29,6 +29,7 @@ ChatEmojis 1.0 Chat/ChatEmojis.qml ChatFullContent 1.0 Chat/ChatFullContent.qml ChatMessagePreview 1.0 Chat/ChatMessagePreview.qml ChatForwardMessage 1.0 Chat/ChatForwardMessage.qml +ChatReactionsDetails 1.0 Chat/ChatReactionsDetails.qml ChatReplyMessage 1.0 Chat/ChatReplyMessage.qml ChatReplyPreview 1.0 Chat/ChatReplyPreview.qml diff --git a/linphone-app/ui/views/App/Settings/SettingsWindow.qml b/linphone-app/ui/views/App/Settings/SettingsWindow.qml index df3623214..de84e039d 100644 --- a/linphone-app/ui/views/App/Settings/SettingsWindow.qml +++ b/linphone-app/ui/views/App/Settings/SettingsWindow.qml @@ -116,7 +116,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.preferredHeight: TabButtonStyle.text.height - color: TabButtonStyle.backgroundColor.normal.color + color: TabButtonStyle.menu.backgroundColor.normal.color MouseArea { anchors.fill: parent @@ -134,7 +134,7 @@ ApplicationWindow { Rectangle{ id: hideBar anchors.fill: parent - color: TabButtonStyle.backgroundColor.normal.color + color: TabButtonStyle.menu.backgroundColor.normal.color visible: logViewer.active } } diff --git a/linphone-sdk b/linphone-sdk index b4e83802c..0063e1fb6 160000 --- a/linphone-sdk +++ b/linphone-sdk @@ -1 +1 @@ -Subproject commit b4e83802c9d24ec019edcf14d90834f304f7c44e +Subproject commit 0063e1fb69a31662edb86e73f537b3eb854c7cfe