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