This commit is contained in:
Gaelle Braud 2025-06-27 12:05:57 +02:00
parent a7e39ab276
commit de6d62021a
33 changed files with 566 additions and 98 deletions

View file

@ -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<VariantObject>(Constants::MainQmlUri, 1, 0, "VariantObject");
qmlRegisterType<VariantList>(Constants::MainQmlUri, 1, 0, "VariantList");
qmlRegisterType<ParticipantInfoProxy>(Constants::MainQmlUri, 1, 0, "ParticipantInfoProxy");
qmlRegisterType<ParticipantProxy>(Constants::MainQmlUri, 1, 0, "ParticipantProxy");
qmlRegisterType<ParticipantGui>(Constants::MainQmlUri, 1, 0, "ParticipantGui");
qmlRegisterType<ConferenceInfoProxy>(Constants::MainQmlUri, 1, 0, "ConferenceInfoProxy");

View file

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

View file

@ -593,3 +593,7 @@ ChatCore::buildParticipants(const std::shared_ptr<linphone::ChatRoom> &chatRoom)
}
return result;
}
QList<QSharedPointer<ParticipantCore>> ChatCore::getParticipants() const {
return mParticipants;
}

View file

@ -132,6 +132,7 @@ public:
std::shared_ptr<ChatModel> getModel() const;
QList<QSharedPointer<ParticipantCore>> buildParticipants(const std::shared_ptr<linphone::ChatRoom> &chatRoom) const;
QList<QSharedPointer<ParticipantCore>> getParticipants() const;
QVariantList getParticipantsGui() const;
QStringList getParticipantsAddresses() const;

View file

@ -171,7 +171,7 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &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<ChatMessageCore> me) {
mChatMessageModelConnection->makeConnectToModel(
&ChatMessageModel::participantImdnStateChanged,
[this](const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<const linphone::ParticipantImdnState> &state) {});
const std::shared_ptr<const linphone::ParticipantImdnState> &state) {
auto imdnStatusList = computeDeliveryStatus(message);
mChatMessageModelConnection->invokeToCore([this, imdnStatusList] { setImdnStatusList(imdnStatusList); });
});
mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::ephemeralMessageTimerStarted,
[this](const std::shared_ptr<linphone::ChatMessage> &message) {});
mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::ephemeralMessageDeleted,

View file

@ -42,6 +42,7 @@ ImdnStatusList::ImdnStatusList(QObject *parent) : AbstractListProxy<ImdnStatus>(
ImdnStatusList::~ImdnStatusList() {
mustBeInMainThread("~" + getClassName());
mList.clear();
}
QList<ImdnStatus> ImdnStatusList::getImdnStatusList() {

View file

@ -45,7 +45,6 @@ signals:
void imdnStatusListChanged();
private:
QList<ImdnStatus> mImdnStatuss;
DECLARE_ABSTRACT_OBJECT
};

View file

@ -551,6 +551,10 @@ void ConferenceInfoCore::writeIntoModel(std::shared_ptr<ConferenceInfoModel> mod
model->setParticipantInfos(participantInfos);
}
std::shared_ptr<ConferenceInfoModel> 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;
}

View file

@ -126,6 +126,8 @@ public:
void writeFromModel(const std::shared_ptr<ConferenceInfoModel> &model);
void writeIntoModel(std::shared_ptr<ConferenceInfoModel> model);
std::shared_ptr<ConferenceInfoModel> getModel() const;
Q_INVOKABLE void save();
Q_INVOKABLE void undo();
Q_INVOKABLE void cancelCreation();

View file

@ -46,11 +46,13 @@ ParticipantCore::ParticipantCore(const std::shared_ptr<linphone::Participant> &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;
}

View file

@ -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<ParticipantCore> 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<QVariant> mParticipantDevices;
QString mDisplayName;
QString mUsername;
QString mSipAddress;
QDateTime mCreationTime;
bool mAdminStatus;

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#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> ParticipantInfoList::create() {
auto model = QSharedPointer<ParticipantInfoList>(new ParticipantInfoList(), &QObject::deleteLater);
model->moveToThread(App::getInstance()->thread());
return model;
}
QSharedPointer<ParticipantInfoList> ParticipantInfoList::create(const QSharedPointer<ChatCore> &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> &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<QSharedPointer<ParticipantGui>> participantList;
auto participants = mChatCore->getParticipants();
resetData<ParticipantCore>(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<ParticipantCore>()));
}
return QVariant();
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<ParticipantInfoList> create();
static QSharedPointer<ParticipantInfoList> create(const QSharedPointer<ChatCore> &chatCore);
ParticipantInfoList(QObject *parent = Q_NULLPTR);
virtual ~ParticipantInfoList();
void setChatCore(const QSharedPointer<ChatCore> &chatCore);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
signals:
void lUpdateParticipants();
private:
QSharedPointer<ChatCore> mChatCore;
DECLARE_ABSTRACT_OBJECT
};
#endif // PARTICIPANT_INFO_LIST_H_

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "ParticipantInfoProxy.hpp"
#include "ParticipantInfoList.hpp"
#include "core/chat/ChatCore.hpp"
#include "model/core/CoreModel.hpp"
#include "tool/Utils.hpp"
#include <QDebug>
// =============================================================================
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;
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef PARTICIPANT_INFO_PROXY_H_
#define PARTICIPANT_INFO_PROXY_H_
#include "../proxy/LimitProxy.hpp"
#include "core/chat/ChatGui.hpp"
#include "tool/AbstractObject.hpp"
#include <memory>
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<ParticipantInfoList> mParticipants;
DECLARE_ABSTRACT_OBJECT
};
#endif // PARTICIPANT_INFO_PROXY_H_

View file

@ -25,7 +25,6 @@
#include "model/core/CoreModel.hpp"
#include "tool/Utils.hpp"
#include "ParticipantList.hpp"
#include "core/participant/ParticipantCore.hpp"
#include <QDebug>

View file

@ -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<linphone::ChatMessage> &message,
linphone::ChatMessage::State state) {
computeDeliveryStatus();
emit msgStateChanged(message, state);
}
@ -184,7 +168,6 @@ void ChatMessageModel::onFileTransferProgressIndication(const std::shared_ptr<li
void ChatMessageModel::onParticipantImdnStateChanged(
const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<const linphone::ParticipantImdnState> &state) {
computeDeliveryStatus();
emit participantImdnStateChanged(message, state);
}

View file

@ -53,8 +53,6 @@ public:
void deleteMessageFromChatRoom();
void computeDeliveryStatus();
void sendReaction(const QString &reaction);
void removeReaction();

View file

@ -40,6 +40,10 @@ QVector<QPair<bool, QString>> UriTools::parseUri(const QString &text) {
return parse(text, gUriTools.mUriRegularExpression);
}
QVector<QPair<bool, QString>> UriTools::parseMention(const QString &text) {
return parse(text, gUriTools.mMentionRegularExpression);
}
// Parse a text and return all lines where regex is matched or not
QVector<QPair<bool, QString>> UriTools::parse(const QString &text, const QRegularExpression regex) {
QVector<QPair<bool, QString>> 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);
}

View file

@ -41,14 +41,16 @@ public:
static QVector<QPair<bool, QString>> parseIri(const QString &text);
static QVector<QPair<bool, QString>> parseUri(const QString &text);
static QVector<QPair<bool, QString>> parseMention(const QString &text);
static QRegularExpression getRegularExpression();
private:
void initRegularExpressions();
static QVector<QPair<bool, QString>> 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

View file

@ -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('&', "&amp;")
.replace('<', "\u2063&lt;")
.replace('>', "\u2063&gt;")
.replace('"', "&quot;")
.replace('\'', "&#039;");
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('&', "&amp;")
.replace('<', "\u2063&lt;")
.replace('>', "\u2063&gt;")
.replace('"', "&quot;")
.replace('\'', "&#039;");
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 += "<a href=\"" + uri + "\"><img" + (
options.contains("imagesWidth") ? QString(" width='") + options["imagesWidth"].toString() + "'" : ""
) + (
options.contains("imagesWidth")
? QString(" height='auto'")
: ""
) + " src=\"" + iriParsed[i].second + "\" />"+uri+"</a>";
}else{
*/
formattedText.append("<a style=\"color:" + primaryColor.name() + ";\" href=\"" + uri + "\">" + iri +
"</a>");
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 += "<a href=\"" + uri + "\"><img" + ( options.contains("imagesWidth")
? QString(" width='") + options["imagesWidth"].toString() + "'"
: ""
) + (
options.contains("imagesWidth")
? QString(" height='auto'")
: ""
) + " src=\"" + iriParsed[i].second + "\" />"+uri+"</a>";
}else{
*/
formattedText.append("<a style=\"color:" + primaryColor.name() + ";\" href=\"" + uri + "\">" + iri +
"</a>");
lastWasUrl = true;
/*}*/
}
}
}
if (lastWasUrl && formattedText.last().back() != ' ') {
formattedText.push_back(" ");
}
return "<p style=\"white-space:pre-wrap;\">" + 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<ParticipantCore> 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 = "<a style=\"color:" + primaryColor.name() +
";\" href=\"mention:" + address + "\">" + part + "</a>";
finalMentions.append(participantLink);
} else {
finalMentions.append(part);
}
} else {
finalMentions.append(part);
}
}
formattedText.push_back(finalMentions.join(" "));
} else {
formattedText.push_back(mentionsParsed[i].second);
}
}
}
return "<p style=\"white-space:pre-wrap;\">" + 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();
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -311,7 +311,7 @@ AbstractMainPage {
FocusScope {
SelectedChatView {
anchors.fill: parent
chat: mainItem.selectedChatGui
chat: mainItem.selectedChatGui || null
}
}
}