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"