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