Add Chat reactions

This commit is contained in:
Julien Wadel 2023-08-31 14:33:30 +02:00
parent a9812245dc
commit 267d70535d
59 changed files with 1446 additions and 70 deletions

View file

@ -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

View file

@ -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

View file

@ -758,6 +758,18 @@ Adresa URL není nakonfigurována.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -752,6 +752,17 @@ Server url ikke konfigureret.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -752,6 +752,17 @@ Server URL ist nicht konfiguriert.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>
@ -1596,7 +1607,7 @@ Server URL ist nicht konfiguriert.</translation>
<message>
<source>incallPauseWarning</source>
<extracomment>&apos;You are currently out of the conference.&apos; : Pause message in video conference.</extracomment>
<translation type="unfinished"></translation>
<translation type="unfinished">Sie haben den Anruf unterbrochen.</translation>
</message>
<message>
<source>incallPauseHint</source>

View file

@ -752,6 +752,17 @@ Server URL not configured.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation>
<numerusform>%1&lt;br&gt;reaction</numerusform>
<numerusform>%1&lt;br&gt;reactions</numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -752,6 +752,17 @@ URL del servidor no configurada.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -752,6 +752,17 @@ URL du serveur non configurée.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation>
<numerusform>%1&lt;br&gt;réaction</numerusform>
<numerusform>%1&lt;br&gt;réactions</numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -746,6 +746,16 @@ A kiszolgáló URL-je nincs konfigurálva.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -752,6 +752,17 @@ URL del server non configurato.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -746,6 +746,16 @@
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -758,6 +758,18 @@ Nesukonfigūruotas serverio url.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -752,6 +752,17 @@ URL do servidor não configurado.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -758,6 +758,18 @@
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -752,6 +752,17 @@ Serverwebbadressen är inte konfigurerad.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -746,6 +746,16 @@ Sunucu url&apos;si yapılandırılmadı.</translation>
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -758,6 +758,18 @@
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -746,6 +746,16 @@
</translation>
</message>
</context>
<context>
<name>ChatReactionsDetails</name>
<message numerus="yes">
<source>reactionsCount</source>
<extracomment>&quot;%1&lt;br&gt;reactions&quot; : count of all chat reactions with a jump line between count and text.</extracomment>
<translation type="unfinished">
<numerusform></numerusform>
</translation>
</message>
</context>
<context>
<name>ChatReplyMessage</name>
<message>

View file

@ -346,6 +346,7 @@
<file>ui/modules/Linphone/Chat/ChatFilePreview.qml</file>
<file>ui/modules/Linphone/Chat/ChatForwardMessage.qml</file>
<file>ui/modules/Linphone/Chat/ChatMessagePreview.qml</file>
<file>ui/modules/Linphone/Chat/ChatReactionsDetails.qml</file>
<file>ui/modules/Linphone/Chat/ChatReplyMessage.qml</file>
<file>ui/modules/Linphone/Chat/ChatReplyPreview.qml</file>
<file>ui/modules/Linphone/Chat/ChatTextMessage.qml</file>
@ -403,6 +404,7 @@
<file>ui/modules/Linphone/Styles/Chat/ChatFilePreviewStyle.qml</file>
<file>ui/modules/Linphone/Styles/Chat/ChatCalendarMessageStyle.qml</file>
<file>ui/modules/Linphone/Styles/Chat/ChatForwardMessageStyle.qml</file>
<file>ui/modules/Linphone/Styles/Chat/ChatReactionsDetailsStyle.qml</file>
<file>ui/modules/Linphone/Styles/Chat/ChatReplyMessageStyle.qml</file>
<file>ui/modules/Linphone/Styles/Codecs/CodecsViewerStyle.qml</file>
<file>ui/modules/Linphone/Styles/Contact/AvatarStyle.qml</file>

View file

@ -722,6 +722,7 @@ void App::registerTypes () {
registerType<CallsListProxyModel>("CallsListProxyModel");
registerType<Camera>("Camera");
registerType<ChatRoomProxyModel>("ChatRoomProxyModel");
registerType<ChatReactionProxyModel>("ChatReactionProxyModel");
registerType<ConferenceHelperModel>("ConferenceHelperModel");
registerType<ConferenceProxyModel>("ConferenceProxyModel");
registerType<ConferenceInfoModel>("ConferenceInfoModel");
@ -762,6 +763,7 @@ void App::registerTypes () {
registerUncreatableType<ChatCallModel>("ChatCallModel");
registerUncreatableType<ChatMessageModel>("ChatMessageModel");
registerUncreatableType<ChatNoticeModel>("ChatNoticeModel");
registerUncreatableType<ChatReactionListModel>("ChatReactionListModel");
registerUncreatableType<ChatRoomModel>("ChatRoomModel");
registerUncreatableType<ColorModel>("ColorModel");
registerUncreatableType<ImageModel>("ImageModel");

View file

@ -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();

View file

@ -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"

View file

@ -49,6 +49,9 @@ void ChatMessageListener::onFileTransferProgressIndication (const std::shared_pt
void ChatMessageListener::onMsgStateChanged (const std::shared_ptr<linphone::ChatMessage> &message, linphone::ChatMessage::State state){
emit msgStateChanged(message, state);
}
void ChatMessageListener::onNewMessageReaction(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction) {
emit newMessageReaction(message, reaction);
}
void ChatMessageListener::onParticipantImdnStateChanged(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ParticipantImdnState> & state){
emit participantImdnStateChanged(message, state);
}

View file

@ -41,6 +41,7 @@ public:
virtual std::shared_ptr<linphone::Buffer> onFileTransferSend(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<linphone::Content> & content, size_t offset, size_t size) override;
virtual void onFileTransferProgressIndication (const std::shared_ptr<linphone::ChatMessage> &message, const std::shared_ptr<linphone::Content> &, size_t offset, size_t) override;
virtual void onMsgStateChanged (const std::shared_ptr<linphone::ChatMessage> &message, linphone::ChatMessage::State state) override;
virtual void onNewMessageReaction(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction) override;
virtual void onParticipantImdnStateChanged(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ParticipantImdnState> & state) override;
virtual void onEphemeralMessageTimerStarted(const std::shared_ptr<linphone::ChatMessage> & message) override;
virtual void onEphemeralMessageDeleted(const std::shared_ptr<linphone::ChatMessage> & message) override;
@ -50,6 +51,7 @@ signals:
std::shared_ptr<linphone::Buffer> fileTransferSend (const std::shared_ptr<linphone::ChatMessage> &,const std::shared_ptr<linphone::Content> &,size_t,size_t);
void fileTransferProgressIndication (const std::shared_ptr<linphone::ChatMessage> &message, const std::shared_ptr<linphone::Content> &, size_t offset, size_t);
void msgStateChanged (const std::shared_ptr<linphone::ChatMessage> &message, linphone::ChatMessage::State state);
void newMessageReaction(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction);
void participantImdnStateChanged(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ParticipantImdnState> & state);
void ephemeralMessageTimerStarted(const std::shared_ptr<linphone::ChatMessage> & message);
void ephemeralMessageDeleted(const std::shared_ptr<linphone::ChatMessage> & message);

View file

@ -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<linphone::ChatMessage> chat
mWasDownloaded = false;
mContentListModel = QSharedPointer<ContentListModel>::create(this);
mChatReactionListModel = QSharedPointer<ChatReactionListModel>::create(this);
}
ChatMessageModel::~ChatMessageModel(){
@ -188,6 +192,10 @@ QSharedPointer<ContentListModel> ChatMessageModel::getContents() const{
return mContentListModel;
}
QSharedPointer<ChatReactionListModel> 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<linphone::ChatMe
}
emit stateChanged();
}
void ChatMessageModel::onNewMessageReaction(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction){
mChatReactionListModel->updateChatReaction(reaction);
}
void ChatMessageModel::onParticipantImdnStateChanged(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ParticipantImdnState> & state){
}

View file

@ -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<ParticipantImdnStateListModel> getParticipantImdnStates() const;
QSharedPointer<ContentListModel> getContents() const;
QSharedPointer<ChatReactionListModel> 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<linphone::Buffer> onFileTransferSend (const std::shared_ptr<linphone::ChatMessage> &,const std::shared_ptr<linphone::Content> &,size_t,size_t);
void onFileTransferProgressIndication (const std::shared_ptr<linphone::ChatMessage> &message, const std::shared_ptr<linphone::Content> &, size_t offset, size_t);
void onMsgStateChanged (const std::shared_ptr<linphone::ChatMessage> &message, linphone::ChatMessage::State state);
void onNewMessageReaction(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction);
void onParticipantImdnStateChanged(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ParticipantImdnState> & state);
void onEphemeralMessageTimerStarted(const std::shared_ptr<linphone::ChatMessage> & message);
void onEphemeralMessageDeleted(const std::shared_ptr<linphone::ChatMessage> & message);
@ -152,6 +157,7 @@ private:
QSharedPointer<ContentModel> mFileTransfertContent;
QSharedPointer<ParticipantImdnStateListModel> mParticipantImdnStateListModel;
QSharedPointer<ChatMessageModel> mReplyChatMessageModel;
QSharedPointer<ChatReactionListModel> mChatReactionListModel;
QString mFromDisplayNameCache;
QString fromDisplayNameReplyMessage;

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "ChatReactionListModel.hpp"
#include "ChatReactionModel.hpp"
#include "components/chat-events/ChatMessageModel.hpp"
#include "utils/Utils.hpp"
// =============================================================================
ChatReactionListModel::ChatReactionListModel (ChatMessageModel * message, QObject* parent) : ProxyAbstractListModel<QVariantMap>(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<ChatReactionModel>::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<ChatReactionModel> ChatReactionListModel::add(std::shared_ptr<linphone::ChatMessageReaction> reaction){
auto reactionModel = QSharedPointer<ChatReactionModel>::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<ChatReactionModel> ChatReactionListModel::getChatReactionModel(const std::shared_ptr<const linphone::ChatMessageReaction>& reaction){
for(auto item : mList){
auto c = item.objectCast<ChatReactionModel>();
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<ContentModel>();
if(c->getContent()->getFilePath() == content->getFilePath())
return c;
}
}
return nullptr;
}
*/
void ChatReactionListModel::updateChatReaction(const std::shared_ptr<const linphone::ChatMessageReaction>& 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<ChatReactionModel>::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<QVariantMap> 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<QVariantMap>::add(data);
emit chatReactionsChanged();
}
bool ChatReactionListModel::exists(std::shared_ptr<linphone::ChatMessageReaction> 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<linphone::ChatMessageReaction> oldReaction, std::shared_ptr<linphone::ChatMessageReaction> 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<linphone::Content> oldContent, std::shared_ptr<linphone::Content> newContent){
int row = 0;
for(auto content = mList.begin() ; content != mList.end() ; ++content, ++row){
auto contentModel = content->objectCast<ContentModel>();
if( contentModel->getContent() == oldContent){
mList.replace(row, QSharedPointer<ContentModel>::create(newContent, contentModel->getChatMessageModel()));
emit dataChanged(index(row,0), index(row,0));
return;
}
}
}
void ContentListModel::updateContents(ChatMessageModel * messageModel){
std::list<std::shared_ptr<linphone::Content>> contents = messageModel->getChatMessage()->getContents() ;
int count = 0;
beginResetModel();
for(auto content : contents){
if( count >= mList.size()){// New content
mList.insert(count, QSharedPointer<ContentModel>::create(content, messageModel));
}else if(mList.at(count).objectCast<ContentModel>()->getContent() != content){ // This content is not at its place
int c = count + 1;
while( c < mList.size() && mList.at(c).objectCast<ContentModel>()->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<ContentModel>::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<ContentModel>()->createThumbnail();
}*/

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef CHAT_REACTION_LIST_MODEL_H_
#define CHAT_REACTION_LIST_MODEL_H_
#include <linphone++/linphone.hh>
#include "app/proxyModel/ProxyAbstractListModel.hpp"
#include <QDateTime>
class ChatReactionModel;
class ChatMessageModel;
class ChatReactionListModel : public ProxyAbstractListModel<QVariantMap> {
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<ChatReactionModel> add(std::shared_ptr<linphone::ChatMessageReaction> reaction);
Q_INVOKABLE void remove(ChatReactionModel * model);
void clear();
ChatReactionListModel::GROUP_BY_TYPE getGroupBy() const;
void setGroupBy(ChatReactionListModel::GROUP_BY_TYPE mode);
//QSharedPointer<ChatReactionModel> getChatReactionModel(const std::shared_ptr<const linphone::ChatMessageReaction>& reaction);
bool exists(std::shared_ptr<linphone::ChatMessageReaction> reaction) const;
void updateChatReaction(std::shared_ptr<linphone::ChatMessageReaction> oldReaction, std::shared_ptr<linphone::ChatMessageReaction> newReaction);
void updateChatReaction(const std::shared_ptr<const linphone::ChatMessageReaction>& reaction);
void updateChatReaction(ChatMessageModel * messageModel);
void updateList();
signals:
void chatReactionsChanged();
void chatReactionCountChanged();
void groupByChanged();
private:
ChatMessageModel * mParent;
QMap<QString, QSharedPointer<ChatReactionModel>> mReactions;
QMap<QString, QVector<QSharedPointer<ChatReactionModel>>> mBodies;
GROUP_BY_TYPE mGroupBy = EMOJIES;
};
Q_DECLARE_METATYPE(std::shared_ptr<ChatReactionListModel>)
#endif

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "ChatReactionModel.hpp"
#include "app/App.hpp"
#include "utils/Utils.hpp"
#include <QQmlApplicationEngine>
ChatReactionModel::ChatReactionModel(const std::shared_ptr<const linphone::ChatMessageReaction>& 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;
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef CHAT_REACTION_MODEL_H_
#define CHAT_REACTION_MODEL_H_
#include <linphone++/linphone.hh>
#include <QDateTime>
#include <QObject>
class ChatReactionModel : public QObject {
Q_OBJECT
public:
ChatReactionModel(const std::shared_ptr<const linphone::ChatMessageReaction>& 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<ChatReactionModel>)
#endif

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "ChatReactionProxyModel.hpp"
#include "ChatReactionListModel.hpp"
#include "ChatReactionModel.hpp"
// =============================================================================
ChatReactionProxyModel::ChatReactionProxyModel (QObject * parent) : SortFilterProxyModel(parent){
mContents = QSharedPointer<ChatReactionListModel>::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<ChatReactionListModel*>(sourceModel());
model->setChatMessageModel(message);
model->setGroupBy(groupByMode);
}
emit chatMessageModelChanged();
emit chatReactionCountChanged();
}
int ChatReactionProxyModel::getChatReactionCount() const {
auto model = qobject_cast<ChatReactionListModel*>(sourceModel());
if(model)
return model->getChatReactionCount();
else
return 0;
}
int ChatReactionProxyModel::getChatReactionCount(const QString& emoji) const {
auto model = qobject_cast<ChatReactionListModel*>(sourceModel());
if(model)
return model->getChatReactionCount(emoji);
else
return 0;
}
ChatReactionListModel::GROUP_BY_TYPE ChatReactionProxyModel::getGroupBy() const {
auto model = qobject_cast<ChatReactionListModel*>(sourceModel());
return model ? model->getGroupBy() : ChatReactionListModel::GROUP_BY_TYPE::EMOJIES;
}
void ChatReactionProxyModel::setGroupBy(ChatReactionListModel::GROUP_BY_TYPE mode) {
auto model = qobject_cast<ChatReactionListModel*>(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<ChatReactionListModel*>(sourceModel());
QModelIndex index = sourceModel()->index(sourceRow, 0, QModelIndex());
auto reaction = sourceModel()->data(index).value<QVariantMap>();
if( model->getGroupBy() == ChatReactionListModel::GROUP_BY_TYPE::REACTIONS) {
if( mFilter == reaction["reaction"].value<ChatReactionModel*>()->getBody())
show = true;
}
}
return show;
}
/*
bool ChatReactionProxyModel::lessThan (const QModelIndex &left, const QModelIndex &right) const {
const ContentModel *contentA = sourceModel()->data(left).value<ContentModel *>();
const ContentModel *contentB = sourceModel()->data(right).value<ContentModel *>();
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<ContentListModel*>(sourceModel())->remove(model);
}
void ChatReactionProxyModel::clear(){
qobject_cast<ContentListModel*>(sourceModel())->clear();
}
ContentProxyModel::FilterContentType ChatReactionProxyModel::getFilter() const{
return mFilter;
}
void ChatReactionProxyModel::setFilter(const FilterContentType& contentType){
if(contentType != mFilter){
mFilter = contentType;
emit filterChanged();
invalidate();
}
}*/

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<ChatReactionListModel> 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<ChatReacListModel> mContents;
QString mFilter = "";
};
#endif

View file

@ -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<linphone::ChatMessage> 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<QObject>& entry ){
auto entry = std::find_if(mList.begin(), mList.end(), [lMessageId](const QSharedPointer<QObject>& entry ){
auto chatEventEntry = entry.objectCast<ChatEvent>();
return chatEventEntry->mType == ChatRoomModel::EntryType::MessageEntry && chatEventEntry.objectCast<ChatMessageModel>()->getChatMessage() == linphoneMessage;
return chatEventEntry->mType == ChatRoomModel::EntryType::MessageEntry && chatEventEntry.objectCast<ChatMessageModel>()->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<ChatEvent>();
while(entryCount < newEntries &&
(chatEventEntry->mType != ChatRoomModel::EntryType::MessageEntry || chatEventEntry.objectCast<ChatMessageModel>()->getChatMessage() != linphoneMessage)
(chatEventEntry->mType != ChatRoomModel::EntryType::MessageEntry || chatEventEntry.objectCast<ChatMessageModel>()->getChatMessage()->getMessageId() != lMessageId)
){
++entryCount;
++entry;
@ -892,36 +907,43 @@ int ChatRoomModel::loadTillMessage(ChatMessageModel * message){
chatEventEntry = entry->objectCast<ChatEvent>();
}
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<ChatMessageModel> ChatRoomModel::getChatMessageModel(const std::shared_ptr<linphone::ChatMessage> message) const {
auto entry = std::find_if(mList.begin(), mList.end(), [message](const QSharedPointer<QObject>& entry ){
auto chatEventEntry = entry.objectCast<ChatEvent>();
return chatEventEntry->mType == ChatRoomModel::EntryType::MessageEntry && chatEventEntry.objectCast<ChatMessageModel>()->getChatMessage() == message;
});
return entry != mList.end() ? entry->objectCast<ChatMessageModel>(): nullptr;
}
bool ChatRoomModel::isTerminated(const std::shared_ptr<linphone::ChatRoom>& chatRoom){
return chatRoom->getState() == linphone::ChatRoom::State::Terminated || chatRoom->getState() == linphone::ChatRoom::State::Deleted;
}
bool ChatRoomModel::exists(const std::shared_ptr<linphone::ChatMessage> message) const{
auto entry = std::find_if(mList.begin(), mList.end(), [message](const QSharedPointer<QObject>& entry ){
auto chatEventEntry = entry.objectCast<ChatEvent>();
return chatEventEntry->mType == ChatRoomModel::EntryType::MessageEntry && chatEventEntry.objectCast<ChatMessageModel>()->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)

View file

@ -184,7 +184,10 @@ public:
Q_INVOKABLE int loadMoreEntries(); // return new entries count
void onCallEnded(std::shared_ptr<linphone::Call> call);
void updateNewMessageNotice(const int& count);
QSharedPointer<ChatMessageModel> getChatMessageModel(const std::shared_ptr<linphone::ChatMessage> 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<linphone::ChatMessage> linphoneMessage);
static bool isTerminated(const std::shared_ptr<linphone::ChatRoom>& chatRoom);
bool exists(const std::shared_ptr<linphone::ChatMessage> 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<ChatRoomModel> model);
void lastEntryRemoved ();
void displayMessageIdRequested(const QString& messageId);
void messageSent (const std::shared_ptr<linphone::ChatMessage> &message);
void messageReceived (const std::shared_ptr<linphone::ChatMessage> &message);

View file

@ -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<ChatRoomModel*>(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<ChatRoomModel*>(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){

View file

@ -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();

View file

@ -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<linphone::Core> & core, const std::shared_ptr<linphone::ChatRoom> & chatRoom, const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction){
QList<QPair<shared_ptr<linphone::ChatMessage>, std::shared_ptr<const linphone::ChatMessageReaction> >> 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<linphone::Core> &,
const shared_ptr<linphone::Friend> &,

View file

@ -53,6 +53,7 @@ signals:
void isComposingChanged (const std::shared_ptr<linphone::ChatRoom> &chatRoom);
void logsUploadStateChanged (linphone::Core::LogCollectionUploadState state, const std::string &info);
void messagesReceived (const std::list<std::shared_ptr<linphone::ChatMessage>> &messages);
void newMessageReaction(const std::shared_ptr<linphone::ChatRoom> & chatRoom, const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction);
void presenceReceived (const QString &sipAddress, const std::shared_ptr<const linphone::PresenceModel> &presenceModel);
void presenceStatusReceived(std::shared_ptr<linphone::Friend> contact);
void registrationStateChanged (const std::shared_ptr<linphone::Account> &account, linphone::RegistrationState state);
@ -82,6 +83,7 @@ public slots:
void onLogCollectionUploadProgressIndication (const std::shared_ptr<linphone::Core> &lc,size_t offset,size_t total);
void onMessageReceived (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::ChatRoom> &room,const std::shared_ptr<linphone::ChatMessage> &message);
void onMessagesReceived (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::ChatRoom> &room,const std::list<std::shared_ptr<linphone::ChatMessage>> &messages);
void onNewMessageReaction(const std::shared_ptr<linphone::Core> & core, const std::shared_ptr<linphone::ChatRoom> & chatRoom, const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction);
void onNotifyPresenceReceivedForUriOrTel (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::Friend> &linphoneFriend,const std::string &uriOrTel,const std::shared_ptr<const linphone::PresenceModel> &presenceModel);
void onNotifyPresenceReceived (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::Friend> &linphoneFriend);
void onQrcodeFound(const std::shared_ptr<linphone::Core> & core, const std::string & result);

View file

@ -81,6 +81,9 @@ void CoreListener::onMessageReceived (const std::shared_ptr<linphone::Core> &cor
void CoreListener::onMessagesReceived (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::ChatRoom> &room,const std::list<std::shared_ptr<linphone::ChatMessage>> &messages){
emit messagesReceived (core,room,messages);
}
void CoreListener::onNewMessageReaction(const std::shared_ptr<linphone::Core> & core, const std::shared_ptr<linphone::ChatRoom> & chatRoom, const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction){
emit newMessageReaction (core,chatRoom,message, reaction);
}
void CoreListener::onNotifyPresenceReceivedForUriOrTel (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::Friend> &linphoneFriend,const std::string &uriOrTel,const std::shared_ptr<const linphone::PresenceModel> &presenceModel){
emit notifyPresenceReceivedForUriOrTel (core,linphoneFriend,uriOrTel,presenceModel);
}

View file

@ -52,6 +52,7 @@ public:
virtual void onLogCollectionUploadProgressIndication (const std::shared_ptr<linphone::Core> &lc,size_t offset,size_t total) override;
virtual void onMessageReceived (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::ChatRoom> &room,const std::shared_ptr<linphone::ChatMessage> &message) override;
virtual void onMessagesReceived (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::ChatRoom> &room,const std::list<std::shared_ptr<linphone::ChatMessage>> &messages) override;
virtual void onNewMessageReaction(const std::shared_ptr<linphone::Core> & core, const std::shared_ptr<linphone::ChatRoom> & chatRoom, const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction) override;
virtual void onNotifyPresenceReceivedForUriOrTel (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::Friend> &linphoneFriend,const std::string &uriOrTel,const std::shared_ptr<const linphone::PresenceModel> &presenceModel) override;
virtual void onNotifyPresenceReceived (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::Friend> &linphoneFriend) override;
virtual void onQrcodeFound(const std::shared_ptr<linphone::Core> & core, const std::string & result) override;
@ -78,6 +79,7 @@ signals:
void logCollectionUploadProgressIndication (const std::shared_ptr<linphone::Core> &lc,size_t offset,size_t total);
void messageReceived (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::ChatRoom> &room,const std::shared_ptr<linphone::ChatMessage> &message);
void messagesReceived (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::ChatRoom> &room,const std::list<std::shared_ptr<linphone::ChatMessage>> &messages);
void newMessageReaction(const std::shared_ptr<linphone::Core> & core, const std::shared_ptr<linphone::ChatRoom> & chatRoom, const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction);
void notifyPresenceReceivedForUriOrTel (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::Friend> &linphoneFriend,const std::string &uriOrTel,const std::shared_ptr<const linphone::PresenceModel> &presenceModel);
void notifyPresenceReceived (const std::shared_ptr<linphone::Core> &core,const std::shared_ptr<linphone::Friend> &linphoneFriend);
void qrcodeFound(const std::shared_ptr<linphone::Core> & core, const std::string & result);

View file

@ -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<shared_ptr<linphone::ChatMessa
}
}
void Notifier::notifyReceivedReactions(const QList<QPair<std::shared_ptr<linphone::ChatMessage>, std::shared_ptr<const linphone::ChatMessageReaction>>> &reactions) {
QVariantMap map;
QString txt;
if( reactions.size() > 0){
ChatMessageModel *redirection = nullptr;
QPair<shared_ptr<linphone::ChatMessage>,std::shared_ptr<const linphone::ChatMessageReaction>> reaction = reactions.front();
shared_ptr<linphone::ChatMessage> message = reaction.first;
shared_ptr<linphone::ChatRoom> 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<linphone::ChatMessage> &message, const shared_ptr<linphone::Content> &content) {
QVariantMap map;
shared_ptr<linphone::ChatRoom> chatRoom(message->getChatRoom());

View file

@ -55,6 +55,7 @@ public:
};
void notifyReceivedMessages (const std::list<std::shared_ptr<linphone::ChatMessage>> &messages);
void notifyReceivedReactions(const QList<QPair<std::shared_ptr<linphone::ChatMessage>, std::shared_ptr<const linphone::ChatMessageReaction>>> &reactions);
void notifyReceivedFileMessage (const std::shared_ptr<linphone::ChatMessage> &message, const std::shared_ptr<linphone::Content> &content);
void notifyReceivedCall (const std::shared_ptr<linphone::Call> &call);
void notifyNewVersionAvailable (const QString &version, const QString &url);

View file

@ -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, "")

View file

@ -30,8 +30,7 @@
#include <linphone++/chat_room.hh>
#include "../contact/ContactModel.hpp"
class ChatMessageModel;
class ChatRoomModel;
class ChatRoomListener;
class TimelineListModel;

View file

@ -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
}
}

View file

@ -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')
}
}

View file

@ -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 () {

View file

@ -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
}
}

View file

@ -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')

View file

@ -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<br>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 <br> 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
}
}
}
}
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
})
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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

View file

@ -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
}
}

@ -1 +1 @@
Subproject commit b4e83802c9d24ec019edcf14d90834f304f7c44e
Subproject commit 0063e1fb69a31662edb86e73f537b3eb854c7cfe