diff --git a/src/api/call.h b/src/api/call.h index 9cafe66def2916beca0bf34d072b716b28786745..496fad1ab56164ae4d3686e97659143d79760fd3 100644 --- a/src/api/call.h +++ b/src/api/call.h @@ -131,6 +131,7 @@ struct Info Status status = Status::INVALID; Type type = Type::INVALID; std::string peer; + bool isOutoging; bool audioMuted = false; bool videoMuted = false; }; diff --git a/src/api/contactmodel.h b/src/api/contactmodel.h index 9ef26dd67851f2561b9b26740cd2423f4f3ffed0..838e14151ea595f5c89a78e15a5f97e095effdf5 100644 --- a/src/api/contactmodel.h +++ b/src/api/contactmodel.h @@ -113,6 +113,11 @@ Q_SIGNALS: * @param contactUri */ void contactAdded(const std::string& contactUri) const; + /** + * Connect this signal to know when a pending contact was accepted. + * @param contactUri + */ + void pendingContactAccepted(const std::string& contactUri) const; /** * Connect this signal to know when an account was removed. * @param contactUri diff --git a/src/authority/databasehelper.cpp b/src/authority/databasehelper.cpp index 8be410d6d21a288fb0e2be92b9eae8f849465c58..7f5da0d629459f0649b82627e38aa70877d2bc79 100644 --- a/src/authority/databasehelper.cpp +++ b/src/authority/databasehelper.cpp @@ -143,7 +143,7 @@ getConversationsBetween(Database& db, const std::string& accountProfile, const s } std::string -beginConversationsBetween(Database& db, const std::string& accountProfile, const std::string& contactProfile) +beginConversationsBetween(Database& db, const std::string& accountProfile, const std::string& contactProfile, const std::string& firstMessage) { // Add conversation between account and profile auto newConversationsId = db.select("IFNULL(MAX(id), 0) + 1", @@ -156,17 +156,18 @@ beginConversationsBetween(Database& db, const std::string& accountProfile, const db.insertInto("conversations", {{":id", "id"}, {":participant_id", "participant_id"}}, {{":id", newConversationsId}, {":participant_id", contactProfile}}); - // Add "Conversation started" interaction - db.insertInto("interactions", - {{":account_id", "account_id"}, {":author_id", "author_id"}, - {":conversation_id", "conversation_id"}, {":timestamp", "timestamp"}, - {":body", "body"}, {":type", "type"}, - {":status", "status"}}, - {{":account_id", accountProfile}, {":author_id", accountProfile}, - {":conversation_id", newConversationsId}, - {":timestamp", std::to_string(std::time(nullptr))}, - {":body", "Conversation started"}, {":type", "CONTACT"}, - {":status", "SUCCEED"}}); + // Add first interaction + if (!firstMessage.empty()) + db.insertInto("interactions", + {{":account_id", "account_id"}, {":author_id", "author_id"}, + {":conversation_id", "conversation_id"}, {":timestamp", "timestamp"}, + {":body", "body"}, {":type", "type"}, + {":status", "status"}}, + {{":account_id", accountProfile}, {":author_id", accountProfile}, + {":conversation_id", newConversationsId}, + {":timestamp", std::to_string(std::time(nullptr))}, + {":body", firstMessage}, {":type", "CONTACT"}, + {":status", "SUCCEED"}}); return newConversationsId; } @@ -208,6 +209,41 @@ addMessageToConversation(Database& db, {":status", to_string(msg.status)}}); } +int +addOrUpdateMessage(Database& db, + const std::string& accountProfile, + const std::string& conversationId, + const api::interaction::Info& msg, + const std::string& daemonId) +{ + // Check if profile is already present. + auto msgAlreadyExists = db.select("id", + "interactions", + "daemon_id=:daemon_id", + {{":daemon_id", daemonId}}); + if (msgAlreadyExists.payloads.empty()) { + return db.insertInto("interactions", + {{":account_id", "account_id"}, {":author_id", "author_id"}, + {":conversation_id", "conversation_id"}, {":timestamp", "timestamp"}, + {":body", "body"}, {":type", "type"}, {":daemon_id", "daemon_id"}, + {":status", "status"}}, + {{":account_id", accountProfile}, {":author_id", msg.authorUri}, + {":conversation_id", conversationId}, + {":timestamp", std::to_string(msg.timestamp)}, + {":body", msg.body}, {":type", to_string(msg.type)}, {":daemon_id", daemonId}, + {":status", to_string(msg.status)}}); + } else { + // already exists + db.update("interactions", + "body=:body", + {{":body", msg.body}}, + "daemon_id=:daemon_id", + {{":daemon_id", daemonId}}); + return std::stoi(msgAlreadyExists.payloads[0]); + } + +} + void addDaemonMsgId(Database& db, const std::string& interactionId, const std::string& daemonId) @@ -241,7 +277,7 @@ void updateInteractionStatus(Database& db, unsigned int id, void clearHistory(Database& db, const std::string& conversationId) { - db.deleteFrom("interactions", "conversation_id=:id AND type!='CONTACT'", {{":id", conversationId}}); + db.deleteFrom("interactions", "conversation_id=:id", {{":id", conversationId}}); } void diff --git a/src/authority/databasehelper.h b/src/authority/databasehelper.h index 11999fa39663b24ee83ebfc23ab1b732a6fab6f2..34d61250077ac9a4ec4d92246b6ef1cd5d608ac0 100644 --- a/src/authority/databasehelper.h +++ b/src/authority/databasehelper.h @@ -103,11 +103,13 @@ std::vector<std::string> getConversationsBetween(Database& db, * @param db * @param accountProfile the id of the account in the database * @param contactProfile the id of the contact in the database + * @param firstMessage the body of the first message * @return conversation_id of the new conversation. */ std::string beginConversationsBetween(Database& db, const std::string& accountProfile, - const std::string& contactProfile); + const std::string& contactProfile, + const std::string& firstMessage = ""); /** * Return interactions from a conversation @@ -129,6 +131,21 @@ int addMessageToConversation(Database& db, const std::string& conversationId, const api::interaction::Info& msg); +/** +* Add or update an entry into interactions linked to a conversation. +* @param db +* @param accountProfile +* @param conversationId +* @param msg +* @param daemonId +* @return the id of the inserted interaction +*/ +int addOrUpdateMessage(Database& db, + const std::string& accountProfile, + const std::string& conversationId, + const api::interaction::Info& msg, + const std::string& daemonId); + /** * Change the daemon_id column for an interaction * @param db diff --git a/src/contactmodel.cpp b/src/contactmodel.cpp index 7ab5ffc79f159285bfc72b1bba772e3ddb53dfa3..c9230aaedba5fa853b5a33bb1cbac23e161085e9 100644 --- a/src/contactmodel.cpp +++ b/src/contactmodel.cpp @@ -200,6 +200,7 @@ ContactModel::addContact(contact::Info contactInfo) break; case profile::Type::PENDING: daemon::addContactFromPending(owner, profile.uri); + emit pendingContactAccepted(profile.uri); daemon::addContact(owner, profile.uri); // BUGS?: daemon::addContactFromPending not always add the contact break; case profile::Type::RING: diff --git a/src/conversationmodel.cpp b/src/conversationmodel.cpp index 18b17d97a4a4005889c5ba7a8368a10b572f832f..9b16f87a9d3c45916e8865eec803073221da2ab6 100644 --- a/src/conversationmodel.cpp +++ b/src/conversationmodel.cpp @@ -98,7 +98,7 @@ public: * @param callId * @param body */ - void addCallMessage(const std::string& callId, const std::string& body); + void addOrUpdateCallMessage(const std::string& callId, const std::string& body); /** * Add a new message from a peer in the database * @param from the peer uri @@ -136,10 +136,15 @@ public Q_SLOTS: */ void slotContactModelUpdated(); /** - * Listen from contactModel when aa new contact is added + * Listen from contactModel when a new contact is added * @param uri */ void slotContactAdded(const std::string& uri); + /** + * Listen from contactModel when a pending contact is accepted + * @param uri + */ + void slotPendingContactAccepted(const std::string& uri); /** * Listen from contactModel when aa new contact is removed * @param uri @@ -459,7 +464,7 @@ ConversationModel::sendMessage(const std::string& uid, const std::string& body) if (not conversation.callId.empty() and (owner.callModel->getCall(conversation.callId).status != call::Status::IN_PROGRESS - or owner.callModel->getCall(conversation.callId).status != call::Status::PAUSED)) { + or owner.callModel->getCall(conversation.callId).status != call::Status::PAUSED)) { owner.callModel->sendSipMessage(conversation.callId, body); @@ -605,6 +610,8 @@ ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked, this, &ConversationModelPimpl::slotContactModelUpdated); connect(&*linked.owner.contactModel, &ContactModel::contactAdded, this, &ConversationModelPimpl::slotContactAdded); + connect(&*linked.owner.contactModel, &ContactModel::pendingContactAccepted, + this, &ConversationModelPimpl::slotPendingContactAccepted); connect(&*linked.owner.contactModel, &ContactModel::contactRemoved, this, &ConversationModelPimpl::slotContactRemoved); @@ -695,11 +702,15 @@ ConversationModelPimpl::sortConversations() conversations.begin(), conversations.end(), [](const auto& conversationA, const auto& conversationB) { + // A or B is a temporary contact + if (conversationA.participants.empty()) return true; + if (conversationB.participants.empty()) return false; auto historyA = conversationA.interactions; auto historyB = conversationB.interactions; // A or B is a new conversation (without CONTACT interaction) - if (historyA.empty()) return true; - if (historyB.empty()) return false; + if (conversationA.uid.empty() || conversationB.uid.empty()) return conversationA.uid.empty(); + if (historyA.empty()) return false; + if (historyB.empty()) return true; // Sort by last Interaction try { @@ -710,7 +721,7 @@ ConversationModelPimpl::sortConversations() catch (const std::exception& e) { qDebug() << "ConversationModel::sortConversations(), can't get lastMessage"; - return true; + return false; } }); dirtyConversations = true; @@ -731,7 +742,18 @@ ConversationModelPimpl::slotContactAdded(const std::string& uri) auto contactProfileId = database::getOrInsertProfile(db, uri); auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId); if (conv.empty()) { - conv.emplace_back(database::beginConversationsBetween(db, accountProfileId, contactProfileId)); + std::string interaction = ""; + try { + auto contact = linked.owner.contactModel->getContact(uri); + interaction = contact.profileInfo.type == profile::Type::PENDING ? + QObject::tr("Invitation received").toStdString() : + QObject::tr("Contact added").toStdString(); + } catch (...) {} + conv.emplace_back( + database::beginConversationsBetween(db, accountProfileId, + contactProfileId, interaction + ) + ); } // Add the conversation if not already here if (indexOf(conv[0]) == -1) @@ -746,6 +768,36 @@ ConversationModelPimpl::slotContactAdded(const std::string& uri) emit linked.modelSorted(); } +void +ConversationModelPimpl::slotPendingContactAccepted(const std::string& uri) +{ + auto contactProfileId = database::getOrInsertProfile(db, uri); + auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId); + if (conv.empty()) { + conv.emplace_back( + database::beginConversationsBetween(db, accountProfileId, + contactProfileId, QObject::tr("Invitation accepted").toStdString() + ) + ); + } else { + try { + auto contact = linked.owner.contactModel->getContact(uri); + auto msg = interaction::Info {accountProfileId, + QObject::tr("Invitation accepted").toStdString(), + std::time(nullptr), interaction::Type::CONTACT, + interaction::Status::SUCCEED}; + auto msgId = database::addMessageToConversation(db, accountProfileId, conv[0], msg); + auto conversationIdx = indexOf(conv[0]); + conversations[conversationIdx].interactions.emplace(msgId, msg); + dirtyConversations = true; + emit linked.newUnreadMessage(conv[0], msgId, msg); + } catch (std::out_of_range& e) { + qDebug() << "ConversationModelPimpl::slotContactAdded can't find contact"; + } + } +} + + void ConversationModelPimpl::slotContactRemoved(const std::string& uri) { @@ -886,24 +938,49 @@ ConversationModelPimpl::slotCallStatusChanged(const std::string& callId) void ConversationModelPimpl::slotCallStarted(const std::string& callId) { - addCallMessage(callId, "📞 Call started"); + try { + auto call = linked.owner.callModel->getCall(callId); + if (call.isOutoging) + addOrUpdateCallMessage(callId, QObject::tr("📞 Outgoing call").toStdString()); + else + addOrUpdateCallMessage(callId, QObject::tr("📞 Incoming call").toStdString()); + } catch (std::out_of_range& e) { + qDebug() << "ConversationModelPimpl::slotCallEnded can't end inexistant call"; + } } void ConversationModelPimpl::slotCallEnded(const std::string& callId) { - addCallMessage(callId, "🕽 Call ended"); - - // reset the callId stored in the conversation - for (auto& conversation: conversations) - if (conversation.callId == callId) { - conversation.callId = ""; - dirtyConversations = true; + try { + auto call = linked.owner.callModel->getCall(callId); + if (call.startTime.time_since_epoch().count() != 0) { + if (call.isOutoging) + addOrUpdateCallMessage(callId, QObject::tr("📞 Outgoing call - ").toStdString() + + linked.owner.callModel->getFormattedCallDuration(callId)); + else + addOrUpdateCallMessage(callId, QObject::tr("📞 Incoming call - ").toStdString() + + linked.owner.callModel->getFormattedCallDuration(callId)); + } else { + if (call.isOutoging) + addOrUpdateCallMessage(callId, QObject::tr("🕽 Missed outgoing call").toStdString()); + else + addOrUpdateCallMessage(callId, QObject::tr("🕽 Missed incoming call").toStdString()); } + + // reset the callId stored in the conversation + for (auto& conversation: conversations) + if (conversation.callId == callId) { + conversation.callId = ""; + dirtyConversations = true; + } + } catch (std::out_of_range& e) { + qDebug() << "ConversationModelPimpl::slotCallEnded can't end inexistant call"; + } } void -ConversationModelPimpl::addCallMessage(const std::string& callId, const std::string& body) +ConversationModelPimpl::addOrUpdateCallMessage(const std::string& callId, const std::string& body) { // Get conversation for (auto& conversation: conversations) { @@ -911,11 +988,19 @@ ConversationModelPimpl::addCallMessage(const std::string& callId, const std::str auto uid = conversation.uid; auto msg = interaction::Info {accountProfileId, body, std::time(nullptr), interaction::Type::CALL, interaction::Status::SUCCEED}; - int msgId = database::addMessageToConversation(db, accountProfileId, conversation.uid, msg); - conversation.interactions.emplace(msgId, msg); - conversation.lastMessageUid = msgId; + int msgId = database::addOrUpdateMessage(db, accountProfileId, conversation.uid, msg, callId); + auto newInteraction = conversation.interactions.find(msgId) == conversation.interactions.end(); + if (newInteraction) { + conversation.lastMessageUid = msgId; + conversation.interactions.emplace(msgId, msg); + } else { + conversation.interactions[msgId] = msg; + } dirtyConversations = true; - emit linked.newUnreadMessage(conversation.uid, msgId, msg); + if (newInteraction) + emit linked.newUnreadMessage(conversation.uid, msgId, msg); + else + emit linked.interactionStatusUpdated(conversation.uid, msgId, msg); sortConversations(); emit linked.modelSorted(); } @@ -964,7 +1049,10 @@ ConversationModelPimpl::addIncomingMessage(const std::string& from, auto accountProfileId = database::getProfileId(db, linked.owner.profileInfo.uri); auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId); if (conv.empty()) { - conv.emplace_back(database::beginConversationsBetween(db, accountProfileId, contactProfileId)); + conv.emplace_back(database::beginConversationsBetween( + db, accountProfileId, contactProfileId, + QObject::tr("Invitation received").toStdString() + )); } auto authorId = authorProfileId.empty()? contactProfileId: authorProfileId; auto msg = interaction::Info {authorId, body, std::time(nullptr), diff --git a/src/database.cpp b/src/database.cpp index 32654565f820bffe9f6f112857623e27b8a9466e..321e5e8cc46b8e1c5ce2ad8790151e9bad0c153e 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -20,6 +20,7 @@ #include "database.h" // Qt +#include <QObject> #include <QtCore/QDir> #include <QtCore/QDebug> #include <QtCore/QFile> @@ -533,25 +534,10 @@ Database::migrateTextHistory() // Load interactions auto groupsArray = loadDoc["groups"].toArray(); - auto beginConversation = true; for (const auto& groupObject: groupsArray) { auto messagesArray = groupObject.toObject()["messages"].toArray(); for (const auto& messageRef: messagesArray) { auto messageObject = messageRef.toObject(); - if (beginConversation) { - // Add "Conversation started" message - insertInto("interactions", - {{":account_id", "account_id"}, {":author_id", "author_id"}, - {":conversation_id", "conversation_id"}, {":timestamp", "timestamp"}, - {":body", "body"}, {":type", "type"}, - {":status", "status"}}, - {{":account_id", accountId}, {":author_id", accountId}, - {":conversation_id", newConversationsId}, - {":timestamp", std::to_string(messageObject["timestamp"].toInt())}, - {":body", "Conversation started"}, {":type", "CONTACT"}, - {":status", "SUCCEED"}}); - beginConversation = false; - } auto direction = messageObject["direction"].toInt(); auto body = messageObject["payloads"].toArray()[0].toObject()["payload"].toString(); insertInto("interactions", diff --git a/src/newcallmodel.cpp b/src/newcallmodel.cpp index 3d1a0fb235d35d02f713698eafa92d86115c31f1..506b3191593827a2789f596f94d7ce79093bb0fc 100644 --- a/src/newcallmodel.cpp +++ b/src/newcallmodel.cpp @@ -138,6 +138,7 @@ NewCallModel::createCall(const std::string& url) auto callInfo = std::make_shared<call::Info>(); callInfo->id = callId.toStdString(); callInfo->peer = url; + callInfo->isOutoging = true; callInfo->status = call::Status::SEARCHING; callInfo->type = call::Type::DIALOG; pimpl_->calls.emplace(callId.toStdString(), std::move(callInfo)); @@ -231,18 +232,23 @@ NewCallModel::togglePause(const std::string& callId) const void NewCallModel::toggleMedia(const std::string& callId, const NewCallModel::Media media, bool flag) const { + auto it = pimpl_->calls.find(callId); + if (it == pimpl_->calls.end()) return; + auto& call = it->second; switch(media) { case NewCallModel::Media::AUDIO: CallManager::instance().muteLocalMedia(callId.c_str(), DRing::Media::Details::MEDIA_TYPE_AUDIO, flag); + call->audioMuted = flag; break; case NewCallModel::Media::VIDEO: CallManager::instance().muteLocalMedia(callId.c_str(), DRing::Media::Details::MEDIA_TYPE_VIDEO, flag); + call->videoMuted = flag; break; case NewCallModel::Media::NONE: @@ -359,6 +365,7 @@ NewCallModelPimpl::slotIncomingCall(const std::string& accountId, const std::str auto callInfo = std::make_shared<call::Info>(); callInfo->id = callId; callInfo->peer = fromId; + callInfo->isOutoging = false; callInfo->status = call::Status::INCOMING_RINGING; callInfo->type = call::Type::DIALOG; calls.emplace(callId, std::move(callInfo)); @@ -378,9 +385,7 @@ NewCallModelPimpl::slotCallStateChanged(const std::string& callId, const std::st } else if (state == "HUNGUP") { calls[callId]->status = call::Status::TERMINATING; } else if (state == "FAILURE" || state == "OVER") { - if (calls[callId]->startTime.time_since_epoch().count() != 0) { - emit linked.callEnded(callId); - } + emit linked.callEnded(callId); calls[callId]->status = call::Status::ENDED; } else if (state == "INACTIVE") { calls[callId]->status = call::Status::INACTIVE; diff --git a/test/conversationmodeltester.cpp b/test/conversationmodeltester.cpp index 0baec70c47fadb6a5c4e7fdee39fb06db9ac6283..78356dfeaf620b1e17e48eda247257811e7b3857 100644 --- a/test/conversationmodeltester.cpp +++ b/test/conversationmodeltester.cpp @@ -170,8 +170,8 @@ ConversationModelTester::testSendMessageAndClearHistory() for (const auto& conversation: conversations) { if (conversation.uid == firstConversation) { conversationExists = true; - // Should contains "Conversation started" + "Hello World!" - CPPUNIT_ASSERT_EQUAL((int)conversation.interactions.size(), 2); + // Should contains "Hello World!" + CPPUNIT_ASSERT_EQUAL((int)conversation.interactions.size(), 1); CPPUNIT_ASSERT_EQUAL((*conversation.interactions.rbegin()).second.body, std::string("Hello World!")); break; } @@ -187,8 +187,7 @@ ConversationModelTester::testSendMessageAndClearHistory() for (const auto& conversation: conversations) { if (conversation.uid == firstConversation) { conversationExists = true; - // contains the "Conversation started" message. - CPPUNIT_ASSERT_EQUAL((int)conversation.interactions.size(), 1); + CPPUNIT_ASSERT_EQUAL((int)conversation.interactions.size(), 0); break; } } @@ -205,7 +204,7 @@ ConversationModelTester::testReceiveMessageAndSetRead() QMap<QString, QString> payloads; payloads["text/plain"] = "This is not a message"; ConfigurationManager::instance().emitIncomingAccountMessage(accInfo_.id.c_str(), - firstConversation.participants.front().c_str(), payloads); + firstConversation.participants.front().c_str(), payloads); auto unreadMessage = WaitForSignalHelper(*accInfo_.conversationModel, SIGNAL(newUnreadMessage(const std::string&, uint64_t, const interaction::Info&))).wait(1000); CPPUNIT_ASSERT_EQUAL(unreadMessage, true);