File indexing completed on 2024-05-05 05:28:46
0001 // SPDX-FileCopyrightText: 2022 Jonah BrĂ¼chert <jbb@kaidan.im> 0002 // SPDX-FileCopyrightText: 2021 Michael Lang <criticaltemp@protonmail.com> 0003 // 0004 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 0006 #include "messagemodel.h" 0007 #include "channelhandler.h" 0008 #include "utils.h" 0009 0010 #include <QJsonObject> 0011 #include <QMimeDatabase> 0012 #include <QMimeType> 0013 0014 #include <contactphonenumbermapper.h> 0015 #include <database.h> 0016 #include <global.h> 0017 #include <phonenumberlist.h> 0018 0019 #include <QCoroFuture> 0020 0021 MessageModel::MessageModel(ChannelHandler &handler, const PhoneNumberList &phoneNumberList, QObject *parent) 0022 : QAbstractListModel(parent) 0023 , m_handler(handler) 0024 , m_phoneNumberList(phoneNumberList) 0025 { 0026 disableNotifications(m_phoneNumberList); 0027 0028 for (const auto &number : phoneNumberList) { 0029 Person person; 0030 person.m_name = KPeople::PersonData(ContactPhoneNumberMapper::instance().uriForNumber(number)).name(); 0031 person.m_phoneNumber = number.toInternational(); 0032 m_peopleData.append(person); 0033 } 0034 0035 connect(m_handler.interface(), &OrgKdeSpacebarDaemonInterface::messageAdded, this, &MessageModel::messageAdded); 0036 0037 connect(m_handler.interface(), &OrgKdeSpacebarDaemonInterface::messageUpdated, this, &MessageModel::messageUpdated); 0038 0039 connect(m_handler.interface(), &OrgKdeSpacebarDaemonInterface::manualDownloadFinished, this, [this](const QString &id, const bool isEmpty) { 0040 if (isEmpty) { 0041 updateMessageState(id, MessageState::Failed, true); 0042 } else { 0043 const auto idx = getMessageIndex(id); 0044 deleteMessage(id, idx.second, QStringList()); 0045 } 0046 }); 0047 0048 fetchMessages(QString()); 0049 } 0050 0051 QCoro::Task<void> MessageModel::fetchMessages(const QString &id, const int limit) 0052 { 0053 const auto messages = co_await m_handler.database().messagesForNumber(m_phoneNumberList, id, limit); 0054 0055 if (limit == -1) { 0056 beginInsertRows({}, 0, messages.size() - 1); 0057 for (auto &&message : messages) { 0058 m_messages.push_back(message); 0059 } 0060 endInsertRows(); 0061 } else { 0062 if (messages.size() == 1) { 0063 beginInsertRows({}, m_messages.size(), m_messages.size()); 0064 m_messages.insert(m_messages.begin(), messages.at(0)); 0065 endInsertRows(); 0066 } else { 0067 beginResetModel(); 0068 m_messages = messages; 0069 endResetModel(); 0070 } 0071 } 0072 0073 Q_EMIT messagesFetched(); 0074 } 0075 0076 QCoro::Task<void> MessageModel::fetchUpdatedMessage(const QString &id) 0077 { 0078 const auto &[message, i] = getMessageIndex(id); 0079 const auto messages = co_await m_handler.database().messagesForNumber(m_phoneNumberList, id); 0080 0081 if (!messages.empty()) { 0082 message->text = messages.front().text; 0083 message->datetime = messages.front().datetime; 0084 message->deliveryStatus = messages.front().deliveryStatus; 0085 message->attachments = messages.front().attachments; 0086 message->smil = messages.front().smil; 0087 message->deliveryReport = messages.front().deliveryReport; 0088 message->readReport = messages.front().readReport; 0089 message->tapbacks = messages.front().tapbacks; 0090 0091 Q_EMIT dataChanged(index(i), index(i)); 0092 } 0093 } 0094 0095 void MessageModel::fetchAllMessages() 0096 { 0097 fetchMessages(QString(), -1); 0098 } 0099 0100 QHash<int, QByteArray> MessageModel::roleNames() const 0101 { 0102 return {{Role::TextRole, BL("text")}, 0103 {Role::TimeRole, BL("time")}, 0104 {Role::DateRole, BL("date")}, 0105 {Role::SentByMeRole, BL("sentByMe")}, 0106 {Role::ReadRole, BL("read")}, 0107 {Role::DeliveryStateRole, BL("deliveryState")}, 0108 {Role::IdRole, BL("id")}, 0109 {Role::AttachmentsRole, BL("attachments")}, 0110 {Role::SmilRole, BL("smil")}, 0111 {Role::FromNumberRole, BL("fromNumber")}, 0112 {Role::MessageIdRole, BL("messageId")}, 0113 {Role::DeliveryReportRole, BL("deliveryReport")}, 0114 {Role::ReadReportRole, BL("readReport")}, 0115 {Role::PendingDownloadRole, BL("pendingDownload")}, 0116 {Role::ContentLocationRole, BL("contentLocation")}, 0117 {Role::ExpiresRole, BL("expires")}, 0118 {Role::ExpiresDateTimeRole, BL("expiresDateTime")}, 0119 {Role::SizeRole, BL("size")}, 0120 {Role::TapbacksRole, BL("tapbacks")}}; 0121 } 0122 0123 QVariant MessageModel::data(const QModelIndex &index, int role) const 0124 { 0125 if (!index.isValid() || index.row() < 0 || size_t(index.row()) >= m_messages.size()) { 0126 return false; 0127 } 0128 0129 // message order is reversed from the C++ side instead of in QML since sectioning doesn't work right otherwise 0130 Message message = m_messages.at((m_messages.size() - 1) - index.row()); 0131 switch (role) { 0132 case Role::TextRole: 0133 return message.text; 0134 case Role::TimeRole: 0135 return message.datetime.time(); 0136 case Role::DateRole: 0137 return message.datetime.date(); 0138 case Role::SentByMeRole: 0139 return message.sentByMe; 0140 case Role::ReadRole: 0141 return message.read; 0142 case Role::DeliveryStateRole: 0143 return DeliveryState(message.deliveryStatus); 0144 case Role::IdRole: 0145 return message.id; 0146 case Role::AttachmentsRole: 0147 return message.attachments; 0148 case Role::SmilRole: 0149 return message.smil; 0150 case Role::FromNumberRole: 0151 return message.fromNumber; 0152 case Role::MessageIdRole: 0153 return message.messageId; 0154 case Role::DeliveryReportRole: 0155 return message.deliveryReport; 0156 case Role::ReadReportRole: 0157 return message.readReport; 0158 case Role::PendingDownloadRole: 0159 return message.pendingDownload; 0160 case Role::ContentLocationRole: 0161 return message.contentLocation; 0162 case Role::ExpiresRole: 0163 return message.expires; 0164 case Role::ExpiresDateTimeRole: 0165 return message.expires.toLocalTime().toString(SL("MMM d, h:mm ap")); 0166 case Role::SizeRole: 0167 return message.size; 0168 case Role::TapbacksRole: 0169 return message.tapbacks; 0170 } 0171 0172 return {}; 0173 } 0174 0175 int MessageModel::rowCount(const QModelIndex &index) const 0176 { 0177 return index.isValid() ? 0 : m_messages.size(); 0178 } 0179 0180 QVector<Person> MessageModel::people() const 0181 { 0182 return m_peopleData; 0183 } 0184 0185 PhoneNumberList MessageModel::phoneNumberList() const 0186 { 0187 return m_phoneNumberList; 0188 } 0189 0190 QString MessageModel::sendingNumber() const 0191 { 0192 QString number = m_handler.interface()->ownNumber(); 0193 if (number == SL("The name org.kde.Spacebar was not provided by any .service files")) { 0194 return QString(); 0195 } else { 0196 return PhoneNumber(number).toInternational(); 0197 } 0198 } 0199 0200 QString MessageModel::attachmentsFolder() const 0201 { 0202 const QString folder = QString::number(hash(m_phoneNumberList.toString())); 0203 return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + SL("/spacebar/attachments/") + folder; 0204 } 0205 0206 QVariant MessageModel::fileInfo(const QUrl &path) 0207 { 0208 QFile file(path.toLocalFile()); 0209 file.open(QIODevice::ReadOnly); 0210 QByteArray data = file.readAll(); 0211 QMimeDatabase db; 0212 QMimeType mime = db.mimeTypeForData(data); 0213 QString fileName = Database::generateRandomId() + SL(".") + mime.preferredSuffix(); 0214 0215 QJsonObject object{{SL("filePath"), path.toString()}, 0216 {SL("name"), path.fileName()}, 0217 {SL("fileName"), fileName}, 0218 {SL("size"), data.length()}, 0219 {SL("mimeType"), mime.name()}, 0220 {SL("iconName"), mime.iconName()}}; 0221 0222 return object; 0223 } 0224 0225 void MessageModel::messageAdded(const QString &numbers, const QString &id) 0226 { 0227 if (PhoneNumberList(numbers) != m_phoneNumberList) { 0228 return; // Message is not for this model 0229 } 0230 0231 fetchMessages(id); 0232 } 0233 0234 void MessageModel::messageUpdated(const QString &numbers, const QString &id) 0235 { 0236 if (PhoneNumberList(numbers) != m_phoneNumberList) { 0237 return; // Message is not for this model 0238 } 0239 0240 fetchUpdatedMessage(id); 0241 } 0242 0243 void MessageModel::sendTapback(const QString &id, const QString &tapback, const bool &isRemoved) 0244 { 0245 m_handler.interface()->sendTapback(m_phoneNumberList.toString(), id, tapback, isRemoved); 0246 } 0247 0248 void MessageModel::sendMessage(const QString &text, const QStringList &files, const qint64 &totalSize) 0249 { 0250 Message message; 0251 message.id = Database::generateRandomId(); 0252 message.phoneNumberList = m_phoneNumberList; 0253 message.text = text; 0254 message.sentByMe = true; 0255 message.deliveryStatus = MessageState::Pending; 0256 0257 beginInsertRows({}, m_messages.size(), m_messages.size()); 0258 m_messages.insert(m_messages.begin(), message); 0259 endInsertRows(); 0260 0261 m_handler.interface()->sendMessage(m_phoneNumberList.toString(), message.id, text, files, totalSize); 0262 0263 // update chat list 0264 Q_EMIT m_handler.messagesChanged(m_phoneNumberList); 0265 } 0266 0267 QPair<Message *, int> MessageModel::getMessageIndex(const QString &id) 0268 { 0269 auto modelIt = std::find_if(m_messages.begin(), m_messages.end(), [&](const Message &message) { 0270 return message.id == id; 0271 }); 0272 0273 Q_ASSERT(modelIt != m_messages.cend()); 0274 0275 const size_t i = (m_messages.size() - 1) - std::distance(m_messages.begin(), modelIt); 0276 return qMakePair(&(*modelIt.base()), i); 0277 } 0278 0279 void MessageModel::updateMessageState(const QString &id, MessageState state, const bool temp) 0280 { 0281 const auto &[message, i] = getMessageIndex(id); 0282 0283 message->deliveryStatus = state; 0284 0285 if (!temp) { 0286 m_handler.database().updateMessageDeliveryState(id, state); 0287 } 0288 0289 Q_EMIT dataChanged(index(i), index(i), {Role::DeliveryStateRole}); 0290 } 0291 0292 void MessageModel::markMessageRead(const int id) 0293 { 0294 m_handler.database().markMessageRead(id); 0295 } 0296 0297 void MessageModel::downloadMessage(const QString &id, const QString &url, const QDateTime &expires) 0298 { 0299 updateMessageState(id, MessageState::Pending, true); 0300 m_handler.interface()->manualDownload(id, url, expires); 0301 } 0302 0303 void MessageModel::deleteMessage(const QString &id, const int index, const QStringList &files) 0304 { 0305 m_handler.database().deleteMessage(id); 0306 0307 // delete attachments 0308 const QString sourceFolder = attachmentsFolder(); 0309 for (const auto &file : files) { 0310 if (!file.isEmpty()) { 0311 QFile::remove(sourceFolder + SL("/") + file); 0312 } 0313 } 0314 0315 beginRemoveRows(QModelIndex(), index, index); 0316 m_messages.erase(m_messages.begin() + (m_messages.size() - index - 1)); 0317 endRemoveRows(); 0318 0319 // update chat list only if it was the most recent message 0320 if (size_t(index) == m_messages.size()) { 0321 Q_EMIT m_handler.messagesChanged(m_phoneNumberList); 0322 } 0323 } 0324 0325 void MessageModel::saveAttachments(const QStringList &attachments) 0326 { 0327 const QString sourceFolder = attachmentsFolder(); 0328 const QString targetFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); 0329 for (const auto &i : attachments) { 0330 QFile::copy(sourceFolder + QStringLiteral("/") + i, targetFolder + QStringLiteral("/") + i); 0331 } 0332 } 0333 0334 void MessageModel::disableNotifications(const PhoneNumberList &phoneNumberList) 0335 { 0336 m_handler.interface()->disableNotificationsForNumber(phoneNumberList.toString().isNull() ? SL("") : phoneNumberList.toString()); 0337 }