message reply

This commit is contained in:
Gaelle Braud 2025-06-23 14:36:54 +02:00
parent f82a4db826
commit 4a1f1a895b
16 changed files with 1691 additions and 1389 deletions

View file

@ -76,9 +76,7 @@ void AccountDeviceList::setAccount(const QSharedPointer<AccountCore> &accountCor
void AccountDeviceList::refreshDevices() { void AccountDeviceList::refreshDevices() {
mustBeInMainThread(log().arg(Q_FUNC_INFO)); mustBeInMainThread(log().arg(Q_FUNC_INFO));
beginResetModel(); resetData();
clearData();
endResetModel();
if (mAccountCore) { if (mAccountCore) {
auto requestDeviceList = [this] { auto requestDeviceList = [this] {
if (!mAccountManagerServicesModelConnection) return; if (!mAccountManagerServicesModelConnection) return;
@ -150,14 +148,14 @@ void AccountDeviceList::setSelf(QSharedPointer<AccountDeviceList> me) {
&AccountManagerServicesModel::requestError, &AccountManagerServicesModel::requestError,
[this](const std::shared_ptr<const linphone::AccountManagerServicesRequest> &request, int statusCode, [this](const std::shared_ptr<const linphone::AccountManagerServicesRequest> &request, int statusCode,
const std::string &errorMessage, const std::string &errorMessage,
const std::shared_ptr<const linphone::Dictionary> &parameterErrors) { const std::shared_ptr<const linphone::Dictionary> &parameterErrors) {
lDebug() << "REQUEST ERROR" << errorMessage << "/" << int(request->getType()); lDebug() << "REQUEST ERROR" << errorMessage << "/" << int(request->getType());
QString message = QString::fromStdString(errorMessage); QString message = QString::fromStdString(errorMessage);
if (request->getType() == linphone::AccountManagerServicesRequest::Type::GetDevicesList) { if (request->getType() == linphone::AccountManagerServicesRequest::Type::GetDevicesList) {
//: "Erreur lors de la récupération des appareils" //: "Erreur lors de la récupération des appareils"
message = tr("manage_account_no_device_found_error_message"); message = tr("manage_account_no_device_found_error_message");
} }
emit requestError(message); emit requestError(message);
}); });
mAccountManagerServicesModelConnection->makeConnectToModel( mAccountManagerServicesModelConnection->makeConnectToModel(
&AccountManagerServicesModel::devicesListFetched, &AccountManagerServicesModel::devicesListFetched,

View file

@ -114,6 +114,7 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
fromAddress->clean(); fromAddress->clean();
mFromAddress = Utils::coreStringToAppString(fromAddress->asStringUriOnly()); mFromAddress = Utils::coreStringToAppString(fromAddress->asStringUriOnly());
mFromName = ToolModel::getDisplayName(chatmessage->getFromAddress()->clone()); mFromName = ToolModel::getDisplayName(chatmessage->getFromAddress()->clone());
mToName = ToolModel::getDisplayName(chatmessage->getToAddress()->clone());
auto chatroom = chatmessage->getChatRoom(); auto chatroom = chatmessage->getChatRoom();
mIsFromChatGroup = chatroom->hasCapability((int)linphone::ChatRoom::Capabilities::Conference) && mIsFromChatGroup = chatroom->hasCapability((int)linphone::ChatRoom::Capabilities::Conference) &&
@ -166,6 +167,13 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
mIsForward = chatmessage->isForward(); mIsForward = chatmessage->isForward();
mIsReply = chatmessage->isReply(); mIsReply = chatmessage->isReply();
if (mIsReply) {
auto replymessage = chatmessage->getReplyMessage();
if (replymessage) {
mReplyText = ToolModel::getMessageFromContent(replymessage->getContents());
if (mIsFromChatGroup) mRepliedToName = ToolModel::getDisplayName(replymessage->getToAddress()->clone());
}
}
mImdnStatusList = computeDeliveryStatus(chatmessage); mImdnStatusList = computeDeliveryStatus(chatmessage);
} }
@ -380,6 +388,10 @@ QString ChatMessageCore::getToAddress() const {
return mToAddress; return mToAddress;
} }
QString ChatMessageCore::getToName() const {
return mToName;
}
QString ChatMessageCore::getMessageId() const { QString ChatMessageCore::getMessageId() const {
return mMessageId; return mMessageId;
} }
@ -575,4 +587,4 @@ std::shared_ptr<ChatMessageModel> ChatMessageCore::getModel() const {
ChatMessageContentGui *ChatMessageCore::getVoiceRecordingContent() const { ChatMessageContentGui *ChatMessageCore::getVoiceRecordingContent() const {
return new ChatMessageContentGui(mVoiceRecordingContent); return new ChatMessageContentGui(mVoiceRecordingContent);
} }

View file

@ -100,6 +100,8 @@ class ChatMessageCore : public QObject, public AbstractObject {
QStringList reactionsSingletonAsStrings READ getReactionsSingletonAsStrings NOTIFY singletonReactionMapChanged) QStringList reactionsSingletonAsStrings READ getReactionsSingletonAsStrings NOTIFY singletonReactionMapChanged)
Q_PROPERTY(bool isForward MEMBER mIsForward CONSTANT) Q_PROPERTY(bool isForward MEMBER mIsForward CONSTANT)
Q_PROPERTY(bool isReply MEMBER mIsReply CONSTANT) Q_PROPERTY(bool isReply MEMBER mIsReply CONSTANT)
Q_PROPERTY(QString replyText MEMBER mReplyText CONSTANT)
Q_PROPERTY(QString repliedToName MEMBER mRepliedToName CONSTANT)
Q_PROPERTY(bool hasFileContent MEMBER mHasFileContent CONSTANT) Q_PROPERTY(bool hasFileContent MEMBER mHasFileContent CONSTANT)
Q_PROPERTY(bool isVoiceRecording MEMBER mIsVoiceRecording CONSTANT) Q_PROPERTY(bool isVoiceRecording MEMBER mIsVoiceRecording CONSTANT)
Q_PROPERTY(bool isCalendarInvite MEMBER mIsCalendarInvite CONSTANT) Q_PROPERTY(bool isCalendarInvite MEMBER mIsCalendarInvite CONSTANT)
@ -123,6 +125,7 @@ public:
QString getFromAddress() const; QString getFromAddress() const;
QString getFromName() const; QString getFromName() const;
QString getToAddress() const; QString getToAddress() const;
QString getToName() const;
QString getMessageId() const; QString getMessageId() const;
bool isRemoteMessage() const; bool isRemoteMessage() const;
@ -182,6 +185,7 @@ private:
QString mFromAddress; QString mFromAddress;
QString mToAddress; QString mToAddress;
QString mFromName; QString mFromName;
QString mToName;
QString mPeerName; QString mPeerName;
QString mMessageId; QString mMessageId;
QString mOwnReaction; QString mOwnReaction;
@ -194,6 +198,8 @@ private:
bool mIsRead = false; bool mIsRead = false;
bool mIsForward = false; bool mIsForward = false;
bool mIsReply = false; bool mIsReply = false;
QString mReplyText;
QString mRepliedToName;
bool mHasFileContent = false; bool mHasFileContent = false;
bool mIsCalendarInvite = false; bool mIsCalendarInvite = false;
bool mIsVoiceRecording = false; bool mIsVoiceRecording = false;

View file

@ -43,11 +43,11 @@ EventLogCore::EventLogCore(const std::shared_ptr<const linphone::EventLog> &even
} else if (eventLog->getCallLog()) { } else if (eventLog->getCallLog()) {
mCallHistoryCore = CallHistoryCore::create(eventLog->getCallLog()); mCallHistoryCore = CallHistoryCore::create(eventLog->getCallLog());
mEventId = Utils::coreStringToAppString(eventLog->getCallLog()->getCallId()); mEventId = Utils::coreStringToAppString(eventLog->getCallLog()->getCallId());
} else { // getNotifyId }
if (mEventId.isEmpty()) { // getNotifyId
QString type = QString::fromLatin1( QString type = QString::fromLatin1(
QMetaEnum::fromType<LinphoneEnums::EventLogType>().valueToKey(static_cast<int>(mEventLogType))); QMetaEnum::fromType<LinphoneEnums::EventLogType>().valueToKey(static_cast<int>(mEventLogType)));
mEventId = type + QString::number(static_cast<qint64>(eventLog->getCreationTime())); mEventId = type + QString::number(static_cast<qint64>(eventLog->getCreationTime()));
;
computeEvent(eventLog); computeEvent(eventLog);
} }
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -137,6 +137,11 @@ ChatModel::createVoiceRecordingMessage(const std::shared_ptr<linphone::Recorder>
return mMonitor->createVoiceRecordingMessage(recorder); return mMonitor->createVoiceRecordingMessage(recorder);
} }
std::shared_ptr<linphone::ChatMessage>
ChatModel::createReplyMessage(const std::shared_ptr<linphone::ChatMessage> &message) {
return mMonitor->createReplyMessage(message);
}
std::shared_ptr<linphone::ChatMessage> ChatModel::createTextMessageFromText(QString text) { std::shared_ptr<linphone::ChatMessage> ChatModel::createTextMessageFromText(QString text) {
return mMonitor->createMessageFromUtf8(Utils::appStringToCoreString(text)); return mMonitor->createMessageFromUtf8(Utils::appStringToCoreString(text));
} }

View file

@ -52,6 +52,9 @@ public:
void leave(); void leave();
std::shared_ptr<linphone::ChatMessage> std::shared_ptr<linphone::ChatMessage>
createVoiceRecordingMessage(const std::shared_ptr<linphone::Recorder> &recorder); createVoiceRecordingMessage(const std::shared_ptr<linphone::Recorder> &recorder);
std::shared_ptr<linphone::ChatMessage> createReplyMessage(const std::shared_ptr<linphone::ChatMessage> &message);
std::shared_ptr<linphone::ChatMessage> createTextMessageFromText(QString text); std::shared_ptr<linphone::ChatMessage> createTextMessageFromText(QString text);
std::shared_ptr<linphone::ChatMessage> createMessage(QString text, std::shared_ptr<linphone::ChatMessage> createMessage(QString text,
QList<std::shared_ptr<ChatMessageContentModel>> filesContent); QList<std::shared_ptr<ChatMessageContentModel>> filesContent);

View file

@ -388,11 +388,13 @@ bool ToolModel::friendIsInFriendList(const std::shared_ptr<linphone::FriendList>
QString ToolModel::getMessageFromContent(std::list<std::shared_ptr<linphone::Content>> contents) { QString ToolModel::getMessageFromContent(std::list<std::shared_ptr<linphone::Content>> contents) {
mustBeInLinphoneThread(sLog().arg(Q_FUNC_INFO)); mustBeInLinphoneThread(sLog().arg(Q_FUNC_INFO));
QString res;
for (auto &content : contents) { for (auto &content : contents) {
if (content->isText()) { if (content->isText()) {
return Utils::coreStringToAppString(content->getUtf8Text()); return Utils::coreStringToAppString(content->getUtf8Text());
} else if (content->isFile()) { } else if (content->isFile()) {
return Utils::coreStringToAppString(content->getName()); if (res.isEmpty()) res.append(Utils::coreStringToAppString(content->getName()));
else res.append(", " + Utils::coreStringToAppString(content->getName()));
} else if (content->isIcalendar()) { } else if (content->isIcalendar()) {
auto conferenceInfo = linphone::Factory::get()->createConferenceInfoFromIcalendarContent(content); auto conferenceInfo = linphone::Factory::get()->createConferenceInfoFromIcalendarContent(content);
auto conferenceInfoCore = ConferenceInfoCore::create(conferenceInfo); auto conferenceInfoCore = ConferenceInfoCore::create(conferenceInfo);
@ -406,7 +408,7 @@ QString ToolModel::getMessageFromContent(std::list<std::shared_ptr<linphone::Con
return getMessageFromContent(content->getParts()); return getMessageFromContent(content->getParts());
} }
} }
return QString(""); return res;
} }
// Load downloaded codecs like OpenH264 (needs to be after core is created and has loaded its plugins, as // Load downloaded codecs like OpenH264 (needs to be after core is created and has loaded its plugins, as

View file

@ -25,6 +25,7 @@
#include "core/call/CallGui.hpp" #include "core/call/CallGui.hpp"
#include "core/chat/ChatCore.hpp" #include "core/chat/ChatCore.hpp"
#include "core/chat/ChatGui.hpp" #include "core/chat/ChatGui.hpp"
#include "core/chat/message/ChatMessageGui.hpp"
#include "core/conference/ConferenceCore.hpp" #include "core/conference/ConferenceCore.hpp"
#include "core/conference/ConferenceInfoCore.hpp" #include "core/conference/ConferenceInfoCore.hpp"
#include "core/conference/ConferenceInfoGui.hpp" #include "core/conference/ConferenceInfoGui.hpp"
@ -1950,6 +1951,50 @@ QString Utils::getSafeFilePath(const QString &filePath, bool *soFarSoGood) {
return QString(""); return QString("");
} }
void Utils::sendReplyMessage(ChatMessageGui *message, ChatGui *chatGui, QString text, QVariantList files) {
auto chatModel = chatGui && chatGui->mCore ? chatGui->mCore->getModel() : nullptr;
auto chatMessageModel = message && message->mCore ? message->mCore->getModel() : nullptr;
if (!chatModel || !chatMessageModel) {
//: Cannot reply to invalid message
QString error = !chatMessageModel ? tr("chatMessage_error")
//: Error in the chat
: tr("chat_error");
//: Error
showInformationPopup(tr("info_popup_error_title"),
//: Could not send voice message : %1
tr("info_popup_send_voice_message_error_message").arg(error));
return;
}
QList<std::shared_ptr<ChatMessageContentModel>> filesContent;
for (auto &file : files) {
auto contentGui = qvariant_cast<ChatMessageContentGui *>(file);
if (contentGui) {
auto contentCore = contentGui->mCore;
filesContent.append(contentCore->getContentModel());
}
}
App::postModelAsync([chatModel, chatMessageModel, text, filesContent] {
mustBeInLinphoneThread(sLog().arg(Q_FUNC_INFO));
auto chat = chatModel->getMonitor();
auto messageToReplyTo = chatMessageModel->getMonitor();
auto linMessage = chatModel->createReplyMessage(messageToReplyTo);
if (linMessage) {
linMessage->addUtf8TextContent(Utils::appStringToCoreString(text));
for (auto &content : filesContent) {
linMessage->addFileContent(content->getContent());
}
linMessage->send();
} else {
App::postCoreAsync([] {
//: Error
showInformationPopup(tr("info_popup_error_title"),
//: Failed to create message from record
tr("info_popup_send_voice_message_sending_error_message"));
});
}
});
}
VariantObject *Utils::createVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui) { VariantObject *Utils::createVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui) {
VariantObject *data = new VariantObject("createVoiceRecordingMessage"); VariantObject *data = new VariantObject("createVoiceRecordingMessage");
if (!data) return nullptr; if (!data) return nullptr;

View file

@ -52,6 +52,7 @@ class ConferenceCore;
class ParticipantDeviceCore; class ParticipantDeviceCore;
class DownloadablePayloadTypeCore; class DownloadablePayloadTypeCore;
class ChatGui; class ChatGui;
class ChatMessageGui;
class RecorderGui; class RecorderGui;
class Utils : public QObject, public AbstractObject { class Utils : public QObject, public AbstractObject {
@ -176,6 +177,8 @@ public:
Q_INVOKABLE static QString toTimeString(QDateTime date, const QString &format = "hh:mm:ss"); Q_INVOKABLE static QString toTimeString(QDateTime date, const QString &format = "hh:mm:ss");
Q_INVOKABLE static VariantObject *createVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui); Q_INVOKABLE static VariantObject *createVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui);
Q_INVOKABLE static void
sendReplyMessage(ChatMessageGui *message, ChatGui *chatGui, QString text, QVariantList files);
Q_INVOKABLE static void sendVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui); Q_INVOKABLE static void sendVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui);
// QDir findDirectoryByName(QString startPath, QString name); // QDir findDirectoryByName(QString startPath, QString name);

View file

@ -19,6 +19,8 @@ Control.Control {
property string fromAddress: chatMessage? chatMessage.core.fromAddress : "" property string fromAddress: chatMessage? chatMessage.core.fromAddress : ""
property bool isRemoteMessage: chatMessage? chatMessage.core.isRemoteMessage : false property bool isRemoteMessage: chatMessage? chatMessage.core.isRemoteMessage : false
property bool isFromChatGroup: chatMessage? chatMessage.core.isFromChatGroup : false property bool isFromChatGroup: chatMessage? chatMessage.core.isFromChatGroup : false
property bool isReply: chatMessage? chatMessage.core.isReply : false
property string replyText: chatMessage? chatMessage.core.replyText : false
property var msgState: chatMessage ? chatMessage.core.messageState : LinphoneEnums.ChatMessageState.StateIdle property var msgState: chatMessage ? chatMessage.core.messageState : LinphoneEnums.ChatMessageState.StateIdle
hoverEnabled: true hoverEnabled: true
property bool linkHovered: false property bool linkHovered: false
@ -30,6 +32,7 @@ Control.Control {
signal isFileHoveringChanged(bool isFileHovering) signal isFileHoveringChanged(bool isFileHovering)
signal showReactionsForMessageRequested() signal showReactionsForMessageRequested()
signal showImdnStatusForMessageRequested() signal showImdnStatusForMessageRequested()
signal replyToMessageRequested()
background: Item { background: Item {
anchors.fill: parent anchors.fill: parent
@ -41,52 +44,119 @@ Control.Control {
} }
} }
contentItem: RowLayout { contentItem: ColumnLayout {
spacing: 0 spacing: Math.round(5 * DefaultStyle.dp)
layoutDirection: mainItem.isRemoteMessage ? Qt.LeftToRight : Qt.RightToLeft Text {
id: fromNameText
Avatar {
id: avatar
visible: mainItem.isFromChatGroup && mainItem.isRemoteMessage
Layout.preferredWidth: mainItem.isRemoteMessage ? 26 * DefaultStyle.dp : 0
Layout.preferredHeight: 26 * DefaultStyle.dp
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0 Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) + avatar.width : 0
_address: chatMessage ? chatMessage.core.fromAddress : "" visible: mainItem.isFromChatGroup && mainItem.isRemoteMessage && mainItem.isFirstMessage && !replyLayout.visible
maximumLineCount: 1
width: implicitWidth
x: mapToItem(this, chatBubble.x, chatBubble.y).x
text: mainItem.chatMessage.core.fromName
color: DefaultStyle.main2_500main
font {
pixelSize: Typography.p4.pixelSize
weight: Typography.p4.weight
}
} }
ColumnLayout { RowLayout {
Layout.alignment: Qt.AlignTop id: replyLayout
spacing: 0 visible: mainItem.isReply
Text { Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) + avatar.width : 0
id: fromNameText layoutDirection: mainItem.isRemoteMessage ? Qt.LeftToRight : Qt.RightToLeft
Layout.alignment: Qt.AlignTop ColumnLayout {
visible: mainItem.isFromChatGroup && mainItem.isRemoteMessage && mainItem.isFirstMessage spacing: Math.round(5 * DefaultStyle.dp)
// anchors.top: parent.top RowLayout {
// anchors.left: parent.left id: replyLabel
// anchors.leftMargin: avatar.width// mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0 spacing: Math.round(8 * DefaultStyle.dp)
maximumLineCount: 1 Layout.fillWidth: false
Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0 Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight
width: implicitWidth EffectImage {
x: mapToItem(this, chatBubble.x, chatBubble.y).x imageSource: AppIcons.reply
text: mainItem.chatMessage.core.fromName colorizationColor: DefaultStyle.main2_500main
color: DefaultStyle.main2_500main Layout.preferredWidth: Math.round(12 * DefaultStyle.dp)
font { Layout.preferredHeight: Math.round(12 * DefaultStyle.dp)
pixelSize: Typography.p4.pixelSize }
weight: Typography.p4.weight Text {
Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft: Qt.AlignRight
text: mainItem.isRemoteMessage
? mainItem.chatMessage.core.repliedToName !== ""
//: %1 replied to %2
? qsTr("chat_message_remote_replied_to").arg(mainItem.chatMessage.core.fromName).arg(mainItem.chatMessage.core.repliedToName)
//: %1 replied
: qsTr("chat_message_remote_replied").arg(mainItem.chatMessage.core.fromName)
: mainItem.chatMessage.core.repliedToName !== ""
//: You replied to %1
? qsTr("chat_message_user_replied_to").arg(mainItem.chatMessage.core.repliedToName)
//: You replied
: qsTr("chat_message_user_replied")
color: DefaultStyle.main2_600
font {
pixelSize: Typography.p4.pixelSize
weight: Typography.p4.weight
}
}
}
Control.Control {
id: replyMessage
visible: mainItem.replyText !== ""
Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight
spacing: Math.round(5 * DefaultStyle.dp)
topPadding: Math.round(12 * DefaultStyle.dp)
bottomPadding: Math.round(19 * DefaultStyle.dp)
leftPadding: Math.round(18 * DefaultStyle.dp)
rightPadding: Math.round(18 * DefaultStyle.dp)
width: Math.min(implicitWidth, mainItem.maxWidth - avatar.implicitWidth)
background: Rectangle {
anchors.fill: parent
color: DefaultStyle.grey_200
radius: Math.round(16 * DefaultStyle.dp)
}
contentItem: Text {
Layout.fillWidth: true
text: mainItem.replyText
color: DefaultStyle.main2_800
font {
pixelSize: Typography.p1.pixelSize
weight: Typography.p1.weight
}
}
} }
} }
Item{Layout.fillWidth: true}
}
RowLayout {
id: bubbleLayout
z: replyLayout.z + 1
spacing: 0
layoutDirection: mainItem.isRemoteMessage ? Qt.LeftToRight : Qt.RightToLeft
Layout.topMargin: replyMessage.visible ? Math.round(-20 * DefaultStyle.dp) : 0
Avatar {
id: avatar
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 : ""
}
Item { Item {
id: bubbleContainer
// Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0 // Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0
Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0 Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0
Layout.preferredHeight: childrenRect.height Layout.preferredHeight: childrenRect.height
Layout.preferredWidth: childrenRect.width Layout.preferredWidth: childrenRect.width
Control.Control { Control.Control {
id: chatBubble id: chatBubble
spacing: Math.round(2 * DefaultStyle.dp) spacing: Math.round(2 * DefaultStyle.dp)
topPadding: Math.round(12 * DefaultStyle.dp) topPadding: Math.round(12 * DefaultStyle.dp)
bottomPadding: Math.round(6 * DefaultStyle.dp) bottomPadding: Math.round(6 * DefaultStyle.dp)
leftPadding: Math.round(12 * DefaultStyle.dp) leftPadding: Math.round(18 * DefaultStyle.dp)
rightPadding: Math.round(12 * DefaultStyle.dp) rightPadding: Math.round(18 * DefaultStyle.dp)
width: Math.min(implicitWidth, mainItem.maxWidth - avatar.implicitWidth) 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. 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.
@ -133,6 +203,7 @@ Control.Control {
} }
} }
RowLayout { RowLayout {
Layout.fillWidth: false
Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight
Text { Text {
text: UtilsCpp.formatDate(mainItem.chatMessage.core.timestamp, true, false, "dd/MM") text: UtilsCpp.formatDate(mainItem.chatMessage.core.timestamp, true, false, "dd/MM")
@ -217,118 +288,130 @@ Control.Control {
} }
} }
} }
} RowLayout {
RowLayout { id: actionsLayout
id: actionsLayout visible: mainItem.hovered || optionsMenu.hovered || optionsMenu.popup.opened || emojiButton.hovered || emojiButton.popup.opened
visible: mainItem.hovered || optionsMenu.hovered || optionsMenu.popup.opened || emojiButton.hovered || emojiButton.popup.opened Layout.leftMargin: Math.round(8 * DefaultStyle.dp)
Layout.leftMargin: Math.round(8 * DefaultStyle.dp) Layout.rightMargin: Math.round(8 * DefaultStyle.dp)
Layout.rightMargin: Math.round(8 * DefaultStyle.dp) Layout.alignment: Qt.AlignVCenter
Layout.alignment: Qt.AlignVCenter // Layout.fillWidth: true
// Layout.fillWidth: true spacing: Math.round(7 * DefaultStyle.dp)
spacing: Math.round(7 * DefaultStyle.dp) layoutDirection: mainItem.isRemoteMessage ? Qt.LeftToRight : Qt.RightToLeft
layoutDirection: mainItem.isRemoteMessage ? Qt.LeftToRight : Qt.RightToLeft PopupButton {
PopupButton { id: optionsMenu
id: optionsMenu popup.padding: 0
popup.padding: 0 popup.contentItem: ColumnLayout {
popup.contentItem: ColumnLayout { spacing: 0
spacing: 0 IconLabelButton {
IconLabelButton { inverseLayout: true
inverseLayout: true //: "Reception info"
text: chatBubbleContent.selectedText != "" text: qsTr("chat_message_reception_info")
//: "Copy selection" icon.source: AppIcons.chatTeardropText
? qsTr("chat_message_copy_selection") Layout.fillWidth: true
//: "Copy" Layout.preferredHeight: Math.round(45 * DefaultStyle.dp)
: qsTr("chat_message_copy")
icon.source: AppIcons.copy
// spacing: Math.round(10 * DefaultStyle.dp)
Layout.fillWidth: true
Layout.preferredHeight: Math.round(45 * DefaultStyle.dp)
onClicked: {
var success = UtilsCpp.copyToClipboard(chatBubbleContent.selectedText != "" ? chatBubbleContent.selectedText : mainItem.chatMessage.core.text)
//: Copied
if (success) UtilsCpp.showInformationPopup(qsTr("chat_message_copied_to_clipboard_title"),
//: "to clipboard"
qsTr("chat_message_copied_to_clipboard_toast"))
optionsMenu.close()
}
}
IconLabelButton {
inverseLayout: true
//: "See message status"
text: qsTr("chat_message_see_status")
icon.source: AppIcons.chatTeardropText
Layout.fillWidth: true
Layout.preferredHeight: Math.round(45 * DefaultStyle.dp)
onClicked: {
mainItem.showImdnStatusForMessageRequested()
optionsMenu.close()
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(1, Math.round(1 * DefaultStyle.dp))
color: DefaultStyle.main2_400
}
IconLabelButton {
inverseLayout: true
//: "Delete"
text: qsTr("chat_message_delete")
icon.source: AppIcons.trashCan
// spacing: Math.round(10 * DefaultStyle.dp)
Layout.fillWidth: true
Layout.preferredHeight: Math.round(45 * DefaultStyle.dp)
onClicked: {
mainItem.messageDeletionRequested()
optionsMenu.close()
}
style: ButtonStyle.hoveredBackgroundRed
}
}
}
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: { onClicked: {
if(modelData) { mainItem.showImdnStatusForMessageRequested()
if (mainItem.ownReaction === modelData) mainItem.chatMessage.core.lRemoveReaction() optionsMenu.close()
else mainItem.chatMessage.core.lSendReaction(modelData)
}
emojiButton.close()
} }
} }
IconLabelButton {
inverseLayout: true
//: Reply
text: qsTr("chat_message_reply")
icon.source: AppIcons.reply
Layout.fillWidth: true
Layout.preferredHeight: Math.round(45 * DefaultStyle.dp)
onClicked: {
mainItem.replyToMessageRequested()
optionsMenu.close()
}
}
IconLabelButton {
inverseLayout: true
text: chatBubbleContent.selectedText != ""
//: "Copy selection"
? qsTr("chat_message_copy_selection")
//: "Copy"
: qsTr("chat_message_copy")
icon.source: AppIcons.copy
// spacing: Math.round(10 * DefaultStyle.dp)
Layout.fillWidth: true
Layout.preferredHeight: Math.round(45 * DefaultStyle.dp)
onClicked: {
var success = UtilsCpp.copyToClipboard(chatBubbleContent.selectedText != "" ? chatBubbleContent.selectedText : mainItem.chatMessage.core.text)
//: Copied
if (success) UtilsCpp.showInformationPopup(qsTr("chat_message_copied_to_clipboard_title"),
//: "to clipboard"
qsTr("chat_message_copied_to_clipboard_toast"))
optionsMenu.close()
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(1, Math.round(1 * DefaultStyle.dp))
color: DefaultStyle.main2_400
}
IconLabelButton {
inverseLayout: true
//: "Delete"
text: qsTr("chat_message_delete")
icon.source: AppIcons.trashCan
// spacing: Math.round(10 * DefaultStyle.dp)
Layout.fillWidth: true
Layout.preferredHeight: Math.round(45 * DefaultStyle.dp)
onClicked: {
mainItem.messageDeletionRequested()
optionsMenu.close()
}
style: ButtonStyle.hoveredBackgroundRed
}
} }
PopupButton { }
id: emojiPickerButton PopupButton {
icon.source: AppIcons.plusCircle id: emojiButton
popup.width: Math.round(393 * DefaultStyle.dp) style: ButtonStyle.noBackground
popup.height: Math.round(291 * DefaultStyle.dp) icon.source: AppIcons.smiley
popup.contentItem: EmojiPicker { popup.contentItem: RowLayout {
id: emojiPicker Repeater {
onEmojiClicked: (emoji) => { model: ConstantsCpp.reactionsList
if (mainItem.chatMessage) { delegate: Button {
if (mainItem.ownReaction === emoji) mainItem.chatMessage.core.lRemoveReaction() text: UtilsCpp.encodeEmojiToQmlRichFormat(modelData)
else mainItem.chatMessage.core.lSendReaction(emoji) 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()
} }
emojiPickerButton.close()
emojiButton.close()
} }
} }
} }
} }
} }
Item{Layout.fillWidth: true}
} }
Item{Layout.fillWidth: true}
} }
} }

View file

@ -16,6 +16,7 @@ ListView {
property color backgroundColor property color backgroundColor
signal showReactionsForMessageRequested(ChatMessageGui chatMessage) signal showReactionsForMessageRequested(ChatMessageGui chatMessage)
signal showImdnStatusForMessageRequested(ChatMessageGui chatMessage) signal showImdnStatusForMessageRequested(ChatMessageGui chatMessage)
signal replyToMessageRequested(ChatMessageGui chatMessage)
Component.onCompleted: { Component.onCompleted: {
var index = eventLogProxy.findFirstUnreadIndex() var index = eventLogProxy.findFirstUnreadIndex()
@ -137,6 +138,7 @@ ListView {
onMessageDeletionRequested: modelData.core.lDelete() onMessageDeletionRequested: modelData.core.lDelete()
onShowReactionsForMessageRequested: mainItem.showReactionsForMessageRequested(modelData) onShowReactionsForMessageRequested: mainItem.showReactionsForMessageRequested(modelData)
onShowImdnStatusForMessageRequested: mainItem.showImdnStatusForMessageRequested(modelData) onShowImdnStatusForMessageRequested: mainItem.showImdnStatusForMessageRequested(modelData)
onReplyToMessageRequested: mainItem.replyToMessageRequested(modelData)
} }
} }

View file

@ -37,7 +37,6 @@ Control.Control {
function _emitFiles (files) { function _emitFiles (files) {
// Filtering files, other urls are forbidden. // Filtering files, other urls are forbidden.
files = files.reduce(function (files, file) { files = files.reduce(function (files, file) {
console.log("dropping", file.toString())
if (file.toString().startsWith("file:")) { if (file.toString().startsWith("file:")) {
files.push(Utils.getSystemPathFromUri(file)) files.push(Utils.getSystemPathFromUri(file))
} }

View file

@ -18,6 +18,7 @@ RowLayout {
property var contact: contactObj?.value || null property var contact: contactObj?.value || null
property CallGui call property CallGui call
property alias callHeaderContent: splitPanel.headerContent property alias callHeaderContent: splitPanel.headerContent
property bool replyingToMessage: false
spacing: 0 spacing: 0
signal oneOneCall(bool video) signal oneOneCall(bool video)
@ -161,6 +162,10 @@ RowLayout {
contentLoader.showingImdnStatus = true contentLoader.showingImdnStatus = true
detailsPanel.visible = true detailsPanel.visible = true
} }
onReplyToMessageRequested: (chatMessage) => {
mainItem.chatMessage = chatMessage
mainItem.replyingToMessage = true
}
Popup { Popup {
id: emojiPickerPopup id: emojiPickerPopup
@ -208,9 +213,9 @@ RowLayout {
} }
Control.Control { Control.Control {
id: selectedFilesArea id: selectedFilesArea
visible: selectedFiles.count > 0 visible: selectedFiles.count > 0 || mainItem.replyingToMessage
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Math.round(104 * DefaultStyle.dp) Layout.preferredHeight: implicitHeight
topPadding: Math.round(12 * DefaultStyle.dp) topPadding: Math.round(12 * DefaultStyle.dp)
bottomPadding: Math.round(12 * DefaultStyle.dp) bottomPadding: Math.round(12 * DefaultStyle.dp)
leftPadding: Math.round(19 * DefaultStyle.dp) leftPadding: Math.round(19 * DefaultStyle.dp)
@ -225,6 +230,7 @@ RowLayout {
style: ButtonStyle.noBackground style: ButtonStyle.noBackground
onClicked: { onClicked: {
contents.clear() contents.clear()
mainItem.replyingToMessage = false
} }
} }
background: Item{ background: Item{
@ -233,7 +239,6 @@ RowLayout {
color: DefaultStyle.grey_0 color: DefaultStyle.grey_0
border.color: DefaultStyle.main2_100 border.color: DefaultStyle.main2_100
border.width: Math.round(2 * DefaultStyle.dp) border.width: Math.round(2 * DefaultStyle.dp)
radius: Math.round(20 * DefaultStyle.dp)
height: parent.height / 2 height: parent.height / 2
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
@ -246,37 +251,73 @@ RowLayout {
height: 2 * parent.height / 3 height: 2 * parent.height / 3
} }
} }
contentItem: ListView { contentItem: ColumnLayout {
id: selectedFiles spacing: Math.round(5 * DefaultStyle.dp)
orientation: ListView.Horizontal ColumnLayout {
spacing: Math.round(16 * DefaultStyle.dp) id: replyLayout
model: ChatMessageContentProxy { spacing: 0
id: contents visible: mainItem.chatMessage && mainItem.replyingToMessage
filterType: ChatMessageContentProxy.FilterContentType.File Text {
} Layout.fillWidth: true
delegate: Item { //: Reply to %1
width: Math.round(80 * DefaultStyle.dp) text: mainItem.chatMessage ? qsTr("reply_to_label").arg(UtilsCpp.boldTextPart(mainItem.chatMessage.core.fromName, mainItem.chatMessage.core.fromName)) : ""
height: Math.round(80 * DefaultStyle.dp) color: DefaultStyle.main2_500main
FileView { font {
contentGui: modelData pixelSize: Typography.p3.pixelSize
anchors.left: parent.left weight: Typography.p3.weight
anchors.bottom: parent.bottom }
width: Math.round(69 * DefaultStyle.dp)
height: Math.round(69 * DefaultStyle.dp)
} }
RoundButton { Text {
icon.source: AppIcons.closeX Layout.fillWidth: true
icon.width: Math.round(12 * DefaultStyle.dp) text: mainItem.chatMessage ? mainItem.chatMessage.core.text : ""
icon.height: Math.round(12 * DefaultStyle.dp) color: DefaultStyle.main2_400
anchors.top: parent.top font {
anchors.right: parent.right pixelSize: Typography.p3.pixelSize
style: ButtonStyle.numericPad weight: Typography.p3.weight
shadowEnabled: true }
padding: Math.round(3 * DefaultStyle.dp)
onClicked: contents.removeContent(modelData)
} }
} }
Control.ScrollBar.horizontal: selectedFilesScrollbar Rectangle {
Layout.fillWidth: true
visible: replyLayout.visible && selectedFiles.visible
color: DefaultStyle.main2_300
Layout.preferredHeight: Math.max(1, Math.round(1 * DefaultStyle.dp))
}
ListView {
id: selectedFiles
orientation: ListView.Horizontal
visible: count > 0
spacing: Math.round(16 * DefaultStyle.dp)
Layout.fillWidth: true
Layout.preferredHeight: Math.round(104 * DefaultStyle.dp)
model: ChatMessageContentProxy {
id: contents
filterType: ChatMessageContentProxy.FilterContentType.File
}
delegate: Item {
width: Math.round(80 * DefaultStyle.dp)
height: Math.round(80 * DefaultStyle.dp)
FileView {
contentGui: modelData
anchors.left: parent.left
anchors.bottom: parent.bottom
width: Math.round(69 * DefaultStyle.dp)
height: Math.round(69 * DefaultStyle.dp)
}
RoundButton {
icon.source: AppIcons.closeX
icon.width: Math.round(12 * DefaultStyle.dp)
icon.height: Math.round(12 * DefaultStyle.dp)
anchors.top: parent.top
anchors.right: parent.right
style: ButtonStyle.numericPad
shadowEnabled: true
padding: Math.round(3 * DefaultStyle.dp)
onClicked: contents.removeContent(modelData)
}
}
Control.ScrollBar.horizontal: selectedFilesScrollbar
}
} }
ScrollBar { ScrollBar {
id: selectedFilesScrollbar id: selectedFilesScrollbar
@ -304,7 +345,11 @@ RowLayout {
} }
onSendMessage: { onSendMessage: {
var filesContents = contents.getAll() var filesContents = contents.getAll()
if (filesContents.length === 0) if (mainItem.replyingToMessage) {
mainItem.replyingToMessage = false
UtilsCpp.sendReplyMessage(mainItem.chatMessage, mainItem.chat, text, filesContents)
}
else if (filesContents.length === 0)
mainItem.chat.core.lSendTextMessage(text) mainItem.chat.core.lSendTextMessage(text)
else mainItem.chat.core.lSendMessage(text, filesContents) else mainItem.chat.core.lSendMessage(text, filesContents)
contents.clear() contents.clear()