File indexing completed on 2024-05-12 16:25:05
0001 // SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org> 0002 // SPDX-License-Identifier: GPL-3.0-only 0003 0004 #include "messageeventmodel.h" 0005 #include "messageeventmodel_logging.h" 0006 0007 #include "neochatconfig.h" 0008 0009 #include <Quotient/connection.h> 0010 #include <Quotient/csapi/rooms.h> 0011 #include <Quotient/events/reactionevent.h> 0012 #include <Quotient/events/redactionevent.h> 0013 #include <Quotient/events/roomavatarevent.h> 0014 #include <Quotient/events/roommemberevent.h> 0015 #include <Quotient/events/simplestateevents.h> 0016 #include <Quotient/user.h> 0017 0018 #include "events/pollevent.h" 0019 #include <Quotient/events/stickerevent.h> 0020 0021 #include <QDebug> 0022 #include <QGuiApplication> 0023 #include <QTimeZone> 0024 0025 #include <KLocalizedString> 0026 0027 #include "models/reactionmodel.h" 0028 #include "neochatuser.h" 0029 #include "texthandler.h" 0030 0031 using namespace Quotient; 0032 0033 QHash<int, QByteArray> MessageEventModel::roleNames() const 0034 { 0035 QHash<int, QByteArray> roles = QAbstractItemModel::roleNames(); 0036 roles[DelegateTypeRole] = "delegateType"; 0037 roles[PlainText] = "plainText"; 0038 roles[EventIdRole] = "eventId"; 0039 roles[TimeRole] = "time"; 0040 roles[SectionRole] = "section"; 0041 roles[AuthorRole] = "author"; 0042 roles[ContentRole] = "content"; 0043 roles[HighlightRole] = "isHighlighted"; 0044 roles[SpecialMarksRole] = "marks"; 0045 roles[ProgressInfoRole] = "progressInfo"; 0046 roles[ShowLinkPreviewRole] = "showLinkPreview"; 0047 roles[LinkPreviewRole] = "linkPreview"; 0048 roles[MediaInfoRole] = "mediaInfo"; 0049 roles[IsReplyRole] = "isReply"; 0050 roles[ReplyAuthor] = "replyAuthor"; 0051 roles[ReplyRole] = "reply"; 0052 roles[ReplyIdRole] = "replyId"; 0053 roles[ReplyMediaInfoRole] = "replyMediaInfo"; 0054 roles[ShowAuthorRole] = "showAuthor"; 0055 roles[ShowSectionRole] = "showSection"; 0056 roles[ReadMarkersRole] = "readMarkers"; 0057 roles[ExcessReadMarkersRole] = "excessReadMarkers"; 0058 roles[ReadMarkersStringRole] = "readMarkersString"; 0059 roles[ShowReadMarkersRole] = "showReadMarkers"; 0060 roles[ReactionRole] = "reaction"; 0061 roles[ShowReactionsRole] = "showReactions"; 0062 roles[SourceRole] = "jsonSource"; 0063 roles[MimeTypeRole] = "mimeType"; 0064 roles[AuthorIdRole] = "authorId"; 0065 roles[VerifiedRole] = "verified"; 0066 roles[DisplayNameForInitialsRole] = "displayNameForInitials"; 0067 roles[AuthorDisplayNameRole] = "authorDisplayName"; 0068 roles[IsRedactedRole] = "isRedacted"; 0069 roles[GenericDisplayRole] = "genericDisplay"; 0070 roles[IsPendingRole] = "isPending"; 0071 roles[LatitudeRole] = "latitude"; 0072 roles[LongitudeRole] = "longitude"; 0073 roles[AssetRole] = "asset"; 0074 return roles; 0075 } 0076 0077 MessageEventModel::MessageEventModel(QObject *parent) 0078 : QAbstractListModel(parent) 0079 { 0080 connect(static_cast<QGuiApplication *>(QGuiApplication::instance()), &QGuiApplication::paletteChanged, this, [this] { 0081 Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReplyRole}); 0082 }); 0083 } 0084 0085 NeoChatRoom *MessageEventModel::room() const 0086 { 0087 return m_currentRoom; 0088 } 0089 0090 void MessageEventModel::setRoom(NeoChatRoom *room) 0091 { 0092 if (room == m_currentRoom) { 0093 return; 0094 } 0095 0096 beginResetModel(); 0097 if (m_currentRoom) { 0098 m_currentRoom->disconnect(this); 0099 m_linkPreviewers.clear(); 0100 qDeleteAll(m_reactionModels); 0101 m_reactionModels.clear(); 0102 } 0103 0104 m_currentRoom = room; 0105 if (room) { 0106 m_lastReadEventIndex = QPersistentModelIndex(QModelIndex()); 0107 room->setDisplayed(); 0108 0109 for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) { 0110 if (auto e = &*event->viewAs<RoomMessageEvent>()) { 0111 createLinkPreviewerForEvent(e); 0112 createReactionModelForEvent(e); 0113 } 0114 } 0115 0116 if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) { 0117 room->getPreviousContent(50); 0118 } 0119 lastReadEventId = room->lastFullyReadEventId(); 0120 connect(m_currentRoom, &NeoChatRoom::replyLoaded, this, [this](const auto &eventId, const auto &replyId) { 0121 Q_UNUSED(replyId); 0122 auto row = eventIdToRow(eventId); 0123 if (row == -1) { 0124 return; 0125 } 0126 Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthor}); 0127 }); 0128 0129 connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) { 0130 for (auto &&event : events) { 0131 const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get()); 0132 if (message != nullptr) { 0133 createLinkPreviewerForEvent(message); 0134 createReactionModelForEvent(message); 0135 0136 if (NeoChatConfig::self()->showFancyEffects()) { 0137 QString planBody = message->plainBody(); 0138 // snowflake 0139 const QString snowlakeEmoji = QString::fromUtf8("\xE2\x9D\x84"); 0140 if (planBody.contains(snowlakeEmoji)) { 0141 Q_EMIT fancyEffectsReasonFound(QStringLiteral("snowflake")); 0142 } 0143 // fireworks 0144 const QString fireworksEmoji = QString::fromUtf8("\xF0\x9F\x8E\x86"); 0145 if (planBody.contains(fireworksEmoji)) { 0146 Q_EMIT fancyEffectsReasonFound(QStringLiteral("fireworks")); 0147 } 0148 // sparkler 0149 const QString sparklerEmoji = QString::fromUtf8("\xF0\x9F\x8E\x87"); 0150 if (planBody.contains(sparklerEmoji)) { 0151 Q_EMIT fancyEffectsReasonFound(QStringLiteral("fireworks")); 0152 } 0153 // party pooper 0154 const QString partyEmoji = QString::fromUtf8("\xF0\x9F\x8E\x89"); 0155 if (planBody.contains(partyEmoji)) { 0156 Q_EMIT fancyEffectsReasonFound(QStringLiteral("confetti")); 0157 } 0158 // confetti ball 0159 const QString confettiEmoji = QString::fromUtf8("\xF0\x9F\x8E\x8A"); 0160 if (planBody.contains(confettiEmoji)) { 0161 Q_EMIT fancyEffectsReasonFound(QStringLiteral("confetti")); 0162 } 0163 } 0164 } 0165 } 0166 m_initialized = true; 0167 beginInsertRows({}, timelineBaseIndex(), timelineBaseIndex() + int(events.size()) - 1); 0168 }); 0169 connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) { 0170 for (auto &event : events) { 0171 RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get()); 0172 if (message) { 0173 createLinkPreviewerForEvent(message); 0174 createReactionModelForEvent(message); 0175 } 0176 } 0177 if (rowCount() > 0) { 0178 rowBelowInserted = rowCount() - 1; // See #312 0179 } 0180 m_initialized = true; 0181 beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1); 0182 }); 0183 connect(m_currentRoom, &Room::addedMessages, this, [this](int lowest, int biggest) { 0184 if (m_initialized) { 0185 endInsertRows(); 0186 } 0187 if (!m_lastReadEventIndex.isValid()) { 0188 // no read marker, so see if we need to create one. 0189 moveReadMarker(m_currentRoom->lastFullyReadEventId()); 0190 } 0191 if (biggest < m_currentRoom->maxTimelineIndex()) { 0192 auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1; 0193 refreshEventRoles(rowBelowInserted, {ShowAuthorRole}); 0194 } 0195 for (auto i = m_currentRoom->maxTimelineIndex() - biggest; i <= m_currentRoom->maxTimelineIndex() - lowest; ++i) { 0196 refreshLastUserEvents(i); 0197 } 0198 }); 0199 connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this] { 0200 m_initialized = true; 0201 beginInsertRows({}, 0, 0); 0202 }); 0203 connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows); 0204 connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, [this](RoomEvent *, int i) { 0205 Q_EMIT dataChanged(index(i, 0), index(i, 0), {IsPendingRole}); 0206 if (i == 0) { 0207 return; // No need to move anything, just refresh 0208 } 0209 0210 movingEvent = true; 0211 // Reverse i because row 0 is bottommost in the model 0212 const auto row = timelineBaseIndex() - i - 1; 0213 beginMoveRows({}, row, row, {}, timelineBaseIndex()); 0214 }); 0215 connect(m_currentRoom, &Room::pendingEventMerged, this, [this] { 0216 if (movingEvent) { 0217 endMoveRows(); 0218 movingEvent = false; 0219 } 0220 refreshRow(timelineBaseIndex()); // Refresh the looks 0221 refreshLastUserEvents(0); 0222 if (timelineBaseIndex() > 0) { // Refresh below, see #312 0223 refreshEventRoles(timelineBaseIndex() - 1, {ShowAuthorRole}); 0224 } 0225 }); 0226 connect(m_currentRoom, &Room::pendingEventChanged, this, &MessageEventModel::refreshRow); 0227 connect(m_currentRoom, &Room::pendingEventAboutToDiscard, this, [this](int i) { 0228 beginRemoveRows({}, i, i); 0229 }); 0230 connect(m_currentRoom, &Room::pendingEventDiscarded, this, &MessageEventModel::endRemoveRows); 0231 connect(m_currentRoom, &Room::fullyReadMarkerMoved, this, [this](const QString &fromEventId, const QString &toEventId) { 0232 Q_UNUSED(fromEventId); 0233 moveReadMarker(toEventId); 0234 }); 0235 connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) { 0236 refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex()); 0237 }); 0238 connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) { 0239 if (eventId.isEmpty()) { // How did we get here? 0240 return; 0241 } 0242 const auto eventIt = m_currentRoom->findInTimeline(eventId); 0243 if (eventIt != m_currentRoom->historyEdge()) { 0244 createReactionModelForEvent(static_cast<const RoomMessageEvent *>(&**eventIt)); 0245 } 0246 refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole}); 0247 }); 0248 connect(m_currentRoom, &Room::changed, this, [this]() { 0249 for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) { 0250 auto event = it->event(); 0251 refreshEventRoles(event->id(), {ReadMarkersRole, ReadMarkersStringRole, ExcessReadMarkersRole}); 0252 } 0253 }); 0254 connect(m_currentRoom, &Room::newFileTransfer, this, &MessageEventModel::refreshEvent); 0255 connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent); 0256 connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent); 0257 connect(m_currentRoom, &Room::fileTransferFailed, this, &MessageEventModel::refreshEvent); 0258 connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] { 0259 beginResetModel(); 0260 endResetModel(); 0261 }); 0262 qCDebug(MessageEvent) << "Connected to room" << room->id() << "as" << room->localUser()->id(); 0263 } else { 0264 lastReadEventId.clear(); 0265 } 0266 endResetModel(); 0267 } 0268 0269 int MessageEventModel::refreshEvent(const QString &eventId) 0270 { 0271 return refreshEventRoles(eventId); 0272 } 0273 0274 void MessageEventModel::refreshRow(int row) 0275 { 0276 refreshEventRoles(row); 0277 } 0278 0279 int MessageEventModel::timelineBaseIndex() const 0280 { 0281 return m_currentRoom ? int(m_currentRoom->pendingEvents().size()) : 0; 0282 } 0283 0284 void MessageEventModel::refreshEventRoles(int row, const QVector<int> &roles) 0285 { 0286 const auto idx = index(row); 0287 Q_EMIT dataChanged(idx, idx, roles); 0288 } 0289 0290 void MessageEventModel::moveReadMarker(const QString &toEventId) 0291 { 0292 const auto timelineIt = m_currentRoom->findInTimeline(toEventId); 0293 if (timelineIt == m_currentRoom->historyEdge()) { 0294 return; 0295 } 0296 int newRow = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex(); 0297 0298 if (!m_lastReadEventIndex.isValid()) { 0299 // Not valid index means we don't display any marker yet, in this case 0300 // we create the new index and insert the row in case the read marker 0301 // need to be displayed. 0302 if (newRow > timelineBaseIndex()) { 0303 // The user didn't read all the messages yet. 0304 m_initialized = true; 0305 beginInsertRows({}, newRow, newRow); 0306 m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0)); 0307 endInsertRows(); 0308 return; 0309 } 0310 // The user read all the messages and we didn't display any read marker yet 0311 // => do nothing 0312 return; 0313 } 0314 if (newRow <= timelineBaseIndex()) { 0315 // The user read all the messages => remove read marker 0316 beginRemoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row()); 0317 m_lastReadEventIndex = QModelIndex(); 0318 endRemoveRows(); 0319 return; 0320 } 0321 0322 // The user didn't read all the messages yet but moved the reader marker. 0323 beginMoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row(), {}, newRow); 0324 m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0)); 0325 endMoveRows(); 0326 } 0327 0328 int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &roles) 0329 { 0330 // On 64-bit platforms, difference_type for std containers is long long 0331 // but Qt uses int throughout its interfaces; hence casting to int below. 0332 int row = -1; 0333 // First try pendingEvents because it is almost always very short. 0334 const auto pendingIt = m_currentRoom->findPendingEvent(id); 0335 if (pendingIt != m_currentRoom->pendingEvents().end()) { 0336 row = int(pendingIt - m_currentRoom->pendingEvents().begin()); 0337 } else { 0338 const auto timelineIt = m_currentRoom->findInTimeline(id); 0339 if (timelineIt == m_currentRoom->historyEdge()) { 0340 return -1; 0341 } 0342 row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex(); 0343 if (data(index(row, 0), DelegateTypeRole).toInt() == ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == Other) { 0344 row++; 0345 } 0346 } 0347 refreshEventRoles(row, roles); 0348 return row; 0349 } 0350 0351 inline bool hasValidTimestamp(const Quotient::TimelineItem &ti) 0352 { 0353 return ti->originTimestamp().isValid(); 0354 } 0355 0356 QDateTime MessageEventModel::makeMessageTimestamp(const Quotient::Room::rev_iter_t &baseIt) const 0357 { 0358 const auto &timeline = m_currentRoom->messageEvents(); 0359 auto ts = baseIt->event()->originTimestamp(); 0360 if (ts.isValid()) { 0361 return ts; 0362 } 0363 0364 // The event is most likely redacted or just invalid. 0365 // Look for the nearest date around and slap zero time to it. 0366 using Quotient::TimelineItem; 0367 auto rit = std::find_if(baseIt, timeline.rend(), hasValidTimestamp); 0368 if (rit != timeline.rend()) { 0369 return {rit->event()->originTimestamp().date(), {0, 0}, Qt::LocalTime}; 0370 }; 0371 auto it = std::find_if(baseIt.base(), timeline.end(), hasValidTimestamp); 0372 if (it != timeline.end()) { 0373 return {it->event()->originTimestamp().date(), {0, 0}, Qt::LocalTime}; 0374 }; 0375 0376 // What kind of room is that?.. 0377 qCCritical(MessageEvent) << "No valid timestamps in the room timeline!"; 0378 return {}; 0379 } 0380 0381 void MessageEventModel::refreshLastUserEvents(int baseTimelineRow) 0382 { 0383 if (!m_currentRoom || m_currentRoom->timelineSize() <= baseTimelineRow) { 0384 return; 0385 } 0386 0387 const auto &timelineBottom = m_currentRoom->messageEvents().rbegin(); 0388 const auto &lastSender = (*(timelineBottom + baseTimelineRow))->senderId(); 0389 const auto limit = timelineBottom + std::min(baseTimelineRow + 10, m_currentRoom->timelineSize()); 0390 for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0); it != limit; ++it) { 0391 if ((*it)->senderId() == lastSender) { 0392 auto idx = index(it - timelineBottom); 0393 Q_EMIT dataChanged(idx, idx); 0394 } 0395 } 0396 } 0397 0398 int MessageEventModel::rowCount(const QModelIndex &parent) const 0399 { 0400 if (!m_currentRoom || parent.isValid()) { 0401 return 0; 0402 } 0403 0404 const auto firstIt = m_currentRoom->messageEvents().crbegin(); 0405 if (firstIt != m_currentRoom->messageEvents().crend()) { 0406 const auto &firstEvt = **firstIt; 0407 return m_currentRoom->timelineSize() + (lastReadEventId != firstEvt.id() ? 1 : 0); 0408 } else { 0409 return m_currentRoom->timelineSize(); 0410 } 0411 } 0412 0413 bool MessageEventModel::canFetchMore(const QModelIndex &parent) const 0414 { 0415 Q_UNUSED(parent); 0416 0417 return m_currentRoom && !m_currentRoom->eventsHistoryJob() && !m_currentRoom->allHistoryLoaded(); 0418 } 0419 0420 void MessageEventModel::fetchMore(const QModelIndex &parent) 0421 { 0422 Q_UNUSED(parent); 0423 if (m_currentRoom) { 0424 m_currentRoom->getPreviousContent(20); 0425 } 0426 } 0427 0428 static LinkPreviewer *emptyLinkPreview = new LinkPreviewer; 0429 0430 QVariant MessageEventModel::data(const QModelIndex &idx, int role) const 0431 { 0432 if (!checkIndex(idx, QAbstractItemModel::CheckIndexOption::IndexIsValid)) { 0433 return {}; 0434 } 0435 const auto row = idx.row(); 0436 0437 if (!m_currentRoom || row < 0 || row >= int(m_currentRoom->pendingEvents().size()) + m_currentRoom->timelineSize()) { 0438 return {}; 0439 }; 0440 0441 bool isPending = row < timelineBaseIndex(); 0442 0443 if (m_lastReadEventIndex.row() == row) { 0444 switch (role) { 0445 case DelegateTypeRole: 0446 return DelegateType::ReadMarker; 0447 case TimeRole: { 0448 const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime().toLocalTime(); 0449 const KFormat format; 0450 return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat); 0451 } 0452 } 0453 return {}; 0454 } 0455 0456 const auto timelineIt = m_currentRoom->messageEvents().crbegin() 0457 + std::max(0, row - timelineBaseIndex() - (m_lastReadEventIndex.isValid() && m_lastReadEventIndex.row() < row ? 1 : 0)); 0458 const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex()); 0459 const auto &evt = isPending ? **pendingIt : **timelineIt; 0460 0461 if (role == Qt::DisplayRole) { 0462 if (evt.isRedacted()) { 0463 auto reason = evt.redactedBecause()->reason(); 0464 return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") 0465 : i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason()); 0466 } 0467 0468 return m_currentRoom->eventToString(evt, Qt::RichText); 0469 } 0470 0471 if (role == GenericDisplayRole) { 0472 if (evt.isRedacted()) { 0473 return i18n("<i>[This message was deleted]</i>"); 0474 } 0475 0476 return m_currentRoom->eventToGenericString(evt); 0477 } 0478 0479 if (role == PlainText) { 0480 return m_currentRoom->eventToString(evt); 0481 } 0482 0483 if (role == SourceRole) { 0484 return QJsonDocument(evt.fullJson()).toJson(); 0485 } 0486 0487 if (role == DelegateTypeRole) { 0488 if (auto e = eventCast<const RoomMessageEvent>(&evt)) { 0489 switch (e->msgtype()) { 0490 case MessageEventType::Emote: 0491 return DelegateType::Emote; 0492 case MessageEventType::Notice: 0493 return DelegateType::Notice; 0494 case MessageEventType::Image: 0495 return DelegateType::Image; 0496 case MessageEventType::Audio: 0497 return DelegateType::Audio; 0498 case MessageEventType::Video: 0499 return DelegateType::Video; 0500 case MessageEventType::Location: 0501 return DelegateType::Location; 0502 default: 0503 break; 0504 } 0505 if (e->hasFileContent()) { 0506 return DelegateType::File; 0507 } 0508 0509 return DelegateType::Message; 0510 } 0511 if (is<const StickerEvent>(evt)) { 0512 return DelegateType::Sticker; 0513 } 0514 if (evt.isStateEvent()) { 0515 if (evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls) { 0516 return DelegateType::LiveLocation; 0517 } 0518 return DelegateType::State; 0519 } 0520 if (is<const EncryptedEvent>(evt)) { 0521 return DelegateType::Encrypted; 0522 } 0523 if (is<PollStartEvent>(evt)) { 0524 if (evt.isRedacted()) { 0525 return DelegateType::Message; 0526 } 0527 return DelegateType::Poll; 0528 } 0529 0530 return DelegateType::Other; 0531 } 0532 0533 if (role == AuthorRole) { 0534 auto author = static_cast<NeoChatUser *>(isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId())); 0535 return m_currentRoom->getUser(author); 0536 } 0537 0538 if (role == ContentRole) { 0539 if (evt.isRedacted()) { 0540 auto reason = evt.redactedBecause()->reason(); 0541 return (reason.isEmpty()) ? i18n("[REDACTED]") : i18n("[REDACTED: %1]").arg(evt.redactedBecause()->reason()); 0542 } 0543 0544 if (auto e = eventCast<const RoomMessageEvent>(&evt)) { 0545 if (e->msgtype() == Quotient::MessageEventType::Location) { 0546 return e->contentJson(); 0547 } 0548 // Cannot use e.contentJson() here because some 0549 // EventContent classes inject values into the copy of the 0550 // content JSON stored in EventContent::Base 0551 return e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant(); 0552 }; 0553 0554 if (auto e = eventCast<const StickerEvent>(&evt)) { 0555 return QVariant::fromValue(e->image().originalJson); 0556 } 0557 return evt.contentJson(); 0558 } 0559 0560 if (role == HighlightRole) { 0561 return !m_currentRoom->isDirectChat() && m_currentRoom->isEventHighlighted(&evt); 0562 } 0563 0564 if (role == MimeTypeRole) { 0565 if (auto e = eventCast<const RoomMessageEvent>(&evt)) { 0566 if (!e || !e->hasFileContent()) { 0567 return QVariant(); 0568 } 0569 0570 return e->content()->fileInfo()->mimeType.name(); 0571 } 0572 0573 if (auto e = eventCast<const StickerEvent>(&evt)) { 0574 return e->image().mimeType.name(); 0575 } 0576 } 0577 0578 if (role == SpecialMarksRole) { 0579 if (isPending) { 0580 // A pending event with an m.new_content key will be merged into the 0581 // original event so don't show. 0582 if (evt.contentJson().contains("m.new_content")) { 0583 return EventStatus::Hidden; 0584 } 0585 return pendingIt->deliveryStatus(); 0586 } 0587 0588 if (evt.isStateEvent() && !NeoChatConfig::self()->showStateEvent()) { 0589 return EventStatus::Hidden; 0590 } 0591 0592 if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt)) { 0593 if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) { 0594 return EventStatus::Hidden; 0595 } else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) { 0596 return EventStatus::Hidden; 0597 } else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() 0598 && !NeoChatConfig::self()->showAvatarUpdate()) { 0599 return EventStatus::Hidden; 0600 } 0601 } 0602 0603 // isReplacement? 0604 if (auto e = eventCast<const RoomMessageEvent>(&evt)) 0605 if (!e->replacedEvent().isEmpty()) 0606 return EventStatus::Hidden; 0607 0608 if (is<RedactionEvent>(evt) || is<ReactionEvent>(evt)) { 0609 return EventStatus::Hidden; 0610 } 0611 0612 if (evt.isStateEvent() && static_cast<const StateEvent &>(evt).repeatsState()) { 0613 return EventStatus::Hidden; 0614 } 0615 0616 if (auto e = eventCast<const RoomMessageEvent>(&evt)) { 0617 if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) { 0618 return EventStatus::Hidden; 0619 } 0620 } 0621 0622 if (m_currentRoom->connection()->isIgnored(m_currentRoom->user(evt.senderId()))) { 0623 return EventStatus::Hidden; 0624 } 0625 0626 // hide ending live location beacons 0627 if (evt.isStateEvent() && evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls && !evt.contentJson()["live"_ls].toBool()) { 0628 return EventStatus::Hidden; 0629 } 0630 0631 return EventStatus::Normal; 0632 } 0633 0634 if (role == EventIdRole) { 0635 return !evt.id().isEmpty() ? evt.id() : evt.transactionId(); 0636 } 0637 0638 if (role == ProgressInfoRole) { 0639 if (auto e = eventCast<const RoomMessageEvent>(&evt)) { 0640 if (e->hasFileContent()) { 0641 return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id())); 0642 } 0643 } 0644 if (auto e = eventCast<const StickerEvent>(&evt)) { 0645 return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id())); 0646 } 0647 } 0648 0649 if (role == TimeRole || role == SectionRole) { 0650 auto ts = isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt); 0651 return role == TimeRole ? QVariant(ts) : m_format.formatRelativeDate(ts.toLocalTime().date(), QLocale::ShortFormat); 0652 } 0653 0654 if (role == ShowLinkPreviewRole) { 0655 return m_linkPreviewers.contains(evt.id()); 0656 } 0657 0658 if (role == LinkPreviewRole) { 0659 if (m_linkPreviewers.contains(evt.id())) { 0660 return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewers[evt.id()]); 0661 } else { 0662 return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview); 0663 } 0664 } 0665 0666 if (role == MediaInfoRole) { 0667 return getMediaInfoForEvent(evt); 0668 } 0669 0670 if (role == IsReplyRole) { 0671 return !evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString().isEmpty(); 0672 } 0673 0674 if (role == ReplyIdRole) { 0675 return evt.contentJson()["m.relates_to"].toObject()["m.in_reply_to"].toObject()["event_id"].toString(); 0676 } 0677 0678 if (role == ReplyAuthor) { 0679 auto replyPtr = m_currentRoom->getReplyForEvent(evt); 0680 0681 if (replyPtr) { 0682 auto replyUser = static_cast<NeoChatUser *>(m_currentRoom->user(replyPtr->senderId())); 0683 return m_currentRoom->getUser(replyUser); 0684 } else { 0685 return m_currentRoom->getUser(nullptr); 0686 } 0687 } 0688 0689 if (role == ReplyMediaInfoRole) { 0690 auto replyPtr = m_currentRoom->getReplyForEvent(evt); 0691 if (!replyPtr) { 0692 return {}; 0693 } 0694 return getMediaInfoForEvent(*replyPtr); 0695 } 0696 0697 if (role == ReplyRole) { 0698 auto replyPtr = m_currentRoom->getReplyForEvent(evt); 0699 if (!replyPtr) { 0700 return {}; 0701 } 0702 0703 DelegateType type; 0704 if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) { 0705 switch (e->msgtype()) { 0706 case MessageEventType::Emote: 0707 type = DelegateType::Emote; 0708 break; 0709 case MessageEventType::Notice: 0710 type = DelegateType::Notice; 0711 break; 0712 case MessageEventType::Image: 0713 type = DelegateType::Image; 0714 break; 0715 case MessageEventType::Audio: 0716 type = DelegateType::Audio; 0717 break; 0718 case MessageEventType::Video: 0719 type = DelegateType::Video; 0720 break; 0721 default: 0722 if (e->hasFileContent()) { 0723 type = DelegateType::File; 0724 break; 0725 } 0726 type = DelegateType::Message; 0727 } 0728 0729 } else if (is<const StickerEvent>(*replyPtr)) { 0730 type = DelegateType::Sticker; 0731 } else { 0732 type = DelegateType::Other; 0733 } 0734 0735 return QVariantMap{ 0736 {"display", m_currentRoom->eventToString(*replyPtr, Qt::RichText)}, 0737 {"type", type}, 0738 }; 0739 } 0740 0741 if (role == ShowAuthorRole) { 0742 for (auto r = row + 1; r < rowCount(); ++r) { 0743 auto i = index(r); 0744 // Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved. 0745 // While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index. 0746 // See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows 0747 if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) { 0748 return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == MessageEventModel::State 0749 || data(i, TimeRole).toDateTime().msecsTo(data(idx, TimeRole).toDateTime()) > 600000 0750 || data(i, TimeRole).toDateTime().toLocalTime().date().day() != data(idx, TimeRole).toDateTime().toLocalTime().date().day(); 0751 } 0752 } 0753 0754 return true; 0755 } 0756 0757 if (role == ShowSectionRole) { 0758 for (auto r = row + 1; r < rowCount(); ++r) { 0759 auto i = index(r); 0760 // Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved. 0761 // While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index. 0762 // See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows 0763 if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) { 0764 const auto day = data(idx, TimeRole).toDateTime().toLocalTime().date().dayOfYear(); 0765 const auto previousEventDay = data(i, TimeRole).toDateTime().toLocalTime().date().dayOfYear(); 0766 return day != previousEventDay; 0767 } 0768 } 0769 0770 return false; 0771 } 0772 0773 if (role == LatitudeRole) { 0774 const auto geoUri = evt.contentJson()["geo_uri"_ls].toString(); 0775 if (geoUri.isEmpty()) { 0776 return {}; 0777 } 0778 const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0]; 0779 return latitude.toFloat(); 0780 } 0781 0782 if (role == LongitudeRole) { 0783 const auto geoUri = evt.contentJson()["geo_uri"_ls].toString(); 0784 if (geoUri.isEmpty()) { 0785 return {}; 0786 } 0787 const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1]; 0788 return latitude.toFloat(); 0789 } 0790 0791 if (role == AssetRole) { 0792 const auto assetType = evt.contentJson()["org.matrix.msc3488.asset"].toObject()["type"].toString(); 0793 if (assetType.isEmpty()) { 0794 return {}; 0795 } 0796 return assetType; 0797 } 0798 0799 if (role == ReadMarkersRole) { 0800 auto userIds_temp = room()->userIdsAtEvent(evt.id()); 0801 userIds_temp.remove(m_currentRoom->localUser()->id()); 0802 0803 auto userIds = userIds_temp.values(); 0804 if (userIds.count() > 5) { 0805 userIds = userIds.mid(0, 5); 0806 } 0807 0808 QVariantList users; 0809 users.reserve(userIds.size()); 0810 for (const auto &userId : userIds) { 0811 auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId)); 0812 users += m_currentRoom->getUser(user); 0813 } 0814 0815 return users; 0816 } 0817 0818 if (role == ExcessReadMarkersRole) { 0819 auto userIds = room()->userIdsAtEvent(evt.id()); 0820 userIds.remove(m_currentRoom->localUser()->id()); 0821 0822 if (userIds.count() > 5) { 0823 return QStringLiteral("+ ") + QString::number(userIds.count() - 5); 0824 } else { 0825 return QString(); 0826 } 0827 } 0828 0829 if (role == ReadMarkersStringRole) { 0830 auto userIds = room()->userIdsAtEvent(evt.id()); 0831 userIds.remove(m_currentRoom->localUser()->id()); 0832 0833 /** 0834 * The string ends up in the form 0835 * "x users: user1DisplayName, user2DisplayName, etc." 0836 */ 0837 QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size()); 0838 for (const auto &userId : userIds) { 0839 auto user = static_cast<NeoChatUser *>(m_currentRoom->user(userId)); 0840 readMarkersString += user->displayname(m_currentRoom) + i18nc("list separator", ", "); 0841 } 0842 readMarkersString.chop(2); 0843 return readMarkersString; 0844 } 0845 0846 if (role == ShowReadMarkersRole) { 0847 auto userIds = room()->userIdsAtEvent(evt.id()); 0848 userIds.remove(m_currentRoom->localUser()->id()); 0849 return userIds.size() > 0; 0850 } 0851 0852 if (role == ReactionRole) { 0853 if (m_reactionModels.contains(evt.id())) { 0854 return QVariant::fromValue<ReactionModel *>(m_reactionModels[evt.id()]); 0855 } else { 0856 return QVariantList(); 0857 } 0858 } 0859 0860 if (role == ShowReactionsRole) { 0861 return m_reactionModels.contains(evt.id()); 0862 } 0863 0864 if (role == AuthorIdRole) { 0865 return evt.senderId(); 0866 } 0867 0868 if (role == VerifiedRole) { 0869 #ifdef Quotient_E2EE_ENABLED 0870 if (evt.originalEvent()) { 0871 auto encrypted = dynamic_cast<const EncryptedEvent *>(evt.originalEvent()); 0872 Q_ASSERT(encrypted); 0873 return m_currentRoom->connection()->isVerifiedSession(encrypted->sessionId().toLatin1()); 0874 } 0875 #endif 0876 return false; 0877 } 0878 0879 if (role == DisplayNameForInitialsRole) { 0880 auto user = static_cast<NeoChatUser *>(isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId())); 0881 return user->displayname(m_currentRoom).remove(QStringLiteral(" (%1)").arg(user->id())); 0882 } 0883 0884 if (role == AuthorDisplayNameRole) { 0885 if (is<RoomMemberEvent>(evt) && !evt.unsignedJson()["prev_content"]["displayname"].isNull() && evt.stateKey() == evt.senderId()) { 0886 auto previousDisplayName = evt.unsignedJson()["prev_content"]["displayname"].toString().toHtmlEscaped(); 0887 if (previousDisplayName.isEmpty()) { 0888 previousDisplayName = evt.senderId(); 0889 } 0890 return previousDisplayName; 0891 } else { 0892 auto author = static_cast<NeoChatUser *>(isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId())); 0893 return m_currentRoom->htmlSafeMemberName(author->id()); 0894 } 0895 } 0896 0897 if (role == IsRedactedRole) { 0898 return evt.isRedacted(); 0899 } 0900 0901 if (role == IsPendingRole) { 0902 return row < static_cast<int>(m_currentRoom->pendingEvents().size()); 0903 } 0904 0905 return {}; 0906 } 0907 0908 int MessageEventModel::eventIdToRow(const QString &eventID) const 0909 { 0910 const auto it = m_currentRoom->findInTimeline(eventID); 0911 if (it == m_currentRoom->historyEdge()) { 0912 // qWarning() << "Trying to find inexistent event:" << eventID; 0913 return -1; 0914 } 0915 return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex(); 0916 } 0917 0918 QVariantMap MessageEventModel::getMediaInfoForEvent(const RoomEvent &event) const 0919 { 0920 QVariantMap mediaInfo; 0921 0922 QString eventId = event.id(); 0923 0924 // Get the file info for the event. 0925 const EventContent::FileInfo *fileInfo; 0926 if (event.is<RoomMessageEvent>()) { 0927 auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event); 0928 if (!roomMessageEvent->hasFileContent()) { 0929 return {}; 0930 } 0931 fileInfo = roomMessageEvent->content()->fileInfo(); 0932 } else if (event.is<StickerEvent>()) { 0933 auto stickerEvent = eventCast<const StickerEvent>(&event); 0934 fileInfo = &stickerEvent->image(); 0935 } else { 0936 return {}; 0937 } 0938 0939 return getMediaInfoFromFileInfo(fileInfo, eventId); 0940 } 0941 0942 QVariantMap MessageEventModel::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const 0943 { 0944 QVariantMap mediaInfo; 0945 0946 // Get the mxc URL for the media. 0947 if (!fileInfo->url().isValid() || fileInfo->url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) { 0948 mediaInfo["source"] = QUrl(); 0949 } else { 0950 QUrl source = m_currentRoom->makeMediaUrl(eventId, fileInfo->url()); 0951 0952 if (source.isValid()) { 0953 mediaInfo["source"] = source; 0954 } else { 0955 mediaInfo["source"] = QUrl(); 0956 } 0957 } 0958 0959 auto mimeType = fileInfo->mimeType; 0960 // Add the MIME type for the media if available. 0961 mediaInfo["mimeType"] = mimeType.name(); 0962 0963 // Add the MIME type icon if available. 0964 mediaInfo["mimeIcon"] = mimeType.iconName(); 0965 0966 // Add media size if available. 0967 mediaInfo["size"] = fileInfo->payloadSize; 0968 0969 // Add parameter depending on media type. 0970 if (mimeType.name().contains(QStringLiteral("image"))) { 0971 if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) { 0972 mediaInfo["width"] = castInfo->imageSize.width(); 0973 mediaInfo["height"] = castInfo->imageSize.height(); 0974 0975 if (!isThumbnail) { 0976 QVariantMap tempInfo; 0977 auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true); 0978 if (thumbnailInfo["source"].toUrl().scheme() == "mxc") { 0979 tempInfo = thumbnailInfo; 0980 } else { 0981 QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"].toString(); 0982 if (blurhash.isEmpty()) { 0983 tempInfo["source"] = QUrl(); 0984 } else { 0985 tempInfo["source"] = QUrl("image://blurhash/" + blurhash); 0986 } 0987 } 0988 mediaInfo["tempInfo"] = tempInfo; 0989 } 0990 } 0991 } 0992 if (mimeType.name().contains(QStringLiteral("video"))) { 0993 if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) { 0994 mediaInfo["width"] = castInfo->imageSize.width(); 0995 mediaInfo["height"] = castInfo->imageSize.height(); 0996 mediaInfo["duration"] = castInfo->duration; 0997 0998 if (!isThumbnail) { 0999 QVariantMap tempInfo; 1000 auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true); 1001 if (thumbnailInfo["source"].toUrl().scheme() == "mxc") { 1002 tempInfo = thumbnailInfo; 1003 } else { 1004 QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"].toString(); 1005 if (blurhash.isEmpty()) { 1006 tempInfo["source"] = QUrl(); 1007 } else { 1008 tempInfo["source"] = QUrl("image://blurhash/" + blurhash); 1009 } 1010 } 1011 mediaInfo["tempInfo"] = tempInfo; 1012 } 1013 } 1014 } 1015 if (mimeType.name().contains(QStringLiteral("audio"))) { 1016 if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) { 1017 mediaInfo["duration"] = castInfo->duration; 1018 } 1019 } 1020 1021 return mediaInfo; 1022 } 1023 1024 void MessageEventModel::createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event) 1025 { 1026 if (m_linkPreviewers.contains(event->id())) { 1027 return; 1028 } else { 1029 QString text; 1030 if (event->hasTextContent()) { 1031 auto textContent = static_cast<const EventContent::TextContent *>(event->content()); 1032 if (textContent) { 1033 text = textContent->body; 1034 } else { 1035 text = event->plainBody(); 1036 } 1037 } else { 1038 text = event->plainBody(); 1039 } 1040 TextHandler textHandler; 1041 textHandler.setData(text); 1042 1043 QList<QUrl> links = textHandler.getLinkPreviews(); 1044 if (links.size() > 0) { 1045 m_linkPreviewers[event->id()] = new LinkPreviewer(nullptr, m_currentRoom, links.size() > 0 ? links[0] : QUrl()); 1046 } 1047 } 1048 } 1049 1050 void MessageEventModel::createReactionModelForEvent(const Quotient::RoomMessageEvent *event) 1051 { 1052 if (event == nullptr) { 1053 return; 1054 } 1055 auto eventId = event->id(); 1056 const auto &annotations = m_currentRoom->relatedEvents(eventId, EventRelation::AnnotationType); 1057 if (annotations.isEmpty()) { 1058 if (m_reactionModels.contains(eventId)) { 1059 delete m_reactionModels[eventId]; 1060 m_reactionModels.remove(eventId); 1061 } 1062 return; 1063 }; 1064 1065 QMap<QString, QList<NeoChatUser *>> reactions = {}; 1066 for (const auto &a : annotations) { 1067 if (a->isRedacted()) { // Just in case? 1068 continue; 1069 } 1070 if (const auto &e = eventCast<const ReactionEvent>(a)) { 1071 reactions[e->key()].append(static_cast<NeoChatUser *>(m_currentRoom->user(e->senderId()))); 1072 } 1073 } 1074 1075 if (reactions.isEmpty()) { 1076 if (m_reactionModels.contains(eventId)) { 1077 delete m_reactionModels[eventId]; 1078 m_reactionModels.remove(eventId); 1079 } 1080 return; 1081 } 1082 1083 QList<ReactionModel::Reaction> res; 1084 auto i = reactions.constBegin(); 1085 while (i != reactions.constEnd()) { 1086 QVariantList authors; 1087 for (const auto &author : i.value()) { 1088 authors.append(m_currentRoom->getUser(author)); 1089 } 1090 1091 res.append(ReactionModel::Reaction{i.key(), authors}); 1092 ++i; 1093 } 1094 1095 if (m_reactionModels.contains(eventId)) { 1096 m_reactionModels[eventId]->setReactions(res); 1097 } else if (res.size() > 0) { 1098 m_reactionModels[eventId] = new ReactionModel(this, res, static_cast<NeoChatUser *>(m_currentRoom->localUser())); 1099 } else { 1100 if (m_reactionModels.contains(eventId)) { 1101 delete m_reactionModels[eventId]; 1102 m_reactionModels.remove(eventId); 1103 } 1104 } 1105 } 1106 1107 #include "moc_messageeventmodel.cpp"