diff --git a/smsapp/conversationlistmodel.cpp b/smsapp/conversationlistmodel.cpp index 6872fcb0..e235e0f2 100644 --- a/smsapp/conversationlistmodel.cpp +++ b/smsapp/conversationlistmodel.cpp @@ -1,291 +1,291 @@ /** * Copyright (C) 2018 Aleix Pol Gonzalez * Copyright (C) 2018 Simon Redman * * 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 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "conversationlistmodel.h" #include #include #include #include #include "interfaces/conversationmessage.h" #include "interfaces/dbusinterfaces.h" #include "smshelper.h" Q_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL, "kdeconnect.sms.conversations_list") #define INVALID_THREAD_ID -1 #define INVALID_DATE -1 ConversationListModel::ConversationListModel(QObject* parent) : QStandardItemModel(parent) , m_conversationsInterface(nullptr) { //qCDebug(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Constructing" << this; auto roles = roleNames(); roles.insert(FromMeRole, "fromMe"); roles.insert(SenderRole, "sender"); roles.insert(DateRole, "date"); roles.insert(AddressesRole, "addresses"); roles.insert(ConversationIdRole, "conversationId"); roles.insert(MultitargetRole, "isMultitarget"); setItemRoleNames(roles); ConversationMessage::registerDbusType(); } ConversationListModel::~ConversationListModel() { } void ConversationListModel::setDeviceId(const QString& deviceId) { if (deviceId == m_deviceId) { return; } if (deviceId.isEmpty()) { return; } qCDebug(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "setDeviceId" << deviceId << "of" << this; if (m_conversationsInterface) { disconnect(m_conversationsInterface, SIGNAL(conversationCreated(QDBusVariant)), this, SLOT(handleCreatedConversation(QDBusVariant))); disconnect(m_conversationsInterface, SIGNAL(conversationUpdated(QDBusVariant)), this, SLOT(handleConversationUpdated(QDBusVariant))); delete m_conversationsInterface; m_conversationsInterface = nullptr; } // This method still gets called *with a valid deviceID* when the device is not connected while the component is setting up // Detect that case and don't do anything. DeviceDbusInterface device(deviceId); if (!(device.isValid() && device.isReachable())) { return; } m_deviceId = deviceId; Q_EMIT deviceIdChanged(); m_conversationsInterface = new DeviceConversationsDbusInterface(deviceId, this); connect(m_conversationsInterface, SIGNAL(conversationCreated(QDBusVariant)), this, SLOT(handleCreatedConversation(QDBusVariant))); connect(m_conversationsInterface, SIGNAL(conversationUpdated(QDBusVariant)), this, SLOT(handleConversationUpdated(QDBusVariant))); refresh(); } void ConversationListModel::refresh() { if (m_deviceId.isEmpty()) { qWarning() << "refreshing null device"; return; } prepareConversationsList(); m_conversationsInterface->requestAllConversationThreads(); } void ConversationListModel::prepareConversationsList() { if (!m_conversationsInterface->isValid()) { qCWarning(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << "Tried to prepareConversationsList with an invalid interface!"; return; } const QDBusPendingReply validThreadIDsReply = m_conversationsInterface->activeConversations(); setWhenAvailable(validThreadIDsReply, [this](const QVariantList& convs) { clear(); // If we clear before we receive the reply, there might be a (several second) visual gap! for (const QVariant& headMessage : convs) { const QDBusArgument data = headMessage.value(); ConversationMessage message; data >> message; createRowFromMessage(message); } displayContacts(); }, this); } void ConversationListModel::handleCreatedConversation(const QDBusVariant& msg) { const ConversationMessage message = ConversationMessage::fromDBus(msg); createRowFromMessage(message); } void ConversationListModel::handleConversationUpdated(const QDBusVariant& msg) { const ConversationMessage message = ConversationMessage::fromDBus(msg); createRowFromMessage(message); } void ConversationListModel::printDBusError(const QDBusError& error) { qCWarning(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << error; } -QStandardItem * ConversationListModel::conversationForThreadId(qint32 threadId) +QStandardItem* ConversationListModel::conversationForThreadId(qint32 threadId) { for(int i=0, c=rowCount(); idata(ConversationIdRole) == threadId) return it; } return nullptr; } -QStandardItem * ConversationListModel::getConversationForAddress(const QString& address) +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) { // The Android side currently hacks in -1 if something weird comes up // TODO: Remove this hack when MMS support is implemented return; } /** The address of everyone involved in this conversation, which we should not display (check if they are known contacts first) */ const QList 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(); const QString displayNames = SmsHelper::getTitleForAddresses(rawAddresses); const QIcon displayIcon = SmsHelper::getIconForAddresses(rawAddresses); item->setText(displayNames); item->setIcon(displayIcon); item->setData(message.threadID(), ConversationIdRole); item->setData(rawAddresses[0].address(), SenderRole); } // TODO: Upgrade to support other kinds of media // Get the body that we should display QString displayBody = message.containsTextBody() ? message.body() : i18n("(Unsupported Message Type)"); // Prepend the sender's name if (message.isOutgoing()) { displayBody = i18n("You: %1", displayBody); } else { // If the message is incoming, the sender is the first Address const QString senderAddress = item->data(SenderRole).toString(); const auto sender = SmsHelper::lookupPersonByAddress(senderAddress); const QString senderName = sender == nullptr? senderAddress : SmsHelper::lookupPersonByAddress(senderAddress)->name(); displayBody = i18n("%1: %2", senderName, displayBody); } // Update the message if the data is newer // This will be true if a conversation receives a new message, but false when the user // does something to trigger past conversation history loading bool oldDateExists; const qint64 oldDate = item->data(DateRole).toLongLong(&oldDateExists); if (!oldDateExists || message.date() >= oldDate) { // If there was no old data or incoming data is newer, update the record item->setData(QVariant::fromValue(message.addresses()), AddressesRole); item->setData(message.isOutgoing(), FromMeRole); item->setData(displayBody, Qt::ToolTipRole); item->setData(message.date(), DateRole); item->setData(message.isMultitarget(), MultitargetRole); } if (toadd) appendRow(item); } void ConversationListModel::displayContacts() { const QList> 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 addresses; addresses.append(ConversationAddress(rawPhoneNumber.toString())); item->setData(QVariant::fromValue(addresses), AddressesRole); const 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); } } } } void ConversationListModel::createConversationForAddress(const QString& address) { QStandardItem* item = new QStandardItem(); const QString canonicalizedAddress = SmsHelper::canonicalizePhoneNumber(address); item->setText(canonicalizedAddress); QList addresses; addresses.append(ConversationAddress(canonicalizedAddress)); item->setData(QVariant::fromValue(addresses), AddressesRole); QString displayBody = i18n("%1", canonicalizedAddress); item->setData(displayBody, Qt::ToolTipRole); item->setData(false, MultitargetRole); item->setData(qint64(INVALID_THREAD_ID), ConversationIdRole); item->setData(qint64(INVALID_DATE), DateRole); item->setData(address, SenderRole); appendRow(item); } bool ConversationListModel::isPhoneNumberValid(const QString& number) { return SmsHelper::isPhoneNumberValid(number); } diff --git a/smsapp/conversationssortfilterproxymodel.cpp b/smsapp/conversationssortfilterproxymodel.cpp index da21c895..8b84496a 100644 --- a/smsapp/conversationssortfilterproxymodel.cpp +++ b/smsapp/conversationssortfilterproxymodel.cpp @@ -1,83 +1,107 @@ /** * Copyright (C) 2018 Aleix Pol Gonzalez * Copyright (C) 2018 Simon Redman * * 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 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 "conversationssortfilterproxymodel.h" #include "conversationlistmodel.h" #include "smshelper.h" #include #include Q_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATIONS_SORT_FILTER_PROXY_MODEL, "kdeconnect.sms.conversations_sort_filter_proxy") #define INVALID_THREAD_ID -1 ConversationsSortFilterProxyModel::ConversationsSortFilterProxyModel() { setFilterRole(ConversationListModel::ConversationIdRole); } ConversationsSortFilterProxyModel::~ConversationsSortFilterProxyModel() {} void ConversationsSortFilterProxyModel::setConversationsFilterRole(int role) { setFilterRole(role); } bool ConversationsSortFilterProxyModel::lessThan(const QModelIndex& leftIndex, const QModelIndex& rightIndex) const { + // This if block checks for multitarget conversations and sorts it at bottom of the list when the filtring is done on the basis of SenderRole + // This keeps the individual contacts with matching address at the top of the list + if (filterRole() == ConversationListModel::SenderRole) { + const bool isLeftMultitarget = sourceModel()->data(leftIndex, ConversationListModel::MultitargetRole).toBool(); + const bool isRightMultitarget = sourceModel()->data(leftIndex, ConversationListModel::MultitargetRole).toBool(); + if (isLeftMultitarget && !isRightMultitarget) { + return false; + } + if (!isLeftMultitarget && isRightMultitarget) { + return true; + } + } + 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 ConversationsSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); if (filterRole() == Qt::DisplayRole) { return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); } - if (filterRole() == ConversationListModel::SenderRole && !sourceModel()->data(index, ConversationListModel::MultitargetRole).toBool()) { - return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); + if (filterRole() == ConversationListModel::SenderRole) { + if (!sourceModel()->data(index, ConversationListModel::MultitargetRole).toBool()) { + return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); + } else { + // This block of code compares each address in the multi target conversation to find a match + QList addressList = sourceModel()->data(index, ConversationListModel::AddressesRole).value>(); + for (const ConversationAddress address : addressList) { + if (address.address().contains(filterRegExp())) { + return true; + } + } + return false; + } } return sourceModel()->data(index, ConversationListModel::ConversationIdRole) != INVALID_THREAD_ID; } -bool ConversationsSortFilterProxyModel::isPhoneNumberExists(const QString &address) +bool ConversationsSortFilterProxyModel::doesPhoneNumberExists(const QString &address) { for(int i = 0; i < rowCount(); ++i) { if (!data(index(i, 0), ConversationListModel::MultitargetRole).toBool()) { QVariant senderAddress = data(index(i, 0), ConversationListModel::SenderRole); if (SmsHelper::isPhoneNumberMatch(senderAddress.toString(), address)) { return true; } } } return false; } diff --git a/smsapp/conversationssortfilterproxymodel.h b/smsapp/conversationssortfilterproxymodel.h index e84f6b8e..c60f15f4 100644 --- a/smsapp/conversationssortfilterproxymodel.h +++ b/smsapp/conversationssortfilterproxymodel.h @@ -1,76 +1,76 @@ /** * Copyright (C) 2018 Aleix Pol Gonzalez * Copyright (C) 2018 Simon Redman * * 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 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 . */ #ifndef CONVERSATIONSSORTFILTERPROXYMODEL_H #define CONVERSATIONSSORTFILTERPROXYMODEL_H #include #include #include Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_SMS_CONVERSATIONS_SORT_FILTER_PROXY_MODEL) class ConversationsSortFilterProxyModel : public QSortFilterProxyModel, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder) public: Qt::SortOrder sortOrder() const { return m_sortOrder; } void setSortOrder(Qt::SortOrder sortOrder) { if (m_sortOrder != sortOrder) { m_sortOrder = sortOrder; sortNow(); } } void classBegin() override {} void componentComplete() override { m_completed = true; sortNow(); } Q_INVOKABLE void setConversationsFilterRole(int role); /** * This method gets name of conversations or contact if it find any matching address - * Needed for checking if the converstion already or contact already exist or no before adding an arbbitrary contact + * Needed to check if the conversation or contact already exist or no before adding an arbitrary contact */ - Q_INVOKABLE bool isPhoneNumberExists(const QString& address); + Q_INVOKABLE bool doesPhoneNumberExists(const QString& address); ConversationsSortFilterProxyModel(); ~ConversationsSortFilterProxyModel(); 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()) sort(0, m_sortOrder); } bool m_completed = false; Qt::SortOrder m_sortOrder = Qt::AscendingOrder; }; #endif // CONVERSATIONSSORTFILTERPROXYMODEL_H diff --git a/smsapp/qml/ConversationList.qml b/smsapp/qml/ConversationList.qml index 8cfa1376..e915552b 100644 --- a/smsapp/qml/ConversationList.qml +++ b/smsapp/qml/ConversationList.qml @@ -1,278 +1,284 @@ /** * Copyright (C) 2018 Aleix Pol Gonzalez * Copyright (C) 2018 Nicolas Fella * Copyright (C) 2018 Simon Redman * * 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 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 . */ import QtQuick 2.5 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.1 import org.kde.people 1.0 import org.kde.kirigami 2.6 as Kirigami import org.kde.kdeconnect 1.0 import org.kde.kdeconnect.sms 1.0 Kirigami.ScrollablePage { id: page ToolTip { id: noDevicesWarning visible: !page.deviceConnected timeout: -1 text: "⚠️ " + i18nd("kdeconnect-sms", "No devices available") + " ⚠️" MouseArea { // Detect mouseover and show another tooltip with more information anchors.fill: parent hoverEnabled: true ToolTip.visible: containsMouse ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval // TODO: Wrap text if line is too long for the screen ToolTip.text: i18nd("kdeconnect-sms", "No new messages can be sent or received, but you can browse cached content") } } contextualActions: [ Kirigami.Action { text: i18nd("kdeconnect-sms", "Refresh") icon.name: "view-refresh" enabled: devicesCombo.count > 0 onTriggered: { conversationListModel.refresh() } } ] ColumnLayout { id: loadingMessage visible: deviceConnected && view.count == 0 anchors.centerIn: parent BusyIndicator { running: loadingMessage.visible Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter } Label { text: "Loading conversations from device. If this takes a long time, please wake up your device and then click refresh." Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.preferredWidth: page.width / 2 horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap } Label { text: "Tip: If you plug in your device, it should not go into doze mode and should load quickly." Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.preferredWidth: page.width / 2 horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap } } property string initialMessage property string initialDevice header: Kirigami.InlineMessage { Layout.fillWidth: true visible: page.initialMessage.length > 0 text: i18nd("kdeconnect-sms", "Choose recipient") actions: [ Kirigami.Action { iconName: "dialog-cancel" text: i18nd("kdeconnect-sms", "Cancel") onTriggered: initialMessage = "" } ] } footer: ComboBox { id: devicesCombo enabled: count > 0 displayText: !enabled ? i18nd("kdeconnect-sms", "No devices available") : undefined model: DevicesSortProxyModel { id: devicesModel //TODO: make it possible to filter if they can do sms sourceModel: DevicesModel { displayFilter: DevicesModel.Paired | DevicesModel.Reachable } onRowsInserted: if (devicesCombo.currentIndex < 0) { if (page.initialDevice) devicesCombo.currentIndex = devicesModel.rowForDevice(page.initialDevice); else devicesCombo.currentIndex = 0 } } textRole: "display" } readonly property bool deviceConnected: devicesCombo.enabled readonly property QtObject device: devicesCombo.currentIndex >= 0 ? devicesModel.data(devicesModel.index(devicesCombo.currentIndex, 0), DevicesModel.DeviceRole) : null readonly property alias lastDeviceId: conversationListModel.deviceId Component { id: chatView ConversationDisplay { deviceId: page.lastDeviceId deviceConnected: page.deviceConnected } } ListView { id: view currentIndex: 0 model: QSortFilterProxyModel { sortOrder: Qt.DescendingOrder filterCaseSensitivity: Qt.CaseInsensitive sourceModel: ConversationListModel { id: conversationListModel deviceId: device ? device.id() : "" } } - header: TextField { - /** - * Used as the filter of the list of messages - */ - id: filter - placeholderText: i18nd("kdeconnect-sms", "Filter...") - width: parent.width - newButton.width + header: RowLayout { + width: parent.width z: 10 - onTextChanged: { - if (filter.text != "") { - if (conversationListModel.isPhoneNumberValid(filter.text)) { - view.model.setConversationsFilterRole(ConversationListModel.SenderRole) + Keys.forwardTo: [filter] + TextField { + /** + * Used as the filter of the list of messages + */ + id: filter + placeholderText: i18nd("kdeconnect-sms", "Filter...") + Layout.fillWidth: true + onTextChanged: { + if (filter.text != "") { + if (conversationListModel.isPhoneNumberValid(filter.text)) { + view.model.setConversationsFilterRole(ConversationListModel.SenderRole) + newButton.enabled = true + } else { + view.model.setConversationsFilterRole(Qt.DisplayRole) + newButton.enabled = false + } } else { - view.model.setConversationsFilterRole(Qt.DisplayRole) + view.model.setConversationsFilterRole(ConversationListModel.ConversationIdRole) } - } else { - view.model.setConversationsFilterRole(ConversationListModel.ConversationIdRole) - } - view.model.setFilterFixedString(filter.text) + view.model.setFilterFixedString(filter.text) - view.currentIndex = 0 - } - onAccepted: { - view.currentItem.startChat() - } - Keys.onReturnPressed: { - event.accepted = true - filter.onAccepted() - } - Keys.onEscapePressed: { - event.accepted = filter.text != "" - filter.text = "" - } - Shortcut { - sequence: "Ctrl+F" - onActivated: filter.forceActiveFocus() + view.currentIndex = 0 + } + onAccepted: { + view.currentItem.startChat() + } + Keys.onReturnPressed: { + event.accepted = true + filter.onAccepted() + } + Keys.onEscapePressed: { + event.accepted = filter.text != "" + filter.text = "" + } + Shortcut { + sequence: "Ctrl+F" + onActivated: filter.forceActiveFocus() + } } - } - headerPositioning: ListView.OverlayHeader + Button { + id: newButton + text: i18nd("kdeconnect-sms", "New") + visible: true + enabled: false + ToolTip.visible: hovered + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + ToolTip.text: i18nd("kdeconnect-sms", "Start new conversation") - Keys.forwardTo: [headerItem] - - Button { - id: newButton - text: i18nd("kdeconnect-sms", "New") - anchors.right: parent.right - height: view.headerItem.height - visible: true + onClicked: { + // We have to disable the filter temporarily in order to avoid getting key inputs accidently while processing the request + filter.enabled = false - onClicked: { - // We have to disable the filter temporarily in order to avoid getting key inputs accidently while processing the request - view.headerItem.enabled = false + // If the address entered by the user already exists then ignore adding new contact + if (!view.model.doesPhoneNumberExists(filter.text) && conversationListModel.isPhoneNumberValid(filter.text)) { + conversationListModel.createConversationForAddress(filter.text) + view.currentIndex = 0 + } - // If the address entered by the user already exists then ignore adding new contact - if (!view.model.isPhoneNumberExists(view.headerItem.text) && conversationListModel.isPhoneNumberValid(view.headerItem.text)) { - conversationListModel.createConversationForAddress(view.headerItem.text) + filter.enabled = true } - view.headerItem.enabled = true - } - Keys.onReturnPressed: { - event.clicked = true - newButton.onClicked() - } - Shortcut { - sequence: "Ctrl+N" - onActivated: newButton.forceActiveFocus() + Shortcut { + sequence: "Ctrl+N" + onActivated: newButton.onClicked() + } } } + headerPositioning: ListView.OverlayHeader + + Keys.forwardTo: [headerItem] + delegate: Kirigami.AbstractListItem { id: listItem contentItem: RowLayout { Kirigami.Icon { id: iconItem source: decoration readonly property int size: Kirigami.Units.iconSizes.smallMedium Layout.minimumHeight: size Layout.maximumHeight: size Layout.minimumWidth: size selected: listItem.highlighted || listItem.checked || (listItem.pressed && listItem.supportsMouseEvents) } ColumnLayout { Label { Layout.fillWidth: true font.weight: Font.Bold text: display maximumLineCount: 1 elide: Text.ElideRight textFormat: Text.PlainText } Label { Layout.fillWidth: true text: toolTip maximumLineCount: 1 elide: Text.ElideRight textFormat: Text.PlainText } } } function startChat() { applicationWindow().pageStack.push(chatView, { addresses: addresses, conversationId: model.conversationId, isMultitarget: isMultitarget, initialMessage: page.initialMessage, device: device, otherParty: sender}) initialMessage = "" } onClicked: { startChat(); view.currentIndex = index } // Keep the currently-open chat highlighted even if this element is not focused highlighted: chatView.conversationId == model.conversationId } Component.onCompleted: { currentIndex = -1 focus = true } } }