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 ×tamp) 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"