File indexing completed on 2024-05-05 05:28:46

0001 // SPDX-FileCopyrightText: 2020 Anthony Fieroni <bvbfan@abv.bg>
0002 // SPDX-FileCopyrightText: 2022 Jonah BrĂ¼chert <jbb@kaidan.im>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 
0006 #include "chatlistmodel.h"
0007 
0008 #include <QDebug>
0009 #include <QQmlApplicationEngine>
0010 #include <QSqlError>
0011 #include <QSqlQuery>
0012 #include <QStandardPaths>
0013 
0014 #include <QCoroFuture>
0015 
0016 #include <KLocalizedString>
0017 #include <KPeople/PersonData>
0018 
0019 #include "channelhandler.h"
0020 #include "messagemodel.h"
0021 #include "settingsmanager.h"
0022 #include "utils.h"
0023 #include <database.h>
0024 #include <global.h>
0025 
0026 ChatListModel::ChatListModel(ChannelHandler &handler, QObject *parent)
0027     : QAbstractListModel(parent)
0028     , m_handler(handler)
0029     , m_mapper(ContactPhoneNumberMapper::instance())
0030 {
0031     connect(&m_handler, &ChannelHandler::messagesChanged, this, &ChatListModel::fetchChats);
0032     connect(&m_mapper, &ContactPhoneNumberMapper::contactsChanged, this, [this](const QVector<PhoneNumber> &affectedNumbers) {
0033         qDebug() << "New data for" << affectedNumbers;
0034         for (const auto &number : affectedNumbers) {
0035             // Find the Chat object for the phone number
0036             const auto chatIt = std::find_if(m_chats.begin(), m_chats.end(), [&number](const Chat &chat) {
0037                 return chat.phoneNumberList.contains(number);
0038             });
0039 
0040             int i = std::distance(m_chats.begin(), chatIt);
0041             const auto row = index(i);
0042             Q_EMIT dataChanged(row, row, {Role::DisplayNameRole});
0043         }
0044     });
0045 
0046     fetchChats(PhoneNumberList(SL("")));
0047 
0048     Q_EMIT m_handler.interface()->disableNotificationsForNumber(QString());
0049 }
0050 
0051 ChatListModel::~ChatListModel()
0052 {
0053     Q_EMIT m_handler.interface()->disableNotificationsForNumber(QString());
0054 }
0055 
0056 QHash<int, QByteArray> ChatListModel::roleNames() const
0057 {
0058     return {
0059         {Role::DisplayNameRole, BL("displayName")},
0060         {Role::PhoneNumberListRole, BL("phoneNumberList")},
0061         {Role::UnreadMessagesRole, BL("unreadMessages")},
0062         {Role::LastDateTimeRole, BL("lastDateTime")},
0063         {Role::LastMessageRole, BL("lastMessage")},
0064         {Role::LastSentByMeRole, BL("lastSentByMe")},
0065         {Role::LastAttachmentRole, BL("lastAttachment")},
0066         {Role::LastContactedRole, BL("lastContacted")},
0067         {Role::IsContactRole, BL("isContact")},
0068     };
0069 }
0070 
0071 QVariant ChatListModel::data(const QModelIndex &index, int role) const
0072 {
0073     if (!index.isValid() || index.row() < 0 || index.row() >= m_chats.count()) {
0074         return false;
0075     }
0076 
0077     PhoneNumberList phoneNumberList = m_chats.at(index.row()).phoneNumberList;
0078 
0079     switch (role) {
0080     // All roles that need the personData object
0081     case DisplayNameRole: {
0082         QString names;
0083         for (int i = 0; const auto &number : phoneNumberList) {
0084             if (!names.isEmpty()) {
0085                 names.append(SL(", "));
0086             }
0087             QString name = KPeople::PersonData(m_mapper.uriForNumber(number)).name();
0088             if (name.isEmpty()) {
0089                 name = number.toInternational();
0090             } else if (phoneNumberList.count() >= 2) {
0091                 name = name.split(SL(" ")).first();
0092             }
0093 
0094             if (i > 0 && names.size() + name.size() + 5 > m_characters) {
0095                 names.append(i18n("and %1 more", phoneNumberList.count() - i));
0096                 break;
0097             }
0098 
0099             names.append(name);
0100 
0101             i++;
0102         }
0103         return names;
0104     }
0105     case PhoneNumberListRole:
0106         return QVariant::fromValue(phoneNumberList);
0107     case UnreadMessagesRole:
0108         return m_chats.at(index.row()).unreadMessages;
0109     case LastMessageRole:
0110         return m_chats.at(index.row()).lastMessage;
0111     case LastDateTimeRole:
0112         return m_chats.at(index.row()).lastDateTime;
0113     case LastSentByMeRole:
0114         return m_chats.at(index.row()).lastSentByMe;
0115     case LastAttachmentRole:
0116         return m_chats.at(index.row()).lastAttachment;
0117     case LastContactedRole: {
0118         QDateTime lastContacted = m_chats.at(index.row()).lastDateTime;
0119         if (lastContacted.daysTo(QDateTime::currentDateTime()) == 0) {
0120             QString format = Utils::instance()->isLocale24HourTime() ? SL("hh:mm") : SL("h:mm ap");
0121             return lastContacted.toString(format);
0122         } else {
0123             return lastContacted.toString(QLocale::system().dateFormat(QLocale::ShortFormat));
0124         }
0125     }
0126     case IsContactRole:
0127         return phoneNumberList.size() == 1 && !KPeople::PersonData(m_mapper.uriForNumber(phoneNumberList.first())).name().isEmpty();
0128     }
0129 
0130     return {};
0131 }
0132 
0133 int ChatListModel::rowCount(const QModelIndex &parent) const
0134 {
0135     return parent.isValid() ? 0 : m_chats.count();
0136 }
0137 
0138 QPair<Chat *, int> ChatListModel::getChatIndex(const PhoneNumberList &phoneNumberList)
0139 {
0140     auto modelIt = std::find_if(m_chats.begin(), m_chats.end(), [&](const Chat &chat) {
0141         return chat.phoneNumberList.toString() == phoneNumberList.toString();
0142     });
0143 
0144     if (modelIt != m_chats.cend()) {
0145         const int i = std::distance(m_chats.begin(), modelIt);
0146         return qMakePair(&(*modelIt), i);
0147     } else {
0148         return qMakePair(&(*modelIt), -1);
0149     }
0150 }
0151 
0152 void ChatListModel::startChat(const PhoneNumberList &phoneNumberList)
0153 {
0154     if (m_messageModel != nullptr)
0155         m_messageModel->deleteLater();
0156 
0157     m_messageModel = new MessageModel(m_handler, phoneNumberList, this);
0158     chatStarted(m_messageModel);
0159 }
0160 
0161 void ChatListModel::markChatAsRead(const PhoneNumberList &phoneNumberList)
0162 {
0163     m_handler.database().markChatAsRead(phoneNumberList);
0164 }
0165 
0166 void ChatListModel::fetchChats(const PhoneNumberList &phoneNumberList)
0167 {
0168     if (phoneNumberList.count() == 0) {
0169         fetchChatsInternal();
0170     } else {
0171         const auto &[chat, idx] = getChatIndex(phoneNumberList);
0172 
0173         if (idx == -1) {
0174             // add to chat list
0175             Chat chat;
0176             chat.phoneNumberList = phoneNumberList;
0177 
0178             beginInsertRows({}, 0, 0);
0179             m_chats.prepend(chat);
0180             endInsertRows();
0181 
0182             fetchChatDetails(phoneNumberList, false);
0183         } else {
0184             // update existing and sort
0185             fetchChatDetails(phoneNumberList, true);
0186         }
0187     }
0188 }
0189 
0190 QCoro::Task<void> ChatListModel::fetchChatsInternal()
0191 {
0192     const auto chats = co_await m_handler.database().chats(PhoneNumberList());
0193 
0194     beginResetModel();
0195     m_chats = chats;
0196     endResetModel();
0197 
0198     Q_EMIT chatsFetched();
0199 }
0200 
0201 void ChatListModel::fetchChatDetails(const PhoneNumberList &phoneNumberList, const bool sort)
0202 {
0203     [this, phoneNumberList, sort]() -> QCoro::Task<void> {
0204         co_await fetchChatDetailsInternal(phoneNumberList, sort);
0205     }();
0206 }
0207 
0208 QCoro::Task<void> ChatListModel::fetchChatDetailsInternal(const PhoneNumberList &phoneNumberList, const bool sort)
0209 {
0210     const auto &[chat, idx] = getChatIndex(phoneNumberList);
0211 
0212     if (idx > -1) {
0213         const QVector<Chat> chats = co_await m_handler.database().chats(phoneNumberList);
0214 
0215         chat->phoneNumberList = chats.first().phoneNumberList;
0216         chat->unreadMessages = chats.first().unreadMessages;
0217         chat->lastMessage = chats.first().lastMessage;
0218         chat->lastDateTime = chats.first().lastDateTime;
0219         chat->lastSentByMe = chats.first().lastSentByMe;
0220         chat->lastAttachment = chats.first().lastAttachment;
0221 
0222         Q_EMIT dataChanged(index(idx), index(idx));
0223     }
0224 
0225     if (sort) {
0226         beginResetModel();
0227         // Sort chat list by most recent chat
0228         std::sort(m_chats.begin(), m_chats.end(), [](const Chat &a, const Chat &b) -> bool {
0229             return a.lastDateTime > b.lastDateTime;
0230         });
0231         endResetModel();
0232     }
0233 }
0234 
0235 void ChatListModel::deleteChat(const PhoneNumberList &phoneNumberList)
0236 {
0237     m_handler.database().deleteChat(phoneNumberList);
0238 
0239     const QString folder = QString::number(hash(phoneNumberList.toString()));
0240     QDir dir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + SL("/spacebar/attachments/") + folder);
0241     dir.removeRecursively();
0242 
0243     const auto &[chat, idx] = getChatIndex(phoneNumberList);
0244 
0245     if (idx > -1) {
0246         beginRemoveRows(QModelIndex(), idx, idx);
0247         m_chats.remove(idx);
0248         endRemoveRows();
0249     }
0250 }
0251 
0252 void ChatListModel::restoreDefaults()
0253 {
0254     SettingsManager::self()->setDefaults();
0255 }
0256 
0257 void ChatListModel::saveSettings()
0258 {
0259     SettingsManager::self()->save();
0260     Q_EMIT m_handler.interface()->syncSettings();
0261 }
0262 
0263 QString ChatListModel::attachmentsFolder(const PhoneNumberList &phoneNumberList) const
0264 {
0265     const QString folder = QString::number(hash(phoneNumberList.toString()));
0266     return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + SL("/spacebar/attachments/") + folder;
0267 }
0268 
0269 void ChatListModel::setCharacterLimit(const int &width)
0270 {
0271     // 170 is the pixels taken up by other elements on the list row
0272     m_characters = (width - 170) / 6;
0273 }