File indexing completed on 2024-04-28 08:51:59

0001 // SPDX-FileCopyrightText: 2018-2020 Black Hat <bhat@encom.eu.org>
0002 // SPDX-License-Identifier: GPL-3.0-only
0003 
0004 #include "neochatroom.h"
0005 
0006 #include <QFileInfo>
0007 #include <QMediaMetaData>
0008 #include <QMediaPlayer>
0009 #include <QMimeDatabase>
0010 #include <QTemporaryFile>
0011 
0012 #include <Quotient/jobs/basejob.h>
0013 #include <Quotient/user.h>
0014 #include <qcoro/qcorosignal.h>
0015 
0016 #include <Quotient/connection.h>
0017 #include <Quotient/csapi/account-data.h>
0018 #include <Quotient/csapi/directory.h>
0019 #include <Quotient/csapi/pushrules.h>
0020 #include <Quotient/csapi/redaction.h>
0021 #include <Quotient/csapi/report_content.h>
0022 #include <Quotient/csapi/room_state.h>
0023 #include <Quotient/csapi/rooms.h>
0024 #include <Quotient/csapi/typing.h>
0025 #include <Quotient/events/encryptionevent.h>
0026 #include <Quotient/events/reactionevent.h>
0027 #include <Quotient/events/redactionevent.h>
0028 #include <Quotient/events/roomavatarevent.h>
0029 #include <Quotient/events/roomcanonicalaliasevent.h>
0030 #include <Quotient/events/roommemberevent.h>
0031 #include <Quotient/events/roompowerlevelsevent.h>
0032 #include <Quotient/events/simplestateevents.h>
0033 #include <Quotient/jobs/downloadfilejob.h>
0034 #include <Quotient/qt_connection_util.h>
0035 
0036 #include "chatbarcache.h"
0037 #include "clipboard.h"
0038 #include "eventhandler.h"
0039 #include "events/joinrulesevent.h"
0040 #include "events/pollevent.h"
0041 #include "filetransferpseudojob.h"
0042 #include "neochatconfig.h"
0043 #include "notificationsmanager.h"
0044 #include "roomlastmessageprovider.h"
0045 #include "texthandler.h"
0046 #include "urlhelper.h"
0047 #include "utils.h"
0048 
0049 #ifndef Q_OS_ANDROID
0050 #include <KIO/Job>
0051 #include <KIO/JobTracker>
0052 #endif
0053 #include <KJobTrackerInterface>
0054 #include <KLocalizedString>
0055 
0056 using namespace Quotient;
0057 
0058 NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinState)
0059     : Room(connection, std::move(roomId), joinState)
0060 {
0061     m_mainCache = new ChatBarCache(this);
0062     m_editCache = new ChatBarCache(this);
0063 
0064     connect(connection, &Connection::accountDataChanged, this, &NeoChatRoom::updatePushNotificationState);
0065     connect(this, &Room::fileTransferCompleted, this, [this] {
0066         setFileUploadingProgress(0);
0067         setHasFileUploading(false);
0068     });
0069 
0070     connect(this, &Room::aboutToAddHistoricalMessages, this, &NeoChatRoom::readMarkerLoadedChanged);
0071 
0072     const auto &roomLastMessageProvider = RoomLastMessageProvider::self();
0073 
0074     if (roomLastMessageProvider.hasKey(id())) {
0075         auto eventJson = QJsonDocument::fromJson(roomLastMessageProvider.read(id())).object();
0076         if (!eventJson.isEmpty()) {
0077             auto event = loadEvent<RoomEvent>(eventJson);
0078 
0079             if (event != nullptr) {
0080                 m_cachedEvent = std::move(event);
0081             }
0082         }
0083     }
0084     connect(this, &Room::addedMessages, this, &NeoChatRoom::cacheLastEvent);
0085 
0086     connect(this, &Quotient::Room::eventsHistoryJobChanged, this, &NeoChatRoom::lastActiveTimeChanged);
0087 
0088     connect(this, &Room::joinStateChanged, this, [this](JoinState oldState, JoinState newState) {
0089         if (oldState == JoinState::Invite && newState != JoinState::Invite) {
0090             Q_EMIT isInviteChanged();
0091         }
0092     });
0093     connect(this, &Room::displaynameChanged, this, &NeoChatRoom::displayNameChanged);
0094 
0095     connectSingleShot(this, &Room::baseStateLoaded, this, [this]() {
0096         updatePushNotificationState(QStringLiteral("m.push_rules"));
0097 
0098         Q_EMIT canEncryptRoomChanged();
0099         if (this->joinState() != JoinState::Invite) {
0100             return;
0101         }
0102         auto roomMemberEvent = currentState().get<RoomMemberEvent>(localUser()->id());
0103         QImage avatar_image;
0104         if (roomMemberEvent && !user(roomMemberEvent->senderId())->avatarUrl(this).isEmpty()) {
0105             avatar_image = user(roomMemberEvent->senderId())->avatar(128, this);
0106         } else {
0107             qWarning() << "using this room's avatar";
0108             avatar_image = avatar(128);
0109         }
0110         NotificationsManager::instance().postInviteNotification(this, displayName(), htmlSafeMemberName(roomMemberEvent->senderId()), avatar_image);
0111     });
0112     connect(this, &Room::changed, this, [this] {
0113         Q_EMIT canEncryptRoomChanged();
0114         Q_EMIT parentIdsChanged();
0115         Q_EMIT canonicalParentChanged();
0116         Q_EMIT joinRuleChanged();
0117         Q_EMIT readOnlyChanged();
0118     });
0119     connect(connection, &Connection::capabilitiesLoaded, this, &NeoChatRoom::maxRoomVersionChanged);
0120     connect(this, &Room::changed, this, [this]() {
0121         Q_EMIT defaultUrlPreviewStateChanged();
0122     });
0123     connect(this, &Room::accountDataChanged, this, [this](QString type) {
0124         if (type == "org.matrix.room.preview_urls"_ls) {
0125             Q_EMIT urlPreviewEnabledChanged();
0126         }
0127     });
0128 }
0129 
0130 bool NeoChatRoom::hasFileUploading() const
0131 {
0132     return m_hasFileUploading;
0133 }
0134 
0135 void NeoChatRoom::setHasFileUploading(bool value)
0136 {
0137     if (value == m_hasFileUploading) {
0138         return;
0139     }
0140     m_hasFileUploading = value;
0141     Q_EMIT hasFileUploadingChanged();
0142 }
0143 
0144 int NeoChatRoom::fileUploadingProgress() const
0145 {
0146     return m_fileUploadingProgress;
0147 }
0148 
0149 void NeoChatRoom::setFileUploadingProgress(int value)
0150 {
0151     if (m_fileUploadingProgress == value) {
0152         return;
0153     }
0154     m_fileUploadingProgress = value;
0155     Q_EMIT fileUploadingProgressChanged();
0156 }
0157 
0158 void NeoChatRoom::uploadFile(const QUrl &url, const QString &body)
0159 {
0160     doUploadFile(url, body);
0161 }
0162 
0163 QCoro::Task<void> NeoChatRoom::doUploadFile(QUrl url, QString body)
0164 {
0165     if (url.isEmpty()) {
0166         co_return;
0167     }
0168 
0169     auto mime = QMimeDatabase().mimeTypeForUrl(url);
0170     url.setScheme("file"_ls);
0171     QFileInfo fileInfo(url.isLocalFile() ? url.toLocalFile() : url.toString());
0172     EventContent::TypedBase *content;
0173     if (mime.name().startsWith("image/"_ls)) {
0174         QImage image(url.toLocalFile());
0175         content = new EventContent::ImageContent(url, fileInfo.size(), mime, image.size(), fileInfo.fileName());
0176     } else if (mime.name().startsWith("audio/"_ls)) {
0177         content = new EventContent::AudioContent(url, fileInfo.size(), mime, fileInfo.fileName());
0178     } else if (mime.name().startsWith("video/"_ls)) {
0179         QMediaPlayer player;
0180         player.setSource(url);
0181         co_await qCoro(&player, &QMediaPlayer::mediaStatusChanged);
0182         auto resolution = player.metaData().value(QMediaMetaData::Resolution).toSize();
0183         content = new EventContent::VideoContent(url, fileInfo.size(), mime, resolution, fileInfo.fileName());
0184     } else {
0185         content = new EventContent::FileContent(url, fileInfo.size(), mime, fileInfo.fileName());
0186     }
0187     QString txnId = postFile(body.isEmpty() ? url.fileName() : body, content);
0188     setHasFileUploading(true);
0189     connect(this, &Room::fileTransferCompleted, [this, txnId](const QString &id, FileSourceInfo) {
0190         if (id == txnId) {
0191             setFileUploadingProgress(0);
0192             setHasFileUploading(false);
0193         }
0194     });
0195     connect(this, &Room::fileTransferFailed, [this, txnId](const QString &id, const QString & /*error*/) {
0196         if (id == txnId) {
0197             setFileUploadingProgress(0);
0198             setHasFileUploading(false);
0199         }
0200     });
0201     connect(this, &Room::fileTransferProgress, [this, txnId](const QString &id, qint64 progress, qint64 total) {
0202         if (id == txnId) {
0203             setFileUploadingProgress(int(float(progress) / float(total) * 100));
0204         }
0205     });
0206 #ifndef Q_OS_ANDROID
0207     auto job = new FileTransferPseudoJob(FileTransferPseudoJob::Upload, url.toLocalFile(), txnId);
0208     connect(this, &Room::fileTransferProgress, job, &FileTransferPseudoJob::fileTransferProgress);
0209     connect(this, &Room::fileTransferCompleted, job, &FileTransferPseudoJob::fileTransferCompleted);
0210     connect(this, &Room::fileTransferFailed, job, &FileTransferPseudoJob::fileTransferFailed);
0211     KIO::getJobTracker()->registerJob(job);
0212     job->start();
0213 #endif
0214 }
0215 
0216 void NeoChatRoom::acceptInvitation()
0217 {
0218     connection()->joinRoom(id());
0219 }
0220 
0221 void NeoChatRoom::forget()
0222 {
0223     connection()->forgetRoom(id());
0224 }
0225 
0226 QVariantList NeoChatRoom::getUsersTyping() const
0227 {
0228     auto users = usersTyping();
0229     users.removeAll(localUser());
0230     QVariantList userVariants;
0231     for (User *user : users) {
0232         userVariants.append(QVariantMap{
0233             {"id"_ls, user->id()},
0234             {"avatarMediaId"_ls, user->avatarMediaId(this)},
0235             {"displayName"_ls, user->displayname(this)},
0236             {"display"_ls, user->name()},
0237         });
0238     }
0239     return userVariants;
0240 }
0241 
0242 void NeoChatRoom::sendTypingNotification(bool isTyping)
0243 {
0244     connection()->callApi<SetTypingJob>(BackgroundRequest, localUser()->id(), id(), isTyping, 10000);
0245 }
0246 
0247 const RoomEvent *NeoChatRoom::lastEvent() const
0248 {
0249     for (auto timelineItem = messageEvents().rbegin(); timelineItem < messageEvents().rend(); timelineItem++) {
0250         const RoomEvent *event = timelineItem->get();
0251 
0252         if (is<RedactionEvent>(*event) || is<ReactionEvent>(*event)) {
0253             continue;
0254         }
0255         if (event->isRedacted()) {
0256             continue;
0257         }
0258 
0259         if (event->isStateEvent() && !NeoChatConfig::showStateEvent()) {
0260             continue;
0261         }
0262 
0263         if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(event)) {
0264             if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::showLeaveJoinEvent()) {
0265                 continue;
0266             } else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showRename()) {
0267                 continue;
0268             } else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::showAvatarUpdate()) {
0269                 continue;
0270             }
0271         }
0272         if (event->isStateEvent() && static_cast<const StateEvent &>(*event).repeatsState()) {
0273             continue;
0274         }
0275 
0276         if (auto roomEvent = eventCast<const RoomMessageEvent>(event)) {
0277             if (!roomEvent->replacedEvent().isEmpty() && roomEvent->replacedEvent() != roomEvent->id()) {
0278                 continue;
0279             }
0280         }
0281 
0282         if (connection()->isIgnored(user(event->senderId()))) {
0283             continue;
0284         }
0285 
0286         if (auto lastEvent = eventCast<const StateEvent>(event)) {
0287             return lastEvent;
0288         }
0289 
0290         if (auto lastEvent = eventCast<const RoomMessageEvent>(event)) {
0291             return lastEvent;
0292         }
0293         if (auto lastEvent = eventCast<const PollStartEvent>(event)) {
0294             return lastEvent;
0295         }
0296     }
0297 
0298     if (m_cachedEvent != nullptr) {
0299         return std::to_address(m_cachedEvent);
0300     }
0301 
0302     return nullptr;
0303 }
0304 
0305 void NeoChatRoom::cacheLastEvent()
0306 {
0307     auto event = lastEvent();
0308     if (event != nullptr) {
0309         auto &roomLastMessageProvider = RoomLastMessageProvider::self();
0310 
0311         auto eventJson = QJsonDocument(event->fullJson()).toJson(QJsonDocument::Compact);
0312         roomLastMessageProvider.write(id(), eventJson);
0313 
0314         auto uniqueEvent = loadEvent<RoomEvent>(event->fullJson());
0315 
0316         if (event != nullptr) {
0317             m_cachedEvent = std::move(uniqueEvent);
0318         }
0319     }
0320 }
0321 
0322 bool NeoChatRoom::lastEventIsSpoiler() const
0323 {
0324     if (auto event = lastEvent()) {
0325         if (auto e = eventCast<const RoomMessageEvent>(event)) {
0326             if (e->hasTextContent() && e->content() && e->mimeType().name() == "text/html"_ls) {
0327                 auto htmlBody = static_cast<const Quotient::EventContent::TextContent *>(e->content())->body;
0328                 return htmlBody.contains("data-mx-spoiler"_ls);
0329             }
0330         }
0331     }
0332     return false;
0333 }
0334 
0335 bool NeoChatRoom::isEventHighlighted(const RoomEvent *e) const
0336 {
0337     return highlights.contains(e);
0338 }
0339 
0340 void NeoChatRoom::checkForHighlights(const Quotient::TimelineItem &ti)
0341 {
0342     auto localUserId = localUser()->id();
0343     if (ti->senderId() == localUserId) {
0344         return;
0345     }
0346     if (auto *e = ti.viewAs<RoomMessageEvent>()) {
0347         const auto &text = e->plainBody();
0348         if (text.contains(localUserId) || text.contains(safeMemberName(localUserId))) {
0349             highlights.insert(e);
0350         }
0351     }
0352 }
0353 
0354 void NeoChatRoom::onAddNewTimelineEvents(timeline_iter_t from)
0355 {
0356     std::for_each(from, messageEvents().cend(), [this](const TimelineItem &ti) {
0357         checkForHighlights(ti);
0358     });
0359 }
0360 
0361 void NeoChatRoom::onAddHistoricalTimelineEvents(rev_iter_t from)
0362 {
0363     std::for_each(from, messageEvents().crend(), [this](const TimelineItem &ti) {
0364         checkForHighlights(ti);
0365     });
0366 }
0367 
0368 void NeoChatRoom::onRedaction(const RoomEvent &prevEvent, const RoomEvent & /*after*/)
0369 {
0370     if (const auto &e = eventCast<const ReactionEvent>(&prevEvent)) {
0371         if (auto relatedEventId = e->eventId(); !relatedEventId.isEmpty()) {
0372             Q_EMIT updatedEvent(relatedEventId);
0373         }
0374     }
0375 }
0376 
0377 QDateTime NeoChatRoom::lastActiveTime()
0378 {
0379     if (timelineSize() == 0) {
0380         if (m_cachedEvent != nullptr) {
0381             return m_cachedEvent->originTimestamp();
0382         }
0383         return QDateTime();
0384     }
0385 
0386     if (auto event = lastEvent()) {
0387         return event->originTimestamp();
0388     }
0389 
0390     // no message found, take last event
0391     return messageEvents().rbegin()->get()->originTimestamp();
0392 }
0393 
0394 // An empty user is useful for returning as a model value to avoid properties being undefined.
0395 static const QVariantMap emptyUser = {
0396     {"isLocalUser"_ls, false},
0397     {"id"_ls, QString()},
0398     {"displayName"_ls, QString()},
0399     {"avatarSource"_ls, QUrl()},
0400     {"avatarMediaId"_ls, QString()},
0401     {"color"_ls, QColor()},
0402     {"object"_ls, QVariant()},
0403 };
0404 
0405 QVariantMap NeoChatRoom::getUser(const QString &userID) const
0406 {
0407     return getUser(user(userID));
0408 }
0409 
0410 QVariantMap NeoChatRoom::getUser(User *user) const
0411 {
0412     if (user == nullptr) {
0413         return emptyUser;
0414     }
0415 
0416     return QVariantMap{
0417         {QStringLiteral("isLocalUser"), user->id() == localUser()->id()},
0418         {QStringLiteral("id"), user->id()},
0419         {QStringLiteral("displayName"), user->displayname(this)},
0420         {QStringLiteral("escapedDisplayName"), htmlSafeMemberName(user->id())},
0421         {QStringLiteral("avatarSource"), avatarForMember(user)},
0422         {QStringLiteral("avatarMediaId"), user->avatarMediaId(this)},
0423         {QStringLiteral("color"), Utils::getUserColor(user->hueF())},
0424         {QStringLiteral("object"), QVariant::fromValue(user)},
0425     };
0426 }
0427 
0428 QString NeoChatRoom::avatarMediaId() const
0429 {
0430     if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) {
0431         return avatar;
0432     }
0433 
0434     // Use the first (excluding self) user's avatar for direct chats
0435     const auto dcUsers = directChatUsers();
0436     for (const auto u : dcUsers) {
0437         if (u != localUser()) {
0438             return u->avatarMediaId(this);
0439         }
0440     }
0441 
0442     return {};
0443 }
0444 
0445 void NeoChatRoom::changeAvatar(const QUrl &localFile)
0446 {
0447     const auto job = connection()->uploadFile(localFile.toLocalFile());
0448     if (isJobPending(job)) {
0449         connect(job, &BaseJob::success, this, [this, job] {
0450             connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar"_ls, QString(), QJsonObject{{"url"_ls, job->contentUri().toString()}});
0451         });
0452     }
0453 }
0454 
0455 QString msgTypeToString(MessageEventType msgType)
0456 {
0457     switch (msgType) {
0458     case MessageEventType::Text:
0459         return "m.text"_ls;
0460     case MessageEventType::File:
0461         return "m.file"_ls;
0462     case MessageEventType::Audio:
0463         return "m.audio"_ls;
0464     case MessageEventType::Emote:
0465         return "m.emote"_ls;
0466     case MessageEventType::Image:
0467         return "m.image"_ls;
0468     case MessageEventType::Video:
0469         return "m.video"_ls;
0470     case MessageEventType::Notice:
0471         return "m.notice"_ls;
0472     case MessageEventType::Location:
0473         return "m.location"_ls;
0474     default:
0475         return "m.text"_ls;
0476     }
0477 }
0478 
0479 void NeoChatRoom::postMessage(const QString &rawText,
0480                               const QString &text,
0481                               MessageEventType type,
0482                               const QString &replyEventId,
0483                               const QString &relateToEventId,
0484                               const QString &threadRootId)
0485 {
0486     postHtmlMessage(rawText, text, type, replyEventId, relateToEventId, threadRootId);
0487 }
0488 
0489 void NeoChatRoom::postHtmlMessage(const QString &text,
0490                                   const QString &html,
0491                                   MessageEventType type,
0492                                   const QString &replyEventId,
0493                                   const QString &relateToEventId,
0494                                   const QString &threadRootId)
0495 {
0496     bool isReply = !replyEventId.isEmpty();
0497     bool isEdit = !relateToEventId.isEmpty();
0498     bool isThread = !threadRootId.isEmpty();
0499     const auto replyIt = findInTimeline(replyEventId);
0500     if (replyIt == historyEdge()) {
0501         isReply = false;
0502     }
0503 
0504     if (isThread) {
0505         EventHandler eventHandler;
0506         eventHandler.setRoom(this);
0507         eventHandler.setEvent(&**replyIt);
0508 
0509         const bool isFallingBack = !eventHandler.isThreaded();
0510 
0511         // clang-format off
0512         QJsonObject json{
0513           {"msgtype"_ls, msgTypeToString(type)},
0514           {"body"_ls, text},
0515           {"format"_ls, "org.matrix.custom.html"_ls},
0516           {"m.relates_to"_ls,
0517             QJsonObject {
0518               {"rel_type"_ls, "m.thread"_ls},
0519               {"event_id"_ls, threadRootId},
0520               {"is_falling_back"_ls, isFallingBack},
0521               {"m.in_reply_to"_ls,
0522                 QJsonObject {
0523                   {"event_id"_ls, replyEventId}
0524                 }
0525               }
0526             }
0527           },
0528           {"formatted_body"_ls, html}
0529         };
0530         // clang-format on
0531 
0532         postJson("m.room.message"_ls, json);
0533         return;
0534     }
0535 
0536     if (isEdit) {
0537         QJsonObject json{
0538             {"type"_ls, "m.room.message"_ls},
0539             {"msgtype"_ls, msgTypeToString(type)},
0540             {"body"_ls, "* %1"_ls.arg(text)},
0541             {"format"_ls, "org.matrix.custom.html"_ls},
0542             {"formatted_body"_ls, html},
0543             {"m.new_content"_ls,
0544              QJsonObject{{"body"_ls, text}, {"msgtype"_ls, msgTypeToString(type)}, {"format"_ls, "org.matrix.custom.html"_ls}, {"formatted_body"_ls, html}}},
0545             {"m.relates_to"_ls, QJsonObject{{"rel_type"_ls, "m.replace"_ls}, {"event_id"_ls, relateToEventId}}}};
0546 
0547         postJson("m.room.message"_ls, json);
0548         return;
0549     }
0550 
0551     if (isReply) {
0552         const auto &replyEvt = **replyIt;
0553 
0554         EventHandler eventHandler;
0555         eventHandler.setRoom(this);
0556         eventHandler.setEvent(&**replyIt);
0557 
0558         // clang-format off
0559         QJsonObject json{
0560           {"msgtype"_ls, msgTypeToString(type)},
0561           {"body"_ls, "> <%1> %2\n\n%3"_ls.arg(replyEvt.senderId(), eventHandler.getPlainBody(), text)},
0562           {"format"_ls, "org.matrix.custom.html"_ls},
0563           {"m.relates_to"_ls,
0564             QJsonObject {
0565               {"m.in_reply_to"_ls,
0566                 QJsonObject {
0567                   {"event_id"_ls, replyEventId}
0568                 }
0569               }
0570             }
0571           },
0572           {"formatted_body"_ls,
0573               "<mx-reply><blockquote><a href=\"https://matrix.to/#/%1/%2\">In reply to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br>%5</blockquote></mx-reply>%6"_ls.arg(id(), replyEventId, replyEvt.senderId(), replyEvt.senderId(), eventHandler.getRichBody(), html)
0574           }
0575         };
0576         // clang-format on
0577 
0578         postJson("m.room.message"_ls, json);
0579 
0580         return;
0581     }
0582 
0583     Room::postHtmlMessage(text, html, type);
0584 }
0585 
0586 void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction)
0587 {
0588     if (eventId.isEmpty() || reaction.isEmpty()) {
0589         return;
0590     }
0591 
0592     const auto eventIt = findInTimeline(eventId);
0593     if (eventIt == historyEdge()) {
0594         return;
0595     }
0596 
0597     const auto &evt = **eventIt;
0598 
0599     QStringList redactEventIds; // What if there are multiple reaction events?
0600 
0601     const auto &annotations = relatedEvents(evt, EventRelation::AnnotationType);
0602     if (!annotations.isEmpty()) {
0603         for (const auto &a : annotations) {
0604             if (auto e = eventCast<const ReactionEvent>(a)) {
0605                 if (e->key() != reaction) {
0606                     continue;
0607                 }
0608 
0609                 if (e->senderId() == localUser()->id()) {
0610                     redactEventIds.push_back(e->id());
0611                     break;
0612                 }
0613             }
0614         }
0615     }
0616 
0617     if (!redactEventIds.isEmpty()) {
0618         for (const auto &redactEventId : redactEventIds) {
0619             redactEvent(redactEventId);
0620         }
0621     } else {
0622         postReaction(eventId, reaction);
0623     }
0624 }
0625 
0626 bool NeoChatRoom::containsUser(const QString &userID) const
0627 {
0628     return memberState(userID) != Membership::Leave;
0629 }
0630 
0631 bool NeoChatRoom::canSendEvent(const QString &eventType) const
0632 {
0633     auto plEvent = currentState().get<RoomPowerLevelsEvent>();
0634     if (!plEvent) {
0635         return false;
0636     }
0637     auto pl = plEvent->powerLevelForEvent(eventType);
0638     auto currentPl = plEvent->powerLevelForUser(localUser()->id());
0639 
0640     return currentPl >= pl;
0641 }
0642 
0643 bool NeoChatRoom::canSendState(const QString &eventType) const
0644 {
0645     auto plEvent = currentState().get<RoomPowerLevelsEvent>();
0646     if (!plEvent) {
0647         return false;
0648     }
0649     auto pl = plEvent->powerLevelForState(eventType);
0650     auto currentPl = plEvent->powerLevelForUser(localUser()->id());
0651 
0652     return currentPl >= pl;
0653 }
0654 
0655 bool NeoChatRoom::readMarkerLoaded() const
0656 {
0657     const auto it = findInTimeline(lastFullyReadEventId());
0658     return it != historyEdge();
0659 }
0660 
0661 bool NeoChatRoom::isInvite() const
0662 {
0663     return joinState() == JoinState::Invite;
0664 }
0665 
0666 bool NeoChatRoom::readOnly() const
0667 {
0668     return !canSendEvent("m.room.message"_ls);
0669 }
0670 
0671 bool NeoChatRoom::isUserBanned(const QString &user) const
0672 {
0673     auto roomMemberEvent = currentState().get<RoomMemberEvent>(user);
0674     if (!roomMemberEvent) {
0675         return false;
0676     }
0677     return roomMemberEvent->membership() == Membership::Ban;
0678 }
0679 
0680 void NeoChatRoom::deleteMessagesByUser(const QString &user, const QString &reason)
0681 {
0682     doDeleteMessagesByUser(user, reason);
0683 }
0684 
0685 QString NeoChatRoom::joinRule() const
0686 {
0687     auto joinRulesEvent = currentState().get<JoinRulesEvent>();
0688     if (!joinRulesEvent) {
0689         return {};
0690     }
0691     return joinRulesEvent->joinRule();
0692 }
0693 
0694 void NeoChatRoom::setJoinRule(const QString &joinRule, const QList<QString> &allowedSpaces)
0695 {
0696     if (!canSendState("m.room.join_rules"_ls)) {
0697         qWarning() << "Power level too low to set join rules";
0698         return;
0699     }
0700     auto actualRule = joinRule;
0701     if (joinRule == "restricted"_ls && allowedSpaces.isEmpty()) {
0702         actualRule = "private"_ls;
0703     }
0704 
0705     QJsonArray allowConditions;
0706     if (actualRule == "restricted"_ls) {
0707         for (auto allowedSpace : allowedSpaces) {
0708             allowConditions += QJsonObject{{"type"_ls, "m.room_membership"_ls}, {"room_id"_ls, allowedSpace}};
0709         }
0710     }
0711 
0712     QJsonObject content;
0713     content.insert("join_rule"_ls, joinRule);
0714     if (!allowConditions.isEmpty()) {
0715         content.insert("allow"_ls, allowConditions);
0716     }
0717     qWarning() << content;
0718     setState("m.room.join_rules"_ls, {}, content);
0719     // Not emitting joinRuleChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
0720 }
0721 
0722 QList<QString> NeoChatRoom::restrictedIds() const
0723 {
0724     auto joinRulesEvent = currentState().get<JoinRulesEvent>();
0725     if (!joinRulesEvent) {
0726         return {};
0727     }
0728     if (joinRulesEvent->joinRule() != "restricted"_ls) {
0729         return {};
0730     }
0731 
0732     QList<QString> roomIds;
0733     for (auto allow : joinRulesEvent->allow()) {
0734         roomIds += allow.toObject().value("room_id"_ls).toString();
0735     }
0736     return roomIds;
0737 }
0738 
0739 QString NeoChatRoom::historyVisibility() const
0740 {
0741     return currentState().get("m.room.history_visibility"_ls)->contentJson()["history_visibility"_ls].toString();
0742 }
0743 
0744 void NeoChatRoom::setHistoryVisibility(const QString &historyVisibilityRule)
0745 {
0746     if (!canSendState("m.room.history_visibility"_ls)) {
0747         qWarning() << "Power level too low to set history visibility";
0748         return;
0749     }
0750 
0751     setState("m.room.history_visibility"_ls, {}, QJsonObject{{"history_visibility"_ls, historyVisibilityRule}});
0752     // Not emitting historyVisibilityChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value.
0753 }
0754 
0755 bool NeoChatRoom::defaultUrlPreviewState() const
0756 {
0757     auto urlPreviewsDisabled = currentState().get("org.matrix.room.preview_urls"_ls);
0758 
0759     // Some rooms will not have this state event set so check for a nullptr return.
0760     if (urlPreviewsDisabled != nullptr) {
0761         return !urlPreviewsDisabled->contentJson()["disable"_ls].toBool();
0762     } else {
0763         return false;
0764     }
0765 }
0766 
0767 void NeoChatRoom::setDefaultUrlPreviewState(const bool &defaultUrlPreviewState)
0768 {
0769     if (!canSendState("org.matrix.room.preview_urls"_ls)) {
0770         qWarning() << "Power level too low to set the default URL preview state for the room";
0771         return;
0772     }
0773 
0774     /**
0775      * Note the org.matrix.room.preview_urls room state event is completely undocumented
0776      * so here it is because I'm nice.
0777      *
0778      * Also note this is a different event to org.matrix.room.preview_urls for room
0779      * account data, because even though it has the same name and content it's totally different.
0780      *
0781      * {
0782      *  "content": {
0783      *      "disable": false
0784      *  },
0785      *  "origin_server_ts": 1673115224071,
0786      *  "sender": "@bob:kde.org",
0787      *  "state_key": "",
0788      *  "type": "org.matrix.room.preview_urls",
0789      *  "unsigned": {
0790      *      "replaces_state": "replaced_event_id",
0791      *      "prev_content": {
0792      *          "disable": true
0793      *      },
0794      *      "prev_sender": "@jeff:kde.org",
0795      *      "age": 99
0796      *  },
0797      *  "event_id": "$event_id",
0798      *  "room_id": "!room_id:kde.org"
0799      * }
0800      *
0801      * You just have to set disable to true to disable URL previews by default.
0802      */
0803     setState("org.matrix.room.preview_urls"_ls, {}, QJsonObject{{"disable"_ls, !defaultUrlPreviewState}});
0804 }
0805 
0806 bool NeoChatRoom::urlPreviewEnabled() const
0807 {
0808     if (hasAccountData("org.matrix.room.preview_urls"_ls)) {
0809         return !accountData("org.matrix.room.preview_urls"_ls)->contentJson()["disable"_ls].toBool();
0810     } else {
0811         return defaultUrlPreviewState();
0812     }
0813 }
0814 
0815 void NeoChatRoom::setUrlPreviewEnabled(const bool &urlPreviewEnabled)
0816 {
0817     /**
0818      * Once again this is undocumented and even though the name and content are the
0819      * same this is a different event to the org.matrix.room.preview_urls room state event.
0820      *
0821      * {
0822      *  "content": {
0823      *      "disable": true
0824      *  }
0825      *  "type": "org.matrix.room.preview_urls",
0826      * }
0827      */
0828     connection()->callApi<SetAccountDataPerRoomJob>(localUser()->id(),
0829                                                     id(),
0830                                                     "org.matrix.room.preview_urls"_ls,
0831                                                     QJsonObject{{"disable"_ls, !urlPreviewEnabled}});
0832 }
0833 
0834 void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel)
0835 {
0836     if (joinedCount() <= 1) {
0837         qWarning() << "Cannot modify the power level of the only user";
0838         return;
0839     }
0840     if (!canSendState("m.room.power_levels"_ls)) {
0841         qWarning() << "Power level too low to set user power levels";
0842         return;
0843     }
0844     if (!isMember(userID)) {
0845         qWarning() << "User is not a member of this room so power level cannot be set";
0846         return;
0847     }
0848     int clampPowerLevel = std::clamp(powerLevel, 0, 100);
0849 
0850     auto powerLevelContent = currentState().get("m.room.power_levels"_ls)->contentJson();
0851     auto powerLevelUserOverrides = powerLevelContent["users"_ls].toObject();
0852 
0853     if (powerLevelUserOverrides[userID] != clampPowerLevel) {
0854         powerLevelUserOverrides[userID] = clampPowerLevel;
0855         powerLevelContent["users"_ls] = powerLevelUserOverrides;
0856 
0857         setState("m.room.power_levels"_ls, {}, powerLevelContent);
0858     }
0859 }
0860 
0861 int NeoChatRoom::getUserPowerLevel(const QString &userId) const
0862 {
0863     auto powerLevelEvent = currentState().get<RoomPowerLevelsEvent>();
0864     if (!powerLevelEvent) {
0865         return 0;
0866     }
0867     return powerLevelEvent->powerLevelForUser(userId);
0868 }
0869 
0870 int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent) const
0871 {
0872     const auto powerLevelEvent = currentState().get<RoomPowerLevelsEvent>();
0873     if (eventName == "ban"_ls) {
0874         return powerLevelEvent->ban();
0875     } else if (eventName == "kick"_ls) {
0876         return powerLevelEvent->kick();
0877     } else if (eventName == "invite"_ls) {
0878         return powerLevelEvent->invite();
0879     } else if (eventName == "redact"_ls) {
0880         return powerLevelEvent->redact();
0881     } else if (eventName == "users_default"_ls) {
0882         return powerLevelEvent->usersDefault();
0883     } else if (eventName == "state_default"_ls) {
0884         return powerLevelEvent->stateDefault();
0885     } else if (eventName == "events_default"_ls) {
0886         return powerLevelEvent->eventsDefault();
0887     } else if (isStateEvent) {
0888         return powerLevelEvent->powerLevelForState(eventName);
0889     } else {
0890         return powerLevelEvent->powerLevelForEvent(eventName);
0891     }
0892 }
0893 
0894 void NeoChatRoom::setPowerLevel(const QString &eventName, const int &newPowerLevel, const bool &isStateEvent)
0895 {
0896     auto powerLevelContent = currentState().get("m.room.power_levels"_ls)->contentJson();
0897     int clampPowerLevel = std::clamp(newPowerLevel, 0, 100);
0898     int powerLevel = 0;
0899 
0900     if (powerLevelContent.contains(eventName)) {
0901         powerLevel = powerLevelContent[eventName].toInt();
0902 
0903         if (powerLevel != clampPowerLevel) {
0904             powerLevelContent[eventName] = clampPowerLevel;
0905         }
0906     } else {
0907         auto eventPowerLevels = powerLevelContent["events"_ls].toObject();
0908 
0909         if (eventPowerLevels.contains(eventName)) {
0910             powerLevel = eventPowerLevels[eventName].toInt();
0911         } else {
0912             if (isStateEvent) {
0913                 powerLevel = powerLevelContent["state_default"_ls].toInt();
0914             } else {
0915                 powerLevel = powerLevelContent["events_default"_ls].toInt();
0916             }
0917         }
0918 
0919         if (powerLevel != clampPowerLevel) {
0920             eventPowerLevels[eventName] = clampPowerLevel;
0921             powerLevelContent["events"_ls] = eventPowerLevels;
0922         }
0923     }
0924 
0925     setState("m.room.power_levels"_ls, {}, powerLevelContent);
0926 }
0927 
0928 int NeoChatRoom::defaultUserPowerLevel() const
0929 {
0930     return powerLevel("users_default"_ls);
0931 }
0932 
0933 void NeoChatRoom::setDefaultUserPowerLevel(const int &newPowerLevel)
0934 {
0935     setPowerLevel("users_default"_ls, newPowerLevel);
0936 }
0937 
0938 int NeoChatRoom::invitePowerLevel() const
0939 {
0940     return powerLevel("invite"_ls);
0941 }
0942 
0943 void NeoChatRoom::setInvitePowerLevel(const int &newPowerLevel)
0944 {
0945     setPowerLevel("invite"_ls, newPowerLevel);
0946 }
0947 
0948 int NeoChatRoom::kickPowerLevel() const
0949 {
0950     return powerLevel("kick"_ls);
0951 }
0952 
0953 void NeoChatRoom::setKickPowerLevel(const int &newPowerLevel)
0954 {
0955     setPowerLevel("kick"_ls, newPowerLevel);
0956 }
0957 
0958 int NeoChatRoom::banPowerLevel() const
0959 {
0960     return powerLevel("ban"_ls);
0961 }
0962 
0963 void NeoChatRoom::setBanPowerLevel(const int &newPowerLevel)
0964 {
0965     setPowerLevel("ban"_ls, newPowerLevel);
0966 }
0967 
0968 int NeoChatRoom::redactPowerLevel() const
0969 {
0970     return powerLevel("redact"_ls);
0971 }
0972 
0973 void NeoChatRoom::setRedactPowerLevel(const int &newPowerLevel)
0974 {
0975     setPowerLevel("redact"_ls, newPowerLevel);
0976 }
0977 
0978 int NeoChatRoom::statePowerLevel() const
0979 {
0980     return powerLevel("state_default"_ls);
0981 }
0982 
0983 void NeoChatRoom::setStatePowerLevel(const int &newPowerLevel)
0984 {
0985     setPowerLevel("state_default"_ls, newPowerLevel);
0986 }
0987 
0988 int NeoChatRoom::defaultEventPowerLevel() const
0989 {
0990     return powerLevel("events_default"_ls);
0991 }
0992 
0993 void NeoChatRoom::setDefaultEventPowerLevel(const int &newPowerLevel)
0994 {
0995     setPowerLevel("events_default"_ls, newPowerLevel);
0996 }
0997 
0998 int NeoChatRoom::powerLevelPowerLevel() const
0999 {
1000     return powerLevel("m.room.power_levels"_ls, true);
1001 }
1002 
1003 void NeoChatRoom::setPowerLevelPowerLevel(const int &newPowerLevel)
1004 {
1005     setPowerLevel("m.room.power_levels"_ls, newPowerLevel, true);
1006 }
1007 
1008 int NeoChatRoom::namePowerLevel() const
1009 {
1010     return powerLevel("m.room.name"_ls, true);
1011 }
1012 
1013 void NeoChatRoom::setNamePowerLevel(const int &newPowerLevel)
1014 {
1015     setPowerLevel("m.room.name"_ls, newPowerLevel, true);
1016 }
1017 
1018 int NeoChatRoom::avatarPowerLevel() const
1019 {
1020     return powerLevel("m.room.avatar"_ls, true);
1021 }
1022 
1023 void NeoChatRoom::setAvatarPowerLevel(const int &newPowerLevel)
1024 {
1025     setPowerLevel("m.room.avatar"_ls, newPowerLevel, true);
1026 }
1027 
1028 int NeoChatRoom::canonicalAliasPowerLevel() const
1029 {
1030     return powerLevel("m.room.canonical_alias"_ls, true);
1031 }
1032 
1033 void NeoChatRoom::setCanonicalAliasPowerLevel(const int &newPowerLevel)
1034 {
1035     setPowerLevel("m.room.canonical_alias"_ls, newPowerLevel, true);
1036 }
1037 
1038 int NeoChatRoom::topicPowerLevel() const
1039 {
1040     return powerLevel("m.room.topic"_ls, true);
1041 }
1042 
1043 void NeoChatRoom::setTopicPowerLevel(const int &newPowerLevel)
1044 {
1045     setPowerLevel("m.room.topic"_ls, newPowerLevel, true);
1046 }
1047 
1048 int NeoChatRoom::encryptionPowerLevel() const
1049 {
1050     return powerLevel("m.room.encryption"_ls, true);
1051 }
1052 
1053 void NeoChatRoom::setEncryptionPowerLevel(const int &newPowerLevel)
1054 {
1055     setPowerLevel("m.room.encryption"_ls, newPowerLevel, true);
1056 }
1057 
1058 int NeoChatRoom::historyVisibilityPowerLevel() const
1059 {
1060     return powerLevel("m.room.history_visibility"_ls, true);
1061 }
1062 
1063 void NeoChatRoom::setHistoryVisibilityPowerLevel(const int &newPowerLevel)
1064 {
1065     setPowerLevel("m.room.history_visibility"_ls, newPowerLevel, true);
1066 }
1067 
1068 int NeoChatRoom::pinnedEventsPowerLevel() const
1069 {
1070     return powerLevel("m.room.pinned_events"_ls, true);
1071 }
1072 
1073 void NeoChatRoom::setPinnedEventsPowerLevel(const int &newPowerLevel)
1074 {
1075     setPowerLevel("m.room.pinned_events"_ls, newPowerLevel, true);
1076 }
1077 
1078 int NeoChatRoom::tombstonePowerLevel() const
1079 {
1080     return powerLevel("m.room.tombstone"_ls, true);
1081 }
1082 
1083 void NeoChatRoom::setTombstonePowerLevel(const int &newPowerLevel)
1084 {
1085     setPowerLevel("m.room.tombstone"_ls, newPowerLevel, true);
1086 }
1087 
1088 int NeoChatRoom::serverAclPowerLevel() const
1089 {
1090     return powerLevel("m.room.server_acl"_ls, true);
1091 }
1092 
1093 void NeoChatRoom::setServerAclPowerLevel(const int &newPowerLevel)
1094 {
1095     setPowerLevel("m.room.server_acl"_ls, newPowerLevel, true);
1096 }
1097 
1098 int NeoChatRoom::spaceChildPowerLevel() const
1099 {
1100     return powerLevel("m.space.child"_ls, true);
1101 }
1102 
1103 void NeoChatRoom::setSpaceChildPowerLevel(const int &newPowerLevel)
1104 {
1105     setPowerLevel("m.space.child"_ls, newPowerLevel, true);
1106 }
1107 
1108 int NeoChatRoom::spaceParentPowerLevel() const
1109 {
1110     return powerLevel("m.space.parent"_ls, true);
1111 }
1112 
1113 void NeoChatRoom::setSpaceParentPowerLevel(const int &newPowerLevel)
1114 {
1115     setPowerLevel("m.space.parent"_ls, newPowerLevel, true);
1116 }
1117 
1118 QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user, QString reason)
1119 {
1120     QStringList events;
1121     for (const auto &event : messageEvents()) {
1122         if (event->senderId() == user && !event->isRedacted() && !event.viewAs<RedactionEvent>() && !event->isStateEvent()) {
1123             events += event->id();
1124         }
1125     }
1126     for (const auto &e : events) {
1127         auto job = connection()->callApi<RedactEventJob>(id(), QString::fromLatin1(QUrl::toPercentEncoding(e)), connection()->generateTxnId(), reason);
1128         co_await qCoro(job, &BaseJob::finished);
1129         if (job->error() != BaseJob::Success) {
1130             qWarning() << "Error: \"" << job->error() << "\" while deleting messages. Aborting";
1131             break;
1132         }
1133     }
1134 }
1135 
1136 void NeoChatRoom::clearInvitationNotification()
1137 {
1138     NotificationsManager::instance().clearInvitationNotification(id());
1139 }
1140 
1141 bool NeoChatRoom::hasParent() const
1142 {
1143     return currentState().eventsOfType("m.space.parent"_ls).size() > 0;
1144 }
1145 
1146 QList<QString> NeoChatRoom::parentIds() const
1147 {
1148     auto parentEvents = currentState().eventsOfType("m.space.parent"_ls);
1149     QList<QString> parentIds;
1150     for (const auto &parentEvent : parentEvents) {
1151         if (parentEvent->contentJson().contains("via"_ls) && !parentEvent->contentPart<QJsonArray>("via"_ls).isEmpty()) {
1152             parentIds += parentEvent->stateKey();
1153         }
1154     }
1155     return parentIds;
1156 }
1157 
1158 QList<NeoChatRoom *> NeoChatRoom::parentObjects(bool multiLevel) const
1159 {
1160     QList<NeoChatRoom *> parentObjects;
1161     QList<QString> parentIds = this->parentIds();
1162     for (const auto &parentId : parentIds) {
1163         if (auto parentObject = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
1164             parentObjects += parentObject;
1165             if (multiLevel) {
1166                 parentObjects += parentObject->parentObjects(true);
1167             }
1168         }
1169     }
1170     return parentObjects;
1171 }
1172 
1173 QString NeoChatRoom::canonicalParent() const
1174 {
1175     auto parentEvents = currentState().eventsOfType("m.space.parent"_ls);
1176     for (const auto &parentEvent : parentEvents) {
1177         if (parentEvent->contentJson().contains("via"_ls) && !parentEvent->contentPart<QJsonArray>("via"_ls).isEmpty()) {
1178             if (parentEvent->contentPart<bool>("canonical"_ls)) {
1179                 return parentEvent->stateKey();
1180             }
1181         }
1182     }
1183     return {};
1184 }
1185 
1186 void NeoChatRoom::setCanonicalParent(const QString &parentId)
1187 {
1188     if (!canModifyParent(parentId)) {
1189         return;
1190     }
1191     if (const auto &parent = currentState().get("m.space.parent"_ls, parentId)) {
1192         auto content = parent->contentJson();
1193         content.insert("canonical"_ls, true);
1194         setState("m.space.parent"_ls, parentId, content);
1195     } else {
1196         return;
1197     }
1198 
1199     // Only one canonical parent can exist so make sure others are set false.
1200     auto parentEvents = currentState().eventsOfType("m.space.parent"_ls);
1201     for (const auto &parentEvent : parentEvents) {
1202         if (parentEvent->contentPart<bool>("canonical"_ls) && parentEvent->stateKey() != parentId) {
1203             auto content = parentEvent->contentJson();
1204             content.insert("canonical"_ls, false);
1205             setState("m.space.parent"_ls, parentEvent->stateKey(), content);
1206         }
1207     }
1208 }
1209 
1210 bool NeoChatRoom::canModifyParent(const QString &parentId) const
1211 {
1212     if (!canSendState("m.space.parent"_ls)) {
1213         return false;
1214     }
1215     // If we can't peek the parent we assume that we neither have permission nor is
1216     // there an existing space child event for this room.
1217     if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
1218         if (!parent->isSpace()) {
1219             return false;
1220         }
1221         // If the user is allowed to set space child events in the parent they are
1222         // allowed to set the space as a parent (even if a space child event doesn't
1223         // exist).
1224         if (parent->canSendState("m.space.child"_ls)) {
1225             return true;
1226         }
1227         // If the parent has a space child event the user can set as a parent (even
1228         // if they don't have permission to set space child events in that parent).
1229         if (parent->currentState().contains("m.space.child"_ls, id())) {
1230             return true;
1231         }
1232     }
1233     return false;
1234 }
1235 
1236 void NeoChatRoom::addParent(const QString &parentId, bool canonical, bool setParentChild)
1237 {
1238     if (!canModifyParent(parentId)) {
1239         return;
1240     }
1241     if (canonical) {
1242         // Only one canonical parent can exist so make sure others are set false.
1243         auto parentEvents = currentState().eventsOfType("m.space.parent"_ls);
1244         for (const auto &parentEvent : parentEvents) {
1245             if (parentEvent->contentPart<bool>("canonical"_ls)) {
1246                 auto content = parentEvent->contentJson();
1247                 content.insert("canonical"_ls, false);
1248                 setState("m.space.parent"_ls, parentEvent->stateKey(), content);
1249             }
1250         }
1251     }
1252 
1253     setState("m.space.parent"_ls, parentId, QJsonObject{{"canonical"_ls, canonical}, {"via"_ls, QJsonArray{connection()->domain()}}});
1254 
1255     if (setParentChild) {
1256         if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
1257             parent->setState("m.space.child"_ls, id(), QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}});
1258         }
1259     }
1260 }
1261 
1262 void NeoChatRoom::removeParent(const QString &parentId)
1263 {
1264     if (!canModifyParent(parentId)) {
1265         return;
1266     }
1267     if (!currentState().contains("m.space.parent"_ls, parentId)) {
1268         return;
1269     }
1270     if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
1271         setState("m.space.parent"_ls, parentId, {});
1272     }
1273 }
1274 
1275 bool NeoChatRoom::isSpace()
1276 {
1277     const auto creationEvent = this->creation();
1278     if (!creationEvent) {
1279         return false;
1280     }
1281 
1282     return creationEvent->roomType() == RoomType::Space;
1283 }
1284 
1285 void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical, bool suggested)
1286 {
1287     if (!isSpace()) {
1288         return;
1289     }
1290     if (!canSendEvent("m.space.child"_ls)) {
1291         return;
1292     }
1293     setState("m.space.child"_ls, childId, QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}, {"suggested"_ls, suggested}});
1294 
1295     if (setChildParent) {
1296         if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
1297             if (child->canSendState("m.space.parent"_ls)) {
1298                 child->setState("m.space.parent"_ls, id(), QJsonObject{{"canonical"_ls, canonical}, {"via"_ls, QJsonArray{connection()->domain()}}});
1299 
1300                 if (canonical) {
1301                     // Only one canonical parent can exist so make sure others are set to false.
1302                     auto parentEvents = child->currentState().eventsOfType("m.space.parent"_ls);
1303                     for (const auto &parentEvent : parentEvents) {
1304                         if (parentEvent->contentPart<bool>("canonical"_ls)) {
1305                             auto content = parentEvent->contentJson();
1306                             content.insert("canonical"_ls, false);
1307                             setState("m.space.parent"_ls, parentEvent->stateKey(), content);
1308                         }
1309                     }
1310                 }
1311             }
1312         }
1313     }
1314 }
1315 
1316 void NeoChatRoom::removeChild(const QString &childId, bool unsetChildParent)
1317 {
1318     if (!isSpace()) {
1319         return;
1320     }
1321     if (!canSendEvent("m.space.child"_ls)) {
1322         return;
1323     }
1324     setState("m.space.child"_ls, childId, {});
1325 
1326     if (unsetChildParent) {
1327         if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
1328             if (child->canSendState("m.space.parent"_ls) && child->currentState().contains("m.space.parent"_ls, id())) {
1329                 child->setState("m.space.parent"_ls, id(), {});
1330             }
1331         }
1332     }
1333 }
1334 
1335 bool NeoChatRoom::isSuggested(const QString &childId)
1336 {
1337     if (!currentState().contains("m.space.child"_ls, childId)) {
1338         return false;
1339     }
1340     const auto childEvent = currentState().get("m.space.child"_ls, childId);
1341     return childEvent->contentPart<bool>("suggested"_ls);
1342 }
1343 
1344 void NeoChatRoom::toggleChildSuggested(const QString &childId)
1345 {
1346     if (!isSpace()) {
1347         return;
1348     }
1349     if (!canSendEvent("m.space.child"_ls)) {
1350         return;
1351     }
1352     if (const auto childEvent = currentState().get("m.space.child"_ls, childId)) {
1353         auto content = childEvent->contentJson();
1354         content.insert("suggested"_ls, !childEvent->contentPart<bool>("suggested"_ls));
1355         setState("m.space.child"_ls, childId, content);
1356     }
1357 }
1358 
1359 PushNotificationState::State NeoChatRoom::pushNotificationState() const
1360 {
1361     return m_currentPushNotificationState;
1362 }
1363 
1364 void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
1365 {
1366     // The caller should never try to set the state to unknown.
1367     // It exists only as a default state to diable the settings options until the actual state is retrieved from the server.
1368     if (state == PushNotificationState::Unknown) {
1369         Q_ASSERT(false);
1370         return;
1371     }
1372 
1373     /**
1374      * This stops updatePushNotificationState from temporarily changing
1375      * m_pushNotificationStateUpdating to default after the exisitng rules are deleted but
1376      * before a new rule is added.
1377      * The value is set to false after the rule enable job is successful.
1378      */
1379     m_pushNotificationStateUpdating = true;
1380 
1381     /**
1382      * First remove any existing room rules of the wrong type.
1383      * Note to prevent race conditions any rule that is going ot be overridden later is not removed.
1384      * If the default push notification state is chosen any existing rule needs to be removed.
1385      */
1386     QJsonObject accountData = connection()->accountDataJson("m.push_rules"_ls);
1387 
1388     // For default and mute check for a room rule and remove if found.
1389     if (state == PushNotificationState::Default || state == PushNotificationState::Mute) {
1390         QJsonArray roomRuleArray = accountData["global"_ls].toObject()["room"_ls].toArray();
1391         for (const auto &i : roomRuleArray) {
1392             QJsonObject roomRule = i.toObject();
1393             if (roomRule["rule_id"_ls] == id()) {
1394                 connection()->callApi<DeletePushRuleJob>("global"_ls, "room"_ls, id());
1395             }
1396         }
1397     }
1398 
1399     // For default, all and @mentions and keywords check for an override rule and remove if found.
1400     if (state == PushNotificationState::Default || state == PushNotificationState::All || state == PushNotificationState::MentionKeyword) {
1401         QJsonArray overrideRuleArray = accountData["global"_ls].toObject()["override"_ls].toArray();
1402         for (const auto &i : overrideRuleArray) {
1403             QJsonObject overrideRule = i.toObject();
1404             if (overrideRule["rule_id"_ls] == id()) {
1405                 connection()->callApi<DeletePushRuleJob>("global"_ls, "override"_ls, id());
1406             }
1407         }
1408     }
1409 
1410     if (state == PushNotificationState::Mute) {
1411         /**
1412          * To mute a room an override rule with "don't notify is set".
1413          *
1414          * Setup the rule action to "don't notify" to stop all room notifications
1415          * see https://spec.matrix.org/v1.3/client-server-api/#actions
1416          *
1417          * "actions": [
1418          *      "don't_notify"
1419          * ]
1420          */
1421         const QList<QVariant> actions = {"dont_notify"_ls};
1422         /**
1423          * Setup the push condition to get all events for the current room
1424          * see https://spec.matrix.org/v1.3/client-server-api/#conditions-1
1425          *
1426          * "conditions": [
1427          *      {
1428          *          "key": "type",
1429          *          "kind": "event_match",
1430          *          "pattern": "room_id"
1431          *      }
1432          * ]
1433          */
1434         PushCondition pushCondition;
1435         pushCondition.kind = "event_match"_ls;
1436         pushCondition.key = "room_id"_ls;
1437         pushCondition.pattern = id();
1438         const QList<PushCondition> conditions = {pushCondition};
1439 
1440         // Add new override rule and make sure it's enabled
1441         auto job = connection()->callApi<SetPushRuleJob>("global"_ls, "override"_ls, id(), actions, QString(), QString(), conditions, QString());
1442         connect(job, &BaseJob::success, this, [this]() {
1443             auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("global"_ls, "override"_ls, id(), true);
1444             connect(enableJob, &BaseJob::success, this, [this]() {
1445                 m_pushNotificationStateUpdating = false;
1446             });
1447         });
1448     } else if (state == PushNotificationState::MentionKeyword) {
1449         /**
1450          * To only get notifcations for @ mentions and keywords a room rule with "don't_notify" is set.
1451          *
1452          * Note -  This works becuase a default override rule which catches all user mentions will
1453          * take precedent and notify. See https://spec.matrix.org/v1.3/client-server-api/#default-override-rules. Any keywords will also have a similar override
1454          * rule.
1455          *
1456          * Setup the rule action to "don't notify" to stop all room event notifications
1457          * see https://spec.matrix.org/v1.3/client-server-api/#actions
1458          *
1459          * "actions": [
1460          *      "don't_notify"
1461          * ]
1462          */
1463         const QList<QVariant> actions = {"dont_notify"_ls};
1464         // No conditions for a room rule
1465         const QList<PushCondition> conditions;
1466 
1467         auto setJob = connection()->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString());
1468         connect(setJob, &BaseJob::success, this, [this]() {
1469             auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("global"_ls, "room"_ls, id(), true);
1470             connect(enableJob, &BaseJob::success, this, [this]() {
1471                 m_pushNotificationStateUpdating = false;
1472             });
1473         });
1474     } else if (state == PushNotificationState::All) {
1475         /**
1476          * To send a notification for all room messages a room rule with "notify" is set.
1477          *
1478          * Setup the rule action to "notify" so all room events give notifications.
1479          * Tweeks is also set to follow default sound settings
1480          * see https://spec.matrix.org/v1.3/client-server-api/#actions
1481          *
1482          * "actions": [
1483          *      "notify",
1484          *      {
1485          *          "set_tweek": "sound",
1486          *          "value": "default",
1487          *      }
1488          * ]
1489          */
1490         QJsonObject tweaks;
1491         tweaks.insert("set_tweak"_ls, "sound"_ls);
1492         tweaks.insert("value"_ls, "default"_ls);
1493         const QList<QVariant> actions = {"notify"_ls, tweaks};
1494         // No conditions for a room rule
1495         const QList<PushCondition> conditions;
1496 
1497         // Add new room rule and make sure enabled
1498         auto setJob = connection()->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString());
1499         connect(setJob, &BaseJob::success, this, [this]() {
1500             auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("global"_ls, "room"_ls, id(), true);
1501             connect(enableJob, &BaseJob::success, this, [this]() {
1502                 m_pushNotificationStateUpdating = false;
1503             });
1504         });
1505     }
1506 
1507     m_currentPushNotificationState = state;
1508     Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
1509 
1510 }
1511 
1512 void NeoChatRoom::updatePushNotificationState(QString type)
1513 {
1514     if (type != "m.push_rules"_ls || m_pushNotificationStateUpdating) {
1515         return;
1516     }
1517 
1518     QJsonObject accountData = connection()->accountDataJson("m.push_rules"_ls);
1519 
1520     // First look for a room rule with the room id
1521     QJsonArray roomRuleArray = accountData["global"_ls].toObject()["room"_ls].toArray();
1522     for (const auto &i : roomRuleArray) {
1523         QJsonObject roomRule = i.toObject();
1524         if (roomRule["rule_id"_ls] == id()) {
1525             if (roomRule["actions"_ls].toArray().size() == 0) {
1526                 m_currentPushNotificationState = PushNotificationState::MentionKeyword;
1527                 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
1528                 return;
1529             }
1530             QString notifyAction = roomRule["actions"_ls].toArray()[0].toString();
1531             if (notifyAction == "notify"_ls) {
1532                 m_currentPushNotificationState = PushNotificationState::All;
1533                 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
1534                 return;
1535             } else if (notifyAction == "dont_notify"_ls) {
1536                 m_currentPushNotificationState = PushNotificationState::MentionKeyword;
1537                 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
1538                 return;
1539             }
1540         }
1541     }
1542 
1543     // Check for an override rule with the room id
1544     QJsonArray overrideRuleArray = accountData["global"_ls].toObject()["override"_ls].toArray();
1545     for (const auto &i : overrideRuleArray) {
1546         QJsonObject overrideRule = i.toObject();
1547         if (overrideRule["rule_id"_ls] == id()) {
1548             if (overrideRule["actions"_ls].toArray().isEmpty()) {
1549                 m_currentPushNotificationState = PushNotificationState::Mute;
1550                 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
1551                 return;
1552             }
1553             QString notifyAction = overrideRule["actions"_ls].toArray()[0].toString();
1554             if (notifyAction == "dont_notify"_ls) {
1555                 m_currentPushNotificationState = PushNotificationState::Mute;
1556                 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
1557                 return;
1558             }
1559         }
1560     }
1561 
1562     // If neither a room or override rule exist for the room then the setting must be default
1563     m_currentPushNotificationState = PushNotificationState::Default;
1564     Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
1565 }
1566 
1567 void NeoChatRoom::reportEvent(const QString &eventId, const QString &reason)
1568 {
1569     auto job = connection()->callApi<ReportContentJob>(id(), eventId, -50, reason);
1570     connect(job, &BaseJob::finished, this, [this, job]() {
1571         if (job->error() == BaseJob::Success) {
1572             Q_EMIT showMessage(Positive, i18n("Report sent successfully."));
1573             Q_EMIT showMessage(MessageType::Positive, i18n("Report sent successfully."));
1574         }
1575     });
1576 }
1577 
1578 QByteArray NeoChatRoom::getEventJsonSource(const QString &eventId)
1579 {
1580     auto evtIt = findInTimeline(eventId);
1581     if (evtIt != messageEvents().rend() && is<RoomEvent>(**evtIt)) {
1582         const auto event = evtIt->viewAs<RoomEvent>();
1583         return QJsonDocument(event->fullJson()).toJson();
1584     }
1585     return {};
1586 }
1587 
1588 void NeoChatRoom::openEventMediaExternally(const QString &eventId)
1589 {
1590     const auto evtIt = findInTimeline(eventId);
1591     if (evtIt != messageEvents().rend() && is<RoomMessageEvent>(**evtIt)) {
1592         const auto event = evtIt->viewAs<RoomMessageEvent>();
1593         if (event->hasFileContent()) {
1594             const auto transferInfo = fileTransferInfo(eventId);
1595             if (transferInfo.completed()) {
1596                 UrlHelper helper;
1597                 helper.openUrl(transferInfo.localPath);
1598             } else {
1599                 downloadFile(eventId,
1600                              QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
1601                                   + event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
1602                 connect(this, &Room::fileTransferCompleted, this, [this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
1603                     Q_UNUSED(localFile);
1604                     Q_UNUSED(fileMetadata);
1605                     if (id == eventId) {
1606                         auto transferInfo = fileTransferInfo(eventId);
1607                         UrlHelper helper;
1608                         helper.openUrl(transferInfo.localPath);
1609                     }
1610                 });
1611             }
1612         }
1613     }
1614 }
1615 
1616 void NeoChatRoom::copyEventMedia(const QString &eventId)
1617 {
1618     const auto evtIt = findInTimeline(eventId);
1619     if (evtIt != messageEvents().rend() && is<RoomMessageEvent>(**evtIt)) {
1620         const auto event = evtIt->viewAs<RoomMessageEvent>();
1621         if (event->hasFileContent()) {
1622             const auto transferInfo = fileTransferInfo(eventId);
1623             if (transferInfo.completed()) {
1624                 Clipboard clipboard;
1625                 clipboard.setImage(transferInfo.localPath);
1626             } else {
1627                 downloadFile(eventId,
1628                              QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
1629                                   + event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
1630                 connect(this, &Room::fileTransferCompleted, this, [this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
1631                     Q_UNUSED(localFile);
1632                     Q_UNUSED(fileMetadata);
1633                     if (id == eventId) {
1634                         auto transferInfo = fileTransferInfo(eventId);
1635                         Clipboard clipboard;
1636                         clipboard.setImage(transferInfo.localPath);
1637                     }
1638                 });
1639             }
1640         }
1641     }
1642 }
1643 
1644 ChatBarCache *NeoChatRoom::mainCache() const
1645 {
1646     return m_mainCache;
1647 }
1648 
1649 ChatBarCache *NeoChatRoom::editCache() const
1650 {
1651     return m_editCache;
1652 }
1653 
1654 void NeoChatRoom::replyLastMessage()
1655 {
1656     const auto &timelineBottom = messageEvents().rbegin();
1657 
1658     // set a cap limit of startRow + 35 messages, to prevent loading a lot of messages
1659     // in rooms where the user has not sent many messages
1660     const auto limit = timelineBottom + std::min(35, timelineSize());
1661 
1662     for (auto it = timelineBottom; it != limit; ++it) {
1663         auto evt = it->event();
1664         auto e = eventCast<const RoomMessageEvent>(evt);
1665         if (!e) {
1666             continue;
1667         }
1668 
1669         auto content = (*it)->contentJson();
1670 
1671         if (e->msgtype() != MessageEventType::Unknown) {
1672             QString eventId;
1673             if (content.contains("m.new_content"_ls)) {
1674                 // The message has been edited so we have to return the id of the original message instead of the replacement
1675                 eventId = content["m.relates_to"_ls].toObject()["event_id"_ls].toString();
1676             } else {
1677                 // For any message that isn't an edit return the id of the current message
1678                 eventId = (*it)->id();
1679             }
1680             mainCache()->setReplyId(eventId);
1681             return;
1682         }
1683     }
1684 }
1685 
1686 void NeoChatRoom::editLastMessage()
1687 {
1688     const auto &timelineBottom = messageEvents().rbegin();
1689 
1690     // set a cap limit of 35 messages, to prevent loading a lot of messages
1691     // in rooms where the user has not sent many messages
1692     const auto limit = timelineBottom + std::min(35, timelineSize());
1693 
1694     for (auto it = timelineBottom; it != limit; ++it) {
1695         auto evt = it->event();
1696         auto e = eventCast<const RoomMessageEvent>(evt);
1697         if (!e) {
1698             continue;
1699         }
1700 
1701         // check if the current message's sender's id is same as the user's id
1702         if ((*it)->senderId() == localUser()->id()) {
1703             auto content = (*it)->contentJson();
1704 
1705             if (e->msgtype() != MessageEventType::Unknown) {
1706                 QString eventId;
1707                 if (content.contains("m.new_content"_ls)) {
1708                     // The message has been edited so we have to return the id of the original message instead of the replacement
1709                     eventId = content["m.relates_to"_ls].toObject()["event_id"_ls].toString();
1710                 } else {
1711                     // For any message that isn't an edit return the id of the current message
1712                     eventId = (*it)->id();
1713                 }
1714                 editCache()->setEditId(eventId);
1715                 return;
1716             }
1717         }
1718     }
1719 }
1720 
1721 bool NeoChatRoom::canEncryptRoom() const
1722 {
1723     return !usesEncryption() && canSendState("m.room.encryption"_ls);
1724 }
1725 
1726 static PollHandler *emptyPollHandler = new PollHandler;
1727 
1728 PollHandler *NeoChatRoom::poll(const QString &eventId) const
1729 {
1730     if (auto pollHandler = m_polls[eventId]) {
1731         return pollHandler;
1732     }
1733     return emptyPollHandler;
1734 }
1735 
1736 void NeoChatRoom::createPollHandler(const Quotient::PollStartEvent *event)
1737 {
1738     if (event == nullptr) {
1739         return;
1740     }
1741     auto eventId = event->id();
1742     if (!m_polls.contains(eventId)) {
1743         auto handler = new PollHandler(this, event);
1744         m_polls.insert(eventId, handler);
1745     }
1746 }
1747 
1748 bool NeoChatRoom::downloadTempFile(const QString &eventId)
1749 {
1750     QTemporaryFile file;
1751     file.setAutoRemove(false);
1752     if (!file.open()) {
1753         return false;
1754     }
1755 
1756     download(eventId, QUrl::fromLocalFile(file.fileName()));
1757     return true;
1758 }
1759 
1760 void NeoChatRoom::download(const QString &eventId, const QUrl &localFilename)
1761 {
1762     downloadFile(eventId, localFilename);
1763 #ifndef Q_OS_ANDROID
1764     auto job = new FileTransferPseudoJob(FileTransferPseudoJob::Download, localFilename.toLocalFile(), eventId);
1765     connect(this, &Room::fileTransferProgress, job, &FileTransferPseudoJob::fileTransferProgress);
1766     connect(this, &Room::fileTransferCompleted, job, &FileTransferPseudoJob::fileTransferCompleted);
1767     connect(this, &Room::fileTransferFailed, job, &FileTransferPseudoJob::fileTransferFailed);
1768     KIO::getJobTracker()->registerJob(job);
1769     job->start();
1770 #endif
1771 }
1772 
1773 void NeoChatRoom::mapAlias(const QString &alias)
1774 {
1775     auto getLocalAliasesJob = connection()->callApi<GetLocalAliasesJob>(id());
1776     connect(getLocalAliasesJob, &BaseJob::success, this, [this, getLocalAliasesJob, alias] {
1777         if (getLocalAliasesJob->aliases().contains(alias)) {
1778             return;
1779         } else {
1780             auto setRoomAliasJob = connection()->callApi<SetRoomAliasJob>(alias, id());
1781             connect(setRoomAliasJob, &BaseJob::success, this, [this, alias] {
1782                 auto newAltAliases = altAliases();
1783                 newAltAliases.append(alias);
1784                 setLocalAliases(newAltAliases);
1785             });
1786         }
1787     });
1788 }
1789 
1790 void NeoChatRoom::unmapAlias(const QString &alias)
1791 {
1792     connection()->callApi<DeleteRoomAliasJob>(alias);
1793 }
1794 
1795 void NeoChatRoom::setCanonicalAlias(const QString &newAlias)
1796 {
1797     QString oldCanonicalAlias = canonicalAlias();
1798     Room::setCanonicalAlias(newAlias);
1799 
1800     connect(this, &Room::namesChanged, this, [this, newAlias, oldCanonicalAlias] {
1801         if (canonicalAlias() == newAlias) {
1802             // If the new canonical alias is already a published alt alias remove it otherwise it will be in both lists.
1803             // The server doesn't prevent this so we need to handle it.
1804             auto newAltAliases = altAliases();
1805             if (!oldCanonicalAlias.isEmpty()) {
1806                 newAltAliases.append(oldCanonicalAlias);
1807             }
1808             if (newAltAliases.contains(newAlias)) {
1809                 newAltAliases.removeAll(newAlias);
1810                 Room::setLocalAliases(newAltAliases);
1811             }
1812         }
1813     });
1814 }
1815 
1816 int NeoChatRoom::maxRoomVersion() const
1817 {
1818     int maxVersion = 0;
1819     for (auto roomVersion : connection()->availableRoomVersions()) {
1820         if (roomVersion.id.toInt() > maxVersion) {
1821             maxVersion = roomVersion.id.toInt();
1822         }
1823     }
1824     return maxVersion;
1825 }
1826 
1827 Quotient::User *NeoChatRoom::directChatRemoteUser() const
1828 {
1829     return connection()->directChatUsers(this)[0];
1830 }
1831 
1832 void NeoChatRoom::sendLocation(float lat, float lon, const QString &description)
1833 {
1834     QJsonObject locationContent{
1835         {"uri"_ls, "geo:%1,%2"_ls.arg(QString::number(lat), QString::number(lon))},
1836     };
1837 
1838     if (!description.isEmpty()) {
1839         locationContent["description"_ls] = description;
1840     }
1841 
1842     QJsonObject content{
1843         {"body"_ls, i18nc("'Lat' and 'Lon' as in Latitude and Longitude", "Lat: %1, Lon: %2", lat, lon)},
1844         {"msgtype"_ls, "m.location"_ls},
1845         {"geo_uri"_ls, "geo:%1,%2"_ls.arg(QString::number(lat), QString::number(lon))},
1846         {"org.matrix.msc3488.location"_ls, locationContent},
1847         {"org.matrix.msc3488.asset"_ls,
1848          QJsonObject{
1849              {"type"_ls, "m.pin"_ls},
1850          }},
1851         {"org.matrix.msc1767.text"_ls, i18nc("'Lat' and 'Lon' as in Latitude and Longitude", "Lat: %1, Lon: %2", lat, lon)},
1852     };
1853     postJson("m.room.message"_ls, content);
1854 }
1855 
1856 QByteArray NeoChatRoom::roomAcountDataJson(const QString &eventType)
1857 {
1858     return QJsonDocument(accountData(eventType)->fullJson()).toJson();
1859 }
1860 
1861 QUrl NeoChatRoom::avatarForMember(Quotient::User *user) const
1862 {
1863     const auto &url = memberAvatarUrl(user->id());
1864     if (url.isEmpty() || url.scheme() != "mxc"_ls) {
1865         return {};
1866     }
1867     auto avatar = connection()->makeMediaUrl(url);
1868     if (avatar.isValid() && avatar.scheme() == QStringLiteral("mxc")) {
1869         return avatar;
1870     } else {
1871         return QUrl();
1872     }
1873 }
1874 
1875 const RoomEvent *NeoChatRoom::getReplyForEvent(const RoomEvent &event) const
1876 {
1877     const QString &replyEventId = event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
1878     if (replyEventId.isEmpty()) {
1879         return {};
1880     };
1881 
1882     const auto replyIt = findInTimeline(replyEventId);
1883     const RoomEvent *replyPtr = replyIt != historyEdge() ? &**replyIt : nullptr;
1884     if (!replyPtr) {
1885         for (const auto &e : m_extraEvents) {
1886             if (e->id() == replyEventId) {
1887                 replyPtr = e.get();
1888                 break;
1889             }
1890         }
1891     }
1892     return replyPtr;
1893 }
1894 
1895 void NeoChatRoom::loadReply(const QString &eventId, const QString &replyId)
1896 {
1897     auto job = connection()->callApi<GetOneRoomEventJob>(id(), replyId);
1898     connect(job, &BaseJob::success, this, [this, job, eventId, replyId] {
1899         m_extraEvents.push_back(fromJson<event_ptr_tt<RoomEvent>>(job->jsonData()));
1900         Q_EMIT replyLoaded(eventId, replyId);
1901     });
1902 }
1903 
1904 User *NeoChatRoom::invitingUser() const
1905 {
1906     return connection()->user(currentState().get<RoomMemberEvent>(connection()->userId())->senderId());
1907 }
1908 
1909 #include "moc_neochatroom.cpp"