diff --git a/Linphone/core/chat/message/ChatMessageCore.cpp b/Linphone/core/chat/message/ChatMessageCore.cpp index 578ba3a43..188e81f74 100644 --- a/Linphone/core/chat/message/ChatMessageCore.cpp +++ b/Linphone/core/chat/message/ChatMessageCore.cpp @@ -120,8 +120,12 @@ ChatMessageCore::ChatMessageCore(const std::shared_ptr &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 me) { setRetracted(); }); }); + mChatMessageModelConnection->makeConnectToModel(&ChatMessageModel::contentEdited, + [this](const std::shared_ptr &message) { + mChatMessageModelConnection->invokeToCore([this] { + mIsEdited = true; + emit edited(); + }); + }); } QList ChatMessageCore::computeDeliveryStatus(const std::shared_ptr &message) { @@ -498,6 +509,10 @@ bool ChatMessageCore::isRetracted() const { return mIsRetracted; } +bool ChatMessageCore::isEdited() const { + return mIsEdited; +} + QString ChatMessageCore::getOwnReaction() const { return mOwnReaction; } diff --git a/Linphone/core/chat/message/ChatMessageCore.hpp b/Linphone/core/chat/message/ChatMessageCore.hpp index 3ec7fea3d..b4115b3f6 100644 --- a/Linphone/core/chat/message/ChatMessageCore.hpp +++ b/Linphone/core/chat/message/ChatMessageCore.hpp @@ -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 create(const std::shared_ptr &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; diff --git a/Linphone/core/chat/message/EventLogList.cpp b/Linphone/core/chat/message/EventLogList.cpp index 16831dfaf..1505d88f7 100644 --- a/Linphone/core/chat/message/EventLogList.cpp +++ b/Linphone/core/chat/message/EventLogList.cpp @@ -64,6 +64,7 @@ void EventLogList::disconnectItem(const QSharedPointer &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 &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()); + }); + }); + }); } } diff --git a/Linphone/core/chat/message/EventLogProxy.cpp b/Linphone/core/chat/message/EventLogProxy.cpp index a1e45850d..5f03799fe 100644 --- a/Linphone/core/chat/message/EventLogProxy.cpp +++ b/Linphone/core/chat/message/EventLogProxy.cpp @@ -205,4 +205,4 @@ void EventLogProxy::findIndexCorrespondingToFilter(int startIndex, bool forward, auto listIndex = mapToSource(index(startIndex, 0)).row(); eventLogList->findChatMessageWithFilter(filter, listIndex, forward, isFirstResearch); } -} \ No newline at end of file +} diff --git a/Linphone/data/languages/de.ts b/Linphone/data/languages/de.ts index e8980d8fb..bd8b10dbc 100644 --- a/Linphone/data/languages/de.ts +++ b/Linphone/data/languages/de.ts @@ -2225,6 +2225,12 @@ "Reception info" + + + menu_edit_chat_message + "Edit" + + chat_message_reply diff --git a/Linphone/data/languages/en.ts b/Linphone/data/languages/en.ts index bad87c1bd..db09e7ede 100644 --- a/Linphone/data/languages/en.ts +++ b/Linphone/data/languages/en.ts @@ -2218,6 +2218,12 @@ "Reception info" Reception info + + + menu_edit_chat_message + "Edit" + Edit + chat_message_reply @@ -2236,6 +2242,12 @@ "Delete" Delete + + + conversation_message_edited_label + "Edited" + Edited + ChatMessageContentCore @@ -5795,6 +5807,12 @@ To enable them in a commercial project, please contact us. Reply to %1 Reply to %1 + + + conversation_editing_message_title + Message beeing edited + Message beeing edited + shared_medias_title @@ -6038,18 +6056,36 @@ To enable them in a commercial project, please contact us. Cannot reply to invalid message Cannot reply to invalid message + + + chat_message_edit_error + Cannot modify invalid message + Cannot modify invalid message + info_popup_reply_message_error Could not send reply message : %1 Could not send reply message : %1 + + + info_popup_edited_message_error + Could not send edited message : %1 + Could not send edited message : %1 + info_popup_send_reply_message_error_message Failed to create reply message Failed to create reply message + + + info_popup_send_edited_message_error_message + Failed to create edited message + Failed to create edited message + nHour diff --git a/Linphone/data/languages/fr.ts b/Linphone/data/languages/fr.ts index d85b11cce..0d1f6f4a1 100644 --- a/Linphone/data/languages/fr.ts +++ b/Linphone/data/languages/fr.ts @@ -2218,6 +2218,12 @@ "Reception info" Info de réception + + + menu_edit_chat_message + "Edit" + Modifier + chat_message_reply @@ -2236,6 +2242,12 @@ "Delete" Supprimer + + + conversation_message_edited_label + "Edited" + Modifié + ChatMessageContentCore @@ -5795,6 +5807,12 @@ Pour les activer dans un projet commercial, merci de nous contacter.Reply to %1 Réponse à %1 + + + conversation_editing_message_title + Message beeing edited + Modification du message + shared_medias_title @@ -6210,18 +6228,36 @@ Failed to create 1-1 conversation with %1 ! Cannot reply to invalid message Impossible de répondre : message invalide + + + chat_message_edit_error + Cannot modify invalid message + Impossible de modifier le message : message invalide + info_popup_reply_message_error Could not send reply message : %1 Impossible d'envoyer la réponse : %1 + + + info_popup_edited_message_error + Could not send edited message : %1 + Impossible d'envoyer le message modifié : %1 + info_popup_send_reply_message_error_message Failed to create reply message Impossible de créer le message + + + info_popup_send_edited_message_error_message + Failed to create edited message + Impossible de créer le message modifié + info_popup_send_voice_message_error_message diff --git a/Linphone/model/chat/ChatModel.cpp b/Linphone/model/chat/ChatModel.cpp index 1adad7fbe..1235e8c96 100644 --- a/Linphone/model/chat/ChatModel.cpp +++ b/Linphone/model/chat/ChatModel.cpp @@ -185,6 +185,11 @@ ChatModel::createReplyMessage(const std::shared_ptr &mess return mMonitor->createReplyMessage(message); } +std::shared_ptr +ChatModel::createReplacesMessage(const std::shared_ptr &message) { + return mMonitor->createReplacesMessage(message); +} + std::shared_ptr ChatModel::createForwardMessage(const std::shared_ptr &message) { return mMonitor->createForwardMessage(message); diff --git a/Linphone/model/chat/ChatModel.hpp b/Linphone/model/chat/ChatModel.hpp index 9ed20975f..e5df4b390 100644 --- a/Linphone/model/chat/ChatModel.hpp +++ b/Linphone/model/chat/ChatModel.hpp @@ -68,6 +68,7 @@ public: std::shared_ptr createReplyMessage(const std::shared_ptr &message); std::shared_ptr createForwardMessage(const std::shared_ptr &message); + std::shared_ptr createReplacesMessage(const std::shared_ptr &message); std::shared_ptr createTextMessageFromText(QString text); std::shared_ptr createMessage(QString text, diff --git a/Linphone/model/chat/message/ChatMessageModel.cpp b/Linphone/model/chat/message/ChatMessageModel.cpp index 347b4bd83..cf0f57c34 100644 --- a/Linphone/model/chat/message/ChatMessageModel.cpp +++ b/Linphone/model/chat/message/ChatMessageModel.cpp @@ -199,3 +199,7 @@ void ChatMessageModel::onEphemeralMessageDeleted(const std::shared_ptr &message) { emit retracted(message); } + +void ChatMessageModel::onContentEdited(const std::shared_ptr &message) { + emit contentEdited(message); +} diff --git a/Linphone/model/chat/message/ChatMessageModel.hpp b/Linphone/model/chat/message/ChatMessageModel.hpp index 6d916f5bd..56c9419fc 100644 --- a/Linphone/model/chat/message/ChatMessageModel.hpp +++ b/Linphone/model/chat/message/ChatMessageModel.hpp @@ -97,6 +97,7 @@ signals: void ephemeralMessageDeleted(const std::shared_ptr &message); void ephemeralMessageTimeUpdated(const std::shared_ptr &message, int expireTime); void retracted(const std::shared_ptr &message); + void contentEdited(const std::shared_ptr &message); private: linphone::ChatMessage::State mMessageState; @@ -133,6 +134,7 @@ private: void onEphemeralMessageTimerStarted(const std::shared_ptr &message) override; void onEphemeralMessageDeleted(const std::shared_ptr &message) override; void onRetracted(const std::shared_ptr &message) override; + void onContentEdited(const std::shared_ptr &message) override; }; #endif diff --git a/Linphone/tool/Utils.cpp b/Linphone/tool/Utils.cpp index 451c11c3a..630f40a26 100644 --- a/Linphone/tool/Utils.cpp +++ b/Linphone/tool/Utils.cpp @@ -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> filesContent; + for (auto &file : files) { + auto contentGui = qvariant_cast(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; diff --git a/Linphone/tool/Utils.hpp b/Linphone/tool/Utils.hpp index 0e74b556e..6f6fad91f 100644 --- a/Linphone/tool/Utils.hpp +++ b/Linphone/tool/Utils.hpp @@ -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); diff --git a/Linphone/view/Control/Display/Chat/ChatMessage.qml b/Linphone/view/Control/Display/Chat/ChatMessage.qml index aa68b8f9f..0c7402e2d 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessage.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessage.qml @@ -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,16 +298,26 @@ 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") color: DefaultStyle.main2_500_main font { - pixelSize: Typography.p3.pixelSize - weight: Typography.p3.weight + pixelSize: Typography.p3.pixelSize + weight: Typography.p3.weight } } EffectImage { @@ -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 diff --git a/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml b/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml index b042c5f2e..343e5b9f3 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessagesListView.qml @@ -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) diff --git a/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml b/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml index 55122e209..d5a2295af 100644 --- a/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml +++ b/Linphone/view/Control/Input/Chat/ChatDroppableTextArea.qml @@ -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() } diff --git a/Linphone/view/Page/Form/Chat/SelectedChatView.qml b/Linphone/view/Page/Form/Chat/SelectedChatView.qml index 96cdd5c1c..9784284c6 100644 --- a/Linphone/view/Page/Form/Chat/SelectedChatView.qml +++ b/Linphone/view/Page/Form/Chat/SelectedChatView.qml @@ -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() - mainItem.replyingToMessage = false + 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)