File indexing completed on 2024-09-15 04:28:34
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(this, &**replyIt); 0506 0507 const bool isFallingBack = !eventHandler.isThreaded(); 0508 0509 // clang-format off 0510 QJsonObject json{ 0511 {"msgtype"_ls, msgTypeToString(type)}, 0512 {"body"_ls, text}, 0513 {"format"_ls, "org.matrix.custom.html"_ls}, 0514 {"m.relates_to"_ls, 0515 QJsonObject { 0516 {"rel_type"_ls, "m.thread"_ls}, 0517 {"event_id"_ls, threadRootId}, 0518 {"is_falling_back"_ls, isFallingBack}, 0519 {"m.in_reply_to"_ls, 0520 QJsonObject { 0521 {"event_id"_ls, replyEventId} 0522 } 0523 } 0524 } 0525 }, 0526 {"formatted_body"_ls, html} 0527 }; 0528 // clang-format on 0529 0530 postJson("m.room.message"_ls, json); 0531 return; 0532 } 0533 0534 if (isEdit) { 0535 QJsonObject json{ 0536 {"type"_ls, "m.room.message"_ls}, 0537 {"msgtype"_ls, msgTypeToString(type)}, 0538 {"body"_ls, "* %1"_ls.arg(text)}, 0539 {"format"_ls, "org.matrix.custom.html"_ls}, 0540 {"formatted_body"_ls, html}, 0541 {"m.new_content"_ls, 0542 QJsonObject{{"body"_ls, text}, {"msgtype"_ls, msgTypeToString(type)}, {"format"_ls, "org.matrix.custom.html"_ls}, {"formatted_body"_ls, html}}}, 0543 {"m.relates_to"_ls, QJsonObject{{"rel_type"_ls, "m.replace"_ls}, {"event_id"_ls, relateToEventId}}}}; 0544 0545 postJson("m.room.message"_ls, json); 0546 return; 0547 } 0548 0549 if (isReply) { 0550 const auto &replyEvt = **replyIt; 0551 0552 EventHandler eventHandler(this, &**replyIt); 0553 0554 // clang-format off 0555 QJsonObject json{ 0556 {"msgtype"_ls, msgTypeToString(type)}, 0557 {"body"_ls, "> <%1> %2\n\n%3"_ls.arg(replyEvt.senderId(), eventHandler.getPlainBody(), text)}, 0558 {"format"_ls, "org.matrix.custom.html"_ls}, 0559 {"m.relates_to"_ls, 0560 QJsonObject { 0561 {"m.in_reply_to"_ls, 0562 QJsonObject { 0563 {"event_id"_ls, replyEventId} 0564 } 0565 } 0566 } 0567 }, 0568 {"formatted_body"_ls, 0569 "<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) 0570 } 0571 }; 0572 // clang-format on 0573 0574 postJson("m.room.message"_ls, json); 0575 0576 return; 0577 } 0578 0579 Room::postHtmlMessage(text, html, type); 0580 } 0581 0582 void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction) 0583 { 0584 if (eventId.isEmpty() || reaction.isEmpty()) { 0585 return; 0586 } 0587 0588 const auto eventIt = findInTimeline(eventId); 0589 if (eventIt == historyEdge()) { 0590 return; 0591 } 0592 0593 const auto &evt = **eventIt; 0594 0595 QStringList redactEventIds; // What if there are multiple reaction events? 0596 0597 const auto &annotations = relatedEvents(evt, EventRelation::AnnotationType); 0598 if (!annotations.isEmpty()) { 0599 for (const auto &a : annotations) { 0600 if (auto e = eventCast<const ReactionEvent>(a)) { 0601 if (e->key() != reaction) { 0602 continue; 0603 } 0604 0605 if (e->senderId() == localUser()->id()) { 0606 redactEventIds.push_back(e->id()); 0607 break; 0608 } 0609 } 0610 } 0611 } 0612 0613 if (!redactEventIds.isEmpty()) { 0614 for (const auto &redactEventId : redactEventIds) { 0615 redactEvent(redactEventId); 0616 } 0617 } else { 0618 postReaction(eventId, reaction); 0619 } 0620 } 0621 0622 bool NeoChatRoom::containsUser(const QString &userID) const 0623 { 0624 return memberState(userID) != Membership::Leave; 0625 } 0626 0627 bool NeoChatRoom::canSendEvent(const QString &eventType) const 0628 { 0629 auto plEvent = currentState().get<RoomPowerLevelsEvent>(); 0630 if (!plEvent) { 0631 return false; 0632 } 0633 auto pl = plEvent->powerLevelForEvent(eventType); 0634 auto currentPl = plEvent->powerLevelForUser(localUser()->id()); 0635 0636 return currentPl >= pl; 0637 } 0638 0639 bool NeoChatRoom::canSendState(const QString &eventType) const 0640 { 0641 auto plEvent = currentState().get<RoomPowerLevelsEvent>(); 0642 if (!plEvent) { 0643 return false; 0644 } 0645 auto pl = plEvent->powerLevelForState(eventType); 0646 auto currentPl = plEvent->powerLevelForUser(localUser()->id()); 0647 0648 return currentPl >= pl; 0649 } 0650 0651 bool NeoChatRoom::readMarkerLoaded() const 0652 { 0653 const auto it = findInTimeline(lastFullyReadEventId()); 0654 return it != historyEdge(); 0655 } 0656 0657 bool NeoChatRoom::isInvite() const 0658 { 0659 return joinState() == JoinState::Invite; 0660 } 0661 0662 bool NeoChatRoom::readOnly() const 0663 { 0664 return !canSendEvent("m.room.message"_ls); 0665 } 0666 0667 bool NeoChatRoom::isUserBanned(const QString &user) const 0668 { 0669 auto roomMemberEvent = currentState().get<RoomMemberEvent>(user); 0670 if (!roomMemberEvent) { 0671 return false; 0672 } 0673 return roomMemberEvent->membership() == Membership::Ban; 0674 } 0675 0676 void NeoChatRoom::deleteMessagesByUser(const QString &user, const QString &reason) 0677 { 0678 doDeleteMessagesByUser(user, reason); 0679 } 0680 0681 QString NeoChatRoom::joinRule() const 0682 { 0683 auto joinRulesEvent = currentState().get<JoinRulesEvent>(); 0684 if (!joinRulesEvent) { 0685 return {}; 0686 } 0687 return joinRulesEvent->joinRule(); 0688 } 0689 0690 void NeoChatRoom::setJoinRule(const QString &joinRule, const QList<QString> &allowedSpaces) 0691 { 0692 if (!canSendState("m.room.join_rules"_ls)) { 0693 qWarning() << "Power level too low to set join rules"; 0694 return; 0695 } 0696 auto actualRule = joinRule; 0697 if (joinRule == "restricted"_ls && allowedSpaces.isEmpty()) { 0698 actualRule = "private"_ls; 0699 } 0700 0701 QJsonArray allowConditions; 0702 if (actualRule == "restricted"_ls) { 0703 for (auto allowedSpace : allowedSpaces) { 0704 allowConditions += QJsonObject{{"type"_ls, "m.room_membership"_ls}, {"room_id"_ls, allowedSpace}}; 0705 } 0706 } 0707 0708 QJsonObject content; 0709 content.insert("join_rule"_ls, joinRule); 0710 if (!allowConditions.isEmpty()) { 0711 content.insert("allow"_ls, allowConditions); 0712 } 0713 qWarning() << content; 0714 setState("m.room.join_rules"_ls, {}, content); 0715 // Not emitting joinRuleChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value. 0716 } 0717 0718 QList<QString> NeoChatRoom::restrictedIds() const 0719 { 0720 auto joinRulesEvent = currentState().get<JoinRulesEvent>(); 0721 if (!joinRulesEvent) { 0722 return {}; 0723 } 0724 if (joinRulesEvent->joinRule() != "restricted"_ls) { 0725 return {}; 0726 } 0727 0728 QList<QString> roomIds; 0729 for (auto allow : joinRulesEvent->allow()) { 0730 roomIds += allow.toObject().value("room_id"_ls).toString(); 0731 } 0732 return roomIds; 0733 } 0734 0735 QString NeoChatRoom::historyVisibility() const 0736 { 0737 return currentState().get("m.room.history_visibility"_ls)->contentJson()["history_visibility"_ls].toString(); 0738 } 0739 0740 void NeoChatRoom::setHistoryVisibility(const QString &historyVisibilityRule) 0741 { 0742 if (!canSendState("m.room.history_visibility"_ls)) { 0743 qWarning() << "Power level too low to set history visibility"; 0744 return; 0745 } 0746 0747 setState("m.room.history_visibility"_ls, {}, QJsonObject{{"history_visibility"_ls, historyVisibilityRule}}); 0748 // Not emitting historyVisibilityChanged() here, since that would override the change in the UI with the *current* value, which is not the *new* value. 0749 } 0750 0751 bool NeoChatRoom::defaultUrlPreviewState() const 0752 { 0753 auto urlPreviewsDisabled = currentState().get("org.matrix.room.preview_urls"_ls); 0754 0755 // Some rooms will not have this state event set so check for a nullptr return. 0756 if (urlPreviewsDisabled != nullptr) { 0757 return !urlPreviewsDisabled->contentJson()["disable"_ls].toBool(); 0758 } else { 0759 return false; 0760 } 0761 } 0762 0763 void NeoChatRoom::setDefaultUrlPreviewState(const bool &defaultUrlPreviewState) 0764 { 0765 if (!canSendState("org.matrix.room.preview_urls"_ls)) { 0766 qWarning() << "Power level too low to set the default URL preview state for the room"; 0767 return; 0768 } 0769 0770 /** 0771 * Note the org.matrix.room.preview_urls room state event is completely undocumented 0772 * so here it is because I'm nice. 0773 * 0774 * Also note this is a different event to org.matrix.room.preview_urls for room 0775 * account data, because even though it has the same name and content it's totally different. 0776 * 0777 * { 0778 * "content": { 0779 * "disable": false 0780 * }, 0781 * "origin_server_ts": 1673115224071, 0782 * "sender": "@bob:kde.org", 0783 * "state_key": "", 0784 * "type": "org.matrix.room.preview_urls", 0785 * "unsigned": { 0786 * "replaces_state": "replaced_event_id", 0787 * "prev_content": { 0788 * "disable": true 0789 * }, 0790 * "prev_sender": "@jeff:kde.org", 0791 * "age": 99 0792 * }, 0793 * "event_id": "$event_id", 0794 * "room_id": "!room_id:kde.org" 0795 * } 0796 * 0797 * You just have to set disable to true to disable URL previews by default. 0798 */ 0799 setState("org.matrix.room.preview_urls"_ls, {}, QJsonObject{{"disable"_ls, !defaultUrlPreviewState}}); 0800 } 0801 0802 bool NeoChatRoom::urlPreviewEnabled() const 0803 { 0804 if (hasAccountData("org.matrix.room.preview_urls"_ls)) { 0805 return !accountData("org.matrix.room.preview_urls"_ls)->contentJson()["disable"_ls].toBool(); 0806 } else { 0807 return defaultUrlPreviewState(); 0808 } 0809 } 0810 0811 void NeoChatRoom::setUrlPreviewEnabled(const bool &urlPreviewEnabled) 0812 { 0813 /** 0814 * Once again this is undocumented and even though the name and content are the 0815 * same this is a different event to the org.matrix.room.preview_urls room state event. 0816 * 0817 * { 0818 * "content": { 0819 * "disable": true 0820 * } 0821 * "type": "org.matrix.room.preview_urls", 0822 * } 0823 */ 0824 connection()->callApi<SetAccountDataPerRoomJob>(localUser()->id(), 0825 id(), 0826 "org.matrix.room.preview_urls"_ls, 0827 QJsonObject{{"disable"_ls, !urlPreviewEnabled}}); 0828 } 0829 0830 void NeoChatRoom::setUserPowerLevel(const QString &userID, const int &powerLevel) 0831 { 0832 if (joinedCount() <= 1) { 0833 qWarning() << "Cannot modify the power level of the only user"; 0834 return; 0835 } 0836 if (!canSendState("m.room.power_levels"_ls)) { 0837 qWarning() << "Power level too low to set user power levels"; 0838 return; 0839 } 0840 if (!isMember(userID)) { 0841 qWarning() << "User is not a member of this room so power level cannot be set"; 0842 return; 0843 } 0844 int clampPowerLevel = std::clamp(powerLevel, 0, 100); 0845 0846 auto powerLevelContent = currentState().get("m.room.power_levels"_ls)->contentJson(); 0847 auto powerLevelUserOverrides = powerLevelContent["users"_ls].toObject(); 0848 0849 if (powerLevelUserOverrides[userID] != clampPowerLevel) { 0850 powerLevelUserOverrides[userID] = clampPowerLevel; 0851 powerLevelContent["users"_ls] = powerLevelUserOverrides; 0852 0853 setState("m.room.power_levels"_ls, {}, powerLevelContent); 0854 } 0855 } 0856 0857 int NeoChatRoom::getUserPowerLevel(const QString &userId) const 0858 { 0859 auto powerLevelEvent = currentState().get<RoomPowerLevelsEvent>(); 0860 if (!powerLevelEvent) { 0861 return 0; 0862 } 0863 return powerLevelEvent->powerLevelForUser(userId); 0864 } 0865 0866 int NeoChatRoom::powerLevel(const QString &eventName, const bool &isStateEvent) const 0867 { 0868 const auto powerLevelEvent = currentState().get<RoomPowerLevelsEvent>(); 0869 if (eventName == "ban"_ls) { 0870 return powerLevelEvent->ban(); 0871 } else if (eventName == "kick"_ls) { 0872 return powerLevelEvent->kick(); 0873 } else if (eventName == "invite"_ls) { 0874 return powerLevelEvent->invite(); 0875 } else if (eventName == "redact"_ls) { 0876 return powerLevelEvent->redact(); 0877 } else if (eventName == "users_default"_ls) { 0878 return powerLevelEvent->usersDefault(); 0879 } else if (eventName == "state_default"_ls) { 0880 return powerLevelEvent->stateDefault(); 0881 } else if (eventName == "events_default"_ls) { 0882 return powerLevelEvent->eventsDefault(); 0883 } else if (isStateEvent) { 0884 return powerLevelEvent->powerLevelForState(eventName); 0885 } else { 0886 return powerLevelEvent->powerLevelForEvent(eventName); 0887 } 0888 } 0889 0890 void NeoChatRoom::setPowerLevel(const QString &eventName, const int &newPowerLevel, const bool &isStateEvent) 0891 { 0892 auto powerLevelContent = currentState().get("m.room.power_levels"_ls)->contentJson(); 0893 int clampPowerLevel = std::clamp(newPowerLevel, 0, 100); 0894 int powerLevel = 0; 0895 0896 if (powerLevelContent.contains(eventName)) { 0897 powerLevel = powerLevelContent[eventName].toInt(); 0898 0899 if (powerLevel != clampPowerLevel) { 0900 powerLevelContent[eventName] = clampPowerLevel; 0901 } 0902 } else { 0903 auto eventPowerLevels = powerLevelContent["events"_ls].toObject(); 0904 0905 if (eventPowerLevels.contains(eventName)) { 0906 powerLevel = eventPowerLevels[eventName].toInt(); 0907 } else { 0908 if (isStateEvent) { 0909 powerLevel = powerLevelContent["state_default"_ls].toInt(); 0910 } else { 0911 powerLevel = powerLevelContent["events_default"_ls].toInt(); 0912 } 0913 } 0914 0915 if (powerLevel != clampPowerLevel) { 0916 eventPowerLevels[eventName] = clampPowerLevel; 0917 powerLevelContent["events"_ls] = eventPowerLevels; 0918 } 0919 } 0920 0921 setState("m.room.power_levels"_ls, {}, powerLevelContent); 0922 } 0923 0924 int NeoChatRoom::defaultUserPowerLevel() const 0925 { 0926 return powerLevel("users_default"_ls); 0927 } 0928 0929 void NeoChatRoom::setDefaultUserPowerLevel(const int &newPowerLevel) 0930 { 0931 setPowerLevel("users_default"_ls, newPowerLevel); 0932 } 0933 0934 int NeoChatRoom::invitePowerLevel() const 0935 { 0936 return powerLevel("invite"_ls); 0937 } 0938 0939 void NeoChatRoom::setInvitePowerLevel(const int &newPowerLevel) 0940 { 0941 setPowerLevel("invite"_ls, newPowerLevel); 0942 } 0943 0944 int NeoChatRoom::kickPowerLevel() const 0945 { 0946 return powerLevel("kick"_ls); 0947 } 0948 0949 void NeoChatRoom::setKickPowerLevel(const int &newPowerLevel) 0950 { 0951 setPowerLevel("kick"_ls, newPowerLevel); 0952 } 0953 0954 int NeoChatRoom::banPowerLevel() const 0955 { 0956 return powerLevel("ban"_ls); 0957 } 0958 0959 void NeoChatRoom::setBanPowerLevel(const int &newPowerLevel) 0960 { 0961 setPowerLevel("ban"_ls, newPowerLevel); 0962 } 0963 0964 int NeoChatRoom::redactPowerLevel() const 0965 { 0966 return powerLevel("redact"_ls); 0967 } 0968 0969 void NeoChatRoom::setRedactPowerLevel(const int &newPowerLevel) 0970 { 0971 setPowerLevel("redact"_ls, newPowerLevel); 0972 } 0973 0974 int NeoChatRoom::statePowerLevel() const 0975 { 0976 return powerLevel("state_default"_ls); 0977 } 0978 0979 void NeoChatRoom::setStatePowerLevel(const int &newPowerLevel) 0980 { 0981 setPowerLevel("state_default"_ls, newPowerLevel); 0982 } 0983 0984 int NeoChatRoom::defaultEventPowerLevel() const 0985 { 0986 return powerLevel("events_default"_ls); 0987 } 0988 0989 void NeoChatRoom::setDefaultEventPowerLevel(const int &newPowerLevel) 0990 { 0991 setPowerLevel("events_default"_ls, newPowerLevel); 0992 } 0993 0994 int NeoChatRoom::powerLevelPowerLevel() const 0995 { 0996 return powerLevel("m.room.power_levels"_ls, true); 0997 } 0998 0999 void NeoChatRoom::setPowerLevelPowerLevel(const int &newPowerLevel) 1000 { 1001 setPowerLevel("m.room.power_levels"_ls, newPowerLevel, true); 1002 } 1003 1004 int NeoChatRoom::namePowerLevel() const 1005 { 1006 return powerLevel("m.room.name"_ls, true); 1007 } 1008 1009 void NeoChatRoom::setNamePowerLevel(const int &newPowerLevel) 1010 { 1011 setPowerLevel("m.room.name"_ls, newPowerLevel, true); 1012 } 1013 1014 int NeoChatRoom::avatarPowerLevel() const 1015 { 1016 return powerLevel("m.room.avatar"_ls, true); 1017 } 1018 1019 void NeoChatRoom::setAvatarPowerLevel(const int &newPowerLevel) 1020 { 1021 setPowerLevel("m.room.avatar"_ls, newPowerLevel, true); 1022 } 1023 1024 int NeoChatRoom::canonicalAliasPowerLevel() const 1025 { 1026 return powerLevel("m.room.canonical_alias"_ls, true); 1027 } 1028 1029 void NeoChatRoom::setCanonicalAliasPowerLevel(const int &newPowerLevel) 1030 { 1031 setPowerLevel("m.room.canonical_alias"_ls, newPowerLevel, true); 1032 } 1033 1034 int NeoChatRoom::topicPowerLevel() const 1035 { 1036 return powerLevel("m.room.topic"_ls, true); 1037 } 1038 1039 void NeoChatRoom::setTopicPowerLevel(const int &newPowerLevel) 1040 { 1041 setPowerLevel("m.room.topic"_ls, newPowerLevel, true); 1042 } 1043 1044 int NeoChatRoom::encryptionPowerLevel() const 1045 { 1046 return powerLevel("m.room.encryption"_ls, true); 1047 } 1048 1049 void NeoChatRoom::setEncryptionPowerLevel(const int &newPowerLevel) 1050 { 1051 setPowerLevel("m.room.encryption"_ls, newPowerLevel, true); 1052 } 1053 1054 int NeoChatRoom::historyVisibilityPowerLevel() const 1055 { 1056 return powerLevel("m.room.history_visibility"_ls, true); 1057 } 1058 1059 void NeoChatRoom::setHistoryVisibilityPowerLevel(const int &newPowerLevel) 1060 { 1061 setPowerLevel("m.room.history_visibility"_ls, newPowerLevel, true); 1062 } 1063 1064 int NeoChatRoom::pinnedEventsPowerLevel() const 1065 { 1066 return powerLevel("m.room.pinned_events"_ls, true); 1067 } 1068 1069 void NeoChatRoom::setPinnedEventsPowerLevel(const int &newPowerLevel) 1070 { 1071 setPowerLevel("m.room.pinned_events"_ls, newPowerLevel, true); 1072 } 1073 1074 int NeoChatRoom::tombstonePowerLevel() const 1075 { 1076 return powerLevel("m.room.tombstone"_ls, true); 1077 } 1078 1079 void NeoChatRoom::setTombstonePowerLevel(const int &newPowerLevel) 1080 { 1081 setPowerLevel("m.room.tombstone"_ls, newPowerLevel, true); 1082 } 1083 1084 int NeoChatRoom::serverAclPowerLevel() const 1085 { 1086 return powerLevel("m.room.server_acl"_ls, true); 1087 } 1088 1089 void NeoChatRoom::setServerAclPowerLevel(const int &newPowerLevel) 1090 { 1091 setPowerLevel("m.room.server_acl"_ls, newPowerLevel, true); 1092 } 1093 1094 int NeoChatRoom::spaceChildPowerLevel() const 1095 { 1096 return powerLevel("m.space.child"_ls, true); 1097 } 1098 1099 void NeoChatRoom::setSpaceChildPowerLevel(const int &newPowerLevel) 1100 { 1101 setPowerLevel("m.space.child"_ls, newPowerLevel, true); 1102 } 1103 1104 int NeoChatRoom::spaceParentPowerLevel() const 1105 { 1106 return powerLevel("m.space.parent"_ls, true); 1107 } 1108 1109 void NeoChatRoom::setSpaceParentPowerLevel(const int &newPowerLevel) 1110 { 1111 setPowerLevel("m.space.parent"_ls, newPowerLevel, true); 1112 } 1113 1114 QCoro::Task<void> NeoChatRoom::doDeleteMessagesByUser(const QString &user, QString reason) 1115 { 1116 QStringList events; 1117 for (const auto &event : messageEvents()) { 1118 if (event->senderId() == user && !event->isRedacted() && !event.viewAs<RedactionEvent>() && !event->isStateEvent()) { 1119 events += event->id(); 1120 } 1121 } 1122 for (const auto &e : events) { 1123 auto job = connection()->callApi<RedactEventJob>(id(), QString::fromLatin1(QUrl::toPercentEncoding(e)), connection()->generateTxnId(), reason); 1124 co_await qCoro(job, &BaseJob::finished); 1125 if (job->error() != BaseJob::Success) { 1126 qWarning() << "Error: \"" << job->error() << "\" while deleting messages. Aborting"; 1127 break; 1128 } 1129 } 1130 } 1131 1132 void NeoChatRoom::clearInvitationNotification() 1133 { 1134 NotificationsManager::instance().clearInvitationNotification(id()); 1135 } 1136 1137 bool NeoChatRoom::hasParent() const 1138 { 1139 return currentState().eventsOfType("m.space.parent"_ls).size() > 0; 1140 } 1141 1142 QList<QString> NeoChatRoom::parentIds() const 1143 { 1144 auto parentEvents = currentState().eventsOfType("m.space.parent"_ls); 1145 QList<QString> parentIds; 1146 for (const auto &parentEvent : parentEvents) { 1147 if (parentEvent->contentJson().contains("via"_ls) && !parentEvent->contentPart<QJsonArray>("via"_ls).isEmpty()) { 1148 parentIds += parentEvent->stateKey(); 1149 } 1150 } 1151 return parentIds; 1152 } 1153 1154 QList<NeoChatRoom *> NeoChatRoom::parentObjects(bool multiLevel) const 1155 { 1156 QList<NeoChatRoom *> parentObjects; 1157 QList<QString> parentIds = this->parentIds(); 1158 for (const auto &parentId : parentIds) { 1159 if (auto parentObject = static_cast<NeoChatRoom *>(connection()->room(parentId))) { 1160 parentObjects += parentObject; 1161 if (multiLevel) { 1162 parentObjects += parentObject->parentObjects(true); 1163 } 1164 } 1165 } 1166 return parentObjects; 1167 } 1168 1169 QString NeoChatRoom::canonicalParent() const 1170 { 1171 auto parentEvents = currentState().eventsOfType("m.space.parent"_ls); 1172 for (const auto &parentEvent : parentEvents) { 1173 if (parentEvent->contentJson().contains("via"_ls) && !parentEvent->contentPart<QJsonArray>("via"_ls).isEmpty()) { 1174 if (parentEvent->contentPart<bool>("canonical"_ls)) { 1175 return parentEvent->stateKey(); 1176 } 1177 } 1178 } 1179 return {}; 1180 } 1181 1182 void NeoChatRoom::setCanonicalParent(const QString &parentId) 1183 { 1184 if (!canModifyParent(parentId)) { 1185 return; 1186 } 1187 if (const auto &parent = currentState().get("m.space.parent"_ls, parentId)) { 1188 auto content = parent->contentJson(); 1189 content.insert("canonical"_ls, true); 1190 setState("m.space.parent"_ls, parentId, content); 1191 } else { 1192 return; 1193 } 1194 1195 // Only one canonical parent can exist so make sure others are set false. 1196 auto parentEvents = currentState().eventsOfType("m.space.parent"_ls); 1197 for (const auto &parentEvent : parentEvents) { 1198 if (parentEvent->contentPart<bool>("canonical"_ls) && parentEvent->stateKey() != parentId) { 1199 auto content = parentEvent->contentJson(); 1200 content.insert("canonical"_ls, false); 1201 setState("m.space.parent"_ls, parentEvent->stateKey(), content); 1202 } 1203 } 1204 } 1205 1206 bool NeoChatRoom::canModifyParent(const QString &parentId) const 1207 { 1208 if (!canSendState("m.space.parent"_ls)) { 1209 return false; 1210 } 1211 // If we can't peek the parent we assume that we neither have permission nor is 1212 // there an existing space child event for this room. 1213 if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) { 1214 if (!parent->isSpace()) { 1215 return false; 1216 } 1217 // If the user is allowed to set space child events in the parent they are 1218 // allowed to set the space as a parent (even if a space child event doesn't 1219 // exist). 1220 if (parent->canSendState("m.space.child"_ls)) { 1221 return true; 1222 } 1223 // If the parent has a space child event the user can set as a parent (even 1224 // if they don't have permission to set space child events in that parent). 1225 if (parent->currentState().contains("m.space.child"_ls, id())) { 1226 return true; 1227 } 1228 } 1229 return false; 1230 } 1231 1232 void NeoChatRoom::addParent(const QString &parentId, bool canonical, bool setParentChild) 1233 { 1234 if (!canModifyParent(parentId)) { 1235 return; 1236 } 1237 if (canonical) { 1238 // Only one canonical parent can exist so make sure others are set false. 1239 auto parentEvents = currentState().eventsOfType("m.space.parent"_ls); 1240 for (const auto &parentEvent : parentEvents) { 1241 if (parentEvent->contentPart<bool>("canonical"_ls)) { 1242 auto content = parentEvent->contentJson(); 1243 content.insert("canonical"_ls, false); 1244 setState("m.space.parent"_ls, parentEvent->stateKey(), content); 1245 } 1246 } 1247 } 1248 1249 setState("m.space.parent"_ls, parentId, QJsonObject{{"canonical"_ls, canonical}, {"via"_ls, QJsonArray{connection()->domain()}}}); 1250 1251 if (setParentChild) { 1252 if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) { 1253 parent->setState("m.space.child"_ls, id(), QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}}); 1254 } 1255 } 1256 } 1257 1258 void NeoChatRoom::removeParent(const QString &parentId) 1259 { 1260 if (!canModifyParent(parentId)) { 1261 return; 1262 } 1263 if (!currentState().contains("m.space.parent"_ls, parentId)) { 1264 return; 1265 } 1266 if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) { 1267 setState("m.space.parent"_ls, parentId, {}); 1268 } 1269 } 1270 1271 bool NeoChatRoom::isSpace() 1272 { 1273 const auto creationEvent = this->creation(); 1274 if (!creationEvent) { 1275 return false; 1276 } 1277 1278 return creationEvent->roomType() == RoomType::Space; 1279 } 1280 1281 void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool canonical, bool suggested) 1282 { 1283 if (!isSpace()) { 1284 return; 1285 } 1286 if (!canSendEvent("m.space.child"_ls)) { 1287 return; 1288 } 1289 setState("m.space.child"_ls, childId, QJsonObject{{QLatin1String("via"), QJsonArray{connection()->domain()}}, {"suggested"_ls, suggested}}); 1290 1291 if (setChildParent) { 1292 if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) { 1293 if (child->canSendState("m.space.parent"_ls)) { 1294 child->setState("m.space.parent"_ls, id(), QJsonObject{{"canonical"_ls, canonical}, {"via"_ls, QJsonArray{connection()->domain()}}}); 1295 1296 if (canonical) { 1297 // Only one canonical parent can exist so make sure others are set to false. 1298 auto parentEvents = child->currentState().eventsOfType("m.space.parent"_ls); 1299 for (const auto &parentEvent : parentEvents) { 1300 if (parentEvent->contentPart<bool>("canonical"_ls)) { 1301 auto content = parentEvent->contentJson(); 1302 content.insert("canonical"_ls, false); 1303 setState("m.space.parent"_ls, parentEvent->stateKey(), content); 1304 } 1305 } 1306 } 1307 } 1308 } 1309 } 1310 } 1311 1312 void NeoChatRoom::removeChild(const QString &childId, bool unsetChildParent) 1313 { 1314 if (!isSpace()) { 1315 return; 1316 } 1317 if (!canSendEvent("m.space.child"_ls)) { 1318 return; 1319 } 1320 setState("m.space.child"_ls, childId, {}); 1321 1322 if (unsetChildParent) { 1323 if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) { 1324 if (child->canSendState("m.space.parent"_ls) && child->currentState().contains("m.space.parent"_ls, id())) { 1325 child->setState("m.space.parent"_ls, id(), {}); 1326 } 1327 } 1328 } 1329 } 1330 1331 bool NeoChatRoom::isSuggested(const QString &childId) 1332 { 1333 if (!currentState().contains("m.space.child"_ls, childId)) { 1334 return false; 1335 } 1336 const auto childEvent = currentState().get("m.space.child"_ls, childId); 1337 return childEvent->contentPart<bool>("suggested"_ls); 1338 } 1339 1340 void NeoChatRoom::toggleChildSuggested(const QString &childId) 1341 { 1342 if (!isSpace()) { 1343 return; 1344 } 1345 if (!canSendEvent("m.space.child"_ls)) { 1346 return; 1347 } 1348 if (const auto childEvent = currentState().get("m.space.child"_ls, childId)) { 1349 auto content = childEvent->contentJson(); 1350 content.insert("suggested"_ls, !childEvent->contentPart<bool>("suggested"_ls)); 1351 setState("m.space.child"_ls, childId, content); 1352 } 1353 } 1354 1355 PushNotificationState::State NeoChatRoom::pushNotificationState() const 1356 { 1357 return m_currentPushNotificationState; 1358 } 1359 1360 void NeoChatRoom::setPushNotificationState(PushNotificationState::State state) 1361 { 1362 // The caller should never try to set the state to unknown. 1363 // It exists only as a default state to diable the settings options until the actual state is retrieved from the server. 1364 if (state == PushNotificationState::Unknown) { 1365 Q_ASSERT(false); 1366 return; 1367 } 1368 1369 /** 1370 * This stops updatePushNotificationState from temporarily changing 1371 * m_pushNotificationStateUpdating to default after the exisitng rules are deleted but 1372 * before a new rule is added. 1373 * The value is set to false after the rule enable job is successful. 1374 */ 1375 m_pushNotificationStateUpdating = true; 1376 1377 /** 1378 * First remove any existing room rules of the wrong type. 1379 * Note to prevent race conditions any rule that is going ot be overridden later is not removed. 1380 * If the default push notification state is chosen any existing rule needs to be removed. 1381 */ 1382 QJsonObject accountData = connection()->accountDataJson("m.push_rules"_ls); 1383 1384 // For default and mute check for a room rule and remove if found. 1385 if (state == PushNotificationState::Default || state == PushNotificationState::Mute) { 1386 QJsonArray roomRuleArray = accountData["global"_ls].toObject()["room"_ls].toArray(); 1387 for (const auto &i : roomRuleArray) { 1388 QJsonObject roomRule = i.toObject(); 1389 if (roomRule["rule_id"_ls] == id()) { 1390 connection()->callApi<DeletePushRuleJob>("global"_ls, "room"_ls, id()); 1391 } 1392 } 1393 } 1394 1395 // For default, all and @mentions and keywords check for an override rule and remove if found. 1396 if (state == PushNotificationState::Default || state == PushNotificationState::All || state == PushNotificationState::MentionKeyword) { 1397 QJsonArray overrideRuleArray = accountData["global"_ls].toObject()["override"_ls].toArray(); 1398 for (const auto &i : overrideRuleArray) { 1399 QJsonObject overrideRule = i.toObject(); 1400 if (overrideRule["rule_id"_ls] == id()) { 1401 connection()->callApi<DeletePushRuleJob>("global"_ls, "override"_ls, id()); 1402 } 1403 } 1404 } 1405 1406 if (state == PushNotificationState::Mute) { 1407 /** 1408 * To mute a room an override rule with "don't notify is set". 1409 * 1410 * Setup the rule action to "don't notify" to stop all room notifications 1411 * see https://spec.matrix.org/v1.3/client-server-api/#actions 1412 * 1413 * "actions": [ 1414 * "don't_notify" 1415 * ] 1416 */ 1417 const QList<QVariant> actions = {"dont_notify"_ls}; 1418 /** 1419 * Setup the push condition to get all events for the current room 1420 * see https://spec.matrix.org/v1.3/client-server-api/#conditions-1 1421 * 1422 * "conditions": [ 1423 * { 1424 * "key": "type", 1425 * "kind": "event_match", 1426 * "pattern": "room_id" 1427 * } 1428 * ] 1429 */ 1430 PushCondition pushCondition; 1431 pushCondition.kind = "event_match"_ls; 1432 pushCondition.key = "room_id"_ls; 1433 pushCondition.pattern = id(); 1434 const QList<PushCondition> conditions = {pushCondition}; 1435 1436 // Add new override rule and make sure it's enabled 1437 auto job = connection()->callApi<SetPushRuleJob>("global"_ls, "override"_ls, id(), actions, QString(), QString(), conditions, QString()); 1438 connect(job, &BaseJob::success, this, [this]() { 1439 auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("global"_ls, "override"_ls, id(), true); 1440 connect(enableJob, &BaseJob::success, this, [this]() { 1441 m_pushNotificationStateUpdating = false; 1442 }); 1443 }); 1444 } else if (state == PushNotificationState::MentionKeyword) { 1445 /** 1446 * To only get notifcations for @ mentions and keywords a room rule with "don't_notify" is set. 1447 * 1448 * Note - This works becuase a default override rule which catches all user mentions will 1449 * 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 1450 * rule. 1451 * 1452 * Setup the rule action to "don't notify" to stop all room event notifications 1453 * see https://spec.matrix.org/v1.3/client-server-api/#actions 1454 * 1455 * "actions": [ 1456 * "don't_notify" 1457 * ] 1458 */ 1459 const QList<QVariant> actions = {"dont_notify"_ls}; 1460 // No conditions for a room rule 1461 const QList<PushCondition> conditions; 1462 1463 auto setJob = connection()->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString()); 1464 connect(setJob, &BaseJob::success, this, [this]() { 1465 auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("global"_ls, "room"_ls, id(), true); 1466 connect(enableJob, &BaseJob::success, this, [this]() { 1467 m_pushNotificationStateUpdating = false; 1468 }); 1469 }); 1470 } else if (state == PushNotificationState::All) { 1471 /** 1472 * To send a notification for all room messages a room rule with "notify" is set. 1473 * 1474 * Setup the rule action to "notify" so all room events give notifications. 1475 * Tweeks is also set to follow default sound settings 1476 * see https://spec.matrix.org/v1.3/client-server-api/#actions 1477 * 1478 * "actions": [ 1479 * "notify", 1480 * { 1481 * "set_tweek": "sound", 1482 * "value": "default", 1483 * } 1484 * ] 1485 */ 1486 QJsonObject tweaks; 1487 tweaks.insert("set_tweak"_ls, "sound"_ls); 1488 tweaks.insert("value"_ls, "default"_ls); 1489 const QList<QVariant> actions = {"notify"_ls, tweaks}; 1490 // No conditions for a room rule 1491 const QList<PushCondition> conditions; 1492 1493 // Add new room rule and make sure enabled 1494 auto setJob = connection()->callApi<SetPushRuleJob>("global"_ls, "room"_ls, id(), actions, QString(), QString(), conditions, QString()); 1495 connect(setJob, &BaseJob::success, this, [this]() { 1496 auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("global"_ls, "room"_ls, id(), true); 1497 connect(enableJob, &BaseJob::success, this, [this]() { 1498 m_pushNotificationStateUpdating = false; 1499 }); 1500 }); 1501 } 1502 1503 m_currentPushNotificationState = state; 1504 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState); 1505 1506 } 1507 1508 void NeoChatRoom::updatePushNotificationState(QString type) 1509 { 1510 if (type != "m.push_rules"_ls || m_pushNotificationStateUpdating) { 1511 return; 1512 } 1513 1514 QJsonObject accountData = connection()->accountDataJson("m.push_rules"_ls); 1515 1516 // First look for a room rule with the room id 1517 QJsonArray roomRuleArray = accountData["global"_ls].toObject()["room"_ls].toArray(); 1518 for (const auto &i : roomRuleArray) { 1519 QJsonObject roomRule = i.toObject(); 1520 if (roomRule["rule_id"_ls] == id()) { 1521 if (roomRule["actions"_ls].toArray().size() == 0) { 1522 m_currentPushNotificationState = PushNotificationState::MentionKeyword; 1523 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState); 1524 return; 1525 } 1526 QString notifyAction = roomRule["actions"_ls].toArray()[0].toString(); 1527 if (notifyAction == "notify"_ls) { 1528 m_currentPushNotificationState = PushNotificationState::All; 1529 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState); 1530 return; 1531 } else if (notifyAction == "dont_notify"_ls) { 1532 m_currentPushNotificationState = PushNotificationState::MentionKeyword; 1533 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState); 1534 return; 1535 } 1536 } 1537 } 1538 1539 // Check for an override rule with the room id 1540 QJsonArray overrideRuleArray = accountData["global"_ls].toObject()["override"_ls].toArray(); 1541 for (const auto &i : overrideRuleArray) { 1542 QJsonObject overrideRule = i.toObject(); 1543 if (overrideRule["rule_id"_ls] == id()) { 1544 if (overrideRule["actions"_ls].toArray().isEmpty()) { 1545 m_currentPushNotificationState = PushNotificationState::Mute; 1546 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState); 1547 return; 1548 } 1549 QString notifyAction = overrideRule["actions"_ls].toArray()[0].toString(); 1550 if (notifyAction == "dont_notify"_ls) { 1551 m_currentPushNotificationState = PushNotificationState::Mute; 1552 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState); 1553 return; 1554 } 1555 } 1556 } 1557 1558 // If neither a room or override rule exist for the room then the setting must be default 1559 m_currentPushNotificationState = PushNotificationState::Default; 1560 Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState); 1561 } 1562 1563 void NeoChatRoom::reportEvent(const QString &eventId, const QString &reason) 1564 { 1565 auto job = connection()->callApi<ReportContentJob>(id(), eventId, -50, reason); 1566 connect(job, &BaseJob::finished, this, [this, job]() { 1567 if (job->error() == BaseJob::Success) { 1568 Q_EMIT showMessage(Positive, i18n("Report sent successfully.")); 1569 Q_EMIT showMessage(MessageType::Positive, i18n("Report sent successfully.")); 1570 } 1571 }); 1572 } 1573 1574 QByteArray NeoChatRoom::getEventJsonSource(const QString &eventId) 1575 { 1576 auto evtIt = findInTimeline(eventId); 1577 if (evtIt != messageEvents().rend() && is<RoomEvent>(**evtIt)) { 1578 const auto event = evtIt->viewAs<RoomEvent>(); 1579 return QJsonDocument(event->fullJson()).toJson(); 1580 } 1581 return {}; 1582 } 1583 1584 void NeoChatRoom::openEventMediaExternally(const QString &eventId) 1585 { 1586 const auto evtIt = findInTimeline(eventId); 1587 if (evtIt != messageEvents().rend() && is<RoomMessageEvent>(**evtIt)) { 1588 const auto event = evtIt->viewAs<RoomMessageEvent>(); 1589 if (event->hasFileContent()) { 1590 const auto transferInfo = fileTransferInfo(eventId); 1591 if (transferInfo.completed()) { 1592 UrlHelper helper; 1593 helper.openUrl(transferInfo.localPath); 1594 } else { 1595 downloadFile(eventId, 1596 QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/' 1597 + event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId))); 1598 connect(this, &Room::fileTransferCompleted, this, [this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) { 1599 Q_UNUSED(localFile); 1600 Q_UNUSED(fileMetadata); 1601 if (id == eventId) { 1602 auto transferInfo = fileTransferInfo(eventId); 1603 UrlHelper helper; 1604 helper.openUrl(transferInfo.localPath); 1605 } 1606 }); 1607 } 1608 } 1609 } 1610 } 1611 1612 void NeoChatRoom::copyEventMedia(const QString &eventId) 1613 { 1614 const auto evtIt = findInTimeline(eventId); 1615 if (evtIt != messageEvents().rend() && is<RoomMessageEvent>(**evtIt)) { 1616 const auto event = evtIt->viewAs<RoomMessageEvent>(); 1617 if (event->hasFileContent()) { 1618 const auto transferInfo = fileTransferInfo(eventId); 1619 if (transferInfo.completed()) { 1620 Clipboard clipboard; 1621 clipboard.setImage(transferInfo.localPath); 1622 } else { 1623 downloadFile(eventId, 1624 QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/' 1625 + event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId))); 1626 connect(this, &Room::fileTransferCompleted, this, [this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) { 1627 Q_UNUSED(localFile); 1628 Q_UNUSED(fileMetadata); 1629 if (id == eventId) { 1630 auto transferInfo = fileTransferInfo(eventId); 1631 Clipboard clipboard; 1632 clipboard.setImage(transferInfo.localPath); 1633 } 1634 }); 1635 } 1636 } 1637 } 1638 } 1639 1640 ChatBarCache *NeoChatRoom::mainCache() const 1641 { 1642 return m_mainCache; 1643 } 1644 1645 ChatBarCache *NeoChatRoom::editCache() const 1646 { 1647 return m_editCache; 1648 } 1649 1650 void NeoChatRoom::replyLastMessage() 1651 { 1652 const auto &timelineBottom = messageEvents().rbegin(); 1653 1654 // set a cap limit of startRow + 35 messages, to prevent loading a lot of messages 1655 // in rooms where the user has not sent many messages 1656 const auto limit = timelineBottom + std::min(35, timelineSize()); 1657 1658 for (auto it = timelineBottom; it != limit; ++it) { 1659 auto evt = it->event(); 1660 auto e = eventCast<const RoomMessageEvent>(evt); 1661 if (!e) { 1662 continue; 1663 } 1664 1665 auto content = (*it)->contentJson(); 1666 1667 if (e->msgtype() != MessageEventType::Unknown) { 1668 QString eventId; 1669 if (content.contains("m.new_content"_ls)) { 1670 // The message has been edited so we have to return the id of the original message instead of the replacement 1671 eventId = content["m.relates_to"_ls].toObject()["event_id"_ls].toString(); 1672 } else { 1673 // For any message that isn't an edit return the id of the current message 1674 eventId = (*it)->id(); 1675 } 1676 mainCache()->setReplyId(eventId); 1677 return; 1678 } 1679 } 1680 } 1681 1682 void NeoChatRoom::editLastMessage() 1683 { 1684 const auto &timelineBottom = messageEvents().rbegin(); 1685 1686 // set a cap limit of 35 messages, to prevent loading a lot of messages 1687 // in rooms where the user has not sent many messages 1688 const auto limit = timelineBottom + std::min(35, timelineSize()); 1689 1690 for (auto it = timelineBottom; it != limit; ++it) { 1691 auto evt = it->event(); 1692 auto e = eventCast<const RoomMessageEvent>(evt); 1693 if (!e) { 1694 continue; 1695 } 1696 1697 // check if the current message's sender's id is same as the user's id 1698 if ((*it)->senderId() == localUser()->id()) { 1699 auto content = (*it)->contentJson(); 1700 1701 if (e->msgtype() != MessageEventType::Unknown) { 1702 QString eventId; 1703 if (content.contains("m.new_content"_ls)) { 1704 // The message has been edited so we have to return the id of the original message instead of the replacement 1705 eventId = content["m.relates_to"_ls].toObject()["event_id"_ls].toString(); 1706 } else { 1707 // For any message that isn't an edit return the id of the current message 1708 eventId = (*it)->id(); 1709 } 1710 editCache()->setEditId(eventId); 1711 return; 1712 } 1713 } 1714 } 1715 } 1716 1717 bool NeoChatRoom::canEncryptRoom() const 1718 { 1719 return !usesEncryption() && canSendState("m.room.encryption"_ls); 1720 } 1721 1722 static PollHandler *emptyPollHandler = new PollHandler; 1723 1724 PollHandler *NeoChatRoom::poll(const QString &eventId) const 1725 { 1726 if (auto pollHandler = m_polls[eventId]) { 1727 return pollHandler; 1728 } 1729 return emptyPollHandler; 1730 } 1731 1732 void NeoChatRoom::createPollHandler(const Quotient::PollStartEvent *event) 1733 { 1734 if (event == nullptr) { 1735 return; 1736 } 1737 auto eventId = event->id(); 1738 if (!m_polls.contains(eventId)) { 1739 auto handler = new PollHandler(this, event); 1740 m_polls.insert(eventId, handler); 1741 } 1742 } 1743 1744 bool NeoChatRoom::downloadTempFile(const QString &eventId) 1745 { 1746 QTemporaryFile file; 1747 file.setAutoRemove(false); 1748 if (!file.open()) { 1749 return false; 1750 } 1751 1752 download(eventId, QUrl::fromLocalFile(file.fileName())); 1753 return true; 1754 } 1755 1756 void NeoChatRoom::download(const QString &eventId, const QUrl &localFilename) 1757 { 1758 downloadFile(eventId, localFilename); 1759 #ifndef Q_OS_ANDROID 1760 auto job = new FileTransferPseudoJob(FileTransferPseudoJob::Download, localFilename.toLocalFile(), eventId); 1761 connect(this, &Room::fileTransferProgress, job, &FileTransferPseudoJob::fileTransferProgress); 1762 connect(this, &Room::fileTransferCompleted, job, &FileTransferPseudoJob::fileTransferCompleted); 1763 connect(this, &Room::fileTransferFailed, job, &FileTransferPseudoJob::fileTransferFailed); 1764 KIO::getJobTracker()->registerJob(job); 1765 job->start(); 1766 #endif 1767 } 1768 1769 void NeoChatRoom::mapAlias(const QString &alias) 1770 { 1771 auto getLocalAliasesJob = connection()->callApi<GetLocalAliasesJob>(id()); 1772 connect(getLocalAliasesJob, &BaseJob::success, this, [this, getLocalAliasesJob, alias] { 1773 if (getLocalAliasesJob->aliases().contains(alias)) { 1774 return; 1775 } else { 1776 auto setRoomAliasJob = connection()->callApi<SetRoomAliasJob>(alias, id()); 1777 connect(setRoomAliasJob, &BaseJob::success, this, [this, alias] { 1778 auto newAltAliases = altAliases(); 1779 newAltAliases.append(alias); 1780 setLocalAliases(newAltAliases); 1781 }); 1782 } 1783 }); 1784 } 1785 1786 void NeoChatRoom::unmapAlias(const QString &alias) 1787 { 1788 connection()->callApi<DeleteRoomAliasJob>(alias); 1789 } 1790 1791 void NeoChatRoom::setCanonicalAlias(const QString &newAlias) 1792 { 1793 QString oldCanonicalAlias = canonicalAlias(); 1794 Room::setCanonicalAlias(newAlias); 1795 1796 connect(this, &Room::namesChanged, this, [this, newAlias, oldCanonicalAlias] { 1797 if (canonicalAlias() == newAlias) { 1798 // If the new canonical alias is already a published alt alias remove it otherwise it will be in both lists. 1799 // The server doesn't prevent this so we need to handle it. 1800 auto newAltAliases = altAliases(); 1801 if (!oldCanonicalAlias.isEmpty()) { 1802 newAltAliases.append(oldCanonicalAlias); 1803 } 1804 if (newAltAliases.contains(newAlias)) { 1805 newAltAliases.removeAll(newAlias); 1806 Room::setLocalAliases(newAltAliases); 1807 } 1808 } 1809 }); 1810 } 1811 1812 int NeoChatRoom::maxRoomVersion() const 1813 { 1814 int maxVersion = 0; 1815 for (auto roomVersion : connection()->availableRoomVersions()) { 1816 if (roomVersion.id.toInt() > maxVersion) { 1817 maxVersion = roomVersion.id.toInt(); 1818 } 1819 } 1820 return maxVersion; 1821 } 1822 1823 Quotient::User *NeoChatRoom::directChatRemoteUser() const 1824 { 1825 auto users = connection()->directChatUsers(this); 1826 if (users.isEmpty()) { 1827 return nullptr; 1828 } 1829 return users[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"