emoji picker

emoji picker for adding in chat message / sending reaction
This commit is contained in:
Gaelle Braud 2025-05-20 11:32:27 +02:00
parent 01a1fdd04b
commit 481f9500c6
22 changed files with 577 additions and 160 deletions

View file

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

View file

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

View file

@ -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> ChatMessageCore::create(const std::shared_ptr<linphone::ChatMessage> &chatmessage) {
auto sharedPointer = QSharedPointer<ChatMessageCore>(new ChatMessageCore(chatmessage), &QObject::deleteLater);
sharedPointer->setSelf(sharedPointer);
@ -64,6 +94,34 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &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<ChatMessageCore> 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<linphone::ChatMessage> &message,
const std::shared_ptr<const linphone::ChatMessageReaction> &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<Reaction> 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<linphone::ChatMessage> &message,
const std::shared_ptr<const linphone::Address> &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<Reaction> ChatMessageCore::getReactions() const {
return mReactions;
}
QList<QVariant> ChatMessageCore::getReactionsSingleton() const {
return mReactionsSingletonMap;
}
void ChatMessageCore::setReactions(const QList<Reaction> &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;
}

View file

@ -31,6 +31,22 @@
#include <linphone++/linphone.hh>
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<Reaction> reactions READ getReactions WRITE setReactions NOTIFY messageReactionChanged)
Q_PROPERTY(QList<QVariant> reactionsSingleton READ getReactionsSingleton NOTIFY singletonReactionMapChanged)
public:
static QSharedPointer<ChatMessageCore> create(const std::shared_ptr<linphone::ChatMessage> &chatmessage);
@ -76,6 +95,16 @@ public:
bool isRead() const;
void setIsRead(bool read);
QString getOwnReaction() const;
void setOwnReaction(const QString &reaction);
QList<Reaction> getReactions() const;
QList<QVariant> getReactionsSingleton() const;
void removeOneReactionFromSingletonMap(const QString &body);
void resetReactionsSingleton();
void setReactions(const QList<Reaction> &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<Reaction> mReactions;
QList<QVariant> mReactionsSingletonMap;
QDateTime mTimestamp;
bool mIsRemoteMessage = false;
bool mIsFromChatGroup = false;

View file

@ -47,7 +47,6 @@ EmojiModel::EmojiModel() {
}
int EmojiModel::count(QString category) {
qDebug() << "count of category" << category << emojies[category].size();
return emojies[category].size();
}

Binary file not shown.

View file

@ -19,6 +19,9 @@
<file>font/Noto_Sans/NotoSans-Thin.ttf</file>
<file>font/Noto_Sans/NotoSans-ThinItalic.ttf</file>
</qresource>
<qresource prefix="/emoji">
<file>font/EmojiFont.ttf</file>
</qresource>
<qresource prefix="/linux">
<file>font/OpenMoji-color-cbdt.ttf</file>
</qresource>

View file

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

View file

@ -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<linphone::ChatMessage> &message, linphone::ChatMessage::State state);
void onMsgStateChanged(const std::shared_ptr<linphone::ChatMessage> &message,
linphone::ChatMessage::State state) override;
void onNewMessageReaction(const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<const linphone::ChatMessageReaction> &reaction);
const std::shared_ptr<const linphone::ChatMessageReaction> &reaction) override;
void onReactionRemoved(const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<const linphone::Address> &address);
const std::shared_ptr<const linphone::Address> &address) override;
void onFileTransferTerminated(const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<linphone::Content> &content);
const std::shared_ptr<linphone::Content> &content) override;
void onFileTransferRecv(const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<linphone::Content> &content,
const std::shared_ptr<const linphone::Buffer> &buffer);
const std::shared_ptr<const linphone::Buffer> &buffer) override;
std::shared_ptr<linphone::Buffer> onFileTransferSend(const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<linphone::Content> &content,
size_t offset,
size_t size);
size_t size) override;
void onFileTransferSendChunk(const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<linphone::Content> &content,
size_t offset,
size_t size,
const std::shared_ptr<linphone::Buffer> &buffer);
const std::shared_ptr<linphone::Buffer> &buffer) override;
void onFileTransferProgressIndication(const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<linphone::Content> &content,
size_t offset,
size_t total);
size_t total) override;
void onParticipantImdnStateChanged(const std::shared_ptr<linphone::ChatMessage> &message,
const std::shared_ptr<const linphone::ParticipantImdnState> &state);
void onEphemeralMessageTimerStarted(const std::shared_ptr<linphone::ChatMessage> &message);
void onEphemeralMessageDeleted(const std::shared_ptr<linphone::ChatMessage> &message);
const std::shared_ptr<const linphone::ParticipantImdnState> &state) override;
void onEphemeralMessageTimerStarted(const std::shared_ptr<linphone::ChatMessage> &message) override;
void onEphemeralMessageDeleted(const std::shared_ptr<linphone::ChatMessage> &message) override;
};
#endif

View file

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

View file

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

View file

@ -1841,7 +1841,7 @@ QString Utils::encodeTextToQmlRichFormat(const QString &text, const QVariantMap
if (lastWasUrl && formattedText.last().back() != ' ') {
formattedText.push_back(" ");
}
return "<p style=\"white-space:pre-wrap;\">" + formattedText.join("") + "</p>";
return "<p style=\"white-space:pre-wrap;\">" + 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
);
}

View file

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

View file

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

View file

@ -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,24 +59,26 @@ 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)
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
@ -118,12 +123,12 @@ Control.Control {
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
horizontalAlignment: Text.AlignLeft
color: DefaultStyle.main2_700
textFormat: Text.RichText
font {
pixelSize: Typography.p1.pixelSize
weight: Typography.p1.weight
@ -143,13 +148,13 @@ Control.Control {
/////////////////////////////
Loader {
id: invitationLoader
active: modelData.core.conferenceInfo !== null
active: mainItem.chatMessage.core.conferenceInfo !== null
sourceComponent: invitationComponent
}
Component {
id: invitationComponent
ChatMessageInvitationBubble {
conferenceInfoGui: modelData.core.conferenceInfo
conferenceInfoGui: mainItem.chatMessage.core.conferenceInfo
Layout.fillWidth: true
Layout.fillHeight: true
onMouseEvent: mainItem.handleDefaultMouseEvent(event)
@ -158,9 +163,9 @@ Control.Control {
/////////////////////////////
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight
Text {
text: UtilsCpp.formatDate(modelData.core.timestamp, true, false)
text: UtilsCpp.formatDate(mainItem.chatMessage.core.timestamp, true, false)
color: DefaultStyle.main2_500main
font {
pixelSize: Typography.p3.pixelSize
@ -185,9 +190,65 @@ Control.Control {
}
}
}
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}

View file

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

View file

@ -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 = "<img src = '%1' width = '20' height = '20' align = 'top'>"
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)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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