/* * Copyright (c) 2021 Belledonne Communications SARL. * * This file is part of linphone-desktop * (see https://www.linphone.org). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ChatMessageModel.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "ChatMessageListener.hpp" #include "app/App.hpp" #include "app/paths/Paths.hpp" #include "app/providers/ThumbnailProvider.hpp" #include "components/chat-reaction/ChatReactionListModel.hpp" #include "components/chat-reaction/ChatReactionModel.hpp" #include "components/contact/ContactModel.hpp" #include "components/contacts/ContactsListModel.hpp" #include "components/content/ContentListModel.hpp" #include "components/content/ContentModel.hpp" #include "components/content/ContentProxyModel.hpp" #include "components/core/CoreManager.hpp" #include "components/notifier/Notifier.hpp" #include "components/participant-imdn/ParticipantImdnStateListModel.hpp" #include "components/participant-imdn/ParticipantImdnStateProxyModel.hpp" #include "components/settings/AccountSettingsModel.hpp" #include "components/settings/SettingsModel.hpp" #include "utils/Constants.hpp" #include "utils/QExifImageHeader.hpp" #include "utils/Utils.hpp" void ChatMessageModel::connectTo(ChatMessageListener *listener) { connect(listener, &ChatMessageListener::fileTransferRecv, this, &ChatMessageModel::onFileTransferRecv); connect(listener, &ChatMessageListener::fileTransferSendChunk, this, &ChatMessageModel::onFileTransferSendChunk); connect(listener, &ChatMessageListener::fileTransferSend, this, &ChatMessageModel::onFileTransferSend); connect(listener, &ChatMessageListener::fileTransferProgressIndication, this, &ChatMessageModel::onFileTransferProgressIndication); connect(listener, &ChatMessageListener::msgStateChanged, this, &ChatMessageModel::onMsgStateChanged); connect(listener, &ChatMessageListener::newMessageReaction, this, &ChatMessageModel::onNewMessageReaction); connect(listener, &ChatMessageListener::participantImdnStateChanged, this, &ChatMessageModel::onParticipantImdnStateChanged); connect(listener, &ChatMessageListener::ephemeralMessageTimerStarted, this, &ChatMessageModel::onEphemeralMessageTimerStarted); connect(listener, &ChatMessageListener::ephemeralMessageDeleted, this, &ChatMessageModel::onEphemeralMessageDeleted); connect(listener, &ChatMessageListener::participantImdnStateChanged, this->getParticipantImdnStates().get(), &ParticipantImdnStateListModel::onParticipantImdnStateChanged); connect(listener, &ChatMessageListener::reactionRemoved, this, &ChatMessageModel::onReactionRemoved); } // ============================================================================= ChatMessageModel::ChatMessageModel(const std::shared_ptr &chatMessage, const std::shared_ptr &chatMessageLog, QObject *parent) : ChatEvent(ChatRoomModel::EntryType::MessageEntry, parent) { App::getInstance()->getEngine()->setObjectOwnership(this, QQmlEngine::CppOwnership); // Avoid QML to destroy it init(chatMessage, chatMessageLog); } ChatMessageModel::~ChatMessageModel() { if (mChatMessage) mChatMessage->removeListener(mChatMessageListener); } void ChatMessageModel::init(const std::shared_ptr &chatMessage, const std::shared_ptr &chatMessageLog) { if (chatMessage) { mParticipantImdnStateListModel = QSharedPointer::create(chatMessage); mChatMessageListener = std::make_shared(); connectTo(mChatMessageListener.get()); mChatMessage = chatMessage; mChatMessage->addListener(mChatMessageListener); if (mChatMessage->isReply()) { auto replyMessage = mChatMessage->getReplyMessage(); if (replyMessage) // Reply message could be inexistant (for example : when locally deleted) mReplyChatMessageModel = create(replyMessage, this->parent()); } std::list> contents = chatMessage->getContents(); QString txt; for (auto content : contents) { if (content->isText()) txt += content->getUtf8Text().c_str(); } mContent = txt; mTimestamp = QDateTime::fromMSecsSinceEpoch(chatMessage->getTime() * 1000); if (chatMessageLog) mReceivedTimestamp = QDateTime::fromMSecsSinceEpoch(chatMessageLog->getCreationTime() * 1000); else mReceivedTimestamp = mTimestamp; } mWasDownloaded = false; mContentListModel = QSharedPointer::create(this); mChatReactionListModel = QSharedPointer::create(this); } QSharedPointer ChatMessageModel::create(const std::shared_ptr &chatMessageLog, QObject *parent) { auto model = QSharedPointer::create(chatMessageLog->getChatMessage(), chatMessageLog, parent); return model; } QSharedPointer ChatMessageModel::create(const std::shared_ptr &chatMessage, QObject *parent) { auto model = QSharedPointer::create(chatMessage, nullptr, parent); return model; } std::shared_ptr ChatMessageModel::getChatMessage() { return mChatMessage; } QSharedPointer ChatMessageModel::getContentModel(std::shared_ptr content) { return mContentListModel->getContentModel(content); } //----------------------------------------------------------------------------------------------------------------------- QString ChatMessageModel::getFromDisplayName() { if (!mFromDisplayNameCache.isEmpty()) return mFromDisplayNameCache; if (!mChatMessage) return ""; mFromDisplayNameCache = Utils::getDisplayName(mChatMessage->getFromAddress()); return mFromDisplayNameCache; } QString ChatMessageModel::getFromDisplayNameReplyMessage() { if (isReply()) { if (!fromDisplayNameReplyMessage.isEmpty()) return fromDisplayNameReplyMessage; if (!mChatMessage) return ""; fromDisplayNameReplyMessage = Utils::getDisplayName(mChatMessage->getReplyMessageSenderAddress()); return fromDisplayNameReplyMessage; } else return ""; } QString ChatMessageModel::getFromSipAddress() const { return mChatMessage ? Utils::cleanSipAddress(Utils::coreStringToAppString(mChatMessage->getFromAddress()->asStringUriOnly())) : ""; } QString ChatMessageModel::getToDisplayName() const { return mChatMessage ? Utils::getDisplayName(mChatMessage->getToAddress()) : ""; } QString ChatMessageModel::getToSipAddress() const { return mChatMessage ? Utils::cleanSipAddress(Utils::coreStringToAppString(mChatMessage->getToAddress()->asStringUriOnly())) : ""; } QString ChatMessageModel::getMyReaction() const { if (!mChatMessage) return ""; auto myReaction = mChatMessage->getOwnReaction(); return myReaction ? Utils::coreStringToAppString(myReaction->getBody()) : ""; } ContactModel *ChatMessageModel::getContactModel() const { return mChatMessage ? CoreManager::getInstance() ->getContactsListModel() ->findContactModelFromSipAddress(Utils::cleanSipAddress( Utils::coreStringToAppString(mChatMessage->getFromAddress()->asStringUriOnly()))) .get() : nullptr; } bool ChatMessageModel::isEphemeral() const { return mChatMessage && mChatMessage->isEphemeral(); } qint64 ChatMessageModel::getEphemeralExpireTime() const { time_t t = mChatMessage ? mChatMessage->getEphemeralExpireTime() : 0; return t > 0 ? t - QDateTime::currentSecsSinceEpoch() : 0; } long ChatMessageModel::getEphemeralLifetime() const { return mChatMessage ? mChatMessage->getEphemeralLifetime() : 0; } LinphoneEnums::ChatMessageState ChatMessageModel::getState() const { return mChatMessage ? LinphoneEnums::fromLinphone(mChatMessage->getState()) : LinphoneEnums::ChatMessageStateIdle; } bool ChatMessageModel::isOutgoing() const { return mChatMessage && mChatMessage->isOutgoing(); } ParticipantImdnStateProxyModel *ChatMessageModel::getProxyImdnStates() { ParticipantImdnStateProxyModel *proxy = new ParticipantImdnStateProxyModel(); proxy->setChatMessageModel(this); return proxy; } QSharedPointer ChatMessageModel::getParticipantImdnStates() const { return mParticipantImdnStateListModel; } QSharedPointer ChatMessageModel::getContents() const { return mContentListModel; } QSharedPointer ChatMessageModel::getChatReactions() const { return mChatReactionListModel; } bool ChatMessageModel::isReply() const { return mChatMessage && mChatMessage->isReply(); } ChatMessageModel *ChatMessageModel::getReplyChatMessageModel() const { return mReplyChatMessageModel.get(); } bool ChatMessageModel::isForward() const { return mChatMessage && mChatMessage->isForward(); } QString ChatMessageModel::getForwardInfo() const { return mChatMessage ? Utils::coreStringToAppString(mChatMessage->getForwardInfo()) : ""; } QString ChatMessageModel::getForwardInfoDisplayName() const { QString forwardInfo = getForwardInfo(); auto forwardAddress = Utils::interpretUrl(forwardInfo); if (!forwardAddress || Utils::isMe(forwardAddress)) return ""; // myself else return Utils::getDisplayName(forwardInfo); } //----------------------------------------------------------------------------------------------------------------------- void ChatMessageModel::setWasDownloaded(bool wasDownloaded) { if (mWasDownloaded != wasDownloaded) { mWasDownloaded = wasDownloaded; emit wasDownloadedChanged(); } } void ChatMessageModel::setTimestamp(const QDateTime ×tamp) { mTimestamp = timestamp; } void ChatMessageModel::setReceivedTimestamp(const QDateTime ×tamp) { mReceivedTimestamp = timestamp; } //----------------------------------------------------------------------------------------------------------------------- void ChatMessageModel::resendMessage() { switch (getState()) { case LinphoneEnums::ChatMessageStateFileTransferError: case LinphoneEnums::ChatMessageStateNotDelivered: { mChatMessage->send(); emit stateChanged(); break; } default: qWarning() << QStringLiteral("Unable to resend message: %1. Bad state.").arg(getState()); } } void ChatMessageModel::sendChatReaction(const QString &reaction) { auto myReaction = mChatMessage->getOwnReaction(); if (myReaction && Utils::coreStringToAppString(myReaction->getBody()) == reaction) { auto chatReaction = mChatMessage->createReaction(""); chatReaction->send(); // emit reactionRemoved(mChatMessage, chatReaction->getFromAddress()); // Do not emit because we want to // display what the server got } else { auto chatReaction = mChatMessage->createReaction(Utils::appStringToCoreString(reaction)); chatReaction->send(); // emit newMessageReaction(mChatMessage, chatReaction);// Do not emit because we want to display what the server // got } } void ChatMessageModel::deleteEvent() { if (mChatMessage && mChatMessage->getFileTransferInformation()) { mChatMessage->cancelFileTransfer(); } if (mChatMessage) mChatMessage->getChatRoom()->deleteMessage(mChatMessage); } void ChatMessageModel::updateFileTransferInformation() { mContentListModel->updateContents(this); } void ChatMessageModel::onFileTransferRecv(const std::shared_ptr &message, const std::shared_ptr &content, const std::shared_ptr &buffer) { } void ChatMessageModel::onFileTransferSendChunk(const std::shared_ptr &message, const std::shared_ptr &content, size_t offset, size_t size, const std::shared_ptr &buffer) { } std::shared_ptr ChatMessageModel::onFileTransferSend( const std::shared_ptr &, const std::shared_ptr &content, size_t, size_t) { return nullptr; } void ChatMessageModel::onFileTransferProgressIndication(const std::shared_ptr &message, const std::shared_ptr &content, size_t offset, size_t total) { auto contentModel = mContentListModel->getContentModel(content); if (contentModel) { contentModel->setFileOffset(offset); if (total == offset && mChatMessage && !mChatMessage->isOutgoing()) { mContentListModel->downloaded(); bool allAreDownloaded = true; for (auto content : mContentListModel->getSharedList()) allAreDownloaded &= content->mWasDownloaded; setWasDownloaded(allAreDownloaded); QTimer::singleShot(60, App::getInstance(), [message, content]() { // on 100% downlaoded, the SDK still need to do stuff on file. It // still need permission on file. Let the application to use the // file after next iteration. App::getInstance()->getNotifier()->notifyReceivedFileMessage(message, content); }); } } } void ChatMessageModel::onMsgStateChanged(const std::shared_ptr &message, linphone::ChatMessage::State state) { updateFileTransferInformation(); // On message state, file transfert information Content can be changed if (state == linphone::ChatMessage::State::FileTransferDone) { mContentListModel->updateContents(this); // Avoid having leak contents if (!mWasDownloaded) { // Update states bool allAreDownloaded = true; for (auto content : mContentListModel->getSharedList()) allAreDownloaded &= content->mWasDownloaded; setWasDownloaded(allAreDownloaded); } } emit stateChanged(); } void ChatMessageModel::onNewMessageReaction(const std::shared_ptr &message, const std::shared_ptr &reaction) { if (reaction->getFromAddress()->weakEqual(message->getLocalAddress())) emit myReactionChanged(); emit newMessageReaction(message, reaction); } void ChatMessageModel::onParticipantImdnStateChanged( const std::shared_ptr &message, const std::shared_ptr &state) { } void ChatMessageModel::onEphemeralMessageTimerStarted(const std::shared_ptr &message) { emit ephemeralExpireTimeChanged(); } void ChatMessageModel::onEphemeralMessageDeleted(const std::shared_ptr &message) { // emit remove(mSelf.lock()); if (!isOutgoing()) mContentListModel->removeDownloadedFiles(); emit remove(this); } void ChatMessageModel::onReactionRemoved(const std::shared_ptr &message, const std::shared_ptr &address) { if (address->weakEqual(message->getLocalAddress())) emit myReactionChanged(); emit reactionRemoved(message, address); } //-------------------------------------------------------------------------------------------------------