From de6d62021ab0a8fd4754d13a61869f9758daa5bd Mon Sep 17 00:00:00 2001 From: Gaelle Braud Date: Fri, 27 Jun 2025 12:05:57 +0200 Subject: [PATCH] mentions --- Linphone/core/App.cpp | 2 + Linphone/core/CMakeLists.txt | 2 + Linphone/core/chat/ChatCore.cpp | 4 + Linphone/core/chat/ChatCore.hpp | 1 + .../core/chat/message/ChatMessageCore.cpp | 7 +- .../core/chat/message/imdn/ImdnStatusList.cpp | 1 + .../core/chat/message/imdn/ImdnStatusList.hpp | 1 - .../core/conference/ConferenceInfoCore.cpp | 8 +- .../core/conference/ConferenceInfoCore.hpp | 2 + Linphone/core/participant/ParticipantCore.cpp | 19 +- Linphone/core/participant/ParticipantCore.hpp | 5 + .../core/participant/ParticipantInfoList.cpp | 74 ++++++++ .../core/participant/ParticipantInfoList.hpp | 51 ++++++ .../core/participant/ParticipantInfoProxy.cpp | 63 +++++++ .../core/participant/ParticipantInfoProxy.hpp | 59 +++++++ .../core/participant/ParticipantProxy.cpp | 1 - .../model/chat/message/ChatMessageModel.cpp | 17 -- .../model/chat/message/ChatMessageModel.hpp | 2 - Linphone/tool/UriTools.cpp | 8 +- Linphone/tool/UriTools.hpp | 6 +- Linphone/tool/Utils.cpp | 163 ++++++++++++------ Linphone/tool/Utils.hpp | 5 +- Linphone/view/CMakeLists.txt | 1 + .../view/Control/Display/Chat/ChatMessage.qml | 3 +- .../Display/Chat/ChatMessageContent.qml | 2 + .../Chat/ChatMessageInvitationBubble.qml | 5 +- .../Display/Chat/ChatMessagesListView.qml | 1 + .../Control/Display/Chat/ChatTextContent.qml | 11 +- .../Participant/ParticipantInfoListView.qml | 65 +++++++ .../Input/Chat/ChatDroppableTextArea.qml | 11 +- Linphone/view/Control/Input/TextArea.qml | 5 +- .../view/Page/Form/Chat/SelectedChatView.qml | 57 +++++- Linphone/view/Page/Main/Chat/ChatPage.qml | 2 +- 33 files changed, 566 insertions(+), 98 deletions(-) create mode 100644 Linphone/core/participant/ParticipantInfoList.cpp create mode 100644 Linphone/core/participant/ParticipantInfoList.hpp create mode 100644 Linphone/core/participant/ParticipantInfoProxy.cpp create mode 100644 Linphone/core/participant/ParticipantInfoProxy.hpp create mode 100644 Linphone/view/Control/Display/Participant/ParticipantInfoListView.qml diff --git a/Linphone/core/App.cpp b/Linphone/core/App.cpp index 13e79aaf1..a50ae9a05 100644 --- a/Linphone/core/App.cpp +++ b/Linphone/core/App.cpp @@ -73,6 +73,7 @@ #include "core/notifier/Notifier.hpp" #include "core/participant/ParticipantDeviceProxy.hpp" #include "core/participant/ParticipantGui.hpp" +#include "core/participant/ParticipantInfoProxy.hpp" #include "core/participant/ParticipantProxy.hpp" #include "core/payload-type/PayloadTypeCore.hpp" #include "core/payload-type/PayloadTypeGui.hpp" @@ -652,6 +653,7 @@ void App::initCppInterfaces() { qmlRegisterType(Constants::MainQmlUri, 1, 0, "VariantObject"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "VariantList"); + qmlRegisterType(Constants::MainQmlUri, 1, 0, "ParticipantInfoProxy"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "ParticipantProxy"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "ParticipantGui"); qmlRegisterType(Constants::MainQmlUri, 1, 0, "ConferenceInfoProxy"); diff --git a/Linphone/core/CMakeLists.txt b/Linphone/core/CMakeLists.txt index ac0074ee9..dd36f50c1 100644 --- a/Linphone/core/CMakeLists.txt +++ b/Linphone/core/CMakeLists.txt @@ -83,6 +83,8 @@ list(APPEND _LINPHONEAPP_SOURCES core/participant/ParticipantDeviceProxy.cpp core/participant/ParticipantList.cpp core/participant/ParticipantProxy.cpp + core/participant/ParticipantInfoList.cpp + core/participant/ParticipantInfoProxy.cpp core/screen/ScreenList.cpp core/screen/ScreenProxy.cpp diff --git a/Linphone/core/chat/ChatCore.cpp b/Linphone/core/chat/ChatCore.cpp index e955437b2..9ceea15d6 100644 --- a/Linphone/core/chat/ChatCore.cpp +++ b/Linphone/core/chat/ChatCore.cpp @@ -593,3 +593,7 @@ ChatCore::buildParticipants(const std::shared_ptr &chatRoom) } return result; } + +QList> ChatCore::getParticipants() const { + return mParticipants; +} diff --git a/Linphone/core/chat/ChatCore.hpp b/Linphone/core/chat/ChatCore.hpp index a12a22662..bb51cd161 100644 --- a/Linphone/core/chat/ChatCore.hpp +++ b/Linphone/core/chat/ChatCore.hpp @@ -132,6 +132,7 @@ public: std::shared_ptr getModel() const; QList> buildParticipants(const std::shared_ptr &chatRoom) const; + QList> getParticipants() const; QVariantList getParticipantsGui() const; QStringList getParticipantsAddresses() const; diff --git a/Linphone/core/chat/message/ChatMessageCore.cpp b/Linphone/core/chat/message/ChatMessageCore.cpp index f6dd6d203..bda8e7432 100644 --- a/Linphone/core/chat/message/ChatMessageCore.cpp +++ b/Linphone/core/chat/message/ChatMessageCore.cpp @@ -171,7 +171,7 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr &c auto replymessage = chatmessage->getReplyMessage(); if (replymessage) { mReplyText = ToolModel::getMessageFromContent(replymessage->getContents()); - if (mIsFromChatGroup) mRepliedToName = ToolModel::getDisplayName(replymessage->getToAddress()->clone()); + if (mIsFromChatGroup) mRepliedToName = ToolModel::getDisplayName(replymessage->getFromAddress()->clone()); } } mImdnStatusList = computeDeliveryStatus(chatmessage); @@ -305,7 +305,10 @@ void ChatMessageCore::setSelf(QSharedPointer me) { mChatMessageModelConnection->makeConnectToModel( &ChatMessageModel::participantImdnStateChanged, [this](const std::shared_ptr &message, - const std::shared_ptr &state) {}); + const std::shared_ptr &state) { + auto imdnStatusList = computeDeliveryStatus(message); + mChatMessageModelConnection->invokeToCore([this, imdnStatusList] { setImdnStatusList(imdnStatusList); }); + }); mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::ephemeralMessageTimerStarted, [this](const std::shared_ptr &message) {}); mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::ephemeralMessageDeleted, diff --git a/Linphone/core/chat/message/imdn/ImdnStatusList.cpp b/Linphone/core/chat/message/imdn/ImdnStatusList.cpp index 3eeac8556..09c2a88da 100644 --- a/Linphone/core/chat/message/imdn/ImdnStatusList.cpp +++ b/Linphone/core/chat/message/imdn/ImdnStatusList.cpp @@ -42,6 +42,7 @@ ImdnStatusList::ImdnStatusList(QObject *parent) : AbstractListProxy( ImdnStatusList::~ImdnStatusList() { mustBeInMainThread("~" + getClassName()); + mList.clear(); } QList ImdnStatusList::getImdnStatusList() { diff --git a/Linphone/core/chat/message/imdn/ImdnStatusList.hpp b/Linphone/core/chat/message/imdn/ImdnStatusList.hpp index 2a7b31405..cfb5b8e46 100644 --- a/Linphone/core/chat/message/imdn/ImdnStatusList.hpp +++ b/Linphone/core/chat/message/imdn/ImdnStatusList.hpp @@ -45,7 +45,6 @@ signals: void imdnStatusListChanged(); private: - QList mImdnStatuss; DECLARE_ABSTRACT_OBJECT }; diff --git a/Linphone/core/conference/ConferenceInfoCore.cpp b/Linphone/core/conference/ConferenceInfoCore.cpp index 4d1ea0294..f7b9c89bc 100644 --- a/Linphone/core/conference/ConferenceInfoCore.cpp +++ b/Linphone/core/conference/ConferenceInfoCore.cpp @@ -551,6 +551,10 @@ void ConferenceInfoCore::writeIntoModel(std::shared_ptr mod model->setParticipantInfos(participantInfos); } +std::shared_ptr ConferenceInfoCore::getModel() const { + return mConferenceInfoModel; +} + void ConferenceInfoCore::save() { mustBeInMainThread(getClassName() + "::save()"); ConferenceInfoCore *thisCopy = new ConferenceInfoCore(*this); // Pointer to avoid multiple copies in lambdas @@ -571,8 +575,8 @@ void ConferenceInfoCore::save() { linphone::RegistrationState::Ok) { //: "Erreur" Utils::showInformationPopup(tr("information_popup_error_title"), - //: "Votre compte est déconnecté" - tr("information_popup_disconnected_account_message"), false); + //: "Votre compte est déconnecté" + tr("information_popup_disconnected_account_message"), false); emit saveFailed(); return; } diff --git a/Linphone/core/conference/ConferenceInfoCore.hpp b/Linphone/core/conference/ConferenceInfoCore.hpp index 2b0f5bcd4..5cadfd742 100644 --- a/Linphone/core/conference/ConferenceInfoCore.hpp +++ b/Linphone/core/conference/ConferenceInfoCore.hpp @@ -126,6 +126,8 @@ public: void writeFromModel(const std::shared_ptr &model); void writeIntoModel(std::shared_ptr model); + std::shared_ptr getModel() const; + Q_INVOKABLE void save(); Q_INVOKABLE void undo(); Q_INVOKABLE void cancelCreation(); diff --git a/Linphone/core/participant/ParticipantCore.cpp b/Linphone/core/participant/ParticipantCore.cpp index 8825cac7c..cfea10294 100644 --- a/Linphone/core/participant/ParticipantCore.cpp +++ b/Linphone/core/participant/ParticipantCore.cpp @@ -46,11 +46,13 @@ ParticipantCore::ParticipantCore(const std::shared_ptr &p mParticipantModel->moveToThread(CoreModel::getInstance()->thread()); if (participant) { mAdminStatus = participant->isAdmin(); - mSipAddress = Utils::coreStringToAppString(participant->getAddress()->asStringUriOnly()); + auto participantAddress = participant->getAddress(); + mUsername = Utils::coreStringToAppString(participantAddress->getUsername()); + mSipAddress = Utils::coreStringToAppString(participantAddress->asStringUriOnly()); mIsMe = ToolModel::isMe(mSipAddress); mCreationTime = QDateTime::fromSecsSinceEpoch(participant->getCreationTime()); - mDisplayName = Utils::coreStringToAppString(participant->getAddress()->getDisplayName()); - if (mDisplayName.isEmpty()) mDisplayName = ToolModel::getDisplayName(participant->getAddress()->clone()); + mDisplayName = Utils::coreStringToAppString(participantAddress->getDisplayName()); + if (mDisplayName.isEmpty()) mDisplayName = ToolModel::getDisplayName(participantAddress->clone()); for (auto &device : participant->getDevices()) { auto name = Utils::coreStringToAppString(device->getName()); auto address = Utils::coreStringToAppString(device->getAddress()->asStringUriOnly()); @@ -120,6 +122,17 @@ QString ParticipantCore::getDisplayName() const { return mDisplayName; } +void ParticipantCore::setUsername(const QString &name) { + if (mUsername != name) { + mUsername = name; + emit usernameChanged(); + } +} + +QString ParticipantCore::getUsername() const { + return mUsername; +} + QDateTime ParticipantCore::getCreationTime() const { return mCreationTime; } diff --git a/Linphone/core/participant/ParticipantCore.hpp b/Linphone/core/participant/ParticipantCore.hpp index b40359739..5b6001ffe 100644 --- a/Linphone/core/participant/ParticipantCore.hpp +++ b/Linphone/core/participant/ParticipantCore.hpp @@ -40,6 +40,7 @@ class ParticipantCore : public QObject, public AbstractObject { Q_PROPERTY(QString sipAddress READ getSipAddress WRITE setSipAddress NOTIFY sipAddressChanged) Q_PROPERTY(QString displayName READ getDisplayName WRITE setDisplayName NOTIFY displayNameChanged) + Q_PROPERTY(QString username READ getUsername WRITE setUsername NOTIFY usernameChanged) Q_PROPERTY(bool isAdmin READ isAdmin WRITE setIsAdmin NOTIFY isAdminChanged) Q_PROPERTY(bool isMe READ isMe NOTIFY isMeChanged) Q_PROPERTY(QDateTime creationTime READ getCreationTime CONSTANT) @@ -56,6 +57,7 @@ public: void setSelf(QSharedPointer me); QString getDisplayName() const; + QString getUsername() const; QString getSipAddress() const; QDateTime getCreationTime() const; bool isAdmin() const; @@ -69,6 +71,7 @@ public: void setSipAddress(const QString &address); void setDisplayName(const QString &name); + void setUsername(const QString &name); void setCreationTime(const QDateTime &date); void setIsAdmin(const bool &status); void setIsFocus(const bool &focus); @@ -92,6 +95,7 @@ signals: void invitingChanged(); void creationTimeChanged(); void displayNameChanged(); + void usernameChanged(); void lStartInvitation(const int &secs = 30); void lSetIsAdmin(bool status); @@ -107,6 +111,7 @@ private: QList mParticipantDevices; QString mDisplayName; + QString mUsername; QString mSipAddress; QDateTime mCreationTime; bool mAdminStatus; diff --git a/Linphone/core/participant/ParticipantInfoList.cpp b/Linphone/core/participant/ParticipantInfoList.cpp new file mode 100644 index 000000000..4132a8716 --- /dev/null +++ b/Linphone/core/participant/ParticipantInfoList.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2010-2020 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 "ParticipantInfoList.hpp" +#include "core/App.hpp" +#include "core/chat/ChatCore.hpp" +#include "core/participant/ParticipantGui.hpp" +#include "tool/Utils.hpp" + +DEFINE_ABSTRACT_OBJECT(ParticipantInfoList) + +QSharedPointer ParticipantInfoList::create() { + auto model = QSharedPointer(new ParticipantInfoList(), &QObject::deleteLater); + model->moveToThread(App::getInstance()->thread()); + return model; +} + +QSharedPointer ParticipantInfoList::create(const QSharedPointer &chatCore) { + auto model = create(); + model->setChatCore(chatCore); + return model; +} + +ParticipantInfoList::ParticipantInfoList(QObject *parent) : ListProxy(parent) { + App::getInstance()->mEngine->setObjectOwnership(this, QQmlEngine::CppOwnership); +} + +ParticipantInfoList::~ParticipantInfoList() { + mList.clear(); +} + +void ParticipantInfoList::setChatCore(const QSharedPointer &chatCore) { + mustBeInMainThread(log().arg(Q_FUNC_INFO)); + if (mChatCore) disconnect(mChatCore.get()); + mChatCore = chatCore; + lDebug() << "[ParticipantInfoList] : set Chat " << mChatCore.get(); + clearData(); + if (mChatCore) { + auto buildList = [this] { + QStringList participantAddresses; + QList> participantList; + auto participants = mChatCore->getParticipants(); + resetData(participants); + }; + connect(mChatCore.get(), &ChatCore::participantsChanged, this, buildList); + buildList(); + } +} + +QVariant ParticipantInfoList::data(const QModelIndex &index, int role) const { + int row = index.row(); + if (!index.isValid() || row < 0 || row >= mList.count()) return QVariant(); + if (role == Qt::DisplayRole) { + return QVariant::fromValue(new ParticipantGui(mList[row].objectCast())); + } + return QVariant(); +} \ No newline at end of file diff --git a/Linphone/core/participant/ParticipantInfoList.hpp b/Linphone/core/participant/ParticipantInfoList.hpp new file mode 100644 index 000000000..b77609151 --- /dev/null +++ b/Linphone/core/participant/ParticipantInfoList.hpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2020 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 PARTICIPANT_INFO_LIST_H_ +#define PARTICIPANT_INFO_LIST_H_ + +#include "../proxy/ListProxy.hpp" +#include "model/chat/ChatModel.hpp" +#include "tool/thread/SafeConnection.hpp" + +class ChatCore; + +// ============================================================================= + +class ParticipantInfoList : public ListProxy, public AbstractObject { + Q_OBJECT +public: + static QSharedPointer create(); + static QSharedPointer create(const QSharedPointer &chatCore); + + ParticipantInfoList(QObject *parent = Q_NULLPTR); + virtual ~ParticipantInfoList(); + + void setChatCore(const QSharedPointer &chatCore); + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +signals: + void lUpdateParticipants(); + +private: + QSharedPointer mChatCore; + DECLARE_ABSTRACT_OBJECT +}; +#endif // PARTICIPANT_INFO_LIST_H_ diff --git a/Linphone/core/participant/ParticipantInfoProxy.cpp b/Linphone/core/participant/ParticipantInfoProxy.cpp new file mode 100644 index 000000000..926ec1cdc --- /dev/null +++ b/Linphone/core/participant/ParticipantInfoProxy.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2010-2020 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 "ParticipantInfoProxy.hpp" +#include "ParticipantInfoList.hpp" + +#include "core/chat/ChatCore.hpp" +#include "model/core/CoreModel.hpp" +#include "tool/Utils.hpp" + +#include + +// ============================================================================= + +DEFINE_ABSTRACT_OBJECT(ParticipantInfoProxy) + +ParticipantInfoProxy::ParticipantInfoProxy(QObject *parent) : LimitProxy(parent) { + mParticipants = ParticipantInfoList::create(); + setSourceModels(new SortFilterList(mParticipants.get(), Qt::AscendingOrder)); +} + +ParticipantInfoProxy::~ParticipantInfoProxy() { +} + +ChatGui *ParticipantInfoProxy::getChat() const { + return mChat; +} + +void ParticipantInfoProxy::setChat(ChatGui *chat) { + lDebug() << "[ParticipantInfoProxy] set current chat " << this << " => " << chat; + if (mChat != chat) { + mChat = chat; + mParticipants->setChatCore(chat ? chat->mCore : nullptr); + emit chatChanged(); + } +} + +// ----------------------------------------------------------------------------- + +bool ParticipantInfoProxy::SortFilterList::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { + return true; +} + +bool ParticipantInfoProxy::SortFilterList::lessThan(const QModelIndex &left, const QModelIndex &right) const { + return true; +} diff --git a/Linphone/core/participant/ParticipantInfoProxy.hpp b/Linphone/core/participant/ParticipantInfoProxy.hpp new file mode 100644 index 000000000..18c966e3c --- /dev/null +++ b/Linphone/core/participant/ParticipantInfoProxy.hpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 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 PARTICIPANT_INFO_PROXY_H_ +#define PARTICIPANT_INFO_PROXY_H_ + +#include "../proxy/LimitProxy.hpp" +#include "core/chat/ChatGui.hpp" +#include "tool/AbstractObject.hpp" + +#include + +class ParticipantInfoList; +class ChatModel; +// ============================================================================= + +class QWindow; + +class ParticipantInfoProxy : public LimitProxy, public AbstractObject { + + Q_OBJECT + Q_PROPERTY(ChatGui *chat READ getChat WRITE setChat NOTIFY chatChanged) + +public: + DECLARE_SORTFILTER_CLASS(bool mShowMe;) + + ParticipantInfoProxy(QObject *parent = Q_NULLPTR); + ~ParticipantInfoProxy(); + + ChatGui *getChat() const; + void setChat(ChatGui *chatGui); + +signals: + void chatChanged(); + +private: + ChatGui *mChat = nullptr; + QSharedPointer mParticipants; + DECLARE_ABSTRACT_OBJECT +}; + +#endif // PARTICIPANT_INFO_PROXY_H_ diff --git a/Linphone/core/participant/ParticipantProxy.cpp b/Linphone/core/participant/ParticipantProxy.cpp index 6c53404f6..1fec44141 100644 --- a/Linphone/core/participant/ParticipantProxy.cpp +++ b/Linphone/core/participant/ParticipantProxy.cpp @@ -25,7 +25,6 @@ #include "model/core/CoreModel.hpp" #include "tool/Utils.hpp" -#include "ParticipantList.hpp" #include "core/participant/ParticipantCore.hpp" #include diff --git a/Linphone/model/chat/message/ChatMessageModel.cpp b/Linphone/model/chat/message/ChatMessageModel.cpp index dc5cbed81..6d073c046 100644 --- a/Linphone/model/chat/message/ChatMessageModel.cpp +++ b/Linphone/model/chat/message/ChatMessageModel.cpp @@ -115,24 +115,8 @@ linphone::ChatMessage::State ChatMessageModel::getState() const { return mMonitor->getState(); } -void ChatMessageModel::computeDeliveryStatus() { - // Read - for (auto &participant : mMonitor->getParticipantsByImdnState(linphone::ChatMessage::State::Displayed)) { - } - // Received - for (auto &participant : mMonitor->getParticipantsByImdnState(linphone::ChatMessage::State::DeliveredToUser)) { - } - // Sent - for (auto &participant : mMonitor->getParticipantsByImdnState(linphone::ChatMessage::State::Delivered)) { - } - // Error - for (auto &participant : mMonitor->getParticipantsByImdnState(linphone::ChatMessage::State::NotDelivered)) { - } -} - void ChatMessageModel::onMsgStateChanged(const std::shared_ptr &message, linphone::ChatMessage::State state) { - computeDeliveryStatus(); emit msgStateChanged(message, state); } @@ -184,7 +168,6 @@ void ChatMessageModel::onFileTransferProgressIndication(const std::shared_ptr
  • &message, const std::shared_ptr &state) { - computeDeliveryStatus(); emit participantImdnStateChanged(message, state); } diff --git a/Linphone/model/chat/message/ChatMessageModel.hpp b/Linphone/model/chat/message/ChatMessageModel.hpp index 73e145c12..1d64df15a 100644 --- a/Linphone/model/chat/message/ChatMessageModel.hpp +++ b/Linphone/model/chat/message/ChatMessageModel.hpp @@ -53,8 +53,6 @@ public: void deleteMessageFromChatRoom(); - void computeDeliveryStatus(); - void sendReaction(const QString &reaction); void removeReaction(); diff --git a/Linphone/tool/UriTools.cpp b/Linphone/tool/UriTools.cpp index ccfdd54be..97f0c81f0 100644 --- a/Linphone/tool/UriTools.cpp +++ b/Linphone/tool/UriTools.cpp @@ -40,6 +40,10 @@ QVector> UriTools::parseUri(const QString &text) { return parse(text, gUriTools.mUriRegularExpression); } +QVector> UriTools::parseMention(const QString &text) { + return parse(text, gUriTools.mMentionRegularExpression); +} + // Parse a text and return all lines where regex is matched or not QVector> UriTools::parse(const QString &text, const QRegularExpression regex) { QVector> results; @@ -200,4 +204,6 @@ void UriTools::initRegularExpressions() { QRegularExpression::UseUnicodePropertiesOption); mUriRegularExpression = QRegularExpression(URI, QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption); -} + mMentionRegularExpression = QRegularExpression( + "@[A-Za-z0-9.-_]+", QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption); +} \ No newline at end of file diff --git a/Linphone/tool/UriTools.hpp b/Linphone/tool/UriTools.hpp index 2fb29fcf3..140f75b00 100644 --- a/Linphone/tool/UriTools.hpp +++ b/Linphone/tool/UriTools.hpp @@ -41,14 +41,16 @@ public: static QVector> parseIri(const QString &text); static QVector> parseUri(const QString &text); + static QVector> parseMention(const QString &text); static QRegularExpression getRegularExpression(); private: void initRegularExpressions(); static QVector> parse(const QString &text, const QRegularExpression regex); - QRegularExpression mIriRegularExpression; // https://tools.ietf.org/html/rfc3987 - QRegularExpression mUriRegularExpression; // https://tools.ietf.org/html/rfc3986 + QRegularExpression mIriRegularExpression; // https://tools.ietf.org/html/rfc3987 + QRegularExpression mUriRegularExpression; // https://tools.ietf.org/html/rfc3986 + QRegularExpression mMentionRegularExpression; // https://tools.ietf.org/html/rfc3986 }; #endif \ No newline at end of file diff --git a/Linphone/tool/Utils.cpp b/Linphone/tool/Utils.cpp index 9e6dd6600..7aa2e3efc 100644 --- a/Linphone/tool/Utils.cpp +++ b/Linphone/tool/Utils.cpp @@ -1798,64 +1798,111 @@ QString Utils::getPresenceStatus(LinphoneEnums::Presence presence) { return presenceStatus; } -QString Utils::encodeTextToQmlRichFormat(const QString &text, const QVariantMap &options) { +VariantObject *Utils::encodeTextToQmlRichFormat(const QString &text, const QVariantMap &options, ChatGui *chat) { /*QString images; QStringList imageFormat; for(auto format : QImageReader::supportedImageFormats()) imageFormat.append(QString::fromLatin1(format).toUpper()); - */ - QStringList formattedText; - bool lastWasUrl = false; + */ + VariantObject *data = new VariantObject("encodeTextToQmlRichFormat"); + if (!data) return nullptr; + auto primaryColor = getDefaultStyleColor("info_500_main"); + data->makeRequest([text, options, chat, primaryColor] { + QStringList formattedText; + bool lastWasUrl = false; - if (options.contains("noLink") && options["noLink"].toBool()) { - formattedText.append(encodeEmojiToQmlRichFormat(text)); - } else { - auto primaryColor = getDefaultStyleColor("info_500_main"); - auto iriParsed = UriTools::parseIri(text); + if (options.contains("noLink") && options["noLink"].toBool()) { + formattedText.append(encodeEmojiToQmlRichFormat(text)); + } else { - for (int i = 0; i < iriParsed.size(); ++i) { - QString iri = iriParsed[i] - .second.replace('&', "&") - .replace('<', "\u2063<") - .replace('>', "\u2063>") - .replace('"', """) - .replace('\'', "'"); - if (!iriParsed[i].first) { - if (lastWasUrl) { - lastWasUrl = false; - if (iri.front() != ' ') iri.push_front(' '); + auto iriParsed = UriTools::parseIri(text); + + for (int i = 0; i < iriParsed.size(); ++i) { + QString iri = iriParsed[i] + .second.replace('&', "&") + .replace('<', "\u2063<") + .replace('>', "\u2063>") + .replace('"', """) + .replace('\'', "'"); + if (!iriParsed[i].first) { + if (lastWasUrl) { + lastWasUrl = false; + if (iri.front() != ' ') iri.push_front(' '); + } + formattedText.append(encodeEmojiToQmlRichFormat(iri)); + } else { + QString uri = + iriParsed[i].second.left(3) == "www" ? "http://" + iriParsed[i].second : iriParsed[i].second; + /* TODO : preview from link + int extIndex = iriParsed[i].second.lastIndexOf('.'); + QString ext; + if( extIndex >= 0) + ext = iriParsed[i].second.mid(extIndex+1).toUpper(); + if(imageFormat.contains(ext.toLatin1())){// imagesHeight is not used because of bugs on display + (blank image if set without width) images += ""+uri+""; + }else{ + */ + formattedText.append("" + iri + + ""); + lastWasUrl = true; + /*}*/ } - formattedText.append(encodeEmojiToQmlRichFormat(iri)); - } else { - QString uri = - iriParsed[i].second.left(3) == "www" ? "http://" + iriParsed[i].second : iriParsed[i].second; - /* TODO : preview from link - int extIndex = iriParsed[i].second.lastIndexOf('.'); - QString ext; - if( extIndex >= 0) - ext = iriParsed[i].second.mid(extIndex+1).toUpper(); - if(imageFormat.contains(ext.toLatin1())){// imagesHeight is not used because of bugs on display (blank - image if set without width) images += ""+uri+""; - }else{ - */ - formattedText.append("" + iri + - ""); - lastWasUrl = true; - /*}*/ } } - } - if (lastWasUrl && formattedText.last().back() != ' ') { - formattedText.push_back(" "); - } - return "

    " + formattedText.join(""); + if (lastWasUrl && formattedText.last().back() != ' ') { + formattedText.push_back(" "); + } + if (chat && chat->mCore) { + auto participants = chat->mCore->getParticipants(); + auto mentionsParsed = UriTools::parseMention(formattedText.join("")); + formattedText.clear(); + + for (int i = 0; i < mentionsParsed.size(); ++i) { + QString mention = mentionsParsed[i].second; + + if (mentionsParsed[i].first) { + QString mentions = mentionsParsed[i].second; + QStringList finalMentions; + QStringList parts = mentions.split(" "); + for (auto part : parts) { + if (part.startsWith("@")) { // mention + QString username = part; + username.removeFirst(); + auto it = std::find_if( + participants.begin(), participants.end(), + [username](QSharedPointer p) { return username == p->getUsername(); }); + if (it != participants.end()) { + auto foundParticipant = participants.at(std::distance(participants.begin(), it)); + auto address = foundParticipant->getSipAddress(); + auto isFriend = ToolModel::findFriendByAddress(address); + if (isFriend) + part = "@" + Utils::coreStringToAppString(isFriend->getAddress()->getDisplayName()); + QString participantLink = "" + part + ""; + finalMentions.append(participantLink); + } else { + finalMentions.append(part); + } + } else { + finalMentions.append(part); + } + } + formattedText.push_back(finalMentions.join(" ")); + } else { + formattedText.push_back(mentionsParsed[i].second); + } + } + } + return "

    " + formattedText.join(""); + }); + data->requestValue(); + return data; } QString Utils::encodeEmojiToQmlRichFormat(const QString &body) { @@ -1896,6 +1943,24 @@ bool Utils::isOnlyEmojis(const QString &text) { return true; } +void Utils::openContactAtAddress(const QString &address) { + App::postModelAsync([address] { + auto isFriend = ToolModel::findFriendByAddress(address); + if (isFriend) { + App::postCoreAsync([address] { + auto window = getMainWindow(); + QMetaObject::invokeMethod(window, "displayContactPage", Q_ARG(QVariant, address)); + }); + } else { + App::postCoreAsync([address] { + auto window = getMainWindow(); + QMetaObject::invokeMethod(window, "displayCreateContactPage", Q_ARG(QVariant, ""), + Q_ARG(QVariant, address)); + }); + } + }); +} + QString Utils::getFilename(QUrl url) { return url.fileName(); } diff --git a/Linphone/tool/Utils.hpp b/Linphone/tool/Utils.hpp index 5d60008f4..2e5c6e4e8 100644 --- a/Linphone/tool/Utils.hpp +++ b/Linphone/tool/Utils.hpp @@ -154,10 +154,11 @@ public: Q_INVOKABLE static VariantObject *createGroupChat(QString subject, QStringList participantAddresses); Q_INVOKABLE static void openChat(ChatGui *chat); Q_INVOKABLE static bool isEmptyMessage(QString message); - Q_INVOKABLE static QString encodeTextToQmlRichFormat(const QString &text, - const QVariantMap &options = QVariantMap()); + Q_INVOKABLE static VariantObject * + encodeTextToQmlRichFormat(const QString &text, const QVariantMap &options = QVariantMap(), ChatGui *chat = nullptr); Q_INVOKABLE static QString encodeEmojiToQmlRichFormat(const QString &body); Q_INVOKABLE static bool isOnlyEmojis(const QString &text); + Q_INVOKABLE static void openContactAtAddress(const QString &address); Q_INVOKABLE static QString getFilename(QUrl url); static bool codepointIsEmoji(uint code); diff --git a/Linphone/view/CMakeLists.txt b/Linphone/view/CMakeLists.txt index 0f7c4c825..8f1fe4899 100644 --- a/Linphone/view/CMakeLists.txt +++ b/Linphone/view/CMakeLists.txt @@ -78,6 +78,7 @@ list(APPEND _LINPHONEAPP_QML_FILES view/Control/Display/Contact/Voicemail.qml view/Control/Display/Meeting/MeetingListView.qml view/Control/Display/Participant/ParticipantDeviceListView.qml + view/Control/Display/Participant/ParticipantInfoListView.qml view/Control/Display/Participant/ParticipantListView.qml view/Control/Display/Settings/SettingsMenuItem.qml diff --git a/Linphone/view/Control/Display/Chat/ChatMessage.qml b/Linphone/view/Control/Display/Chat/ChatMessage.qml index e96bd8a65..cf7204157 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessage.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessage.qml @@ -15,6 +15,7 @@ Control.Control { property bool isFirstMessage property ChatMessageGui chatMessage + property ChatGui chat property string ownReaction: chatMessage? chatMessage.core.ownReaction : "" property string fromAddress: chatMessage? chatMessage.core.fromAddress : "" property bool isRemoteMessage: chatMessage? chatMessage.core.isRemoteMessage : false @@ -140,7 +141,6 @@ Control.Control { Layout.preferredWidth: mainItem.isRemoteMessage ? 26 * DefaultStyle.dp : 0 Layout.preferredHeight: 26 * DefaultStyle.dp Layout.alignment: Qt.AlignTop - Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0 _address: chatMessage ? chatMessage.core.fromAddress : "" } Item { @@ -197,6 +197,7 @@ Control.Control { id: chatBubbleContent Layout.fillWidth: true Layout.fillHeight: true + chatGui: mainItem.chat chatMessageGui: mainItem.chatMessage onMouseEvent: (event) => { mainItem.handleDefaultMouseEvent(event) diff --git a/Linphone/view/Control/Display/Chat/ChatMessageContent.qml b/Linphone/view/Control/Display/Chat/ChatMessageContent.qml index 4c4140bd0..706d35c78 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessageContent.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessageContent.qml @@ -11,6 +11,7 @@ import Linphone ColumnLayout { id: mainItem property ChatMessageGui chatMessageGui: null + property ChatGui chatGui: null signal isFileHoveringChanged(bool isFileHovering) signal lastSelectedTextChanged(string selectedText) @@ -88,6 +89,7 @@ ColumnLayout { Layout.fillWidth: true // height: implicitHeight contentGui: modelData + chatGui: mainItem.chatGui onLastTextSelectedChanged: mainItem.selectedText = selectedText // onRightClicked: mainItem.rightClicked() } diff --git a/Linphone/view/Control/Display/Chat/ChatMessageInvitationBubble.qml b/Linphone/view/Control/Display/Chat/ChatMessageInvitationBubble.qml index bce3875d0..6b31ba74a 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessageInvitationBubble.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessageInvitationBubble.qml @@ -202,13 +202,14 @@ Rectangle { } Text { - text: UtilsCpp.encodeTextToQmlRichFormat(conferenceInfo.description) + property var encodeTextObj: UtilsCpp.encodeTextToQmlRichFormat(conferenceInfo.description) + text: encodeTextObj? encodeTextObj.value : "" wrapMode: Text.WordWrap textFormat: Text.RichText font: Typography.p4 color: DefaultStyle.main2_500main visible: conferenceInfo.description.length > 0 - onLinkActivated: { + onLinkActivated: (link) => { if (link.startsWith('sip')) UtilsCpp.createCall(link) else diff --git a/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml b/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml index ccedd6e26..ac377a817 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml @@ -122,6 +122,7 @@ ListView { delegate: ChatMessage { chatMessage: modelData + chat: mainItem.chat maxWidth: Math.round(mainItem.width * (3/4)) onVisibleChanged: { if (visible && !modelData.core.isRead) modelData.core.lMarkAsRead() diff --git a/Linphone/view/Control/Display/Chat/ChatTextContent.qml b/Linphone/view/Control/Display/Chat/ChatTextContent.qml index 19afaff66..5dce1138a 100644 --- a/Linphone/view/Control/Display/Chat/ChatTextContent.qml +++ b/Linphone/view/Control/Display/Chat/ChatTextContent.qml @@ -9,8 +9,9 @@ import UtilsCpp // TODO : into Loader // ============================================================================= TextEdit { - id: message + id: mainItem property ChatMessageContentGui contentGui + property ChatGui chatGui: null property string lastTextSelected : '' color: DefaultStyle.main2_700 font { @@ -24,15 +25,19 @@ TextEdit { readOnly: true selectByMouse: true - text: visible ? UtilsCpp.encodeTextToQmlRichFormat(contentGui.core.utf8Text) + property var encodeTextObj: visible ? UtilsCpp.encodeTextToQmlRichFormat(contentGui.core.utf8Text, {}, mainItem.chatGui) : '' - + text: encodeTextObj ? encodeTextObj.value : "" textFormat: Text.RichText // To supports links and imgs. wrapMode: TextEdit.Wrap onLinkActivated: (link) => { if (link.startsWith('sip')) UtilsCpp.createCall(link) + else if (link.startsWith('mention:')) { + var mentionAddress = link.substring(8) // remove "mention:" + UtilsCpp.openContactAtAddress(mentionAddress); + } else Qt.openUrlExternally(link) } diff --git a/Linphone/view/Control/Display/Participant/ParticipantInfoListView.qml b/Linphone/view/Control/Display/Participant/ParticipantInfoListView.qml new file mode 100644 index 000000000..4b1e6d93f --- /dev/null +++ b/Linphone/view/Control/Display/Participant/ParticipantInfoListView.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Layouts + +import Linphone +import UtilsCpp + +ListView { + id: mainItem + clip: true + spacing: Math.round(5 * DefaultStyle.dp) + + property bool hoverEnabled: true + property bool displayNameCapitalization: true + + property ChatGui chatGui + height: contentHeight + + signal participantClicked(string username) + + currentIndex: -1 + + model: ParticipantInfoProxy { + id: participantModel + chat: mainItem.chatGui + } + + delegate: Item { + id: participantDelegate + height: Math.round(56 * DefaultStyle.dp) + width: mainItem.width//mainItem.width + RowLayout { + anchors.fill: parent + anchors.leftMargin: Math.round(18 * DefaultStyle.dp) + anchors.rightMargin: Math.round(18 * DefaultStyle.dp) + spacing: Math.round(10 * DefaultStyle.dp) + Avatar { + Layout.preferredWidth: Math.round(45 * DefaultStyle.dp) + Layout.preferredHeight: Math.round(45 * DefaultStyle.dp) + _address: modelData.core.sipAddress + shadowEnabled: false + } + Text { + text: modelData.core.displayName + font.pixelSize: Math.round(14 * DefaultStyle.dp) + font.capitalization: mainItem.displayNameCapitalization ? Font.Capitalize : Font.MixedCase + maximumLineCount: 1 + Layout.fillWidth: true + } + Item{Layout.fillWidth: true} + } + MouseArea { + id: mousearea + anchors.fill: parent + onClicked: mainItem.participantClicked(modelData.core.username) + hoverEnabled: true + cursorShape: containsMouse ? Qt.PointingHandCursor : Qt.ArrowCursor + Rectangle { + anchors.fill: parent + visible: mousearea.containsMouse + color: DefaultStyle.main2_200 + opacity: 0.5 + } + } + } +} diff --git a/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml b/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml index 8a96c087c..21f194279 100644 --- a/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml +++ b/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml @@ -138,8 +138,13 @@ Control.Control { width: sendingAreaFlickable.width height: sendingAreaFlickable.height textFormat: TextEdit.AutoText - onTextChanged: mainItem.text = text - Component.onCompleted: mainItem.textArea = sendingTextArea + onTextChanged: { + mainItem.text = text + } + Component.onCompleted: { + mainItem.textArea = sendingTextArea + sendingTextArea.text = mainItem.text + } //: Say something… : placeholder text for sending message text area placeholderText: qsTr("chat_view_send_area_placeholder_text") placeholderTextColor: DefaultStyle.main2_400 @@ -160,7 +165,7 @@ Control.Control { Connections { target: mainItem function onTextChanged() { - if (mainItem.text !== text) text = mainItem.text + sendingTextArea.text = mainItem.text } function onSendMessage() { sendingTextArea.clear() diff --git a/Linphone/view/Control/Input/TextArea.qml b/Linphone/view/Control/Input/TextArea.qml index 279d9f516..0767136f3 100644 --- a/Linphone/view/Control/Input/TextArea.qml +++ b/Linphone/view/Control/Input/TextArea.qml @@ -19,7 +19,8 @@ TextEdit { activeFocusOnTab: true property bool displayAsRichText: false - property string richFormatText: UtilsCpp.encodeTextToQmlRichFormat(text) + property var encodeTextObj: UtilsCpp.encodeTextToQmlRichFormat(text) + property string richFormatText: encodeTextObj && encodeTextObj.value || "" property color textAreaColor @@ -30,7 +31,7 @@ TextEdit { } onTextChanged: { - richFormatText = UtilsCpp.encodeTextToQmlRichFormat(text) + encodeTextObj = UtilsCpp.encodeTextToQmlRichFormat(text) } MouseArea { diff --git a/Linphone/view/Page/Form/Chat/SelectedChatView.qml b/Linphone/view/Page/Form/Chat/SelectedChatView.qml index 256dc084a..b5a6114de 100644 --- a/Linphone/view/Page/Form/Chat/SelectedChatView.qml +++ b/Linphone/view/Page/Form/Chat/SelectedChatView.qml @@ -210,6 +210,54 @@ RowLayout { anchors.rightMargin: Math.round(5 * DefaultStyle.dp) policy: Control.ScrollBar.AsNeeded } + Control.Control { + id: participantListPopup + width: parent.width + height: Math.min(contentItem.height, Math.round(200 * DefaultStyle.dp)) + visible: false + anchors.bottom: chatMessagesListView.bottom + anchors.left: chatMessagesListView.left + anchors.right: chatMessagesListView.right + + background: Item { + anchors.fill: parent + Rectangle { + id: participantBg + color: DefaultStyle.grey_0 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + radius: Math.round(20 * DefaultStyle.dp) + height: parent.height + } + MultiEffect { + anchors.fill: participantBg + source: participantBg + shadowEnabled: true + shadowBlur: 0.5 + shadowColor: DefaultStyle.grey_1000 + shadowOpacity: 0.3 + } + Rectangle { + id: bg + color: DefaultStyle.grey_0 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height/2 + } + } + contentItem: ParticipantInfoListView { + id: participantInfoList + height: contentHeight + width: participantListPopup.width + chatGui: mainItem.chat + onParticipantClicked: (username) => { + messageSender.text = messageSender.text + username + " " + messageSender.textArea.cursorPosition = messageSender.text.length + } + } + } } Control.Control { id: selectedFilesArea @@ -333,14 +381,16 @@ RowLayout { Control.SplitView.preferredHeight: mainItem.chat.core.isReadOnly ? 0 : Math.round(79 * DefaultStyle.dp) Control.SplitView.minimumHeight: mainItem.chat.core.isReadOnly ? 0 : Math.round(79 * DefaultStyle.dp) chat: mainItem.chat - Component.onCompleted: { - - if (mainItem.chat) text = mainItem.chat.core.sendingText + onChatChanged: { + if (chat) messageSender.text = mainItem.chat.core.sendingText } onTextChanged: { if (text !== "" && mainItem.chat.core.composingName !== "") { mainItem.chat.core.lCompose() } + var lastChar = text.slice(-1) + if (lastChar == "@") participantListPopup.visible = true + else participantListPopup.visible = false mainItem.chat.core.sendingText = text } onSendMessage: { @@ -359,7 +409,6 @@ RowLayout { } } } - } Rectangle { visible: detailsPanel.visible diff --git a/Linphone/view/Page/Main/Chat/ChatPage.qml b/Linphone/view/Page/Main/Chat/ChatPage.qml index 2c0340743..3fc045c07 100644 --- a/Linphone/view/Page/Main/Chat/ChatPage.qml +++ b/Linphone/view/Page/Main/Chat/ChatPage.qml @@ -311,7 +311,7 @@ AbstractMainPage { FocusScope { SelectedChatView { anchors.fill: parent - chat: mainItem.selectedChatGui + chat: mainItem.selectedChatGui || null } } }