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:
parent
2a2481fb6a
commit
f211d7e401
10 changed files with 183 additions and 19 deletions
|
@ -190,7 +190,6 @@ void ConversationsDbusInterface::replyToConversation(const qint64& conversationI
|
|||
{
|
||||
const auto messagesList = m_conversations[conversationID];
|
||||
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!";
|
||||
return;
|
||||
}
|
||||
|
@ -209,6 +208,10 @@ void ConversationsDbusInterface::replyToConversation(const qint64& conversationI
|
|||
m_smsInterface.sendSms(addresses[0].address(), message);
|
||||
}
|
||||
|
||||
void ConversationsDbusInterface::sendWithoutConversation(const QString& address, const QString& message) {
|
||||
m_smsInterface.sendSms(address, message);
|
||||
}
|
||||
|
||||
void ConversationsDbusInterface::requestAllConversationThreads()
|
||||
{
|
||||
// Prepare the list of conversations by requesting the first in every thread
|
||||
|
|
|
@ -87,6 +87,11 @@ public Q_SLOTS:
|
|||
*/
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -33,9 +33,44 @@
|
|||
|
||||
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(){}
|
||||
|
||||
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)
|
||||
: QStandardItemModel(parent)
|
||||
, m_conversationsInterface(nullptr)
|
||||
|
@ -120,6 +155,7 @@ void ConversationListModel::prepareConversationsList()
|
|||
data >> message;
|
||||
createRowFromMessage(message);
|
||||
}
|
||||
displayContacts();
|
||||
}, this);
|
||||
}
|
||||
|
||||
|
@ -150,6 +186,18 @@ QStandardItem * ConversationListModel::conversationForThreadId(qint32 threadId)
|
|||
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)
|
||||
{
|
||||
if (message.type() == -1) {
|
||||
|
@ -158,19 +206,28 @@ void ConversationListModel::createRowFromMessage(const ConversationMessage& mess
|
|||
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;
|
||||
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) {
|
||||
toadd = true;
|
||||
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);
|
||||
QIcon displayIcon = SmsHelper::getIconForAddresses(rawAddresses);
|
||||
|
||||
|
@ -212,3 +269,32 @@ void ConversationListModel::createRowFromMessage(const ConversationMessage& mess
|
|||
if (toadd)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,9 +52,15 @@ public:
|
|||
sortNow();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void setOurFilterRole(int role);
|
||||
|
||||
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:
|
||||
void sortNow() {
|
||||
if (m_completed && dynamicSortFilter())
|
||||
|
@ -97,6 +103,7 @@ public Q_SLOTS:
|
|||
void handleConversationUpdated(const QDBusVariant& msg);
|
||||
void createRowFromMessage(const ConversationMessage& message);
|
||||
void printDBusError(const QDBusError& error);
|
||||
void displayContacts();
|
||||
|
||||
Q_SIGNALS:
|
||||
void deviceIdChanged();
|
||||
|
@ -108,6 +115,7 @@ private:
|
|||
void prepareConversationsList();
|
||||
|
||||
QStandardItem* conversationForThreadId(qint32 threadId);
|
||||
QStandardItem* getConversationForAddress(const QString& address);
|
||||
|
||||
DeviceConversationsDbusInterface* m_conversationsInterface;
|
||||
QString m_deviceId;
|
||||
|
|
|
@ -75,6 +75,7 @@ void ConversationModel::setDeviceId(const QString& deviceId)
|
|||
if (m_conversationsInterface) {
|
||||
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(conversationCreated(QDBusVariant)), this, SLOT(handleConversationCreated(QDBusVariant)));
|
||||
delete m_conversationsInterface;
|
||||
}
|
||||
|
||||
|
@ -83,6 +84,11 @@ void ConversationModel::setDeviceId(const QString& deviceId)
|
|||
m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this);
|
||||
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(conversationCreated(QDBusVariant)), this, SLOT(handleConversationCreated(QDBusVariant)));
|
||||
}
|
||||
|
||||
void ConversationModel::setOtherPartyAddress(const QString& address) {
|
||||
m_otherPartyAddress = address;
|
||||
}
|
||||
|
||||
void ConversationModel::sendReplyToConversation(const QString& message)
|
||||
|
@ -91,6 +97,12 @@ void ConversationModel::sendReplyToConversation(const QString& 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)
|
||||
{
|
||||
if (m_threadId == INVALID_THREAD_ID) {
|
||||
|
@ -157,10 +169,19 @@ void ConversationModel::handleConversationUpdate(const QDBusVariant& msg)
|
|||
<< "but we are currently viewing" << m_threadId;
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
Q_UNUSED(numMessages)
|
||||
|
|
|
@ -39,6 +39,7 @@ class ConversationModel
|
|||
Q_OBJECT
|
||||
Q_PROPERTY(qint64 threadId READ threadId WRITE setThreadId)
|
||||
Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId)
|
||||
Q_PROPERTY(QString otherParty READ otherPartyAddress WRITE setOtherPartyAddress)
|
||||
|
||||
public:
|
||||
ConversationModel(QObject* parent = nullptr);
|
||||
|
@ -59,7 +60,11 @@ public:
|
|||
QString deviceId() const { return m_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 sendMessageWithoutConversation(const QString& message, const QString& address);
|
||||
Q_INVOKABLE void requestMoreMessages(const quint32& howMany = 10);
|
||||
/**
|
||||
* Convert a list of names into a single string suitable for display
|
||||
|
@ -80,6 +85,7 @@ Q_SIGNALS:
|
|||
private Q_SLOTS:
|
||||
void handleConversationUpdate(const QDBusVariant &message);
|
||||
void handleConversationLoaded(qint64 threadID, quint64 numMessages);
|
||||
void handleConversationCreated(const QDBusVariant &message);
|
||||
|
||||
private:
|
||||
void createRowFromMessage(const ConversationMessage &message, int pos);
|
||||
|
@ -87,6 +93,7 @@ private:
|
|||
DeviceConversationsDbusInterface* m_conversationsInterface;
|
||||
QString m_deviceId;
|
||||
qint64 m_threadId = INVALID_THREAD_ID;
|
||||
QString m_otherPartyAddress;
|
||||
QSet<qint32> knownMessageIDs; // Set of known Message uIDs
|
||||
};
|
||||
|
||||
|
|
|
@ -38,12 +38,15 @@ Kirigami.ScrollablePage
|
|||
property string conversationId
|
||||
property bool isMultitarget
|
||||
property string initialMessage
|
||||
property string otherParty
|
||||
property string invalidId: "-1"
|
||||
|
||||
property bool isInitalized: false
|
||||
|
||||
property var conversationModel: ConversationModel {
|
||||
deviceId: page.deviceId
|
||||
threadId: page.conversationId
|
||||
otherParty: page.otherParty
|
||||
|
||||
onLoadingFinished: {
|
||||
page.isInitalized = true
|
||||
|
@ -58,6 +61,9 @@ Kirigami.ScrollablePage
|
|||
messageField.text = initialMessage;
|
||||
initialMessage = ""
|
||||
}
|
||||
if (conversationId == invalidId) {
|
||||
isInitalized = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -239,7 +245,11 @@ Kirigami.ScrollablePage
|
|||
sendButton.enabled = false
|
||||
|
||||
// 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 = ""
|
||||
|
||||
// re-enable the button
|
||||
|
|
|
@ -140,7 +140,6 @@ Kirigami.ScrollablePage
|
|||
|
||||
model: QSortFilterProxyModel {
|
||||
sortOrder: Qt.DescendingOrder
|
||||
sortRole: ConversationListModel.DateRole
|
||||
filterCaseSensitivity: Qt.CaseInsensitive
|
||||
sourceModel: ConversationListModel {
|
||||
id: conversationListModel
|
||||
|
@ -157,7 +156,13 @@ Kirigami.ScrollablePage
|
|||
width: parent.width
|
||||
z: 10
|
||||
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
|
||||
}
|
||||
onAccepted: {
|
||||
|
@ -219,9 +224,11 @@ Kirigami.ScrollablePage
|
|||
conversationId: model.conversationId,
|
||||
isMultitarget: isMultitarget,
|
||||
initialMessage: page.initialMessage,
|
||||
device: device})
|
||||
device: device,
|
||||
otherParty: sender})
|
||||
initialMessage = ""
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
startChat();
|
||||
view.currentIndex = index
|
||||
|
|
|
@ -154,16 +154,26 @@ private:
|
|||
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)
|
||||
{
|
||||
static PersonsCache s_cache;
|
||||
|
||||
const QString& canonicalAddress = SmsHelper::canonicalizePhoneNumber(address);
|
||||
int rowIndex = 0;
|
||||
for (rowIndex = 0; rowIndex < s_cache.count(); rowIndex++) {
|
||||
const auto person = s_cache.personAt(rowIndex);
|
||||
QList<QSharedPointer<KPeople::PersonData>> personDataList = getAllPersons();
|
||||
|
||||
for (const auto& person : personDataList) {
|
||||
const QStringList& allEmails = person->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
|
||||
if (address == email) {
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
|
||||
Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_SMS_SMSHELPER)
|
||||
|
||||
class PersonsCache;
|
||||
|
||||
class KDECONNECTSMSAPPLIB_EXPORT SmsHelper
|
||||
{
|
||||
public:
|
||||
|
@ -100,6 +102,11 @@ public:
|
|||
*/
|
||||
static QIcon getIconForAddresses(const QList<ConversationAddress>& addresses);
|
||||
|
||||
/**
|
||||
* Get the data for all persons currently stored on device
|
||||
*/
|
||||
static QList<QSharedPointer<KPeople::PersonData>> getAllPersons();
|
||||
|
||||
private:
|
||||
SmsHelper(){};
|
||||
~SmsHelper(){};
|
||||
|
|
Loading…
Reference in a new issue