File indexing completed on 2024-04-14 15:03:12

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