[OpenDocString] kdeconnect-kde (cpp)
conversationlistmodel.cpp
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");
    roles.insert(AttachmentPreview, "attachmentPreview");
    setItemRoleNames(roles);

    ConversationMessage::registerDbusType();
}
This constructor builds a conversation list model from an existing parent object, and its interface. It takes the parent object, builds the list of roles, registers itself with DbusType.
ConversationListModel::~ConversationListModel()
{
}
This implements the list model design pattern and returns a reference to a conversation list model.
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();
}
Sets the device id, and connects the signal deviceIdChanged to the device and updates the conversation events. It disconnects the old conversations if it exists, and marks the device as reachable. Then it refreshes the conversation.
void ConversationListModel::refresh()
{
    if (m_deviceId.isEmpty()) {
        qWarning() << "refreshing null device";
        return;
    }

    prepareConversationsList();
    m_conversationsInterface->requestAllConversationThreads();
}
This prepares the list of conversations and requests all conversation threads. It logs a warning if the device id is null.
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) {
                createRowFromMessage(qdbus_cast(headMessage));
            }
            displayContacts();
        },
        this);
}
This sets the list of active conversations to be displayed and sets the when available promise to receive a list of conversation messages. If the interface is invalid, the method returns early. Otherwise, the list is cleared and a visual gap is made. If the request is sent to a thread that is not allowed to receive a conversation, the method clears the list and displays the contacts. If the request is sent to a thread that is not allowed to receive a conversation, the list is displayed in the background. If there is a wait list of active conversations, the function clears the conversation list before the reply is sent, and the list is only valid for the duration of the request.
void ConversationListModel::handleCreatedConversation(const QDBusVariant &msg)
{
    const ConversationMessage message = ConversationMessage::fromDBus(msg);
    createRowFromMessage(message);
}
This creates a row of a conversation record using the message object in the given DBus message.
void ConversationListModel::handleConversationUpdated(const QDBusVariant &msg)
{
    const ConversationMessage message = ConversationMessage::fromDBus(msg);
    createRowFromMessage(message);
}
This creates a row of a conversation record from a DBus message.
void ConversationListModel::printDBusError(const QDBusError &error)
{
    qCWarning(KDECONNECT_SMS_CONVERSATIONS_LIST_MODEL) << error;
}
This implements a warning message.
QStandardItem *ConversationListModel::conversationForThreadId(qint32 threadId)
{
    for (int i = 0, c = rowCount(); i < c; ++i) {
        auto it = item(i, 0);
        if (it->data(ConversationIdRole) == threadId)
            return it;
    }
    return nullptr;
}
Returns a pointer to the first item of the conversation list that matches the given thread id.
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;
}
Returns a pointer to the first item that matches the given address.
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);
    }

    // Get the body that we should display
    QString displayBody;
    if (message.containsTextBody()) {
        displayBody = message.body();
    } else if (message.containsAttachment()) {
        const QString mimeType = message.attachments().last().mimeType();
        if (mimeType.startsWith(QStringLiteral("image"))) {
            displayBody = i18nc("Used as a text placeholder when the most-recent message is an image", "Picture");
        } else if (mimeType.startsWith(QStringLiteral("video"))) {
            displayBody = i18nc("Used as a text placeholder when the most-recent message is a video", "Video");
        } else {
            // Craft a somewhat-descriptive string, like "pdf file"
            displayBody = i18nc("Used as a text placeholder when the most-recent message is an arbitrary attachment, resulting in something like \"pdf file\"",
                                "%1 file",
                                mimeType.right(mimeType.indexOf(QStringLiteral("/"))));
        }
    }

    // Get the preview from the attachment, if it exists
    QIcon attachmentPreview;
    if (message.containsAttachment()) {
        attachmentPreview = SmsHelper::getThumbnailForAttachment(message.attachments().last());
    }

    // For displaying single line subtitle out of the multiline messages to keep the ListItems consistent
    displayBody = displayBody.mid(0, displayBody.indexOf(QStringLiteral("\n")));

    // 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 (!attachmentPreview.isNull()) {
            item->setData(attachmentPreview, AttachmentPreview);
        }
    }

    if (toadd)
        appendRow(item);
}
This creates a row of items from a conversation message. It takes the addresses of the people that are involved in the conversation, and if there is no conversation with the given address, needed for the first message. It also creates the attachments of the first item if it doesn't exist, it creates one. It also checks if the message is a multitarget message, it is a contact with which to associate the first message, it is a video or a picture. If the message is an incoming message, it is newer, it is newer, it updates the item with the data of the message. It also checks if there is a conversation with the given address, which is a contact with the given address, and if the message is outgoing, it updates the record.
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);
            }
        }
    }
}
This function displays contacts for all people, which have a all - phoneNumber property.
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);
}
Creates a conversation item from an address. It takes the address in canonical format, and converts it to canonical format, adds it to the list of addresses, and creates a tooltip item.