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"