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