File indexing completed on 2024-12-08 07:33:46
0001 // SPDX-FileCopyrightText: 2018-2019 Black Hat <bhat@encom.eu.org> 0002 // SPDX-License-Identifier: GPL-3.0-only 0003 0004 #include "roomlistmodel.h" 0005 0006 #include "eventhandler.h" 0007 #include "neochatconfig.h" 0008 #include "neochatroom.h" 0009 #include "roommanager.h" 0010 #include "spacehierarchycache.h" 0011 0012 #include <QDebug> 0013 #if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) 0014 #ifndef Q_OS_ANDROID 0015 #include <QDBusConnection> 0016 #include <QDBusInterface> 0017 #include <QDBusMessage> 0018 #endif 0019 #endif 0020 0021 #include <KLocalizedString> 0022 #include <QGuiApplication> 0023 0024 using namespace Quotient; 0025 0026 Q_DECLARE_METATYPE(Quotient::JoinState) 0027 0028 RoomListModel::RoomListModel(QObject *parent) 0029 : QAbstractListModel(parent) 0030 { 0031 const auto collapsedSections = NeoChatConfig::collapsedSections(); 0032 for (auto collapsedSection : collapsedSections) { 0033 m_categoryVisibility[collapsedSection] = false; 0034 } 0035 0036 connect(this, &RoomListModel::highlightCountChanged, this, [this]() { 0037 #if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) 0038 #ifndef Q_OS_ANDROID 0039 // copied from Telegram desktop 0040 const auto launcherUrl = "application://org.kde.neochat.desktop"_ls; 0041 // Gnome requires that count is a 64bit integer 0042 const qint64 counterSlice = std::min(m_highlightCount, 9999); 0043 QVariantMap dbusUnityProperties; 0044 0045 if (counterSlice > 0) { 0046 dbusUnityProperties["count"_ls] = counterSlice; 0047 dbusUnityProperties["count-visible"_ls] = true; 0048 } else { 0049 dbusUnityProperties["count-visible"_ls] = false; 0050 } 0051 0052 auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat"_ls, "com.canonical.Unity.LauncherEntry"_ls, "Update"_ls); 0053 0054 signal.setArguments({launcherUrl, dbusUnityProperties}); 0055 0056 QDBusConnection::sessionBus().send(signal); 0057 #endif // Q_OS_ANDROID 0058 #else 0059 qGuiApp->setBadgeNumber(m_highlightCount); 0060 #endif // QT_VERSION_CHECK(6, 6, 0) 0061 }); 0062 connect(&SpaceHierarchyCache::instance(), &SpaceHierarchyCache::spaceHierarchyChanged, this, [this]() { 0063 Q_EMIT dataChanged(index(0, 0), index(rowCount(), 0), {IsChildSpaceRole}); 0064 }); 0065 } 0066 0067 RoomListModel::~RoomListModel() = default; 0068 0069 Quotient::Connection *RoomListModel::connection() const 0070 { 0071 return m_connection; 0072 } 0073 0074 void RoomListModel::setConnection(Connection *connection) 0075 { 0076 if (connection == m_connection) { 0077 return; 0078 } 0079 if (m_connection) { 0080 m_connection->disconnect(this); 0081 } 0082 if (!connection) { 0083 qDebug() << "Removing current connection..."; 0084 m_connection = nullptr; 0085 beginResetModel(); 0086 m_rooms.clear(); 0087 endResetModel(); 0088 return; 0089 } 0090 0091 m_connection = connection; 0092 0093 for (NeoChatRoom *room : std::as_const(m_rooms)) { 0094 room->disconnect(this); 0095 } 0096 0097 connect(connection, &Connection::connected, this, &RoomListModel::doResetModel); 0098 connect(connection, &Connection::invitedRoom, this, &RoomListModel::updateRoom); 0099 connect(connection, &Connection::joinedRoom, this, &RoomListModel::updateRoom); 0100 connect(connection, &Connection::leftRoom, this, &RoomListModel::updateRoom); 0101 connect(connection, &Connection::aboutToDeleteRoom, this, &RoomListModel::deleteRoom); 0102 connect(connection, &Connection::directChatsListChanged, this, [this, connection](Quotient::DirectChatsMap additions, Quotient::DirectChatsMap removals) { 0103 auto refreshRooms = [this, &connection](Quotient::DirectChatsMap rooms) { 0104 for (const QString &roomID : std::as_const(rooms)) { 0105 auto room = connection->room(roomID); 0106 if (room) { 0107 refresh(static_cast<NeoChatRoom *>(room)); 0108 } 0109 } 0110 }; 0111 0112 refreshRooms(std::move(additions)); 0113 refreshRooms(std::move(removals)); 0114 }); 0115 0116 doResetModel(); 0117 0118 Q_EMIT connectionChanged(); 0119 } 0120 0121 void RoomListModel::doResetModel() 0122 { 0123 beginResetModel(); 0124 m_rooms.clear(); 0125 const auto rooms = m_connection->allRooms(); 0126 for (const auto &room : rooms) { 0127 doAddRoom(room); 0128 } 0129 endResetModel(); 0130 refreshNotificationCount(); 0131 } 0132 0133 NeoChatRoom *RoomListModel::roomAt(int row) const 0134 { 0135 return m_rooms.at(row); 0136 } 0137 0138 void RoomListModel::doAddRoom(Room *r) 0139 { 0140 if (auto room = static_cast<NeoChatRoom *>(r)) { 0141 m_rooms.append(room); 0142 connectRoomSignals(room); 0143 Q_EMIT roomAdded(room); 0144 } else { 0145 qCritical() << "Attempt to add nullptr to the room list"; 0146 Q_ASSERT(false); 0147 } 0148 } 0149 0150 void RoomListModel::connectRoomSignals(NeoChatRoom *room) 0151 { 0152 connect(room, &Room::displaynameChanged, this, [this, room] { 0153 refresh(room, {DisplayNameRole}); 0154 }); 0155 connect(room, &Room::unreadStatsChanged, this, [this, room] { 0156 refresh(room, {NotificationCountRole, HighlightCountRole}); 0157 }); 0158 connect(room, &Room::notificationCountChanged, this, [this, room] { 0159 refresh(room); 0160 }); 0161 connect(room, &Room::highlightCountChanged, this, [this, room] { 0162 refresh(room); 0163 }); 0164 connect(room, &Room::avatarChanged, this, [this, room] { 0165 refresh(room, {AvatarRole}); 0166 }); 0167 connect(room, &Room::tagsChanged, this, [this, room] { 0168 refresh(room); 0169 }); 0170 connect(room, &Room::joinStateChanged, this, [this, room] { 0171 refresh(room); 0172 }); 0173 connect(room, &Room::addedMessages, this, [this, room] { 0174 refresh(room, {SubtitleTextRole, LastActiveTimeRole}); 0175 }); 0176 connect(room, &Room::pendingEventMerged, this, [this, room] { 0177 refresh(room, {SubtitleTextRole}); 0178 }); 0179 connect(room, &Room::unreadStatsChanged, this, &RoomListModel::refreshNotificationCount); 0180 connect(room, &Room::highlightCountChanged, this, &RoomListModel::refreshHighlightCount); 0181 } 0182 0183 int RoomListModel::notificationCount() const 0184 { 0185 return m_notificationCount; 0186 } 0187 0188 int RoomListModel::highlightCount() const 0189 { 0190 return m_highlightCount; 0191 } 0192 0193 void RoomListModel::refreshNotificationCount() 0194 { 0195 int count = 0; 0196 for (auto room : std::as_const(m_rooms)) { 0197 count += room->notificationCount(); 0198 } 0199 if (m_notificationCount == count) { 0200 return; 0201 } 0202 m_notificationCount = count; 0203 Q_EMIT notificationCountChanged(); 0204 } 0205 0206 void RoomListModel::refreshHighlightCount() 0207 { 0208 int count = 0; 0209 for (auto room : std::as_const(m_rooms)) { 0210 count += room->highlightCount(); 0211 } 0212 if (m_highlightCount == count) { 0213 return; 0214 } 0215 m_highlightCount = count; 0216 Q_EMIT highlightCountChanged(); 0217 } 0218 0219 void RoomListModel::updateRoom(Room *room, Room *prev) 0220 { 0221 // There are two cases when this method is called: 0222 // 1. (prev == nullptr) adding a new room to the room list 0223 // 2. (prev != nullptr) accepting/rejecting an invitation or inviting to 0224 // the previously left room (in both cases prev has the previous state). 0225 if (prev == room) { 0226 qCritical() << "RoomListModel::updateRoom: room tried to replace itself"; 0227 refresh(static_cast<NeoChatRoom *>(room)); 0228 return; 0229 } 0230 if (prev && room->id() != prev->id()) { 0231 qCritical() << "RoomListModel::updateRoom: attempt to update room" << room->id() << "to" << prev->id(); 0232 // That doesn't look right but technically we still can do it. 0233 } 0234 // Ok, we're through with pre-checks, now for the real thing. 0235 auto newRoom = static_cast<NeoChatRoom *>(room); 0236 const auto it = std::find_if(m_rooms.begin(), m_rooms.end(), [prev, newRoom](const NeoChatRoom *r) { 0237 return r == prev || r == newRoom; 0238 }); 0239 if (it != m_rooms.end()) { 0240 const int row = it - m_rooms.begin(); 0241 // There's no guarantee that prev != newRoom 0242 if (*it == prev && *it != newRoom) { 0243 prev->disconnect(this); 0244 m_rooms.replace(row, newRoom); 0245 connectRoomSignals(newRoom); 0246 } 0247 Q_EMIT dataChanged(index(row), index(row)); 0248 } else { 0249 beginInsertRows(QModelIndex(), m_rooms.count(), m_rooms.count()); 0250 doAddRoom(newRoom); 0251 endInsertRows(); 0252 } 0253 } 0254 0255 void RoomListModel::deleteRoom(Room *room) 0256 { 0257 qDebug() << "Deleting room" << room->id(); 0258 const auto it = std::find(m_rooms.begin(), m_rooms.end(), room); 0259 if (it == m_rooms.end()) { 0260 return; // Already deleted, nothing to do 0261 } 0262 qDebug() << "Erasing room" << room->id(); 0263 const int row = it - m_rooms.begin(); 0264 beginRemoveRows(QModelIndex(), row, row); 0265 m_rooms.erase(it); 0266 endRemoveRows(); 0267 } 0268 0269 int RoomListModel::rowCount(const QModelIndex &parent) const 0270 { 0271 if (parent.isValid()) { 0272 return 0; 0273 } 0274 return m_rooms.count(); 0275 } 0276 0277 QVariant RoomListModel::data(const QModelIndex &index, int role) const 0278 { 0279 if (!index.isValid()) { 0280 return QVariant(); 0281 } 0282 0283 if (index.row() >= m_rooms.count()) { 0284 qDebug() << "UserListModel: something wrong here..."; 0285 return QVariant(); 0286 } 0287 NeoChatRoom *room = m_rooms.at(index.row()); 0288 if (role == DisplayNameRole) { 0289 return room->displayName(); 0290 } 0291 if (role == AvatarRole) { 0292 return room->avatarMediaId(); 0293 } 0294 if (role == CanonicalAliasRole) { 0295 return room->canonicalAlias(); 0296 } 0297 if (role == TopicRole) { 0298 return room->topic(); 0299 } 0300 if (role == CategoryRole) { 0301 if (room->joinState() == JoinState::Invite) { 0302 return NeoChatRoomType::Invited; 0303 } 0304 if (room->isFavourite()) { 0305 return NeoChatRoomType::Favorite; 0306 } 0307 if (room->isLowPriority()) { 0308 return NeoChatRoomType::Deprioritized; 0309 } 0310 if (room->isDirectChat()) { 0311 return NeoChatRoomType::Direct; 0312 } 0313 const RoomCreateEvent *creationEvent = room->creation(); 0314 if (!creationEvent) { 0315 return NeoChatRoomType::Normal; 0316 } 0317 QJsonObject contentJson = creationEvent->contentJson(); 0318 QJsonObject::const_iterator typeIter = contentJson.find("type"_ls); 0319 if (typeIter != contentJson.end()) { 0320 if (typeIter.value().toString() == "m.space"_ls) { 0321 return NeoChatRoomType::Space; 0322 } 0323 } 0324 return NeoChatRoomType::Normal; 0325 } 0326 if (role == NotificationCountRole) { 0327 return room->notificationCount(); 0328 } 0329 if (role == HighlightCountRole) { 0330 return room->highlightCount(); 0331 } 0332 if (role == LastActiveTimeRole) { 0333 return room->lastActiveTime(); 0334 } 0335 if (role == JoinStateRole) { 0336 if (!room->successorId().isEmpty()) { 0337 return QStringLiteral("upgraded"); 0338 } 0339 return QVariant::fromValue(room->joinState()); 0340 } 0341 if (role == CurrentRoomRole) { 0342 return QVariant::fromValue(room); 0343 } 0344 if (role == CategoryVisibleRole) { 0345 return m_categoryVisibility.value(data(index, CategoryRole).toInt(), true); 0346 } 0347 if (role == SubtitleTextRole) { 0348 if (room->lastEvent() == nullptr || room->lastEventIsSpoiler()) { 0349 return QString(); 0350 } 0351 EventHandler eventHandler; 0352 eventHandler.setRoom(room); 0353 eventHandler.setEvent(room->lastEvent()); 0354 return eventHandler.subtitleText(); 0355 } 0356 if (role == AvatarImageRole) { 0357 return room->avatar(128); 0358 } 0359 if (role == RoomIdRole) { 0360 return room->id(); 0361 } 0362 if (role == IsSpaceRole) { 0363 return room->isSpace(); 0364 } 0365 if (role == IsChildSpaceRole) { 0366 return SpaceHierarchyCache::instance().isChildSpace(room->id()); 0367 } 0368 if (role == ReplacementIdRole) { 0369 return room->successorId(); 0370 } 0371 0372 return QVariant(); 0373 } 0374 0375 void RoomListModel::refresh(NeoChatRoom *room, const QList<int> &roles) 0376 { 0377 const auto it = std::find(m_rooms.begin(), m_rooms.end(), room); 0378 if (it == m_rooms.end()) { 0379 qCritical() << "Room" << room->id() << "not found in the room list"; 0380 return; 0381 } 0382 const auto idx = index(it - m_rooms.begin()); 0383 Q_EMIT dataChanged(idx, idx, roles); 0384 } 0385 0386 QHash<int, QByteArray> RoomListModel::roleNames() const 0387 { 0388 QHash<int, QByteArray> roles; 0389 roles[DisplayNameRole] = "displayName"; 0390 roles[AvatarRole] = "avatar"; 0391 roles[CanonicalAliasRole] = "canonicalAlias"; 0392 roles[TopicRole] = "topic"; 0393 roles[CategoryRole] = "category"; 0394 roles[NotificationCountRole] = "notificationCount"; 0395 roles[HighlightCountRole] = "highlightCount"; 0396 roles[LastActiveTimeRole] = "lastActiveTime"; 0397 roles[JoinStateRole] = "joinState"; 0398 roles[CurrentRoomRole] = "currentRoom"; 0399 roles[CategoryVisibleRole] = "categoryVisible"; 0400 roles[SubtitleTextRole] = "subtitleText"; 0401 roles[IsSpaceRole] = "isSpace"; 0402 roles[RoomIdRole] = "roomId"; 0403 roles[IsChildSpaceRole] = "isChildSpace"; 0404 return roles; 0405 } 0406 0407 QString RoomListModel::categoryName(int category) 0408 { 0409 switch (category) { 0410 case NeoChatRoomType::Invited: 0411 return i18n("Invited"); 0412 case NeoChatRoomType::Favorite: 0413 return i18n("Favorite"); 0414 case NeoChatRoomType::Direct: 0415 return i18n("Direct Messages"); 0416 case NeoChatRoomType::Normal: 0417 return i18n("Normal"); 0418 case NeoChatRoomType::Deprioritized: 0419 return i18n("Low priority"); 0420 case NeoChatRoomType::Space: 0421 return i18n("Spaces"); 0422 default: 0423 return {}; 0424 } 0425 } 0426 0427 QString RoomListModel::categoryIconName(int category) 0428 { 0429 switch (category) { 0430 case NeoChatRoomType::Invited: 0431 return QStringLiteral("user-invisible"); 0432 case NeoChatRoomType::Favorite: 0433 return QStringLiteral("favorite"); 0434 case NeoChatRoomType::Direct: 0435 return QStringLiteral("dialog-messages"); 0436 case NeoChatRoomType::Normal: 0437 return QStringLiteral("group"); 0438 case NeoChatRoomType::Deprioritized: 0439 return QStringLiteral("object-order-lower"); 0440 case NeoChatRoomType::Space: 0441 return QStringLiteral("group"); 0442 default: 0443 return QStringLiteral("tools-report-bug"); 0444 } 0445 } 0446 0447 void RoomListModel::setCategoryVisible(int category, bool visible) 0448 { 0449 beginResetModel(); 0450 auto collapsedSections = NeoChatConfig::collapsedSections(); 0451 if (visible) { 0452 collapsedSections.removeAll(category); 0453 } else { 0454 collapsedSections.push_back(category); 0455 } 0456 NeoChatConfig::setCollapsedSections(collapsedSections); 0457 NeoChatConfig::self()->save(); 0458 0459 m_categoryVisibility[category] = visible; 0460 endResetModel(); 0461 } 0462 0463 bool RoomListModel::categoryVisible(int category) const 0464 { 0465 return m_categoryVisibility.value(category, true); 0466 } 0467 0468 NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId) 0469 { 0470 for (const auto &room : std::as_const(m_rooms)) { 0471 if (room->aliases().contains(aliasOrId) || room->id() == aliasOrId) { 0472 return room; 0473 } 0474 } 0475 return nullptr; 0476 } 0477 0478 int RoomListModel::rowForRoom(NeoChatRoom *room) const 0479 { 0480 return m_rooms.indexOf(room); 0481 } 0482 0483 #include "moc_roomlistmodel.cpp"