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

View file

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

View file

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

View file

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

View file

@ -52,6 +52,9 @@ public:
void leave();
std::shared_ptr<linphone::ChatMessage>
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> createMessage(QString text,
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) {
mustBeInLinphoneThread(sLog().arg(Q_FUNC_INFO));
QString res;
for (auto &content : contents) {
if (content->isText()) {
return Utils::coreStringToAppString(content->getUtf8Text());
} 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()) {
auto conferenceInfo = linphone::Factory::get()->createConferenceInfoFromIcalendarContent(content);
auto conferenceInfoCore = ConferenceInfoCore::create(conferenceInfo);
@ -406,7 +408,7 @@ QString ToolModel::getMessageFromContent(std::list<std::shared_ptr<linphone::Con
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

View file

@ -25,6 +25,7 @@
#include "core/call/CallGui.hpp"
#include "core/chat/ChatCore.hpp"
#include "core/chat/ChatGui.hpp"
#include "core/chat/message/ChatMessageGui.hpp"
#include "core/conference/ConferenceCore.hpp"
#include "core/conference/ConferenceInfoCore.hpp"
#include "core/conference/ConferenceInfoGui.hpp"
@ -1950,6 +1951,50 @@ QString Utils::getSafeFilePath(const QString &filePath, bool *soFarSoGood) {
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 *data = new VariantObject("createVoiceRecordingMessage");
if (!data) return nullptr;

View file

@ -52,6 +52,7 @@ class ConferenceCore;
class ParticipantDeviceCore;
class DownloadablePayloadTypeCore;
class ChatGui;
class ChatMessageGui;
class RecorderGui;
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 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);
// QDir findDirectoryByName(QString startPath, QString name);

View file

@ -19,6 +19,8 @@ Control.Control {
property string fromAddress: chatMessage? chatMessage.core.fromAddress : ""
property bool isRemoteMessage: chatMessage? chatMessage.core.isRemoteMessage : 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
hoverEnabled: true
property bool linkHovered: false
@ -30,6 +32,7 @@ Control.Control {
signal isFileHoveringChanged(bool isFileHovering)
signal showReactionsForMessageRequested()
signal showImdnStatusForMessageRequested()
signal replyToMessageRequested()
background: Item {
anchors.fill: parent
@ -41,52 +44,119 @@ Control.Control {
}
}
contentItem: RowLayout {
spacing: 0
layoutDirection: mainItem.isRemoteMessage ? Qt.LeftToRight : Qt.RightToLeft
Avatar {
id: avatar
visible: mainItem.isFromChatGroup && mainItem.isRemoteMessage
Layout.preferredWidth: mainItem.isRemoteMessage ? 26 * DefaultStyle.dp : 0
Layout.preferredHeight: 26 * DefaultStyle.dp
contentItem: ColumnLayout {
spacing: Math.round(5 * DefaultStyle.dp)
Text {
id: fromNameText
Layout.alignment: Qt.AlignTop
Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0
_address: chatMessage ? chatMessage.core.fromAddress : ""
Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) + avatar.width : 0
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 {
Layout.alignment: Qt.AlignTop
spacing: 0
Text {
id: fromNameText
Layout.alignment: Qt.AlignTop
visible: mainItem.isFromChatGroup && mainItem.isRemoteMessage && mainItem.isFirstMessage
// anchors.top: parent.top
// anchors.left: parent.left
// anchors.leftMargin: avatar.width// mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0
maximumLineCount: 1
Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0
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
RowLayout {
id: replyLayout
visible: mainItem.isReply
Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) + avatar.width : 0
layoutDirection: mainItem.isRemoteMessage ? Qt.LeftToRight : Qt.RightToLeft
ColumnLayout {
spacing: Math.round(5 * DefaultStyle.dp)
RowLayout {
id: replyLabel
spacing: Math.round(8 * DefaultStyle.dp)
Layout.fillWidth: false
Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight
EffectImage {
imageSource: AppIcons.reply
colorizationColor: DefaultStyle.main2_500main
Layout.preferredWidth: Math.round(12 * DefaultStyle.dp)
Layout.preferredHeight: Math.round(12 * DefaultStyle.dp)
}
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 {
id: bubbleContainer
// Layout.topMargin: isFirstMessage ? 16 * DefaultStyle.dp : 0
Layout.leftMargin: mainItem.isFromChatGroup ? Math.round(9 * DefaultStyle.dp) : 0
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)
leftPadding: Math.round(18 * DefaultStyle.dp)
rightPadding: Math.round(18 * 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.
@ -133,6 +203,7 @@ Control.Control {
}
}
RowLayout {
Layout.fillWidth: false
Layout.alignment: mainItem.isRemoteMessage ? Qt.AlignLeft : Qt.AlignRight
Text {
text: UtilsCpp.formatDate(mainItem.chatMessage.core.timestamp, true, false, "dd/MM")
@ -217,118 +288,130 @@ Control.Control {
}
}
}
}
RowLayout {
id: actionsLayout
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
// Layout.fillWidth: true
spacing: Math.round(7 * DefaultStyle.dp)
layoutDirection: mainItem.isRemoteMessage ? Qt.LeftToRight : Qt.RightToLeft
PopupButton {
id: optionsMenu
popup.padding: 0
popup.contentItem: ColumnLayout {
spacing: 0
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()
}
}
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
}
RowLayout {
id: actionsLayout
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
// Layout.fillWidth: true
spacing: Math.round(7 * DefaultStyle.dp)
layoutDirection: mainItem.isRemoteMessage ? Qt.LeftToRight : Qt.RightToLeft
PopupButton {
id: optionsMenu
popup.padding: 0
popup.contentItem: ColumnLayout {
spacing: 0
IconLabelButton {
inverseLayout: true
//: "Reception info"
text: qsTr("chat_message_reception_info")
icon.source: AppIcons.chatTeardropText
Layout.fillWidth: true
Layout.preferredHeight: Math.round(45 * DefaultStyle.dp)
onClicked: {
if(modelData) {
if (mainItem.ownReaction === modelData) mainItem.chatMessage.core.lRemoveReaction()
else mainItem.chatMessage.core.lSendReaction(modelData)
}
emojiButton.close()
mainItem.showImdnStatusForMessageRequested()
optionsMenu.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
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)
}
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()
}
emojiPickerButton.close()
emojiButton.close()
}
}
}
}
}
Item{Layout.fillWidth: true}
}
Item{Layout.fillWidth: true}
}
}

View file

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

View file

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

View file

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