File indexing completed on 2024-05-12 16:25:06

0001 // SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
0002 // SPDX-License-Identifier: LGPL-2.0-or-later
0003 
0004 #include "searchmodel.h"
0005 
0006 #include "messageeventmodel.h"
0007 #include "neochatroom.h"
0008 #include "neochatuser.h"
0009 #include <Quotient/events/stickerevent.h>
0010 
0011 #include <KLocalizedString>
0012 
0013 #include <Quotient/connection.h>
0014 
0015 using namespace Quotient;
0016 
0017 // TODO search only in the current room
0018 
0019 SearchModel::SearchModel(QObject *parent)
0020     : QAbstractListModel(parent)
0021 {
0022 }
0023 
0024 QString SearchModel::searchText() const
0025 {
0026     return m_searchText;
0027 }
0028 
0029 void SearchModel::setSearchText(const QString &searchText)
0030 {
0031     m_searchText = searchText;
0032     Q_EMIT searchTextChanged();
0033 }
0034 
0035 void SearchModel::search()
0036 {
0037     Q_ASSERT(m_connection);
0038     setSearching(true);
0039     if (m_job) {
0040         m_job->abandon();
0041         m_job = nullptr;
0042     }
0043 
0044     RoomEventFilter filter;
0045     filter.unreadThreadNotifications = none;
0046     filter.lazyLoadMembers = true;
0047     filter.includeRedundantMembers = false;
0048     filter.notRooms = QStringList();
0049     filter.rooms = QStringList{m_room->id()};
0050     filter.containsUrl = false;
0051 
0052     SearchJob::RoomEventsCriteria criteria{
0053         .searchTerm = m_searchText,
0054         .keys = {},
0055         .filter = filter,
0056         .orderBy = "recent",
0057         .eventContext = SearchJob::IncludeEventContext{3, 3, true},
0058         .includeState = false,
0059         .groupings = none,
0060 
0061     };
0062 
0063     auto job = m_connection->callApi<SearchJob>(SearchJob::Categories{criteria});
0064     m_job = job;
0065     connect(job, &BaseJob::finished, this, [this, job] {
0066         beginResetModel();
0067         m_result = job->searchCategories().roomEvents;
0068         endResetModel();
0069         setSearching(false);
0070         m_job = nullptr;
0071         // TODO error handling
0072     });
0073 }
0074 
0075 Connection *SearchModel::connection() const
0076 {
0077     return m_connection;
0078 }
0079 
0080 void SearchModel::setConnection(Connection *connection)
0081 {
0082     m_connection = connection;
0083     Q_EMIT connectionChanged();
0084 }
0085 
0086 QVariant SearchModel::data(const QModelIndex &index, int role) const
0087 {
0088     auto row = index.row();
0089     const auto &event = *m_result->results[row].result;
0090     switch (role) {
0091     case DisplayRole:
0092         return m_room->eventToString(*m_result->results[row].result);
0093     case ShowAuthorRole:
0094         return true;
0095     case AuthorRole:
0096         return m_room->getUser(event.senderId());
0097     case ShowSectionRole:
0098         if (row == 0) {
0099             return true;
0100         }
0101         return event.originTimestamp().date() != m_result->results[row - 1].result->originTimestamp().date();
0102     case SectionRole:
0103         return renderDate(event.originTimestamp());
0104     case TimeRole:
0105         return event.originTimestamp();
0106     case ShowReactionsRole:
0107         return false;
0108     case ShowReadMarkersRole:
0109         return false;
0110     case ReplyAuthorRole:
0111         if (const auto &replyPtr = m_room->getReplyForEvent(event)) {
0112             return m_room->getUser(static_cast<NeoChatUser *>(m_room->user(replyPtr->senderId())));
0113         } else {
0114             return m_room->getUser(nullptr);
0115         }
0116     case ReplyRole:
0117         if (role == ReplyRole) {
0118             auto replyPtr = m_room->getReplyForEvent(event);
0119             if (!replyPtr) {
0120                 return {};
0121             }
0122 
0123             MessageEventModel::DelegateType type;
0124             if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
0125                 switch (e->msgtype()) {
0126                 case MessageEventType::Emote:
0127                     type = MessageEventModel::DelegateType::Emote;
0128                     break;
0129                 case MessageEventType::Notice:
0130                     type = MessageEventModel::DelegateType::Notice;
0131                     break;
0132                 case MessageEventType::Image:
0133                     type = MessageEventModel::DelegateType::Image;
0134                     break;
0135                 case MessageEventType::Audio:
0136                     type = MessageEventModel::DelegateType::Audio;
0137                     break;
0138                 case MessageEventType::Video:
0139                     type = MessageEventModel::DelegateType::Video;
0140                     break;
0141                 default:
0142                     if (e->hasFileContent()) {
0143                         type = MessageEventModel::DelegateType::File;
0144                         break;
0145                     }
0146                     type = MessageEventModel::DelegateType::Message;
0147                 }
0148 
0149             } else if (is<const StickerEvent>(*replyPtr)) {
0150                 type = MessageEventModel::DelegateType::Sticker;
0151             } else {
0152                 type = MessageEventModel::DelegateType::Other;
0153             }
0154 
0155             return QVariantMap{
0156                 {"display", m_room->eventToString(*replyPtr, Qt::RichText)},
0157                 {"type", type},
0158             };
0159         }
0160     case IsPendingRole:
0161         return false;
0162     case ShowLinkPreviewRole:
0163         return false;
0164     case IsReplyRole:
0165         return !event.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty();
0166     case HighlightRole:
0167         return !m_room->isDirectChat() && m_room->isEventHighlighted(&event);
0168     case EventIdRole:
0169         return event.id();
0170     case ReplyIdRole:
0171         return event.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString();
0172     }
0173     return MessageEventModel::DelegateType::Message;
0174 }
0175 
0176 int SearchModel::rowCount(const QModelIndex &parent) const
0177 {
0178     Q_UNUSED(parent);
0179     if (m_result.has_value()) {
0180         return m_result->results.size();
0181     }
0182     return 0;
0183 }
0184 
0185 QHash<int, QByteArray> SearchModel::roleNames() const
0186 {
0187     return {
0188         {DelegateTypeRole, "delegateType"},
0189         {DisplayRole, "display"},
0190         {AuthorRole, "author"},
0191         {ShowSectionRole, "showSection"},
0192         {SectionRole, "section"},
0193         {TimeRole, "time"},
0194         {ShowAuthorRole, "showAuthor"},
0195         {EventIdRole, "eventId"},
0196         {ExcessReadMarkersRole, "excessReadMarkers"},
0197         {HighlightRole, "isHighlighted"},
0198         {ReadMarkersString, "readMarkersString"},
0199         {PlainTextRole, "plainText"},
0200         {VerifiedRole, "verified"},
0201         {ReplyAuthorRole, "replyAuthor"},
0202         {ProgressInfoRole, "progressInfo"},
0203         {IsReplyRole, "isReply"},
0204         {ShowReactionsRole, "showReactions"},
0205         {ReplyRole, "reply"},
0206         {ReactionRole, "reaction"},
0207         {ReplyMediaInfoRole, "replyMediaInfo"},
0208         {ReadMarkersRole, "readMarkers"},
0209         {IsPendingRole, "isPending"},
0210         {ShowReadMarkersRole, "showReadMarkers"},
0211         {ReplyIdRole, "replyId"},
0212         {MimeTypeRole, "mimeType"},
0213         {ShowLinkPreviewRole, "showLinkPreview"},
0214         {LinkPreviewRole, "linkPreview"},
0215         {SourceRole, "source"},
0216     };
0217 }
0218 
0219 NeoChatRoom *SearchModel::room() const
0220 {
0221     return m_room;
0222 }
0223 
0224 void SearchModel::setRoom(NeoChatRoom *room)
0225 {
0226     if (m_room) {
0227         disconnect(m_room, nullptr, this, nullptr);
0228     }
0229     m_room = room;
0230     Q_EMIT roomChanged();
0231 
0232     connect(m_room, &NeoChatRoom::replyLoaded, this, [this](const auto &eventId, const auto &replyId) {
0233         Q_UNUSED(replyId);
0234         const auto &results = m_result->results;
0235         auto it = std::find_if(results.begin(), results.end(), [eventId](const auto &event) {
0236             return event.result->id() == eventId;
0237         });
0238         if (it == results.end()) {
0239             return;
0240         }
0241         auto row = it - results.begin();
0242         Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthorRole});
0243     });
0244 }
0245 
0246 // TODO deduplicate with messageeventmodel
0247 QString renderDate(const QDateTime &timestamp)
0248 {
0249     auto date = timestamp.toLocalTime().date();
0250     if (date == QDate::currentDate()) {
0251         return i18n("Today");
0252     }
0253     if (date == QDate::currentDate().addDays(-1)) {
0254         return i18n("Yesterday");
0255     }
0256     if (date == QDate::currentDate().addDays(-2)) {
0257         return i18n("The day before yesterday");
0258     }
0259     if (date > QDate::currentDate().addDays(-7)) {
0260         return date.toString("dddd");
0261     }
0262 
0263     return QLocale::system().toString(date, QLocale::ShortFormat);
0264 }
0265 
0266 bool SearchModel::searching() const
0267 {
0268     return m_searching;
0269 }
0270 
0271 void SearchModel::setSearching(bool searching)
0272 {
0273     m_searching = searching;
0274     Q_EMIT searchingChanged();
0275 }
0276 
0277 #include "moc_searchmodel.cpp"