File indexing completed on 2024-05-05 05:01:25
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(room, room->lastEvent()); 0352 return eventHandler.subtitleText(); 0353 } 0354 if (role == AvatarImageRole) { 0355 return room->avatar(128); 0356 } 0357 if (role == RoomIdRole) { 0358 return room->id(); 0359 } 0360 if (role == IsSpaceRole) { 0361 return room->isSpace(); 0362 } 0363 if (role == IsChildSpaceRole) { 0364 return SpaceHierarchyCache::instance().isChild(room->id()); 0365 } 0366 if (role == ReplacementIdRole) { 0367 return room->successorId(); 0368 } 0369 if (role == IsDirectChat) { 0370 return room->isDirectChat(); 0371 } 0372 0373 return QVariant(); 0374 } 0375 0376 void RoomListModel::refresh(NeoChatRoom *room, const QList<int> &roles) 0377 { 0378 const auto it = std::find(m_rooms.begin(), m_rooms.end(), room); 0379 if (it == m_rooms.end()) { 0380 qCritical() << "Room" << room->id() << "not found in the room list"; 0381 return; 0382 } 0383 const auto idx = index(it - m_rooms.begin()); 0384 Q_EMIT dataChanged(idx, idx, roles); 0385 } 0386 0387 QHash<int, QByteArray> RoomListModel::roleNames() const 0388 { 0389 QHash<int, QByteArray> roles; 0390 roles[DisplayNameRole] = "displayName"; 0391 roles[AvatarRole] = "avatar"; 0392 roles[CanonicalAliasRole] = "canonicalAlias"; 0393 roles[TopicRole] = "topic"; 0394 roles[CategoryRole] = "category"; 0395 roles[NotificationCountRole] = "notificationCount"; 0396 roles[HighlightCountRole] = "highlightCount"; 0397 roles[LastActiveTimeRole] = "lastActiveTime"; 0398 roles[JoinStateRole] = "joinState"; 0399 roles[CurrentRoomRole] = "currentRoom"; 0400 roles[CategoryVisibleRole] = "categoryVisible"; 0401 roles[SubtitleTextRole] = "subtitleText"; 0402 roles[IsSpaceRole] = "isSpace"; 0403 roles[RoomIdRole] = "roomId"; 0404 roles[IsChildSpaceRole] = "isChildSpace"; 0405 roles[IsDirectChat] = "isDirectChat"; 0406 return roles; 0407 } 0408 0409 QString RoomListModel::categoryName(int category) 0410 { 0411 switch (category) { 0412 case NeoChatRoomType::Invited: 0413 return i18n("Invited"); 0414 case NeoChatRoomType::Favorite: 0415 return i18n("Favorite"); 0416 case NeoChatRoomType::Direct: 0417 return i18n("Friends"); 0418 case NeoChatRoomType::Normal: 0419 return i18n("Normal"); 0420 case NeoChatRoomType::Deprioritized: 0421 return i18n("Low priority"); 0422 case NeoChatRoomType::Space: 0423 return i18n("Spaces"); 0424 default: 0425 return {}; 0426 } 0427 } 0428 0429 QString RoomListModel::categoryIconName(int category) 0430 { 0431 switch (category) { 0432 case NeoChatRoomType::Invited: 0433 return QStringLiteral("user-invisible"); 0434 case NeoChatRoomType::Favorite: 0435 return QStringLiteral("favorite"); 0436 case NeoChatRoomType::Direct: 0437 return QStringLiteral("dialog-messages"); 0438 case NeoChatRoomType::Normal: 0439 return QStringLiteral("group"); 0440 case NeoChatRoomType::Deprioritized: 0441 return QStringLiteral("object-order-lower"); 0442 case NeoChatRoomType::Space: 0443 return QStringLiteral("group"); 0444 default: 0445 return QStringLiteral("tools-report-bug"); 0446 } 0447 } 0448 0449 void RoomListModel::setCategoryVisible(int category, bool visible) 0450 { 0451 beginResetModel(); 0452 auto collapsedSections = NeoChatConfig::collapsedSections(); 0453 if (visible) { 0454 collapsedSections.removeAll(category); 0455 } else { 0456 collapsedSections.push_back(category); 0457 } 0458 NeoChatConfig::setCollapsedSections(collapsedSections); 0459 NeoChatConfig::self()->save(); 0460 0461 m_categoryVisibility[category] = visible; 0462 endResetModel(); 0463 } 0464 0465 bool RoomListModel::categoryVisible(int category) const 0466 { 0467 return m_categoryVisibility.value(category, true); 0468 } 0469 0470 NeoChatRoom *RoomListModel::roomByAliasOrId(const QString &aliasOrId) 0471 { 0472 for (const auto &room : std::as_const(m_rooms)) { 0473 if (room->aliases().contains(aliasOrId) || room->id() == aliasOrId) { 0474 return room; 0475 } 0476 } 0477 return nullptr; 0478 } 0479 0480 int RoomListModel::rowForRoom(NeoChatRoom *room) const 0481 { 0482 return m_rooms.indexOf(room); 0483 } 0484 0485 #include "moc_roomlistmodel.cpp"