File indexing completed on 2024-05-12 05:02:06

0001 /*
0002 
0003  * SPDX-FileCopyrightText: 2016 Riccardo Iaconelli <riccardo@kde.org>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.0-or-later
0006  *
0007  */
0008 
0009 #include <QDataStream>
0010 #include <QFile>
0011 #include <QJsonDocument>
0012 #include <QModelIndex>
0013 #include <QTimeZone>
0014 
0015 #include "emoticons/emojimanager.h"
0016 #include "loadrecenthistorymanager.h"
0017 #include "messagesmodel.h"
0018 #include "rocketchataccount.h"
0019 #include "room.h"
0020 #include "ruqola_debug.h"
0021 #include "textconverter.h"
0022 #include "utils.h"
0023 
0024 #include <KLocalizedString>
0025 
0026 MessagesModel::MessagesModel(const QString &roomID, RocketChatAccount *account, Room *room, QObject *parent)
0027     : QAbstractListModel(parent)
0028     , mRoomId(roomID)
0029     , mRocketChatAccount(account)
0030     , mRoom(room)
0031     , mLoadRecentHistoryManager(new LoadRecentHistoryManager)
0032 {
0033     qCDebug(RUQOLA_LOG) << "Creating message Model";
0034     if (mRoom) {
0035         connect(mRoom, &Room::rolesChanged, this, &MessagesModel::refresh);
0036         connect(mRoom, &Room::ignoredUsersChanged, this, &MessagesModel::refresh);
0037         connect(mRoom, &Room::highlightsWordChanged, this, &MessagesModel::refresh);
0038     }
0039 }
0040 
0041 MessagesModel::~MessagesModel() = default;
0042 
0043 void MessagesModel::activate()
0044 {
0045     if (mRocketChatAccount) {
0046         connect(mRocketChatAccount, &RocketChatAccount::fileDownloaded, this, &MessagesModel::slotFileDownloaded);
0047     }
0048 }
0049 
0050 void MessagesModel::deactivate()
0051 {
0052     if (mRocketChatAccount) {
0053         disconnect(mRocketChatAccount, &RocketChatAccount::fileDownloaded, this, &MessagesModel::slotFileDownloaded);
0054     }
0055 }
0056 
0057 Message MessagesModel::findLastMessageBefore(const QString &messageId, const std::function<bool(const Message &)> &predicate) const
0058 {
0059     auto it = findMessage(messageId); // if it == end, we'll start from there
0060     auto rit = QVector<Message>::const_reverse_iterator(it); // this points to *it-1 already
0061     rit = std::find_if(rit, mAllMessages.rend(), predicate);
0062     return rit == mAllMessages.rend() ? Message() : *rit;
0063 }
0064 
0065 Message MessagesModel::findNextMessageAfter(const QString &messageId, const std::function<bool(const Message &)> &predicate) const
0066 {
0067     auto it = findMessage(messageId);
0068     if (it == mAllMessages.end()) {
0069         return Message(); // no wrap around, otherwise Alt+Key_Up would edit the oldest msg right away
0070     } else {
0071         ++it;
0072     }
0073     it = std::find_if(it, mAllMessages.end(), predicate);
0074     return it == mAllMessages.end() ? Message() : *it;
0075 }
0076 
0077 Message MessagesModel::findMessageById(const QString &messageId) const
0078 {
0079     auto it = findMessage(messageId);
0080     return it == mAllMessages.end() ? Message() : *it;
0081 }
0082 
0083 QModelIndex MessagesModel::indexForMessage(const QString &messageId) const
0084 {
0085     auto it = findMessage(messageId);
0086     if (it == mAllMessages.end()) {
0087         return {};
0088     }
0089     const QModelIndex idx = createIndex(std::distance(mAllMessages.begin(), it), 0);
0090     return idx;
0091 }
0092 
0093 QString MessagesModel::messageIdFromIndex(int rowIndex)
0094 {
0095     if (rowIndex >= 0 && rowIndex < mAllMessages.count()) {
0096         return mAllMessages.at(rowIndex).messageId();
0097     }
0098     return {};
0099 }
0100 
0101 void MessagesModel::refresh()
0102 {
0103     beginResetModel();
0104     endResetModel();
0105 }
0106 
0107 qint64 MessagesModel::lastTimestamp() const
0108 {
0109     if (!mAllMessages.isEmpty()) {
0110         // qCDebug(RUQOLA_LOG) << "returning timestamp" << mAllMessages.last().timeStamp();
0111         return mAllMessages.first().timeStamp();
0112     } else {
0113         return 0;
0114     }
0115 }
0116 
0117 int MessagesModel::rowCount(const QModelIndex &parent) const
0118 {
0119     Q_UNUSED(parent)
0120     return mAllMessages.size();
0121 }
0122 
0123 static bool compareTimeStamps(const Message &lhs, const Message &rhs)
0124 {
0125     return lhs.timeStamp() < rhs.timeStamp();
0126 }
0127 
0128 void MessagesModel::addMessage(const Message &message)
0129 {
0130     auto it = std::upper_bound(mAllMessages.begin(), mAllMessages.end(), message, compareTimeStamps);
0131 
0132     auto emitChanged = [this](int rowNumber, const QVector<int> &roles = QVector<int>()) {
0133         const QModelIndex index = createIndex(rowNumber, 0);
0134         Q_EMIT dataChanged(index, index, roles);
0135     };
0136 
0137     // When we have 1 element.
0138     if (mAllMessages.count() == 1 && (*mAllMessages.begin()).messageId() == message.messageId()) {
0139         (*mAllMessages.begin()) = message;
0140         qCDebug(RUQOLA_LOG) << "Update first message";
0141         emitChanged(0);
0142     } else if (((it) != mAllMessages.begin() && (*(it - 1)).messageId() == message.messageId())) {
0143         qCDebug(RUQOLA_LOG) << "Update message";
0144         if (message.pendingMessage()) {
0145             // If we already have a message and we must add pending message it's that server
0146             // send quickly new message => replace not it by a pending message
0147             return;
0148         }
0149         (*(it - 1)) = message;
0150         emitChanged(std::distance(mAllMessages.begin(), it - 1), {OriginalMessageOrAttachmentDescription});
0151     } else {
0152         const int pos = it - mAllMessages.begin();
0153         beginInsertRows(QModelIndex(), pos, pos);
0154         mAllMessages.insert(it, message);
0155         endInsertRows();
0156     }
0157 }
0158 
0159 void MessagesModel::addMessages(const QVector<Message> &messages, bool insertListMessages)
0160 {
0161     if (messages.isEmpty()) {
0162         return;
0163     }
0164     if (mAllMessages.isEmpty()) {
0165         beginInsertRows(QModelIndex(), 0, messages.count() - 1);
0166         mAllMessages = messages;
0167         std::sort(mAllMessages.begin(), mAllMessages.end(), compareTimeStamps);
0168         endInsertRows();
0169     } else if (insertListMessages) {
0170         beginResetModel();
0171         mAllMessages += messages;
0172         std::sort(mAllMessages.begin(), mAllMessages.end(), compareTimeStamps);
0173         endResetModel();
0174     } else {
0175         // TODO optimize this case as well?
0176         for (const Message &message : messages) {
0177             addMessage(message);
0178         }
0179     }
0180 }
0181 
0182 QVariant MessagesModel::data(const QModelIndex &index, int role) const
0183 {
0184     if (!index.isValid()) {
0185         qCWarning(RUQOLA_LOG) << "ERROR: invalid index";
0186         return {};
0187     }
0188     const int idx = index.row();
0189     const Message &message = mAllMessages.at(idx);
0190 
0191     switch (role) {
0192     case MessagesModel::MessagePointer:
0193         return QVariant::fromValue(&message);
0194     case MessagesModel::Username:
0195         return message.username();
0196     case Qt::AccessibleTextRole:
0197     case MessagesModel::OriginalMessage:
0198         return message.text();
0199     case MessagesModel::DateTimeUtc:
0200         return QDateTime::fromMSecsSinceEpoch(message.timeStamp(), Qt::UTC).toString(Qt::ISODateWithMs);
0201     case MessagesModel::MessageConvertedText:
0202         return convertedText(message, mSearchText);
0203     case MessagesModel::Timestamp:
0204         return message.displayTime();
0205     case MessagesModel::UserId:
0206         return message.userId();
0207     case MessagesModel::SystemMessageType:
0208         return message.systemMessageType();
0209     case MessagesModel::MessageId:
0210         return message.messageId();
0211     case MessagesModel::Alias:
0212         return message.alias();
0213     case MessagesModel::MessageType:
0214         return message.messageType();
0215     case MessagesModel::Avatar:
0216         return message.avatar();
0217     case MessagesModel::EditedAt:
0218         return message.editedAt();
0219     case MessagesModel::EditedByUserName:
0220         return message.editedByUsername();
0221     case MessagesModel::EditedToolTip:
0222         return i18n("Edited at %1 by %2", message.editedDisplayTime(), message.editedByUsername());
0223     case MessagesModel::Attachments: {
0224         QVariantList lst;
0225         lst.reserve(message.attachments().count());
0226         const auto attaches = message.attachments();
0227         for (const MessageAttachment &att : attaches) {
0228             lst.append(QVariant::fromValue(att));
0229         }
0230         return lst;
0231     }
0232     case MessagesModel::Urls: {
0233         QVariantList lst;
0234         lst.reserve(message.urls().count());
0235         const auto urls = message.urls();
0236         for (const MessageUrl &url : urls) {
0237             lst.append(QVariant::fromValue(url));
0238         }
0239         return lst;
0240     }
0241     case MessagesModel::Date: {
0242         const QDateTime currentDate = QDateTime::fromMSecsSinceEpoch(message.timeStamp());
0243         return currentDate.date().toString();
0244     }
0245     case MessagesModel::DateDiffersFromPrevious:
0246         if (idx > 0) {
0247             const QDateTime currentDate = QDateTime::fromMSecsSinceEpoch(message.timeStamp(), QTimeZone::utc());
0248             const Message &previousMessage = mAllMessages.at(idx - 1);
0249             const QDateTime previousDate = QDateTime::fromMSecsSinceEpoch(previousMessage.timeStamp(), QTimeZone::utc());
0250             return currentDate.date() != previousDate.date();
0251         }
0252         return true; // show date at the top
0253     case MessagesModel::CanEditMessage:
0254         return mRocketChatAccount->isMessageEditable(message); // && mRoom && mRoom->hasPermission(QStringLiteral("edit-message"));
0255     case MessagesModel::CanDeleteMessage:
0256         return mRocketChatAccount->isMessageDeletable(message);
0257     case MessagesModel::Starred:
0258         return message.isStarred();
0259     case MessagesModel::UsernameUrl: {
0260         const QString username = message.username();
0261         if (username.isEmpty()) {
0262             return {};
0263         }
0264         return QStringLiteral("<a href=\'ruqola:/user/%1\'>@%1</a>").arg(message.username());
0265     }
0266     case MessagesModel::Roles: {
0267         const QString str = roomRoles(message.userId()).join(QLatin1Char(','));
0268         return str;
0269     }
0270     case MessagesModel::Reactions: {
0271         QVariantList lst;
0272         const auto reactions = message.reactions().reactions();
0273         lst.reserve(reactions.count());
0274         for (const Reaction &react : reactions) {
0275             // Convert reactions
0276             lst.append(QVariant::fromValue(react));
0277         }
0278         return lst;
0279     }
0280     case MessagesModel::Ignored:
0281         return mRoom && mRoom->userIsIgnored(message.userId());
0282     case MessagesModel::DirectChannels:
0283         return mRoom && (mRoom->channelType() == Room::RoomType::Direct);
0284     case MessagesModel::Pinned:
0285         return message.messagePinned().pinned();
0286     case MessagesModel::DiscussionCount:
0287         return message.discussionCount();
0288     case MessagesModel::DiscussionRoomId:
0289         return message.discussionRoomId();
0290     case MessagesModel::DiscussionLastMessage:
0291         return message.discussionLastMessage();
0292     case MessagesModel::ThreadCount:
0293         return message.threadCount();
0294     case MessagesModel::ThreadLastMessage:
0295         return message.threadLastMessage();
0296     case MessagesModel::ThreadMessageId:
0297         return message.threadMessageId();
0298     case MessagesModel::ThreadMessagePreview:
0299         return threadMessagePreview(message.threadMessageId());
0300     case MessagesModel::ThreadMessageFollowed:
0301         return threadMessageFollowed(message.threadMessageId());
0302     case MessagesModel::ThreadMessage: {
0303         const Message tm = threadMessage(message.threadMessageId());
0304         return QVariant::fromValue(tm);
0305     }
0306     case MessagesModel::Groupable:
0307         return message.groupable();
0308     case MessagesModel::ShowTranslatedMessage:
0309         return message.showTranslatedMessage();
0310     case MessagesModel::DisplayAttachment:
0311         return {}; // Unused.
0312     case MessagesModel::DisplayUrlPreview:
0313         return {}; // Unused.
0314     case MessagesModel::DisplayLastSeenMessage:
0315         if (idx > 0) {
0316             if (mRoom) {
0317                 const QDateTime currentDate = QDateTime::fromMSecsSinceEpoch(message.timeStamp(), QTimeZone::utc());
0318                 const QDateTime lastSeenDate = QDateTime::fromMSecsSinceEpoch(mRoom->lastSeenAt(), QTimeZone::utc());
0319                 // qDebug() << " lastSeeDate" << lastSeeDate;
0320                 if (currentDate > lastSeenDate) {
0321                     const Message &previousMessage = mAllMessages.at(idx - 1);
0322                     const QDateTime previewMessageDate = QDateTime::fromMSecsSinceEpoch(previousMessage.timeStamp(), QTimeZone::utc());
0323                     const bool result = (previewMessageDate <= lastSeenDate);
0324                     return result;
0325                 }
0326             }
0327         }
0328         return false;
0329     case MessagesModel::Emoji:
0330         return message.emoji();
0331     case MessagesModel::AvatarInfo:
0332         return QVariant::fromValue(message.avatarInfo());
0333     case MessagesModel::PendingMessage:
0334         return message.pendingMessage();
0335     case MessagesModel::ShowIgnoredMessage:
0336         return message.showIgnoredMessage();
0337     case MessagesModel::MessageInEditMode:
0338         return message.isEditingMode();
0339     case MessagesModel::HoverHighLight:
0340         return message.hoverHighlight();
0341     case MessagesModel::LocalTranslation:
0342         return message.localTranslation();
0343     case MessagesModel::OriginalMessageOrAttachmentDescription:
0344         return message.originalMessageOrAttachmentDescription();
0345     case MessagesModel::GoToMessageBackgroundColor:
0346         return message.goToMessageBackgroundColor();
0347     }
0348 
0349     return {};
0350 }
0351 
0352 QString MessagesModel::convertedText(const Message &message, const QString &searchedText) const
0353 {
0354     if (message.messageType() == Message::System) {
0355         return message.systemMessageText();
0356         //    } else if (message.messageType() == Message::VideoConference) {
0357         //        return QStringLiteral("");
0358     } else {
0359         QStringList highlightWords;
0360         if (mRoom) {
0361             if (mRoom->channelType() != Room::RoomType::Direct) { // We can't ignore message but we can block user in direct message
0362                 if (mRoom->userIsIgnored(message.userId()) && !message.showIgnoredMessage()) {
0363                     return QString(QStringLiteral("<i>") + i18n("Ignored Message") + QStringLiteral("</i>"));
0364                 }
0365             }
0366             highlightWords = mRoom->highlightsWord();
0367         }
0368         const QString userName = mRocketChatAccount ? mRocketChatAccount->userName() : QString();
0369         const QStringList highlightWordsLst = mRocketChatAccount ? mRocketChatAccount->highlightWords() : highlightWords;
0370         return convertMessageText(message, userName, highlightWordsLst, searchedText);
0371     }
0372 }
0373 
0374 bool MessagesModel::setData(const QModelIndex &index, const QVariant &value, int role)
0375 {
0376     if (!index.isValid()) {
0377         qCWarning(RUQOLA_LOG) << "ERROR: invalid index";
0378         return false;
0379     }
0380     const int idx = index.row();
0381     Message &message = mAllMessages[idx];
0382 
0383     switch (role) {
0384     case MessagesModel::DisplayAttachment: {
0385         const auto visibility = value.value<AttachmentAndUrlPreviewVisibility>();
0386         auto attachments = message.attachments();
0387         for (int i = 0, total = attachments.count(); i < total; ++i) {
0388             const MessageAttachment att = attachments.at(i);
0389             if (att.attachmentId() == visibility.ElementId) {
0390                 MessageAttachment changeAttachment = attachments.takeAt(i);
0391                 changeAttachment.setShowAttachment(visibility.show);
0392                 attachments.insert(i, changeAttachment);
0393                 break;
0394             }
0395         }
0396         message.setAttachments(attachments);
0397         Q_EMIT dataChanged(index, index, {MessagesModel::DisplayAttachment});
0398         return true;
0399     }
0400     case MessagesModel::DisplayUrlPreview: {
0401         const auto visibility = value.value<AttachmentAndUrlPreviewVisibility>();
0402         auto urls = message.urls();
0403         for (int i = 0, total = urls.count(); i < total; ++i) {
0404             const MessageUrl att = urls.at(i);
0405             if (att.urlId() == visibility.ElementId) {
0406                 MessageUrl changeUrlPreview = urls.takeAt(i);
0407                 changeUrlPreview.setShowPreview(visibility.show);
0408                 urls.insert(i, changeUrlPreview);
0409                 break;
0410             }
0411         }
0412         message.setUrls(urls);
0413         Q_EMIT dataChanged(index, index, {MessagesModel::DisplayUrlPreview});
0414         return true;
0415     }
0416     case MessagesModel::ShowTranslatedMessage:
0417         message.setShowTranslatedMessage(value.toBool());
0418         Q_EMIT dataChanged(index, index, {MessagesModel::ShowTranslatedMessage});
0419         return true;
0420     case MessagesModel::ShowIgnoredMessage:
0421         message.setShowIgnoredMessage(value.toBool());
0422         Q_EMIT dataChanged(index, index, {MessagesModel::ShowIgnoredMessage});
0423         return true;
0424     case MessagesModel::MessageInEditMode:
0425         message.setIsEditingMode(value.toBool());
0426         Q_EMIT dataChanged(index, index, {MessagesModel::MessageInEditMode});
0427         return true;
0428     case MessagesModel::HoverHighLight:
0429         message.setHoverHighlight(value.toBool());
0430         Q_EMIT dataChanged(index, index, {MessagesModel::HoverHighLight});
0431         return true;
0432     case MessagesModel::LocalTranslation:
0433         message.setLocalTranslation(value.toString());
0434         Q_EMIT dataChanged(index, index, {MessagesModel::LocalTranslation});
0435         return true;
0436     case MessagesModel::GoToMessageBackgroundColor:
0437         message.setGoToMessageBackgroundColor(value.value<QColor>());
0438         Q_EMIT dataChanged(index, index, {MessagesModel::GoToMessageBackgroundColor});
0439         return true;
0440     }
0441     return false;
0442 }
0443 
0444 QStringList MessagesModel::roomRoles(const QString &userId) const
0445 {
0446     if (mRoom) {
0447         return mRoom->rolesForUserId(userId);
0448     }
0449     return {};
0450 }
0451 
0452 QString MessagesModel::convertMessageText(const Message &message, const QString &userName, const QStringList &highlightWords, const QString &searchedText) const
0453 {
0454     QString messageStr = message.text();
0455     EmojiManager *emojiManager = nullptr;
0456     MessageCache *messageCache = nullptr;
0457     int maximumRecursiveQuotedText = -1;
0458     if (mRocketChatAccount) {
0459         emojiManager = mRocketChatAccount->emojiManager();
0460         messageCache = mRocketChatAccount->messageCache();
0461         maximumRecursiveQuotedText = mRocketChatAccount->ruqolaServerConfig()->messageQuoteChainLimit();
0462         if (mRocketChatAccount->hasAutotranslateSupport()) {
0463             if (message.showTranslatedMessage()) {
0464                 if (mRoom && mRoom->autoTranslate() && !mRoom->autoTranslateLanguage().isEmpty()) {
0465                     const QString messageTranslation = message.messageTranslation().translatedStringFromLanguage(mRoom->autoTranslateLanguage());
0466                     if (!messageTranslation.isEmpty()) {
0467                         messageStr = messageTranslation;
0468                     } else if (!message.localTranslation().isEmpty()) {
0469                         messageStr = message.localTranslation();
0470                     }
0471                 }
0472             }
0473         } else if (message.showTranslatedMessage() && !message.localTranslation().isEmpty()) {
0474             messageStr = message.localTranslation();
0475         }
0476     }
0477 
0478     QString needUpdateMessageId;
0479     const TextConverter::ConvertMessageTextSettings settings(messageStr,
0480                                                              userName,
0481                                                              mAllMessages,
0482                                                              highlightWords,
0483                                                              emojiManager,
0484                                                              messageCache,
0485                                                              message.mentions(),
0486                                                              message.channels(),
0487                                                              searchedText,
0488                                                              maximumRecursiveQuotedText);
0489 
0490     int recursiveIndex = 0;
0491     return TextConverter::convertMessageText(settings, needUpdateMessageId, recursiveIndex);
0492 }
0493 
0494 void MessagesModel::setRoomId(const QString &roomId)
0495 {
0496     mRoomId = roomId;
0497 }
0498 
0499 bool MessagesModel::isEmpty() const
0500 {
0501     return mAllMessages.isEmpty();
0502 }
0503 
0504 void MessagesModel::clear()
0505 {
0506     mSearchText.clear();
0507     if (rowCount() != 0) {
0508         beginResetModel();
0509         mAllMessages.clear();
0510         endResetModel();
0511     }
0512 }
0513 
0514 void MessagesModel::changeShowOriginalMessage(const QString &messageId, bool showOriginal)
0515 {
0516     Q_UNUSED(showOriginal)
0517     auto it = findMessage(messageId);
0518     if (it != mAllMessages.end()) {
0519         // TODO implement it
0520     }
0521 }
0522 
0523 void MessagesModel::slotFileDownloaded(const QString &filePath, const QUrl &cacheImageUrl)
0524 {
0525     auto matchesFilePath = [&](const QVector<MessageAttachment> &msgAttachments) {
0526         return std::find_if(msgAttachments.begin(),
0527                             msgAttachments.end(),
0528                             [&](const MessageAttachment &attach) {
0529                                 // Transform link() the way RocketChatCache::downloadFile does it
0530                                 return mRocketChatAccount->urlForLink(attach.imageUrlPreview()).path() == filePath;
0531                             })
0532             != msgAttachments.end();
0533     };
0534     auto it = std::find_if(mAllMessages.begin(), mAllMessages.end(), [&](const Message &msg) {
0535         if (!msg.attachments().isEmpty()) {
0536             return matchesFilePath(msg.attachments());
0537         }
0538         auto *emojiManager = mRocketChatAccount->emojiManager();
0539         const auto reactions = msg.reactions().reactions();
0540         for (const Reaction &reaction : reactions) {
0541             const QString fileName = emojiManager->customEmojiFileName(reaction.reactionName());
0542             if (!fileName.isEmpty() && mRocketChatAccount->urlForLink(fileName).path() == filePath) {
0543                 return true;
0544             }
0545         }
0546         const Utils::AvatarInfo info = msg.avatarInfo();
0547         const QUrl iconUrlStr = QUrl(mRocketChatAccount->avatarUrl(info));
0548         if (!iconUrlStr.isEmpty()) {
0549             if (iconUrlStr == cacheImageUrl) {
0550                 return true;
0551             }
0552         }
0553         return false;
0554     });
0555     if (it != mAllMessages.end()) {
0556         const QModelIndex idx = createIndex(std::distance(mAllMessages.begin(), it), 0);
0557         Q_EMIT dataChanged(idx, idx);
0558     } else {
0559         qCWarning(RUQOLA_LOG) << "Attachment not found:" << filePath;
0560     }
0561 }
0562 
0563 void MessagesModel::deleteMessage(const QString &messageId)
0564 {
0565     auto it = findMessage(messageId);
0566     if (it != mAllMessages.end()) {
0567         const int i = std::distance(mAllMessages.begin(), it);
0568         beginRemoveRows(QModelIndex(), i, i);
0569         mAllMessages.erase(it);
0570         endRemoveRows();
0571     }
0572 }
0573 
0574 qint64 MessagesModel::generateNewStartTimeStamp(qint64 lastTimeStamp)
0575 {
0576     return mLoadRecentHistoryManager->generateNewStartTimeStamp(lastTimeStamp);
0577 }
0578 
0579 Message MessagesModel::threadMessage(const QString &threadMessageId) const
0580 {
0581     if (!threadMessageId.isEmpty()) {
0582         auto it = findMessage(threadMessageId);
0583         if (it != mAllMessages.cend()) {
0584             return (*it);
0585         } else {
0586             qCDebug(RUQOLA_LOG) << "Thread message" << threadMessageId << "not found"; // could be a very old one
0587         }
0588     }
0589     return Message{};
0590 }
0591 
0592 QString MessagesModel::threadMessagePreview(const QString &threadMessageId) const
0593 {
0594     if (!threadMessageId.isEmpty()) {
0595         auto it = findMessage(threadMessageId);
0596         if (it != mAllMessages.cend()) {
0597             const QString userName = mRocketChatAccount ? mRocketChatAccount->userName() : QString();
0598             QString str = convertMessageText((*it), userName, mRocketChatAccount ? mRocketChatAccount->highlightWords() : QStringList(), QString());
0599             if (str.length() > 80) {
0600                 str = str.left(80) + QStringLiteral("...");
0601             }
0602             return str;
0603         } else {
0604             qCDebug(RUQOLA_LOG) << "Thread message" << threadMessageId << "not found"; // could be a very old one
0605         }
0606     }
0607     return {};
0608 }
0609 
0610 bool MessagesModel::threadMessageFollowed(const QString &threadMessageId) const
0611 {
0612     if (!threadMessageId.isEmpty()) {
0613         auto it = findMessage(threadMessageId);
0614         if (it != mAllMessages.cend()) {
0615             const QString userId = mRocketChatAccount ? mRocketChatAccount->userId() : QString();
0616             if (!userId.isEmpty()) {
0617                 return (*it).replies().contains(userId);
0618             }
0619         } else {
0620             qCDebug(RUQOLA_LOG) << "Thread message" << threadMessageId << "not found"; // could be a very old one
0621         }
0622     }
0623     return false;
0624 }
0625 
0626 QVector<Message>::iterator MessagesModel::findMessage(const QString &messageId)
0627 {
0628     return std::find_if(mAllMessages.begin(), mAllMessages.end(), [&](const Message &msg) {
0629         return msg.messageId() == messageId;
0630     });
0631 }
0632 
0633 QVector<Message>::const_iterator MessagesModel::findMessage(const QString &messageId) const
0634 {
0635     return std::find_if(mAllMessages.cbegin(), mAllMessages.cend(), [&](const Message &msg) {
0636         return msg.messageId() == messageId;
0637     });
0638 }
0639 
0640 QString MessagesModel::roomId() const
0641 {
0642     return mRoomId;
0643 }
0644 
0645 QString MessagesModel::searchText() const
0646 {
0647     return mSearchText;
0648 }
0649 
0650 void MessagesModel::setSearchText(const QString &searchText)
0651 {
0652     mSearchText = searchText;
0653 }
0654 
0655 #include "moc_messagesmodel.cpp"