Chat message edition

This commit is contained in:
Christophe Deschamps 2025-12-11 08:51:52 +01:00
parent d40045d5bb
commit 1bae93aab5
17 changed files with 232 additions and 11 deletions

View file

@ -120,8 +120,12 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr<linphone::ChatMessage> &c
mHasTextContent = mChatMessageModel->getHasTextContent();
mTimestamp = QDateTime::fromSecsSinceEpoch(chatmessage->getTime());
mIsOutgoing = chatmessage->isOutgoing();
mIsRetractable = chatmessage->isRetractable();
mIsRetractable =
chatmessage->isOutgoing() && chatmessage->isRetractable() && !chatmessage->getChatRoom()->isReadOnly();
mIsRetracted = chatmessage->isRetracted();
mIsEditable =
chatmessage->isOutgoing() && chatmessage->isEditable() && !chatmessage->getChatRoom()->isReadOnly();
mIsEdited = chatmessage->isEdited();
mIsRemoteMessage = !chatmessage->isOutgoing();
mPeerAddress = Utils::coreStringToAppString(chatmessage->getPeerAddress()->asStringUriOnly());
mPeerName = ToolModel::getDisplayName(chatmessage->getPeerAddress());
@ -364,6 +368,13 @@ void ChatMessageCore::setSelf(QSharedPointer<ChatMessageCore> me) {
setRetracted();
});
});
mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::contentEdited,
[this](const std::shared_ptr<linphone::ChatMessage> &message) {
mChatMessageModelConnection->invokeToCore([this] {
mIsEdited = true;
emit edited();
});
});
}
QList<ImdnStatus> ChatMessageCore::computeDeliveryStatus(const std::shared_ptr<linphone::ChatMessage> &message) {
@ -498,6 +509,10 @@ bool ChatMessageCore::isRetracted() const {
return mIsRetracted;
}
bool ChatMessageCore::isEdited() const {
return mIsEdited;
}
QString ChatMessageCore::getOwnReaction() const {
return mOwnReaction;
}

View file

@ -114,6 +114,8 @@ class ChatMessageCore : public QObject, public AbstractObject {
Q_PROPERTY(bool isOutgoing MEMBER mIsOutgoing CONSTANT)
Q_PROPERTY(bool isRetractable MEMBER mIsRetractable CONSTANT)
Q_PROPERTY(bool isRetracted READ isRetracted NOTIFY isRetractedChanged)
Q_PROPERTY(bool isEditable MEMBER mIsEditable CONSTANT)
Q_PROPERTY(bool isEdited READ isEdited NOTIFY edited)
public:
static QSharedPointer<ChatMessageCore> create(const std::shared_ptr<linphone::ChatMessage> &chatmessage);
@ -148,6 +150,7 @@ public:
bool isRetracted() const;
void setRetracted();
bool isEdited() const;
QString getOwnReaction() const;
void setOwnReaction(const QString &reaction);
@ -183,6 +186,7 @@ signals:
void singletonReactionMapChanged();
void ephemeralDurationChanged(int duration);
void isRetractedChanged();
void edited();
void lDelete();
void deleted();
@ -224,6 +228,8 @@ private:
int mEphemeralDuration = 0;
bool mIsRetractable = false;
bool mIsRetracted = false;
bool mIsEditable = false;
bool mIsEdited = false;
bool mIsOutgoing = false;
QString mTotalReactionsLabel;

View file

@ -64,6 +64,7 @@ void EventLogList::disconnectItem(const QSharedPointer<EventLogCore> &item) {
if (message) {
disconnect(message.get(), &ChatMessageCore::isReadChanged, this, nullptr);
disconnect(message.get(), &ChatMessageCore::deleted, this, nullptr);
disconnect(message.get(), &ChatMessageCore::edited, this, nullptr);
}
}
@ -77,6 +78,20 @@ void EventLogList::connectItem(const QSharedPointer<EventLogCore> &item) {
if (mChatCore) emit mChatCore->lUpdateLastMessage();
remove(item);
});
connect(message.get(), &ChatMessageCore::edited, this, [this, item] {
auto eventLogModel = item->getModel();
mCoreModelConnection->invokeToModel([this, eventLogModel, item]() {
auto chatRoom = mChatCore->getModel()->getMonitor();
auto newEventLog = EventLogCore::create(eventLogModel->getEventLog(), chatRoom);
bool wasLastMessage =
mChatCore->getModel()->getLastChatMessage() == eventLogModel->getEventLog()->getChatMessage();
mCoreModelConnection->invokeToCore([this, newEventLog, wasLastMessage, item] {
connectItem(newEventLog);
replace(item, newEventLog);
if (wasLastMessage) mChatCore->setLastMessage(newEventLog->getChatMessageCore());
});
});
});
}
}

View file

@ -2225,6 +2225,12 @@
<extracomment>&quot;Reception info&quot;</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessage.qml" line="407"/>
<source>menu_edit_chat_message</source>
<extracomment>&quot;Edit&quot;</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessage.qml" line="419"/>
<source>chat_message_reply</source>

View file

@ -2218,6 +2218,12 @@
<extracomment>&quot;Reception info&quot;</extracomment>
<translation>Reception info</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessage.qml" line="407"/>
<source>menu_edit_chat_message</source>
<extracomment>&quot;Edit&quot;</extracomment>
<translation>Edit</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessage.qml" line="419"/>
<source>chat_message_reply</source>
@ -2236,6 +2242,12 @@
<extracomment>&quot;Delete&quot;</extracomment>
<translation>Delete</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessage.qml" line="469"/>
<source>conversation_message_edited_label</source>
<extracomment>&quot;Edited&quot;</extracomment>
<translation>Edited</translation>
</message>
</context>
<context>
<name>ChatMessageContentCore</name>
@ -5795,6 +5807,12 @@ To enable them in a commercial project, please contact us.</translation>
<extracomment>Reply to %1</extracomment>
<translation>Reply to %1</translation>
</message>
<message>
<location filename="../../view/Page/Form/Chat/SelectedChatView.qml" line="417"/>
<source>conversation_editing_message_title</source>
<extracomment>Message beeing edited</extracomment>
<translation>Message beeing edited</translation>
</message>
<message>
<location filename="../../view/Page/Form/Chat/SelectedChatView.qml" line="617"/>
<source>shared_medias_title</source>
@ -6038,18 +6056,36 @@ To enable them in a commercial project, please contact us.</translation>
<extracomment>Cannot reply to invalid message</extracomment>
<translation>Cannot reply to invalid message</translation>
</message>
<message>
<location filename="../../tool/Utils.cpp" line="2123"/>
<source>chat_message_edit_error</source>
<extracomment>Cannot modify invalid message</extracomment>
<translation>Cannot modify invalid message</translation>
</message>
<message>
<location filename="../../tool/Utils.cpp" line="2129"/>
<source>info_popup_reply_message_error</source>
<extracomment>Could not send reply message : %1</extracomment>
<translation>Could not send reply message : %1</translation>
</message>
<message>
<location filename="../../tool/Utils.cpp" line="2129"/>
<source>info_popup_edited_message_error</source>
<extracomment>Could not send edited message : %1</extracomment>
<translation>Could not send edited message : %1</translation>
</message>
<message>
<location filename="../../tool/Utils.cpp" line="2156"/>
<source>info_popup_send_reply_message_error_message</source>
<extracomment>Failed to create reply message</extracomment>
<translation>Failed to create reply message</translation>
</message>
<message>
<location filename="../../tool/Utils.cpp" line="2156"/>
<source>info_popup_send_edited_message_error_message</source>
<extracomment>Failed to create edited message</extracomment>
<translation>Failed to create edited message</translation>
</message>
<message numerus="yes">
<location filename="../../tool/Utils.cpp" line="2278"/>
<source>nHour</source>

View file

@ -2218,6 +2218,12 @@
<extracomment>&quot;Reception info&quot;</extracomment>
<translation>Info de réception</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessage.qml" line="407"/>
<source>menu_edit_chat_message</source>
<extracomment>&quot;Edit&quot;</extracomment>
<translation>Modifier</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessage.qml" line="419"/>
<source>chat_message_reply</source>
@ -2236,6 +2242,12 @@
<extracomment>&quot;Delete&quot;</extracomment>
<translation>Supprimer</translation>
</message>
<message>
<location filename="../../view/Control/Display/Chat/ChatMessage.qml" line="469"/>
<source>conversation_message_edited_label</source>
<extracomment>&quot;Edited&quot;</extracomment>
<translation>Modifié</translation>
</message>
</context>
<context>
<name>ChatMessageContentCore</name>
@ -5795,6 +5807,12 @@ Pour les activer dans un projet commercial, merci de nous contacter.</translatio
<extracomment>Reply to %1</extracomment>
<translation>Réponse à %1</translation>
</message>
<message>
<location filename="../../view/Page/Form/Chat/SelectedChatView.qml" line="417"/>
<source>conversation_editing_message_title</source>
<extracomment>Message beeing edited</extracomment>
<translation>Modification du message</translation>
</message>
<message>
<location filename="../../view/Page/Form/Chat/SelectedChatView.qml" line="617"/>
<source>shared_medias_title</source>
@ -6210,18 +6228,36 @@ Failed to create 1-1 conversation with %1 !</extracomment>
<extracomment>Cannot reply to invalid message</extracomment>
<translation>Impossible de répondre : message invalide</translation>
</message>
<message>
<location filename="../../tool/Utils.cpp" line="2123"/>
<source>chat_message_edit_error</source>
<extracomment>Cannot modify invalid message</extracomment>
<translation>Impossible de modifier le message : message invalide</translation>
</message>
<message>
<location filename="../../tool/Utils.cpp" line="2129"/>
<source>info_popup_reply_message_error</source>
<extracomment>Could not send reply message : %1</extracomment>
<translation>Impossible d&apos;envoyer la réponse : %1</translation>
</message>
<message>
<location filename="../../tool/Utils.cpp" line="2129"/>
<source>info_popup_edited_message_error</source>
<extracomment>Could not send edited message : %1</extracomment>
<translation>Impossible d&apos;envoyer le message modifié : %1</translation>
</message>
<message>
<location filename="../../tool/Utils.cpp" line="2156"/>
<source>info_popup_send_reply_message_error_message</source>
<extracomment>Failed to create reply message</extracomment>
<translation>Impossible de créer le message</translation>
</message>
<message>
<location filename="../../tool/Utils.cpp" line="2156"/>
<source>info_popup_send_edited_message_error_message</source>
<extracomment>Failed to create edited message</extracomment>
<translation>Impossible de créer le message modifié</translation>
</message>
<message>
<location filename="../../tool/Utils.cpp" line="2194"/>
<source>info_popup_send_voice_message_error_message</source>

View file

@ -185,6 +185,11 @@ ChatModel::createReplyMessage(const std::shared_ptr<linphone::ChatMessage> &mess
return mMonitor->createReplyMessage(message);
}
std::shared_ptr<linphone::ChatMessage>
ChatModel::createReplacesMessage(const std::shared_ptr<linphone::ChatMessage> &message) {
return mMonitor->createReplacesMessage(message);
}
std::shared_ptr<linphone::ChatMessage>
ChatModel::createForwardMessage(const std::shared_ptr<linphone::ChatMessage> &message) {
return mMonitor->createForwardMessage(message);

View file

@ -68,6 +68,7 @@ public:
std::shared_ptr<linphone::ChatMessage> createReplyMessage(const std::shared_ptr<linphone::ChatMessage> &message);
std::shared_ptr<linphone::ChatMessage> createForwardMessage(const std::shared_ptr<linphone::ChatMessage> &message);
std::shared_ptr<linphone::ChatMessage> createReplacesMessage(const std::shared_ptr<linphone::ChatMessage> &message);
std::shared_ptr<linphone::ChatMessage> createTextMessageFromText(QString text);
std::shared_ptr<linphone::ChatMessage> createMessage(QString text,

View file

@ -199,3 +199,7 @@ void ChatMessageModel::onEphemeralMessageDeleted(const std::shared_ptr<linphone:
void ChatMessageModel::onRetracted(const std::shared_ptr<linphone::ChatMessage> &message) {
emit retracted(message);
}
void ChatMessageModel::onContentEdited(const std::shared_ptr<linphone::ChatMessage> &message) {
emit contentEdited(message);
}

View file

@ -97,6 +97,7 @@ signals:
void ephemeralMessageDeleted(const std::shared_ptr<linphone::ChatMessage> &message);
void ephemeralMessageTimeUpdated(const std::shared_ptr<linphone::ChatMessage> &message, int expireTime);
void retracted(const std::shared_ptr<linphone::ChatMessage> &message);
void contentEdited(const std::shared_ptr<linphone::ChatMessage> &message);
private:
linphone::ChatMessage::State mMessageState;
@ -133,6 +134,7 @@ private:
void onEphemeralMessageTimerStarted(const std::shared_ptr<linphone::ChatMessage> &message) override;
void onEphemeralMessageDeleted(const std::shared_ptr<linphone::ChatMessage> &message) override;
void onRetracted(const std::shared_ptr<linphone::ChatMessage> &message) override;
void onContentEdited(const std::shared_ptr<linphone::ChatMessage> &message) override;
};
#endif

View file

@ -2159,6 +2159,50 @@ void Utils::sendReplyMessage(ChatMessageGui *message, ChatGui *chatGui, QString
});
}
void Utils::sendReplaceMessage(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 edit to invalid message
QString error = !chatMessageModel ? tr("chat_message_edit_error")
//: Error in the chat
: tr("chat_error");
//: Error
showInformationPopup(tr("info_popup_error_title"),
//: Could not send edited message : %1
tr("info_popup_edited_message_error").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 messageToEdit = chatMessageModel->getMonitor();
auto linMessage = chatModel->createReplacesMessage(messageToEdit);
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 edited message
tr("info_popup_send_edited_message_error_message"));
});
}
});
}
VariantObject *Utils::createVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui) {
VariantObject *data = new VariantObject("createVoiceRecordingMessage");
if (!data) return nullptr;

View file

@ -183,6 +183,8 @@ public:
Q_INVOKABLE static void
sendReplyMessage(ChatMessageGui *message, ChatGui *chatGui, QString text, QVariantList files);
Q_INVOKABLE static void forwardMessageTo(ChatMessageGui *message, ChatGui *chatGui);
Q_INVOKABLE static void
sendReplaceMessage(ChatMessageGui *message, ChatGui *chatGui, QString text, QVariantList files);
Q_INVOKABLE static void sendVoiceRecordingMessage(RecorderGui *recorderGui, ChatGui *chatGui);
Q_INVOKABLE static QString getEphemeralFormatedTime(int selectedTime);

View file

@ -32,6 +32,7 @@ Control.Control {
leftPadding: isRemoteMessage ? Utils.getSizeWithScreenRatio(5) : 0
signal messageDeletionRequested()
signal messageEditionRequested()
signal isFileHoveringChanged(bool isFileHovering)
signal showReactionsForMessageRequested()
signal showImdnStatusForMessageRequested()
@ -297,9 +298,19 @@ Control.Control {
}
}
RowLayout {
spacing: mainItem.isRemoteMessage ? 0 : Utils.getSizeWithScreenRatio(5)
spacing: mainItem.isRemoteMessage && !mainItem.chatMessage.core.isEdited ? 0 : Utils.getSizeWithScreenRatio(5)
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: childrenRect.height
Text {
Layout.alignment: Qt.AlignVCenter
text: qsTr("conversation_message_edited_label")
visible: mainItem.chatMessage.core.isEdited
color: DefaultStyle.main2_500_main
font {
pixelSize: Typography.p3.pixelSize
weight: Typography.p3.weight
}
}
Text {
Layout.alignment: Qt.AlignVCenter
text: UtilsCpp.formatDate(mainItem.chatMessage.core.timestamp, true, false, "dd/MM")
@ -423,6 +434,19 @@ Control.Control {
optionsMenu.close()
}
}
IconLabelButton {
inverseLayout: true
//: "Edit"
text: qsTr("menu_edit_chat_message")
visible: mainItem.chatMessage.core.isEditable
icon.source: AppIcons.pencil
Layout.fillWidth: true
Layout.preferredHeight: Utils.getSizeWithScreenRatio(45)
onClicked: {
mainItem.messageEditionRequested()
optionsMenu.close()
}
}
IconLabelButton {
inverseLayout: true
visible: !mainItem.chatMessage.core.isRetracted

View file

@ -25,6 +25,7 @@ ListView {
signal showImdnStatusForMessageRequested(ChatMessageGui chatMessage)
signal replyToMessageRequested(ChatMessageGui chatMessage)
signal forwardMessageRequested(ChatMessageGui chatMessage)
signal editMessageRequested(ChatMessageGui chatMessage)
signal requestHighlight(int indexToHighlight)
signal requestAutoPlayVoiceRecording(int indexToPlay)
currentIndex: -1
@ -315,6 +316,7 @@ ListView {
chatMessage.core.lDelete()
}
}
onMessageEditionRequested: mainItem.editMessageRequested(chatMessage)
onShowReactionsForMessageRequested: mainItem.showReactionsForMessageRequested(chatMessage)
onShowImdnStatusForMessageRequested: mainItem.showImdnStatusForMessageRequested(chatMessage)
onReplyToMessageRequested: mainItem.replyToMessageRequested(chatMessage)

View file

@ -23,6 +23,7 @@ Control.Control {
// disable record button if call ongoing
property bool callOngoing: false
property bool isEditing: false
property ChatGui chat
@ -78,6 +79,7 @@ Control.Control {
spacing: Utils.getSizeWithScreenRatio(16)
PopupButton {
id: emojiPickerButton
visible: !mainItem.isEditing
style: ButtonStyle.noBackground
icon.source: checked ? AppIcons.closeX : AppIcons.smiley
popup.width: Utils.getSizeWithScreenRatio(393)
@ -189,7 +191,7 @@ Control.Control {
//: Cannot record a message while a call is ongoing
ToolTip.text: qsTr("cannot_record_while_in_call_tooltip")
enabled: !mainItem.callOngoing
visible: !mainItem.callOngoing && sendingTextArea.text.length === 0 && mainItem.selectedFilesCount === 0
visible: !mainItem.callOngoing && sendingTextArea.text.length === 0 && mainItem.selectedFilesCount === 0 && !mainItem.isEditing
style: ButtonStyle.noBackground
hoverEnabled: true
icon.source: AppIcons.microphone
@ -202,7 +204,7 @@ Control.Control {
Layout.preferredHeight: height
visible: sendingTextArea.text.length !== 0 || mainItem.selectedFilesCount > 0
style: ButtonStyle.noBackgroundOrange
icon.source: AppIcons.paperPlaneRight
icon.source: mainItem.isEditing ? AppIcons.pencil : AppIcons.paperPlaneRight
onClicked: {
mainItem.sendMessage()
}

View file

@ -21,6 +21,7 @@ FocusScope {
property CallGui call
property alias callHeaderContent: splitPanel.header.contentItem
property bool replyingToMessage: false
property bool editingMessage: false
enum PanelType { MessageReactions, SharedFiles, Medias, ImdnStatus, ForwardToList, ManageParticipants, EphemeralSettings, None}
signal oneOneCall(bool video)
@ -299,11 +300,19 @@ FocusScope {
onReplyToMessageRequested: (chatMessage) => {
mainItem.chatMessage = chatMessage
mainItem.replyingToMessage = true
if (mainItem.editingMessage) mainItem.editingMessage = false
}
onForwardMessageRequested: (chatMessage) => {
mainItem.chatMessage = chatMessage
contentLoader.panelType = SelectedChatView.PanelType.ForwardToList
detailsPanel.visible = true
if (mainItem.editingMessage) mainItem.editingMessage = false
}
onEditMessageRequested: (chatMessage) => {
mainItem.chatMessage = chatMessage
mainItem.editingMessage = true
if (mainItem.replyingToMessage) mainItem.replyingToMessage = false
messageSender.text = chatMessage.core.text
}
}
ScrollBar {
@ -367,7 +376,7 @@ FocusScope {
}
Control.Control {
id: selectedFilesArea
visible: selectedFiles.count > 0 || mainItem.replyingToMessage
visible: selectedFiles.count > 0 || mainItem.replyingToMessage || mainItem.editingMessage
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
topPadding: Utils.getSizeWithScreenRatio(12)
@ -384,7 +393,12 @@ FocusScope {
style: ButtonStyle.noBackground
onClicked: {
contents.clear()
if (mainItem.replyingToMessage)
mainItem.replyingToMessage = false
else if (mainItem.editingMessage) {
mainItem.editingMessage = false
messageSender.text = ""
}
}
}
background: Item{
@ -410,11 +424,13 @@ FocusScope {
ColumnLayout {
id: replyLayout
spacing: 0
visible: mainItem.chatMessage && mainItem.replyingToMessage
visible: mainItem.chatMessage && (mainItem.replyingToMessage || mainItem.editingMessage)
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)) : ""
text: mainItem.replyingToMessage ?
(mainItem.chatMessage ? qsTr("reply_to_label").arg(UtilsCpp.boldTextPart(mainItem.chatMessage.core.fromName, mainItem.chatMessage.core.fromName)) : "")
: qsTr("conversation_editing_message_title")
color: DefaultStyle.main2_500_main
font {
pixelSize: Typography.p3.pixelSize
@ -489,6 +505,7 @@ FocusScope {
chat: mainItem.chat
selectedFilesCount: contents.count
callOngoing: mainItem.call != null
isEditing: mainItem.editingMessage
onChatChanged: {
if (chat) messageSender.text = mainItem.chat.core.sendingText
}
@ -507,6 +524,10 @@ FocusScope {
mainItem.replyingToMessage = false
UtilsCpp.sendReplyMessage(mainItem.chatMessage, mainItem.chat, text, filesContents)
}
else if (mainItem.editingMessage) {
UtilsCpp.sendReplaceMessage(mainItem.chatMessage, mainItem.chat, text, filesContents)
mainItem.editingMessage = false
}
else if (filesContents.length === 0)
mainItem.chat.core.lSendTextMessage(text)
else mainItem.chat.core.lSendMessage(text, filesContents)