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"