Add feature to start new conversation with existing contacts

## Summary

Start a conversation with contacts having no previous conversation with.

It is currently only possible to use the messaging app to send a message to a conversation which already exists.

This patch implements this feature by integrating all contacts having no prior conversation with the recent conversations in the recent conversation list and at the bottom in a sorted manner, something like this,

The contacts are stored in the recent conversation list model as a conversation but with INVALID conversation ID and INVALID conversation DATE.

## Testing

Testing just needs kdeconnect daemon to be recompiled and executed.
This commit is contained in:
Aniket Kumar 2020-03-21 22:57:28 +00:00 committed by Simon Redman
parent 2a2481fb6a
commit f211d7e401
10 changed files with 183 additions and 19 deletions

View file

@ -190,7 +190,6 @@ void ConversationsDbusInterface::replyToConversation(const qint64& conversationI
{ {
const auto messagesList = m_conversations[conversationID]; const auto messagesList = m_conversations[conversationID];
if (messagesList.isEmpty()) { if (messagesList.isEmpty()) {
// Since there are no messages in the conversation, we can't do anything sensible
qCWarning(KDECONNECT_CONVERSATIONS) << "Got a conversationID for a conversation with no messages!"; qCWarning(KDECONNECT_CONVERSATIONS) << "Got a conversationID for a conversation with no messages!";
return; return;
} }
@ -209,6 +208,10 @@ void ConversationsDbusInterface::replyToConversation(const qint64& conversationI
m_smsInterface.sendSms(addresses[0].address(), message); m_smsInterface.sendSms(addresses[0].address(), message);
} }
void ConversationsDbusInterface::sendWithoutConversation(const QString& address, const QString& message) {
m_smsInterface.sendSms(address, message);
}
void ConversationsDbusInterface::requestAllConversationThreads() void ConversationsDbusInterface::requestAllConversationThreads()
{ {
// Prepare the list of conversations by requesting the first in every thread // Prepare the list of conversations by requesting the first in every thread

View file

@ -87,6 +87,11 @@ public Q_SLOTS:
*/ */
void replyToConversation(const qint64& conversationID, const QString& message); void replyToConversation(const qint64& conversationID, const QString& message);
/**
* Send a new message to the contact having no previous coversation with
*/
void sendWithoutConversation(const QString& address, const QString& message);
/** /**
* Send the request to the Telephony plugin to update the list of conversation threads * Send the request to the Telephony plugin to update the list of conversation threads
*/ */

View file

@ -33,9 +33,44 @@
Q_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL, "kdeconnect.sms.conversations_list") Q_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL, "kdeconnect.sms.conversations_list")
OurSortFilterProxyModel::OurSortFilterProxyModel(){} #define INVALID_THREAD_ID -1
#define INVALID_DATE -1
OurSortFilterProxyModel::OurSortFilterProxyModel()
{
setFilterRole(ConversationListModel::DateRole);
}
OurSortFilterProxyModel::~OurSortFilterProxyModel(){} OurSortFilterProxyModel::~OurSortFilterProxyModel(){}
void OurSortFilterProxyModel::setOurFilterRole(int role)
{
setFilterRole(role);
}
bool OurSortFilterProxyModel::lessThan(const QModelIndex& leftIndex, const QModelIndex& rightIndex) const
{
QVariant leftDataTimeStamp = sourceModel()->data(leftIndex, ConversationListModel::DateRole);
QVariant rightDataTimeStamp = sourceModel()->data(rightIndex, ConversationListModel::DateRole);
if (leftDataTimeStamp == rightDataTimeStamp) {
QVariant leftDataName = sourceModel()->data(leftIndex, Qt::DisplayRole);
QVariant rightDataName = sourceModel()->data(rightIndex, Qt::DisplayRole);
return leftDataName.toString().toLower() > rightDataName.toString().toLower();
}
return leftDataTimeStamp < rightDataTimeStamp;
}
bool OurSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
if (filterRole() == Qt::DisplayRole) {
return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
}
return sourceModel()->data(index, ConversationListModel::DateRole) != INVALID_THREAD_ID;
}
ConversationListModel::ConversationListModel(QObject* parent) ConversationListModel::ConversationListModel(QObject* parent)
: QStandardItemModel(parent) : QStandardItemModel(parent)
, m_conversationsInterface(nullptr) , m_conversationsInterface(nullptr)
@ -120,6 +155,7 @@ void ConversationListModel::prepareConversationsList()
data >> message; data >> message;
createRowFromMessage(message); createRowFromMessage(message);
} }
displayContacts();
}, this); }, this);
} }
@ -150,6 +186,18 @@ QStandardItem * ConversationListModel::conversationForThreadId(qint32 threadId)
return nullptr; return nullptr;
} }
QStandardItem * ConversationListModel::getConversationForAddress(const QString& address) {
for(int i = 0; i < rowCount(); ++i) {
const auto& it = item(i, 0);
if (!it->data(MultitargetRole).toBool()) {
if (SmsHelper::isPhoneNumberMatch(it->data(SenderRole).toString(), address)) {
return it;
}
}
}
return nullptr;
}
void ConversationListModel::createRowFromMessage(const ConversationMessage& message) void ConversationListModel::createRowFromMessage(const ConversationMessage& message)
{ {
if (message.type() == -1) { if (message.type() == -1) {
@ -158,19 +206,28 @@ void ConversationListModel::createRowFromMessage(const ConversationMessage& mess
return; return;
} }
/** The address of everyone involved in this conversation, which we should not display (check if they are known contacts first) */
QList<ConversationAddress> rawAddresses = message.addresses();
if (rawAddresses.isEmpty()) {
qWarning() << "no addresses!" << message.body();
return;
}
bool toadd = false; bool toadd = false;
QStandardItem* item = conversationForThreadId(message.threadID()); QStandardItem* item = conversationForThreadId(message.threadID());
//Check if we have a contact with which to associate this message, needed if there is no conversation with the contact and we received a message from them
if (!item && !message.isMultitarget()) {
item = getConversationForAddress(rawAddresses[0].address());
if (item) {
item->setData(message.threadID(), ConversationIdRole);
}
}
if (!item) { if (!item) {
toadd = true; toadd = true;
item = new QStandardItem(); item = new QStandardItem();
/** The address of everyone involved in this conversation, which we should not display (check if they are known contacts first) */
QList<ConversationAddress> rawAddresses = message.addresses();
if (rawAddresses.isEmpty()) {
qWarning() << "no addresses!" << message.body();
return;
}
QString displayNames = SmsHelper::getTitleForAddresses(rawAddresses); QString displayNames = SmsHelper::getTitleForAddresses(rawAddresses);
QIcon displayIcon = SmsHelper::getIconForAddresses(rawAddresses); QIcon displayIcon = SmsHelper::getIconForAddresses(rawAddresses);
@ -212,3 +269,32 @@ void ConversationListModel::createRowFromMessage(const ConversationMessage& mess
if (toadd) if (toadd)
appendRow(item); appendRow(item);
} }
void ConversationListModel::displayContacts() {
const QList<QSharedPointer<KPeople::PersonData>> personDataList = SmsHelper::getAllPersons();
for(const auto& person : personDataList) {
const QVariantList allPhoneNumbers = person->contactCustomProperty(QStringLiteral("all-phoneNumber")).toList();
for (const QVariant& rawPhoneNumber : allPhoneNumbers) {
//check for any duplicate phoneNumber and eliminate it
if (!getConversationForAddress(rawPhoneNumber.toString())) {
QStandardItem* item = new QStandardItem();
item->setText(person->name());
item->setIcon(person->photo());
QList<ConversationAddress> addresses;
addresses.append(ConversationAddress(rawPhoneNumber.toString()));
item->setData(QVariant::fromValue(addresses), AddressesRole);
QString displayBody = i18n("%1", rawPhoneNumber.toString());
item->setData(displayBody, Qt::ToolTipRole);
item->setData(false, MultitargetRole);
item->setData(qint64(INVALID_THREAD_ID), ConversationIdRole);
item->setData(qint64(INVALID_DATE), DateRole);
item->setData(rawPhoneNumber.toString(), SenderRole);
appendRow(item);
}
}
}
}

View file

@ -52,9 +52,15 @@ public:
sortNow(); sortNow();
} }
Q_INVOKABLE void setOurFilterRole(int role);
OurSortFilterProxyModel(); OurSortFilterProxyModel();
~OurSortFilterProxyModel(); ~OurSortFilterProxyModel();
protected:
bool lessThan(const QModelIndex& leftIndex, const QModelIndex& rightIndex) const override;
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
private: private:
void sortNow() { void sortNow() {
if (m_completed && dynamicSortFilter()) if (m_completed && dynamicSortFilter())
@ -97,6 +103,7 @@ public Q_SLOTS:
void handleConversationUpdated(const QDBusVariant& msg); void handleConversationUpdated(const QDBusVariant& msg);
void createRowFromMessage(const ConversationMessage& message); void createRowFromMessage(const ConversationMessage& message);
void printDBusError(const QDBusError& error); void printDBusError(const QDBusError& error);
void displayContacts();
Q_SIGNALS: Q_SIGNALS:
void deviceIdChanged(); void deviceIdChanged();
@ -108,6 +115,7 @@ private:
void prepareConversationsList(); void prepareConversationsList();
QStandardItem* conversationForThreadId(qint32 threadId); QStandardItem* conversationForThreadId(qint32 threadId);
QStandardItem* getConversationForAddress(const QString& address);
DeviceConversationsDbusInterface* m_conversationsInterface; DeviceConversationsDbusInterface* m_conversationsInterface;
QString m_deviceId; QString m_deviceId;

View file

@ -75,6 +75,7 @@ void ConversationModel::setDeviceId(const QString& deviceId)
if (m_conversationsInterface) { if (m_conversationsInterface) {
disconnect(m_conversationsInterface, SIGNAL(conversationUpdated(QDBusVariant)), this, SLOT(handleConversationUpdate(QDBusVariant))); disconnect(m_conversationsInterface, SIGNAL(conversationUpdated(QDBusVariant)), this, SLOT(handleConversationUpdate(QDBusVariant)));
disconnect(m_conversationsInterface, SIGNAL(conversationLoaded(qint64, quint64)), this, SLOT(handleConversationLoaded(qint64, quint64))); disconnect(m_conversationsInterface, SIGNAL(conversationLoaded(qint64, quint64)), this, SLOT(handleConversationLoaded(qint64, quint64)));
disconnect(m_conversationsInterface, SIGNAL(conversationCreated(QDBusVariant)), this, SLOT(handleConversationCreated(QDBusVariant)));
delete m_conversationsInterface; delete m_conversationsInterface;
} }
@ -83,6 +84,11 @@ void ConversationModel::setDeviceId(const QString& deviceId)
m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this); m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this);
connect(m_conversationsInterface, SIGNAL(conversationUpdated(QDBusVariant)), this, SLOT(handleConversationUpdate(QDBusVariant))); connect(m_conversationsInterface, SIGNAL(conversationUpdated(QDBusVariant)), this, SLOT(handleConversationUpdate(QDBusVariant)));
connect(m_conversationsInterface, SIGNAL(conversationLoaded(qint64, quint64)), this, SLOT(handleConversationLoaded(qint64, quint64))); connect(m_conversationsInterface, SIGNAL(conversationLoaded(qint64, quint64)), this, SLOT(handleConversationLoaded(qint64, quint64)));
connect(m_conversationsInterface, SIGNAL(conversationCreated(QDBusVariant)), this, SLOT(handleConversationCreated(QDBusVariant)));
}
void ConversationModel::setOtherPartyAddress(const QString& address) {
m_otherPartyAddress = address;
} }
void ConversationModel::sendReplyToConversation(const QString& message) void ConversationModel::sendReplyToConversation(const QString& message)
@ -91,6 +97,12 @@ void ConversationModel::sendReplyToConversation(const QString& message)
m_conversationsInterface->replyToConversation(m_threadId, message); m_conversationsInterface->replyToConversation(m_threadId, message);
} }
void ConversationModel::sendMessageWithoutConversation(const QString& message, const QString& address)
{
//qCDebug(KDECONNECT_SMS_CONVERSATION_MODEL) << "Trying to send" << message << "to contact address with no previous Conversation" << "and receiver's address" << address;
m_conversationsInterface->sendWithoutConversation(address, message);
}
void ConversationModel::requestMoreMessages(const quint32& howMany) void ConversationModel::requestMoreMessages(const quint32& howMany)
{ {
if (m_threadId == INVALID_THREAD_ID) { if (m_threadId == INVALID_THREAD_ID) {
@ -157,10 +169,19 @@ void ConversationModel::handleConversationUpdate(const QDBusVariant& msg)
<< "but we are currently viewing" << m_threadId; << "but we are currently viewing" << m_threadId;
return; return;
} }
createRowFromMessage(message, 0); createRowFromMessage(message, 0);
} }
void ConversationModel::handleConversationCreated(const QDBusVariant& msg)
{
ConversationMessage message = ConversationMessage::fromDBus(msg);
if (m_threadId == INVALID_THREAD_ID && SmsHelper::isPhoneNumberMatch(m_otherPartyAddress, message.addresses().first().address()) && !message.isMultitarget()) {
m_threadId = message.threadID();
createRowFromMessage(message, 0);
}
}
void ConversationModel::handleConversationLoaded(qint64 threadID, quint64 numMessages) void ConversationModel::handleConversationLoaded(qint64 threadID, quint64 numMessages)
{ {
Q_UNUSED(numMessages) Q_UNUSED(numMessages)

View file

@ -39,6 +39,7 @@ class ConversationModel
Q_OBJECT Q_OBJECT
Q_PROPERTY(qint64 threadId READ threadId WRITE setThreadId) Q_PROPERTY(qint64 threadId READ threadId WRITE setThreadId)
Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId) Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId)
Q_PROPERTY(QString otherParty READ otherPartyAddress WRITE setOtherPartyAddress)
public: public:
ConversationModel(QObject* parent = nullptr); ConversationModel(QObject* parent = nullptr);
@ -59,7 +60,11 @@ public:
QString deviceId() const { return m_deviceId; } QString deviceId() const { return m_deviceId; }
void setDeviceId(const QString &/*deviceId*/); void setDeviceId(const QString &/*deviceId*/);
QString otherPartyAddress() const { return m_otherPartyAddress; }
void setOtherPartyAddress(const QString& address);
Q_INVOKABLE void sendReplyToConversation(const QString& message); Q_INVOKABLE void sendReplyToConversation(const QString& message);
Q_INVOKABLE void sendMessageWithoutConversation(const QString& message, const QString& address);
Q_INVOKABLE void requestMoreMessages(const quint32& howMany = 10); Q_INVOKABLE void requestMoreMessages(const quint32& howMany = 10);
/** /**
* Convert a list of names into a single string suitable for display * Convert a list of names into a single string suitable for display
@ -80,6 +85,7 @@ Q_SIGNALS:
private Q_SLOTS: private Q_SLOTS:
void handleConversationUpdate(const QDBusVariant &message); void handleConversationUpdate(const QDBusVariant &message);
void handleConversationLoaded(qint64 threadID, quint64 numMessages); void handleConversationLoaded(qint64 threadID, quint64 numMessages);
void handleConversationCreated(const QDBusVariant &message);
private: private:
void createRowFromMessage(const ConversationMessage &message, int pos); void createRowFromMessage(const ConversationMessage &message, int pos);
@ -87,6 +93,7 @@ private:
DeviceConversationsDbusInterface* m_conversationsInterface; DeviceConversationsDbusInterface* m_conversationsInterface;
QString m_deviceId; QString m_deviceId;
qint64 m_threadId = INVALID_THREAD_ID; qint64 m_threadId = INVALID_THREAD_ID;
QString m_otherPartyAddress;
QSet<qint32> knownMessageIDs; // Set of known Message uIDs QSet<qint32> knownMessageIDs; // Set of known Message uIDs
}; };

View file

@ -38,12 +38,15 @@ Kirigami.ScrollablePage
property string conversationId property string conversationId
property bool isMultitarget property bool isMultitarget
property string initialMessage property string initialMessage
property string otherParty
property string invalidId: "-1"
property bool isInitalized: false property bool isInitalized: false
property var conversationModel: ConversationModel { property var conversationModel: ConversationModel {
deviceId: page.deviceId deviceId: page.deviceId
threadId: page.conversationId threadId: page.conversationId
otherParty: page.otherParty
onLoadingFinished: { onLoadingFinished: {
page.isInitalized = true page.isInitalized = true
@ -58,6 +61,9 @@ Kirigami.ScrollablePage
messageField.text = initialMessage; messageField.text = initialMessage;
initialMessage = "" initialMessage = ""
} }
if (conversationId == invalidId) {
isInitalized = true
}
} }
/** /**
@ -239,7 +245,11 @@ Kirigami.ScrollablePage
sendButton.enabled = false sendButton.enabled = false
// send the message // send the message
conversationModel.sendReplyToConversation(messageField.text) if (page.conversationId == page.invalidId) {
conversationModel.sendMessageWithoutConversation(messageField.text, page.otherParty)
} else {
conversationModel.sendReplyToConversation(messageField.text)
}
messageField.text = "" messageField.text = ""
// re-enable the button // re-enable the button

View file

@ -140,7 +140,6 @@ Kirigami.ScrollablePage
model: QSortFilterProxyModel { model: QSortFilterProxyModel {
sortOrder: Qt.DescendingOrder sortOrder: Qt.DescendingOrder
sortRole: ConversationListModel.DateRole
filterCaseSensitivity: Qt.CaseInsensitive filterCaseSensitivity: Qt.CaseInsensitive
sourceModel: ConversationListModel { sourceModel: ConversationListModel {
id: conversationListModel id: conversationListModel
@ -157,7 +156,13 @@ Kirigami.ScrollablePage
width: parent.width width: parent.width
z: 10 z: 10
onTextChanged: { onTextChanged: {
view.model.setFilterFixedString(filter.text); if (filter.text != "") {
view.model.setOurFilterRole(Qt.DisplayRole)
} else {
view.model.setOurFilterRole(ConversationListModel.ConversationIdRole)
}
view.model.setFilterFixedString(filter.text)
view.currentIndex = 0 view.currentIndex = 0
} }
onAccepted: { onAccepted: {
@ -219,9 +224,11 @@ Kirigami.ScrollablePage
conversationId: model.conversationId, conversationId: model.conversationId,
isMultitarget: isMultitarget, isMultitarget: isMultitarget,
initialMessage: page.initialMessage, initialMessage: page.initialMessage,
device: device}) device: device,
otherParty: sender})
initialMessage = "" initialMessage = ""
} }
onClicked: { onClicked: {
startChat(); startChat();
view.currentIndex = index view.currentIndex = index

View file

@ -154,16 +154,26 @@ private:
QHash<QString, QSharedPointer<KPeople::PersonData>> m_personDataCache; QHash<QString, QSharedPointer<KPeople::PersonData>> m_personDataCache;
}; };
QList<QSharedPointer<KPeople::PersonData>> SmsHelper::getAllPersons() {
static PersonsCache s_cache;
QList<QSharedPointer<KPeople::PersonData>> personDataList;
for(int rowIndex = 0; rowIndex < s_cache.count(); rowIndex++) {
const auto person = s_cache.personAt(rowIndex);
personDataList.append(person);
}
return personDataList;
}
QSharedPointer<KPeople::PersonData> SmsHelper::lookupPersonByAddress(const QString& address) QSharedPointer<KPeople::PersonData> SmsHelper::lookupPersonByAddress(const QString& address)
{ {
static PersonsCache s_cache;
const QString& canonicalAddress = SmsHelper::canonicalizePhoneNumber(address); const QString& canonicalAddress = SmsHelper::canonicalizePhoneNumber(address);
int rowIndex = 0; QList<QSharedPointer<KPeople::PersonData>> personDataList = getAllPersons();
for (rowIndex = 0; rowIndex < s_cache.count(); rowIndex++) {
const auto person = s_cache.personAt(rowIndex);
for (const auto& person : personDataList) {
const QStringList& allEmails = person->allEmails(); const QStringList& allEmails = person->allEmails();
for (const QString& email : allEmails) { for (const QString& email : allEmails) {
// Although we are nominally an SMS messaging app, it is possible to send messages to phone numbers using email -> sms bridges // Although we are nominally an SMS messaging app, it is possible to send messages to phone numbers using email -> sms bridges
if (address == email) { if (address == email) {

View file

@ -33,6 +33,8 @@
Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_SMS_SMSHELPER) Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_SMS_SMSHELPER)
class PersonsCache;
class KDECONNECTSMSAPPLIB_EXPORT SmsHelper class KDECONNECTSMSAPPLIB_EXPORT SmsHelper
{ {
public: public:
@ -100,6 +102,11 @@ public:
*/ */
static QIcon getIconForAddresses(const QList<ConversationAddress>& addresses); static QIcon getIconForAddresses(const QList<ConversationAddress>& addresses);
/**
* Get the data for all persons currently stored on device
*/
static QList<QSharedPointer<KPeople::PersonData>> getAllPersons();
private: private:
SmsHelper(){}; SmsHelper(){};
~SmsHelper(){}; ~SmsHelper(){};