diff --git a/Linphone/core/App.cpp b/Linphone/core/App.cpp index 0ac1b33e3..5bb8642f1 100644 --- a/Linphone/core/App.cpp +++ b/Linphone/core/App.cpp @@ -714,6 +714,14 @@ void App::initFonts() { allFamilies << QFontDatabase::applicationFontFamilies(id); } } + QDirIterator itEmojis(":/emoji/font/", QDirIterator::Subdirectories); + while (itEmojis.hasNext()) { + QString ttf = itEmojis.next(); + if (itEmojis.fileInfo().isFile()) { + auto id = QFontDatabase::addApplicationFont(ttf); + allFamilies << QFontDatabase::applicationFontFamilies(id); + } + } #ifdef Q_OS_LINUX QDirIterator itFonts(":/linux/font/", QDirIterator::Subdirectories); while (itFonts.hasNext()) { diff --git a/Linphone/core/chat/ChatCore.hpp b/Linphone/core/chat/ChatCore.hpp index 4453c126c..721e5807f 100644 --- a/Linphone/core/chat/ChatCore.hpp +++ b/Linphone/core/chat/ChatCore.hpp @@ -49,7 +49,7 @@ public: Q_PROPERTY(QString composingName READ getComposingName WRITE setComposingName NOTIFY composingUserChanged) Q_PROPERTY(QString composingAddress READ getComposingAddress WRITE setComposingAddress NOTIFY composingUserChanged) Q_PROPERTY(bool isGroupChat READ isGroupChat CONSTANT) - Q_PROPERTY(bool isEncrypted MEMBER mIsEncrypted) + Q_PROPERTY(bool isEncrypted READ isEncrypted CONSTANT) Q_PROPERTY(bool isReadOnly READ getIsReadOnly WRITE setIsReadOnly NOTIFY readOnlyChanged) // Should be call from model Thread. Will be automatically in App thread after initialization diff --git a/Linphone/core/chat/message/ChatMessageCore.cpp b/Linphone/core/chat/message/ChatMessageCore.cpp index 804e127cf..f5f88faf3 100644 --- a/Linphone/core/chat/message/ChatMessageCore.cpp +++ b/Linphone/core/chat/message/ChatMessageCore.cpp @@ -25,6 +25,36 @@ DEFINE_ABSTRACT_OBJECT(ChatMessageCore) +/***********************************************************************/ + +Reaction Reaction::operator=(Reaction r) { + mAddress = r.mAddress; + mBody = r.mBody; + return *this; +} +bool Reaction::operator==(const Reaction &r) const { + return r.mBody == mBody && r.mAddress == mAddress; +} +bool Reaction::operator!=(Reaction r) { + return r.mBody != mBody || r.mAddress != mAddress; +} + +Reaction Reaction::createMessageReactionVariant(const QString &body, const QString &address) { + Reaction r; + r.mBody = body; + r.mAddress = address; + return r; +} + +QVariant createReactionSingletonVariant(const QString &body, int count = 1) { + QVariantMap map; + map.insert("body", body); + map.insert("count", count); + return map; +} + +/***********************************************************************/ + QSharedPointer ChatMessageCore::create(const std::shared_ptr &chatmessage) { auto sharedPointer = QSharedPointer(new ChatMessageCore(chatmessage), &QObject::deleteLater); sharedPointer->setSelf(sharedPointer); @@ -64,6 +94,34 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr &c mConferenceInfo = ConferenceInfoCore::create(conferenceInfo); } } + auto reac = chatmessage->getOwnReaction(); + mOwnReaction = reac ? Utils::coreStringToAppString(reac->getBody()) : QString(); + for (auto &reaction : chatmessage->getReactions()) { + if (reaction) { + auto fromAddr = reaction->getFromAddress()->clone(); + fromAddr->clean(); + auto reac = + Reaction::createMessageReactionVariant(Utils::coreStringToAppString(reaction->getBody()), + Utils::coreStringToAppString(fromAddr->asStringUriOnly())); + mReactions.append(reac); + + auto it = std::find_if(mReactionsSingletonMap.begin(), mReactionsSingletonMap.end(), + [body = reac.mBody](QVariant data) { + auto dataBody = data.toMap()["body"].toString(); + return body == dataBody; + }); + if (it == mReactionsSingletonMap.end()) + mReactionsSingletonMap.push_back(createReactionSingletonVariant(reac.mBody, 1)); + else { + auto map = it->toMap(); + auto count = map["count"].toInt(); + ++count; + map.remove("count"); + map.insert("count", count); + } + } + } + connect(this, &ChatMessageCore::messageReactionChanged, this, &ChatMessageCore::resetReactionsSingleton); } ChatMessageCore::~ChatMessageCore() { @@ -84,7 +142,51 @@ void ChatMessageCore::setSelf(QSharedPointer me) { mChatMessageModelConnection->makeConnectToCore(&ChatMessageCore::lMarkAsRead, [this] { mChatMessageModelConnection->invokeToModel([this] { mChatMessageModel->markAsRead(); }); }); - mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::messageRead, [this]() { setIsRead(true); }); + mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::messageRead, [this]() { + mChatMessageModelConnection->invokeToCore([this] { setIsRead(true); }); + }); + mChatMessageModelConnection->makeConnectToCore(&ChatMessageCore::lSendReaction, [this](const QString &reaction) { + mChatMessageModelConnection->invokeToModel([this, reaction] { mChatMessageModel->sendReaction(reaction); }); + }); + mChatMessageModelConnection->makeConnectToCore(&ChatMessageCore::lRemoveReaction, [this]() { + mChatMessageModelConnection->invokeToModel([this] { mChatMessageModel->removeReaction(); }); + }); + mChatMessageModelConnection->makeConnectToModel( + &ChatMessageModel::newMessageReaction, + [this](const std::shared_ptr &message, + const std::shared_ptr &reaction) { + auto ownReac = message->getOwnReaction(); + auto own = ownReac ? Utils::coreStringToAppString(message->getOwnReaction()->getBody()) : QString(); + // We must reset all the reactions each time cause reactionRemoved is not emitted + // when someone change its current reaction + QList reactions; + for (auto &reaction : message->getReactions()) { + if (reaction) { + auto fromAddr = reaction->getFromAddress()->clone(); + fromAddr->clean(); + reactions.append(Reaction::createMessageReactionVariant( + Utils::coreStringToAppString(reaction->getBody()), + Utils::coreStringToAppString(fromAddr->asStringUriOnly()))); + } + } + mChatMessageModelConnection->invokeToCore([this, own, reactions] { + setOwnReaction(own); + setReactions(reactions); + }); + }); + mChatMessageModelConnection->makeConnectToModel( + &ChatMessageModel::reactionRemoved, [this](const std::shared_ptr &message, + const std::shared_ptr &address) { + auto reac = message->getOwnReaction(); + auto own = reac ? Utils::coreStringToAppString(message->getOwnReaction()->getBody()) : QString(); + auto addr = address->clone(); + addr->clean(); + QString addressString = Utils::coreStringToAppString(addr->asStringUriOnly()); + mChatMessageModelConnection->invokeToCore([this, own, addressString] { + removeReaction(addressString); + setOwnReaction(own); + }); + }); mChatMessageModelConnection->makeConnectToModel( &ChatMessageModel::msgStateChanged, @@ -159,6 +261,94 @@ void ChatMessageCore::setIsRead(bool read) { } } +QString ChatMessageCore::getOwnReaction() const { + return mOwnReaction; +} + +void ChatMessageCore::setOwnReaction(const QString &reaction) { + if (mOwnReaction != reaction) { + mOwnReaction = reaction; + emit messageReactionChanged(); + } +} + +QList ChatMessageCore::getReactions() const { + return mReactions; +} + +QList ChatMessageCore::getReactionsSingleton() const { + return mReactionsSingletonMap; +} + +void ChatMessageCore::setReactions(const QList &reactions) { + mustBeInMainThread(log().arg(Q_FUNC_INFO)); + mReactions = reactions; + emit messageReactionChanged(); +} + +void ChatMessageCore::resetReactionsSingleton() { + mReactionsSingletonMap.clear(); + for (auto &reac : mReactions) { + auto it = std::find_if(mReactionsSingletonMap.begin(), mReactionsSingletonMap.end(), + [body = reac.mBody](QVariant data) { + auto dataBody = data.toMap()["body"].toString(); + return body == dataBody; + }); + if (it == mReactionsSingletonMap.end()) + mReactionsSingletonMap.push_back(createReactionSingletonVariant(reac.mBody, 1)); + else { + auto map = it->toMap(); + auto count = map["count"].toInt(); + ++count; + map.remove("count"); + map.insert("count", count); + mReactionsSingletonMap.erase(it); + mReactionsSingletonMap.push_back(map); + } + } + emit singletonReactionMapChanged(); +} + +void ChatMessageCore::removeReaction(const Reaction &reaction) { + int i = 0; + for (const auto &r : mReactions) { + if (reaction == r) { + mReactions.removeAt(i); + emit messageReactionChanged(); + } + ++i; + } +} + +void ChatMessageCore::removeOneReactionFromSingletonMap(const QString &body) { + auto it = std::find_if(mReactionsSingletonMap.begin(), mReactionsSingletonMap.end(), [body](QVariant data) { + auto dataBody = data.toMap()["body"].toString(); + return body == dataBody; + }); + if (it != mReactionsSingletonMap.end()) { + auto map = it->toMap(); + auto count = map["count"].toInt(); + if (count <= 1) mReactionsSingletonMap.erase(it); + else { + --count; + map.remove("count"); + map.insert("count", count); + } + emit messageReactionChanged(); + } +} + +void ChatMessageCore::removeReaction(const QString &address) { + int n = mReactions.removeIf([address, this](Reaction r) { + if (r.mAddress == address) { + removeOneReactionFromSingletonMap(r.mBody); + return true; + } + return false; + }); + if (n > 0) emit messageReactionChanged(); +} + LinphoneEnums::ChatMessageState ChatMessageCore::getMessageState() const { return mMessageState; } diff --git a/Linphone/core/chat/message/ChatMessageCore.hpp b/Linphone/core/chat/message/ChatMessageCore.hpp index 16d3ef5e4..52257170b 100644 --- a/Linphone/core/chat/message/ChatMessageCore.hpp +++ b/Linphone/core/chat/message/ChatMessageCore.hpp @@ -31,6 +31,22 @@ #include +struct Reaction { + Q_GADGET + + Q_PROPERTY(QString body MEMBER mBody) + Q_PROPERTY(QString address MEMBER mAddress) + +public: + QString mBody; + QString mAddress; + + Reaction operator=(Reaction r); + bool operator==(const Reaction &r) const; + bool operator!=(Reaction r); + static Reaction createMessageReactionVariant(const QString &body, const QString &address); +}; + class ChatCore; class ChatMessageCore : public QObject, public AbstractObject { @@ -50,6 +66,9 @@ class ChatMessageCore : public QObject, public AbstractObject { Q_PROPERTY(bool isFromChatGroup READ isFromChatGroup CONSTANT) Q_PROPERTY(bool isRead READ isRead WRITE setIsRead NOTIFY isReadChanged) Q_PROPERTY(ConferenceInfoGui *conferenceInfo READ getConferenceInfoGui CONSTANT) + Q_PROPERTY(QString ownReaction READ getOwnReaction WRITE setOwnReaction NOTIFY messageReactionChanged) + Q_PROPERTY(QList reactions READ getReactions WRITE setReactions NOTIFY messageReactionChanged) + Q_PROPERTY(QList reactionsSingleton READ getReactionsSingleton NOTIFY singletonReactionMapChanged) public: static QSharedPointer create(const std::shared_ptr &chatmessage); @@ -76,6 +95,16 @@ public: bool isRead() const; void setIsRead(bool read); + QString getOwnReaction() const; + void setOwnReaction(const QString &reaction); + QList getReactions() const; + QList getReactionsSingleton() const; + void removeOneReactionFromSingletonMap(const QString &body); + void resetReactionsSingleton(); + void setReactions(const QList &reactions); + void removeReaction(const Reaction &reaction); + void removeReaction(const QString &address); + LinphoneEnums::ChatMessageState getMessageState() const; void setMessageState(LinphoneEnums::ChatMessageState state); @@ -89,11 +118,15 @@ signals: void isReadChanged(bool read); void isRemoteMessageChanged(bool isRemote); void messageStateChanged(); + void messageReactionChanged(); + void singletonReactionMapChanged(); void lDelete(); void deleted(); void lMarkAsRead(); void readChanged(); + void lSendReaction(const QString &reaction); + void lRemoveReaction(); private: DECLARE_ABSTRACT_OBJECT QString mText; @@ -105,6 +138,9 @@ private: QString mFromName; QString mPeerName; QString mMessageId; + QString mOwnReaction; + QList mReactions; + QList mReactionsSingletonMap; QDateTime mTimestamp; bool mIsRemoteMessage = false; bool mIsFromChatGroup = false; diff --git a/Linphone/core/emoji/EmojiModel.cpp b/Linphone/core/emoji/EmojiModel.cpp index a7d02cb54..030cd1b95 100644 --- a/Linphone/core/emoji/EmojiModel.cpp +++ b/Linphone/core/emoji/EmojiModel.cpp @@ -47,7 +47,6 @@ EmojiModel::EmojiModel() { } int EmojiModel::count(QString category) { - qDebug() << "count of category" << category << emojies[category].size(); return emojies[category].size(); } diff --git a/Linphone/data/font/EmojiFont.ttf b/Linphone/data/font/EmojiFont.ttf new file mode 100644 index 000000000..d8aed8c61 Binary files /dev/null and b/Linphone/data/font/EmojiFont.ttf differ diff --git a/Linphone/data/fonts.qrc b/Linphone/data/fonts.qrc index bdf0f7bfd..55318a145 100644 --- a/Linphone/data/fonts.qrc +++ b/Linphone/data/fonts.qrc @@ -19,6 +19,9 @@ font/Noto_Sans/NotoSans-Thin.ttf font/Noto_Sans/NotoSans-ThinItalic.ttf + + font/EmojiFont.ttf + font/OpenMoji-color-cbdt.ttf diff --git a/Linphone/model/chat/message/ChatMessageModel.cpp b/Linphone/model/chat/message/ChatMessageModel.cpp index c6457ea9e..856cc17d9 100644 --- a/Linphone/model/chat/message/ChatMessageModel.cpp +++ b/Linphone/model/chat/message/ChatMessageModel.cpp @@ -93,6 +93,20 @@ void ChatMessageModel::deleteMessageFromChatRoom() { } } +void ChatMessageModel::sendReaction(const QString &reaction) { + auto linReaction = mMonitor->createReaction(Utils::appStringToCoreString(reaction)); + linReaction->send(); +} + +void ChatMessageModel::removeReaction() { + sendReaction(QString()); +} + +QString ChatMessageModel::getOwnReaction() const { + auto reaction = mMonitor->getOwnReaction(); + return reaction ? Utils::coreStringToAppString(reaction->getBody()) : QString(); +} + linphone::ChatMessage::State ChatMessageModel::getState() const { return mMonitor->getState(); } diff --git a/Linphone/model/chat/message/ChatMessageModel.hpp b/Linphone/model/chat/message/ChatMessageModel.hpp index 0019d7c5b..f6f0b6325 100644 --- a/Linphone/model/chat/message/ChatMessageModel.hpp +++ b/Linphone/model/chat/message/ChatMessageModel.hpp @@ -55,8 +55,14 @@ public: void computeDeliveryStatus(); + void sendReaction(const QString &reaction); + + void removeReaction(); + linphone::ChatMessage::State getState() const; + QString getOwnReaction() const; + signals: void messageDeleted(); void messageRead(); @@ -94,33 +100,34 @@ private: DECLARE_ABSTRACT_OBJECT - void onMsgStateChanged(const std::shared_ptr &message, linphone::ChatMessage::State state); + void onMsgStateChanged(const std::shared_ptr &message, + linphone::ChatMessage::State state) override; void onNewMessageReaction(const std::shared_ptr &message, - const std::shared_ptr &reaction); + const std::shared_ptr &reaction) override; void onReactionRemoved(const std::shared_ptr &message, - const std::shared_ptr &address); + const std::shared_ptr &address) override; void onFileTransferTerminated(const std::shared_ptr &message, - const std::shared_ptr &content); + const std::shared_ptr &content) override; void onFileTransferRecv(const std::shared_ptr &message, const std::shared_ptr &content, - const std::shared_ptr &buffer); + const std::shared_ptr &buffer) override; std::shared_ptr onFileTransferSend(const std::shared_ptr &message, const std::shared_ptr &content, size_t offset, - size_t size); + size_t size) override; void onFileTransferSendChunk(const std::shared_ptr &message, const std::shared_ptr &content, size_t offset, size_t size, - const std::shared_ptr &buffer); + const std::shared_ptr &buffer) override; void onFileTransferProgressIndication(const std::shared_ptr &message, const std::shared_ptr &content, size_t offset, - size_t total); + size_t total) override; void onParticipantImdnStateChanged(const std::shared_ptr &message, - const std::shared_ptr &state); - void onEphemeralMessageTimerStarted(const std::shared_ptr &message); - void onEphemeralMessageDeleted(const std::shared_ptr &message); + const std::shared_ptr &state) override; + void onEphemeralMessageTimerStarted(const std::shared_ptr &message) override; + void onEphemeralMessageDeleted(const std::shared_ptr &message) override; }; #endif diff --git a/Linphone/tool/Constants.cpp b/Linphone/tool/Constants.cpp index a84a59cb4..fbcb0e95c 100644 --- a/Linphone/tool/Constants.cpp +++ b/Linphone/tool/Constants.cpp @@ -46,7 +46,7 @@ constexpr int Constants::DefaultFontPointSize; constexpr char Constants::DefaultEmojiFont[]; constexpr int Constants::DefaultEmojiFontPointSize; QStringList Constants::getReactionsList() { - return {"โค๏ธ", "๐Ÿ‘", "๐Ÿ˜‚", "๐Ÿ˜ฎ", "๐Ÿ˜ข"}; + return {"โค๏ธ", "๐Ÿ‘", "๐Ÿ˜‚", "๐Ÿ˜ฎ", "๐Ÿ˜ข", "๐Ÿ˜ "}; } constexpr char Constants::AppDomain[]; constexpr size_t Constants::MaxLogsCollectionSize; diff --git a/Linphone/tool/Constants.hpp b/Linphone/tool/Constants.hpp index 46cdc0f0d..c15dd1580 100644 --- a/Linphone/tool/Constants.hpp +++ b/Linphone/tool/Constants.hpp @@ -45,6 +45,7 @@ public: static constexpr char DefaultEmojiFont[] = "Apple Color Emoji"; #else static constexpr char DefaultEmojiFont[] = "Noto Color Emoji"; + #endif static constexpr int DefaultEmojiFontPointSize = 10; static QStringList getReactionsList(); diff --git a/Linphone/tool/Utils.cpp b/Linphone/tool/Utils.cpp index aee851a5c..29c8fe833 100644 --- a/Linphone/tool/Utils.cpp +++ b/Linphone/tool/Utils.cpp @@ -1841,7 +1841,7 @@ QString Utils::encodeTextToQmlRichFormat(const QString &text, const QVariantMap if (lastWasUrl && formattedText.last().back() != ' ') { formattedText.push_back(" "); } - return "

" + formattedText.join("") + "

"; + return "

" + formattedText.join(""); } QString Utils::encodeEmojiToQmlRichFormat(const QString &body) { @@ -1870,7 +1870,18 @@ QString Utils::encodeEmojiToQmlRichFormat(const QString &body) { return fmtBody; } -bool Utils::codepointIsEmoji(uint code) { - return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) || - (code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f; +QString Utils::getFilename(QUrl url) { + return url.fileName(); +} + +bool Utils::codepointIsEmoji(uint code) { + return ((code >= 0x1F600 && code <= 0x1F64F) || // Emoticons + (code >= 0x1F300 && code <= 0x1F5FF) || // Misc Symbols and Pictographs + (code >= 0x1F680 && code <= 0x1F6FF) || // Transport & Map + (code >= 0x1F700 && code <= 0x1F77F) || // Alchemical Symbols + (code >= 0x1F900 && code <= 0x1F9FF) || // Supplemental Symbols & Pictographs + (code >= 0x1FA70 && code <= 0x1FAFF) || // Symbols and Pictographs Extended-A + (code >= 0x2600 && code <= 0x26FF) || // Miscellaneous Symbols + (code >= 0x2700 && code <= 0x27BF) // Dingbats + ); } diff --git a/Linphone/tool/Utils.hpp b/Linphone/tool/Utils.hpp index f9697347e..6aa3690bb 100644 --- a/Linphone/tool/Utils.hpp +++ b/Linphone/tool/Utils.hpp @@ -154,6 +154,8 @@ public: Q_INVOKABLE static QString encodeTextToQmlRichFormat(const QString &text, const QVariantMap &options = QVariantMap()); Q_INVOKABLE static QString encodeEmojiToQmlRichFormat(const QString &body); + + Q_INVOKABLE static QString getFilename(QUrl url); static bool codepointIsEmoji(uint code); // QDir findDirectoryByName(QString startPath, QString name); diff --git a/Linphone/view/Control/Button/Button.qml b/Linphone/view/Control/Button/Button.qml index 65af74487..f5ab3a5c1 100644 --- a/Linphone/view/Control/Button/Button.qml +++ b/Linphone/view/Control/Button/Button.qml @@ -35,6 +35,7 @@ Control.Button { property var checkedImageColor: style?.image?.checked || Qt.darker(contentImageColor, 1.1) property var pressedImageColor: style?.image?.pressed || Qt.darker(contentImageColor, 1.1) property bool asynchronous: false + property var textFormat: Text.AutoText spacing: Math.round(5 * DefaultStyle.dp) hoverEnabled: enabled activeFocusOnTab: true @@ -98,6 +99,7 @@ Control.Button { width: textMetrics.advanceWidth wrapMode: Text.WrapAnywhere text: mainItem.text + textFormat: mainItem.textFormat maximumLineCount: 1 color: mainItem.checkable && mainItem.checked ? mainItem.checkedColor || mainItem.pressedColor diff --git a/Linphone/view/Control/Display/Chat/ChatMessage.qml b/Linphone/view/Control/Display/Chat/ChatMessage.qml index fb06b2860..20969177f 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessage.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessage.qml @@ -5,6 +5,7 @@ import QtQuick.Controls.Basic as Control import Linphone import UtilsCpp import SettingsCpp +import ConstantsCpp import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils @@ -16,13 +17,15 @@ Control.Control { property string imgUrl property ChatMessageGui chatMessage + property string ownReaction: chatMessage? chatMessage.core.ownReaction : "" property string fromAddress: chatMessage? chatMessage.core.fromAddress : "" property bool isRemoteMessage: chatMessage? chatMessage.core.isRemoteMessage : false property bool isFromChatGroup: chatMessage? chatMessage.core.isFromChatGroup : false property var msgState: chatMessage ? chatMessage.core.messageState : LinphoneEnums.ChatMessageState.StateIdle - property string richFormatText: modelData.core.hasTextContent ? UtilsCpp.encodeTextToQmlRichFormat(modelData.core.utf8Text) : "" + property string richFormatText: chatMessage.core.hasTextContent ? UtilsCpp.encodeTextToQmlRichFormat(chatMessage.core.utf8Text) : "" hoverEnabled: true property bool linkHovered: false + property real maxWidth: parent?.width || Math.round(300 * DefaultStyle.dp) signal messageDeletionRequested() @@ -56,138 +59,196 @@ Control.Control { Avatar { id: avatar - visible: mainItem.isFromChatGroup - opacity: mainItem.isRemoteMessage && mainItem.isFirstMessage ? 1 : 0 - Layout.preferredWidth: 26 * DefaultStyle.dp + visible: mainItem.isFromChatGroup && mainItem.isRemoteMessage + 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 : "" } - Control.Control { - id: chatBubble + Item { Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0 Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0 - Layout.preferredWidth: Math.min(implicitWidth, mainItem.maxWidth - avatar.implicitWidth) - spacing: Math.round(2 * DefaultStyle.dp) - topPadding: Math.round(12 * DefaultStyle.dp) - bottomPadding: Math.round(6 * DefaultStyle.dp) - leftPadding: Math.round(12 * DefaultStyle.dp) - rightPadding: Math.round(12 * DefaultStyle.dp) + Layout.preferredHeight: childrenRect.height + Layout.preferredWidth: childrenRect.width + Control.Control { + id: chatBubble + spacing: Math.round(2 * DefaultStyle.dp) + topPadding: Math.round(12 * DefaultStyle.dp) + bottomPadding: Math.round(6 * DefaultStyle.dp) + leftPadding: Math.round(12 * DefaultStyle.dp) + rightPadding: Math.round(12 * DefaultStyle.dp) + width: Math.min(implicitWidth, mainItem.maxWidth - avatar.implicitWidth) - MouseArea { // Default mouse area. Each sub bubble can control the mouse and pass on to the main mouse handler. Child bubble mouse area must cover the entire bubble. - id: defaultMouseArea - visible: invitationLoader.status !== Loader.Ready // Add other bubbles here that could control the mouse themselves, then add in bubble a signal onMouseEvent - anchors.fill: parent - acceptedButtons: Qt.RightButton - onClicked: (mouse) => mainItem.handleDefaultMouseEvent(mouse) - cursorShape: mainItem.linkHovered ? Qt.PointingHandCursor : Qt.ArrowCursor - } - - background: Item { - anchors.fill: parent - Rectangle { + MouseArea { // Default mouse area. Each sub bubble can control the mouse and pass on to the main mouse handler. Child bubble mouse area must cover the entire bubble. + id: defaultMouseArea + visible: invitationLoader.status !== Loader.Ready // Add other bubbles here that could control the mouse themselves, then add in bubble a signal onMouseEvent anchors.fill: parent - color: mainItem.backgroundColor - radius: Math.round(16 * DefaultStyle.dp) + acceptedButtons: Qt.RightButton + onClicked: (mouse) => mainItem.handleDefaultMouseEvent(mouse) + cursorShape: mainItem.linkHovered ? Qt.PointingHandCursor : Qt.ArrowCursor } - Rectangle { - visible: mainItem.isFirstMessage && mainItem.isRemoteMessage - anchors.top: parent.top - anchors.left: parent.left - width: Math.round(parent.width / 4) - height: Math.round(parent.height / 4) - color: mainItem.backgroundColor + + background: Item { + anchors.fill: parent + Rectangle { + anchors.fill: parent + color: mainItem.backgroundColor + radius: Math.round(16 * DefaultStyle.dp) + } + Rectangle { + visible: mainItem.isFirstMessage && mainItem.isRemoteMessage + anchors.top: parent.top + anchors.left: parent.left + width: Math.round(parent.width / 4) + height: Math.round(parent.height / 4) + color: mainItem.backgroundColor + } + Rectangle { + visible: mainItem.isFirstMessage && !mainItem.isRemoteMessage + anchors.bottom: parent.bottom + anchors.right: parent.right + width: Math.round(parent.width / 4) + height: Math.round(parent.height / 4) + color: mainItem.backgroundColor + } } - Rectangle { - visible: mainItem.isFirstMessage && !mainItem.isRemoteMessage - anchors.bottom: parent.bottom - anchors.right: parent.right - width: Math.round(parent.width / 4) - height: Math.round(parent.height / 4) - color: mainItem.backgroundColor - } - } - contentItem: ColumnLayout { - id: contentLayout - Image { - visible: mainItem.imgUrl != undefined - id: contentimage - } - Text { // Uses default mouse area for link hovering. - id: textElement - visible: mainItem.richFormatText !== "" - text: mainItem.richFormatText - textFormat: Text.RichText - wrapMode: Text.Wrap - Layout.fillWidth: true - Layout.fillHeight: true - horizontalAlignment: modelData.core.isRemoteMessage ? Text.AlignLeft : Text.AlignRight - color: DefaultStyle.main2_700 - font { - pixelSize: Typography.p1.pixelSize - weight: Typography.p1.weight - } - onLinkActivated: { - if (link.startsWith('sip')) - UtilsCpp.createCall(link) - else - Qt.openUrlExternally(link) - } - onHoveredLinkChanged: { - mainItem.linkHovered = hoveredLink !== "" - } - } - - // Meeting invitation bubble - ///////////////////////////// - Loader { - id: invitationLoader - active: modelData.core.conferenceInfo !== null - sourceComponent: invitationComponent - } - Component { - id: invitationComponent - ChatMessageInvitationBubble { - conferenceInfoGui: modelData.core.conferenceInfo - Layout.fillWidth: true - Layout.fillHeight: true - onMouseEvent: mainItem.handleDefaultMouseEvent(event) - } - } - ///////////////////////////// - - RowLayout { - Layout.alignment: Qt.AlignRight - Text { - text: UtilsCpp.formatDate(modelData.core.timestamp, true, false) - color: DefaultStyle.main2_500main + contentItem: ColumnLayout { + id: contentLayout + Image { + visible: mainItem.imgUrl != undefined + id: contentimage + } + Text { // Uses default mouse area for link hovering. + id: textElement + visible: mainItem.richFormatText !== "" + text: mainItem.richFormatText + wrapMode: Text.Wrap + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: Text.AlignLeft + color: DefaultStyle.main2_700 + textFormat: Text.RichText font { - pixelSize: Typography.p3.pixelSize - weight: Typography.p3.weight + pixelSize: Typography.p1.pixelSize + weight: Typography.p1.weight + } + onLinkActivated: { + if (link.startsWith('sip')) + UtilsCpp.createCall(link) + else + Qt.openUrlExternally(link) + } + onHoveredLinkChanged: { + mainItem.linkHovered = hoveredLink !== "" } } - EffectImage { - visible: !mainItem.isRemoteMessage - Layout.preferredWidth: visible ? 14 * DefaultStyle.dp : 0 - Layout.preferredHeight: 14 * DefaultStyle.dp - colorizationColor: DefaultStyle.main1_500_main - imageSource: mainItem.msgState === LinphoneEnums.ChatMessageState.StateDelivered - ? AppIcons.envelope - : mainItem.msgState === LinphoneEnums.ChatMessageState.StateDeliveredToUser - ? AppIcons.check - : mainItem.msgState === LinphoneEnums.ChatMessageState.StateNotDelivered - ? AppIcons.warningCircle - : mainItem.msgState === LinphoneEnums.ChatMessageState.StateDisplayed - ? AppIcons.checks - : "" + + // Meeting invitation bubble + ///////////////////////////// + Loader { + id: invitationLoader + active: mainItem.chatMessage.core.conferenceInfo !== null + sourceComponent: invitationComponent + } + Component { + id: invitationComponent + ChatMessageInvitationBubble { + conferenceInfoGui: mainItem.chatMessage.core.conferenceInfo + Layout.fillWidth: true + Layout.fillHeight: true + onMouseEvent: mainItem.handleDefaultMouseEvent(event) + } + } + ///////////////////////////// + + RowLayout { + Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight + Text { + text: UtilsCpp.formatDate(mainItem.chatMessage.core.timestamp, true, false) + color: DefaultStyle.main2_500main + font { + pixelSize: Typography.p3.pixelSize + weight: Typography.p3.weight + } + } + EffectImage { + visible: !mainItem.isRemoteMessage + Layout.preferredWidth: visible ? 14 * DefaultStyle.dp : 0 + Layout.preferredHeight: 14 * DefaultStyle.dp + colorizationColor: DefaultStyle.main1_500_main + imageSource: mainItem.msgState === LinphoneEnums.ChatMessageState.StateDelivered + ? AppIcons.envelope + : mainItem.msgState === LinphoneEnums.ChatMessageState.StateDeliveredToUser + ? AppIcons.check + : mainItem.msgState === LinphoneEnums.ChatMessageState.StateNotDelivered + ? AppIcons.warningCircle + : mainItem.msgState === LinphoneEnums.ChatMessageState.StateDisplayed + ? AppIcons.checks + : "" + } + } + } + } + Button { + id: reactionsButton + visible: reactionList.count > 0 + anchors.top: chatBubble.bottom + Binding { + target: reactionsButton + when: !mainItem.isRemoteMessage + property: "anchors.left" + value: chatBubble.left + } + Binding { + target: reactionsButton + when: mainItem.isRemoteMessage + property: "anchors.right" + value: chatBubble.right + } + anchors.topMargin: Math.round(-6 * DefaultStyle.dp) + topPadding: Math.round(8 * DefaultStyle.dp) + bottomPadding: Math.round(8 * DefaultStyle.dp) + leftPadding: Math.round(8 * DefaultStyle.dp) + rightPadding: Math.round(8 * DefaultStyle.dp) + background: Rectangle { + color: DefaultStyle.grey_100 + border.color: DefaultStyle.grey_0 + border.width: Math.round(2 * DefaultStyle.dp) + radius: Math.round(20 * DefaultStyle.dp) + } + contentItem: RowLayout { + spacing: Math.round(6 * DefaultStyle.dp) + Repeater { + id: reactionList + model: mainItem.chatMessage ? mainItem.chatMessage.core.reactionsSingleton : [] + delegate: RowLayout { + spacing: Math.round(3 * DefaultStyle.dp) + Text { + text: UtilsCpp.encodeEmojiToQmlRichFormat(modelData.body) + textFormat: Text.RichText + font { + pixelSize: Math.round(15 * DefaultStyle.dp) + weight: Math.round(400 * DefaultStyle.dp) + } + } + Text { + visible: modelData.count > 1 + text: modelData.count + verticalAlignment: Text.AlignBottom + font { + pixelSize: Typography.p4.pixelSize + weight: Typography.p4.weight + } + } + } } } } } RowLayout { id: actionsLayout - visible: mainItem.hovered || optionsMenu.hovered || optionsMenu.popup.opened || emojiButton.hovered + visible: mainItem.hovered || optionsMenu.hovered || optionsMenu.popup.opened || emojiButton.hovered || emojiButton.popup.opened Layout.leftMargin: Math.round(8 * DefaultStyle.dp) Layout.rightMargin: Math.round(8 * DefaultStyle.dp) Layout.alignment: Qt.AlignVCenter @@ -208,7 +269,7 @@ Control.Control { Layout.fillWidth: true Layout.preferredHeight: 45 * DefaultStyle.dp onClicked: { - var success = UtilsCpp.copyToClipboard(modelData.core.text) + var success = UtilsCpp.copyToClipboard(mainItem.chatMessage.core.text) //: Copied if (success) UtilsCpp.showInformationPopup(qsTr("chat_message_copied_to_clipboard_title"), //: "to clipboard" @@ -229,12 +290,55 @@ Control.Control { } style: ButtonStyle.hoveredBackgroundRed } + // Rectangle { + // Layout.fillWidth: true + // Layout.preferredHeight: Math.round(1 * DefaultStyle.dp) + // color: DefaultStyle.main2_200 + // } } } - BigButton { + PopupButton { id: emojiButton style: ButtonStyle.noBackground icon.source: AppIcons.smiley + popup.contentItem: RowLayout { + Repeater { + model: ConstantsCpp.reactionsList + delegate: Button { + text: UtilsCpp.encodeEmojiToQmlRichFormat(modelData) + background: Rectangle { + anchors.fill: parent + color: DefaultStyle.grey_200 + radius: parent.width * 4 + visible: mainItem.ownReaction === modelData + } + onClicked: { + if(modelData) { + if (mainItem.ownReaction === modelData) mainItem.chatMessage.core.lRemoveReaction() + else mainItem.chatMessage.core.lSendReaction(modelData) + } + emojiButton.close() + } + } + } + PopupButton { + id: emojiPickerButton + icon.source: AppIcons.plusCircle + popup.width: Math.round(393 * DefaultStyle.dp) + popup.height: Math.round(291 * DefaultStyle.dp) + popup.contentItem: EmojiPicker { + id: emojiPicker + onEmojiClicked: (emoji) => { + if (mainItem.chatMessage) { + if (mainItem.ownReaction === emoji) mainItem.chatMessage.core.lRemoveReaction() + else mainItem.chatMessage.core.lSendReaction(emoji) + } + emojiPickerButton.close() + emojiButton.close() + } + } + } + } } } Item{Layout.fillWidth: true} diff --git a/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml b/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml index d8ddf3cc3..43edced2c 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml @@ -109,8 +109,10 @@ ListView { delegate: ChatMessage { chatMessage: modelData - property real maxWidth: Math.round(mainItem.width * (3/4)) - onVisibleChanged: if (!modelData.core.isRead) modelData.core.lMarkAsRead() + maxWidth: Math.round(mainItem.width * (3/4)) + onVisibleChanged: { + if (visible && !modelData.core.isRead) modelData.core.lMarkAsRead() + } width: mainItem.width property var previousIndex: index - 1 property var previousFromAddress: chatMessageProxy.getChatMessageAtIndex(index-1)?.core.fromAddress diff --git a/Linphone/view/Control/Display/Conversation/Emoji/EmojiPicker.qml b/Linphone/view/Control/Display/Conversation/Emoji/EmojiPicker.qml index 541de6d8c..cb80dd1d5 100644 --- a/Linphone/view/Control/Display/Conversation/Emoji/EmojiPicker.qml +++ b/Linphone/view/Control/Display/Conversation/Emoji/EmojiPicker.qml @@ -26,6 +26,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Linphone +import UtilsCpp +import 'qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js' as Utils // import EmojiModel @@ -42,7 +44,9 @@ ColumnLayout { property var searchModel: ListModel {} property bool searchMode: false property int skinColor: -1 - signal emojiClicked() + + signal emojiClicked(string emoji) + function changeSkinColor(index) { if (index !== skinColors.current) { skinColors.itemAt(skinColors.current + 1).scale = 0.6 @@ -152,13 +156,14 @@ ColumnLayout { ListView { id: list width: mainItem.width - height: mainItem.height - categoriesRow.height + height: Math.round(250 * DefaultStyle.dp) + Layout.fillHeight: true model: mainItem.categories spacing: Math.round(30 * DefaultStyle.dp) topMargin: Math.round(7 * DefaultStyle.dp) bottomMargin: Math.round(7 * DefaultStyle.dp) leftMargin: Math.round(12 * DefaultStyle.dp) - // clip: true + clip: true delegate: GridLayout { id: grid property string category: mainItem.searchMode ? 'Search Result' : modelData @@ -178,13 +183,11 @@ ColumnLayout { Layout.bottomMargin: Math.round(8 * DefaultStyle.dp) } Repeater { - onCountChanged: console.log("emoji list count :", count) model: mainItem.searchMode ? mainItem.searchModel : mainItem.model.count(grid.category) delegate: Rectangle { property alias es: emojiSvg Layout.preferredWidth: Math.round(40 * DefaultStyle.dp) Layout.preferredHeight: Math.round(40 * DefaultStyle.dp) - RectangleTest{anchors.fill: parent} radius: Math.round(40 * DefaultStyle.dp) color: mouseArea.containsMouse ? '#e6e6e6' : '#ffffff' Image { @@ -199,10 +202,11 @@ ColumnLayout { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor + property string imageUrl: emojiSvg.source onClicked: { - var tag = "" - if (mainItem.editor) mainItem.editor.insert(mainItem.editor.cursorPosition, tag.arg(emojiSvg.source)) - mainItem.emojiClicked(tag.arg(emojiSvg.source)) + var emojiInFont = Utils.codepointFromFilename(UtilsCpp.getFilename(emojiSvg.source)) + if (mainItem.editor) mainItem.editor.insert(mainItem.editor.cursorPosition, emojiInFont) + mainItem.emojiClicked(emojiInFont) } } } diff --git a/Linphone/view/Control/Input/TextArea.qml b/Linphone/view/Control/Input/TextArea.qml index 81c6892df..279d9f516 100644 --- a/Linphone/view/Control/Input/TextArea.qml +++ b/Linphone/view/Control/Input/TextArea.qml @@ -12,7 +12,7 @@ TextEdit { property real placeholderWeight: Typography.p1.weight property color placeholderTextColor: color property alias background: background.data - property bool hoverEnabled: false + property bool hoverEnabled: true property bool hovered: mouseArea.hoverEnabled && mouseArea.containsMouse topPadding: Math.round(5 * DefaultStyle.dp) bottomPadding: Math.round(5 * DefaultStyle.dp) @@ -36,10 +36,10 @@ TextEdit { MouseArea { id: mouseArea anchors.fill: parent + enabled: mainItem.hoverEnabled hoverEnabled: mainItem.hoverEnabled - // onPressed: mainItem.forceActiveFocus() acceptedButtons: Qt.NoButton - cursorShape: mainItem.hovered ? Qt.PointingHandCursor : Qt.ArrowCursor + cursorShape: mainItem.hovered ? Qt.IBeamCursor : Qt.ArrowCursor } Item { diff --git a/Linphone/view/Control/Tool/Helper/utils.js b/Linphone/view/Control/Tool/Helper/utils.js index ca3f1da03..ee4b9c5af 100644 --- a/Linphone/view/Control/Tool/Helper/utils.js +++ b/Linphone/view/Control/Tool/Helper/utils.js @@ -819,4 +819,11 @@ function updatePosition(scrollItem, list){ } } - +// Transform svg file to unicode emoji +function codepointFromFilename(filename) { + let baseName = filename.split('.')[0]; + let parts = baseName.replace(/_/g, '-').split('-'); + let codePoints = parts.map(hex => parseInt(hex, 16)); + var unicode = String.fromCodePoint(...codePoints); + return unicode; +} \ No newline at end of file diff --git a/Linphone/view/Page/Form/Chat/SelectedChatView.qml b/Linphone/view/Page/Form/Chat/SelectedChatView.qml index 438789494..6e084ed4f 100644 --- a/Linphone/view/Page/Form/Chat/SelectedChatView.qml +++ b/Linphone/view/Page/Form/Chat/SelectedChatView.qml @@ -91,6 +91,38 @@ RowLayout { anchors.leftMargin: Math.round(18 * DefaultStyle.dp) anchors.rightMargin: Math.round(18 * DefaultStyle.dp) Control.ScrollBar.vertical: scrollbar + + Popup { + id: emojiPickerPopup + y: Math.round(chatMessagesListView.y + chatMessagesListView.height - height - 8*DefaultStyle.dp) + x: Math.round(chatMessagesListView.x + 8*DefaultStyle.dp) + width: Math.round(393 * DefaultStyle.dp) + height: Math.round(291 * DefaultStyle.dp) + visible: emojiPickerButton.checked + closePolicy: Popup.CloseOnPressOutside + onClosed: emojiPickerButton.checked = false + background: Item { + anchors.fill: parent + Rectangle { + id: buttonBackground + anchors.fill: parent + color: DefaultStyle.grey_0 + radius: Math.round(20 * DefaultStyle.dp) + } + MultiEffect { + anchors.fill: buttonBackground + source: buttonBackground + shadowEnabled: true + shadowColor: DefaultStyle.grey_1000 + shadowBlur: 0.1 + shadowOpacity: 0.5 + } + } + contentItem: EmojiPicker { + id: emojiPicker + editor: sendingTextArea + } + } }, ScrollBar { id: scrollbar @@ -130,12 +162,10 @@ RowLayout { RowLayout { spacing: Math.round(16 * DefaultStyle.dp) BigButton { + id: emojiPickerButton style: ButtonStyle.noBackground checkable: true - icon.source: AppIcons.smiley - onCheckedChanged: { - console.log("TODO : emoji") - } + icon.source: checked ? AppIcons.closeX : AppIcons.smiley } BigButton { style: ButtonStyle.noBackground @@ -191,11 +221,9 @@ RowLayout { TextArea { id: sendingTextArea - width: parent.width + width: sendingAreaFlickable.width height: sendingAreaFlickable.height - anchors.left: parent.left - anchors.right: parent.right - wrapMode: TextEdit.WordWrap + textFormat: TextEdit.AutoText //: Say somethingโ€ฆ : placeholder text for sending message text area placeholderText: qsTr("chat_view_send_area_placeholder_text") placeholderTextColor: DefaultStyle.main2_400 @@ -205,9 +233,9 @@ RowLayout { weight: Typography.p1.weight } onCursorRectangleChanged: sendingAreaFlickable.ensureVisible(cursorRectangle) + wrapMode: TextEdit.WordWrap property string previousText Component.onCompleted: previousText = text - displayAsRichText: true onTextChanged: { if (previousText === "" && text !== "") { mainItem.chat.core.lCompose() diff --git a/Linphone/view/Page/Form/Meeting/MeetingForm.qml b/Linphone/view/Page/Form/Meeting/MeetingForm.qml index 09d1ec86c..bc6c37eb0 100644 --- a/Linphone/view/Page/Form/Meeting/MeetingForm.qml +++ b/Linphone/view/Page/Form/Meeting/MeetingForm.qml @@ -212,7 +212,6 @@ FocusScope { Layout.preferredWidth: Math.round(275 * DefaultStyle.dp) leftPadding: Math.round(8 * DefaultStyle.dp) rightPadding: Math.round(8 * DefaultStyle.dp) - hoverEnabled: true //: "Ajouter une description" placeholderText: qsTr("meeting_schedule_description_hint") placeholderTextColor: DefaultStyle.main2_600 diff --git a/Linphone/view/Style/DefaultStyle.qml b/Linphone/view/Style/DefaultStyle.qml index be8448bf4..f8aa8a9f1 100644 --- a/Linphone/view/Style/DefaultStyle.qml +++ b/Linphone/view/Style/DefaultStyle.qml @@ -53,8 +53,8 @@ QtObject { } // Warning: Qt 6.8.1 (current version) and previous versions, Qt only support COLRv0 fonts. Don't try to use v1. - property string emojiFont: "OpenMoji Color" - property string flagFont: "OpenMoji Color" + property string emojiFont: "Noto Color Emoji" + property string flagFont: "Noto Color Emoji" property string defaultFont: "Noto Sans" property color numericPadPressedButtonColor: "#EEF7F8"