File indexing completed on 2024-12-08 04:34:10

0001 /*
0002    SPDX-FileCopyrightText: 2020-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "channellistview.h"
0008 #include "channellistdelegate.h"
0009 #include "connection.h"
0010 #include "dialogs/configurenotificationdialog.h"
0011 #include "model/roomfilterproxymodel.h"
0012 #include "model/roomlistheadingsproxymodel.h"
0013 #include "rocketchataccount.h"
0014 #include "ruqolawidgets_debug.h"
0015 #include "teams/channelsconverttoteamjob.h"
0016 #include "teams/groupsconverttoteamjob.h"
0017 #include "teams/searchteamdialog.h"
0018 #include "teams/teamaddroomsjob.h"
0019 #include "teams/teamconverttochanneldialog.h"
0020 #include "teams/teamconverttochanneljob.h"
0021 #include "teams/teamroom.h"
0022 #include "teams/teamslistroomsjob.h"
0023 
0024 #include <KLocalizedString>
0025 #include <KMessageBox>
0026 
0027 #include <QAction>
0028 #include <QContextMenuEvent>
0029 #include <QMenu>
0030 #include <QPointer>
0031 #include <QVector>
0032 
0033 ChannelListView::ChannelListView(QWidget *parent)
0034     : QTreeView(parent)
0035     , mChannelListDelegate(new ChannelListDelegate(this))
0036     , mRoomListHeadingsProxyModel(new RoomListHeadingsProxyModel(this))
0037     , mRoomFilterProxyModel(new RoomFilterProxyModel(this))
0038 {
0039     mChannelListDelegate->setObjectName(QStringLiteral("mChannelListDelegate"));
0040     mRoomFilterProxyModel->setObjectName(QStringLiteral("mRoomFilterProxyModel"));
0041     setItemDelegate(mChannelListDelegate);
0042     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0043     mRoomFilterProxyModel->setSourceModel(mRoomListHeadingsProxyModel);
0044     setModel(mRoomFilterProxyModel);
0045     setHeaderHidden(true);
0046     setRootIsDecorated(false);
0047     setUniformRowHeights(true);
0048     setItemsExpandable(false);
0049     setIndentation(0);
0050 
0051     connect(selectionModel(), &QItemSelectionModel::currentChanged, this, &ChannelListView::slotClicked);
0052     connect(model(), &QAbstractItemModel::rowsInserted, this, &QTreeView::expandAll);
0053     connect(model(), &QAbstractItemModel::modelReset, this, &QTreeView::expandAll);
0054     connect(model(), &QAbstractItemModel::rowsMoved, this, &QTreeView::expandAll);
0055     connect(model(), &QAbstractItemModel::layoutChanged, this, &QTreeView::expandAll);
0056     connect(this, &QTreeView::pressed, this, &ChannelListView::slotPressed);
0057 }
0058 
0059 ChannelListView::~ChannelListView() = default;
0060 
0061 void ChannelListView::setCurrentRocketChatAccount(RocketChatAccount *currentRocketChatAccount)
0062 {
0063     if (mCurrentRocketChatAccount) {
0064         disconnect(mCurrentRocketChatAccount, &RocketChatAccount::roomRemoved, this, &ChannelListView::slotRoomRemoved);
0065         disconnect(mUpdateChannelViewConnect);
0066     }
0067     mCurrentRocketChatAccount = currentRocketChatAccount;
0068     connect(mCurrentRocketChatAccount, &RocketChatAccount::roomRemoved, this, &ChannelListView::slotRoomRemoved);
0069     mUpdateChannelViewConnect = connect(mCurrentRocketChatAccount, &RocketChatAccount::ownUserPreferencesChanged, this, [this]() {
0070         filterModel()->setSortOrder(mCurrentRocketChatAccount->roomListSortOrder());
0071     });
0072     filterModel()->setSortOrder(mCurrentRocketChatAccount->roomListSortOrder());
0073     mChannelListDelegate->setCurrentRocketChatAccount(currentRocketChatAccount);
0074     mRoomListHeadingsProxyModel->setSourceModel(currentRocketChatAccount->roomModel());
0075 }
0076 
0077 RoomFilterProxyModel *ChannelListView::filterModel() const
0078 {
0079     return mRoomFilterProxyModel;
0080 }
0081 
0082 void ChannelListView::slotPressed(const QModelIndex &index)
0083 {
0084     if (index.isValid()) {
0085         if (!index.parent().isValid())
0086             return;
0087 
0088         const QString roomId = index.data(RoomModel::RoomId).toString();
0089         Q_EMIT roomPressed(roomId);
0090     }
0091 }
0092 
0093 void ChannelListView::slotClicked(const QModelIndex &index)
0094 {
0095     if (index.isValid()) {
0096         channelSelected(index);
0097     }
0098 }
0099 
0100 void ChannelListView::contextMenuEvent(QContextMenuEvent *event)
0101 {
0102     const QModelIndex qmi = indexAt(event->pos());
0103     if (!qmi.isValid()) {
0104         return;
0105     }
0106     if (!qmi.parent().isValid()) {
0107         return;
0108     }
0109     QMenu menu(this);
0110     // Use QPersistentModelIndex as model might have changed by the time an action is triggered
0111     // and will crash us if we use QModelIndex
0112     QPersistentModelIndex index = qmi;
0113 
0114     const auto roomType = index.data(RoomModel::RoomType).value<Room::RoomType>();
0115 
0116     const bool isUnRead = index.data(RoomModel::RoomAlert).toBool();
0117     const QString actionMarkAsText = isUnRead ? i18n("Mark As Read") : i18n("Mark As Unread");
0118     auto markAsChannel = new QAction(actionMarkAsText, &menu);
0119     connect(markAsChannel, &QAction::triggered, this, [this, index, isUnRead]() {
0120         if (index.isValid()) {
0121             slotMarkAsChannel(index, isUnRead);
0122         }
0123     });
0124     menu.addAction(markAsChannel);
0125 
0126     const bool isFavorite = index.data(RoomModel::RoomFavorite).toBool();
0127     const QString actionFavoriteText = isFavorite ? i18n("Unset as Favorite") : i18n("Set as Favorite");
0128     auto favoriteAction = new QAction(QIcon::fromTheme(QStringLiteral("favorite")), actionFavoriteText, &menu);
0129     connect(favoriteAction, &QAction::triggered, this, [this, index, isFavorite]() {
0130         if (index.isValid()) {
0131             slotChangeFavorite(index, isFavorite);
0132         }
0133     });
0134     menu.addAction(favoriteAction);
0135 
0136     auto hideChannel = new QAction(QIcon::fromTheme(QStringLiteral("hide_table_row")), i18n("Hide Channel"), &menu);
0137     connect(hideChannel, &QAction::triggered, this, [this, index, roomType]() {
0138         if (index.isValid()) {
0139             slotHideChannel(index, roomType);
0140         }
0141     });
0142 
0143     if (roomType == Room::RoomType::Channel || roomType == Room::RoomType::Private) { // Not direct channel
0144         const QString roomId = index.data(RoomModel::RoomId).toString();
0145         Room *room = mCurrentRocketChatAccount->room(roomId);
0146         if (mCurrentRocketChatAccount->teamEnabled()) {
0147             if (room) {
0148                 const bool mainTeam = index.data(RoomModel::RoomTeamIsMain).toBool();
0149                 if (!mainTeam) {
0150                     const QString mainTeamId = index.data(RoomModel::RoomTeamId).toString();
0151                     if (mainTeamId.isEmpty() && room->hasPermission(QStringLiteral("convert-team"))) {
0152                         menu.addSeparator();
0153                         auto convertToTeam = new QAction(i18n("Convert to Team"), &menu);
0154                         connect(convertToTeam, &QAction::triggered, this, [this, index, roomType]() {
0155                             if (index.isValid()) {
0156                                 slotConvertToTeam(index, roomType);
0157                             }
0158                         });
0159                         menu.addAction(convertToTeam);
0160                     }
0161                 } else {
0162                     if (room->hasPermission(QStringLiteral("convert-team"))) {
0163                         menu.addSeparator();
0164                         auto convertToChanne = new QAction(i18n("Convert to Channel"), &menu);
0165                         connect(convertToChanne, &QAction::triggered, this, [this, index]() {
0166                             if (index.isValid()) {
0167                                 slotConvertToChannel(index);
0168                             }
0169                         });
0170                         menu.addAction(convertToChanne);
0171                     }
0172                 }
0173                 const QString mainTeamId = index.data(RoomModel::RoomTeamId).toString();
0174                 if (mainTeamId.isEmpty() && !mainTeam && room->hasPermission(QStringLiteral("add-team-channel"))) {
0175                     menu.addSeparator();
0176                     auto moveToTeam = new QAction(i18n("Move to Team"), &menu);
0177                     connect(moveToTeam, &QAction::triggered, this, [this, index]() {
0178                         if (index.isValid()) {
0179                             slotMoveToTeam(index);
0180                         }
0181                     });
0182                     menu.addAction(moveToTeam);
0183                 }
0184             }
0185         }
0186 
0187         menu.addSeparator();
0188         menu.addAction(hideChannel);
0189 
0190         if (room) {
0191             menu.addSeparator();
0192             auto configureNotificationChannel =
0193                 new QAction(QIcon::fromTheme(QStringLiteral("preferences-desktop-notification")), i18n("Configure Notification..."), &menu);
0194             connect(configureNotificationChannel, &QAction::triggered, this, [this, room]() {
0195                 slotConfigureNotification(room);
0196             });
0197             menu.addAction(configureNotificationChannel);
0198         }
0199         menu.addSeparator();
0200         auto quitChannel = new QAction(QIcon::fromTheme(QStringLiteral("dialog-close")), i18n("Quit Channel"), &menu);
0201         connect(quitChannel, &QAction::triggered, this, [this, index, roomType]() {
0202             if (index.isValid()) {
0203                 slotLeaveChannel(index, roomType);
0204             }
0205         });
0206         menu.addAction(quitChannel);
0207     } else {
0208         menu.addSeparator();
0209         menu.addAction(hideChannel);
0210     }
0211     if (!menu.actions().isEmpty()) {
0212         menu.exec(event->globalPos());
0213     }
0214 }
0215 
0216 void ChannelListView::slotConfigureNotification(Room *room)
0217 {
0218     if (!room) {
0219         return;
0220     }
0221     ConfigureNotificationDialog dlg(mCurrentRocketChatAccount, this);
0222     dlg.setRoom(room);
0223     dlg.exec();
0224 }
0225 
0226 void ChannelListView::slotMoveToTeam(const QModelIndex &index)
0227 {
0228     QPointer<SearchTeamDialog> dlg = new SearchTeamDialog(mCurrentRocketChatAccount, this);
0229     if (dlg->exec()) {
0230         const QString teamId = dlg->teamId();
0231         if (!teamId.isEmpty()) {
0232             auto job = new RocketChatRestApi::TeamAddRoomsJob(this);
0233             job->setTeamId(teamId);
0234             const QString roomId = index.data(RoomModel::RoomId).toString();
0235             job->setRoomIds({roomId});
0236 
0237             mCurrentRocketChatAccount->restApi()->initializeRestApiJob(job);
0238             // connect(job, &RocketChatRestApi::TeamAddRoomsJob::teamAddRoomsDone, this, &ChannelListView::slotChannelConvertToTeamDone);
0239             if (!job->start()) {
0240                 qCWarning(RUQOLAWIDGETS_LOG) << "Impossible to start TeamAddRoomsJob job";
0241             }
0242         } else {
0243             KMessageBox::information(this, i18n("Any team selected."), i18n("Move To Team"));
0244         }
0245     }
0246     delete dlg;
0247 }
0248 
0249 void ChannelListView::slotConvertToChannel(const QModelIndex &index)
0250 {
0251     const QString teamId = index.data(RoomModel::RoomTeamId).toString();
0252     auto job = new RocketChatRestApi::TeamsListRoomsJob(this);
0253     job->setTeamId(teamId);
0254     mCurrentRocketChatAccount->restApi()->initializeRestApiJob(job);
0255     connect(job, &RocketChatRestApi::TeamsListRoomsJob::teamListRoomsDone, this, [this, teamId, index](const QJsonObject &obj) {
0256         const QVector<TeamRoom> teamRooms = TeamRoom::parseTeamRooms(obj);
0257         QStringList listRoomIdToDelete;
0258         if (!teamRooms.isEmpty()) {
0259             QPointer<TeamConvertToChannelDialog> dlg = new TeamConvertToChannelDialog(this);
0260             const QString teamName = index.data(RoomModel::RoomName).toString();
0261             dlg->setTeamName(teamName);
0262             dlg->setTeamRooms(teamRooms);
0263             if (dlg->exec()) {
0264                 listRoomIdToDelete = dlg->roomIdsToDelete();
0265             } else {
0266                 delete dlg;
0267                 return;
0268             }
0269             delete dlg;
0270         }
0271         auto job = new RocketChatRestApi::TeamConvertToChannelJob(this);
0272         job->setTeamId(teamId);
0273         job->setRoomsToRemove(listRoomIdToDelete);
0274         mCurrentRocketChatAccount->restApi()->initializeRestApiJob(job);
0275         connect(job, &RocketChatRestApi::TeamConvertToChannelJob::teamConvertToChannelDone, this, []() {
0276             // TODO ?
0277         });
0278         if (!job->start()) {
0279             qCWarning(RUQOLAWIDGETS_LOG) << "Impossible to start TeamConvertToChannelJob job";
0280         }
0281     });
0282 
0283     if (!job->start()) {
0284         qCWarning(RUQOLAWIDGETS_LOG) << "Impossible to start TeamsListRoomsJob job";
0285     }
0286 }
0287 
0288 void ChannelListView::slotConvertToTeam(const QModelIndex &index, Room::RoomType roomType)
0289 {
0290     if (KMessageBox::ButtonCode::PrimaryAction
0291         == KMessageBox::questionTwoActions(this,
0292                                            i18n("Are you sure to convert it to team? It can not be undo."),
0293                                            i18nc("@title:window", "Convert to Team"),
0294                                            KStandardGuiItem::ok(),
0295                                            KStandardGuiItem::cancel())) {
0296         const QString roomId = index.data(RoomModel::RoomId).toString();
0297         switch (roomType) {
0298         case Room::RoomType::Unknown:
0299             qCWarning(RUQOLAWIDGETS_LOG) << "Unknown type used it's a bug";
0300             break;
0301         case Room::RoomType::Direct:
0302             qCWarning(RUQOLAWIDGETS_LOG) << "We can't convert Direct to Team. It's a bug";
0303             break;
0304         case Room::RoomType::Channel: {
0305             auto job = new RocketChatRestApi::ChannelsConvertToTeamJob(this);
0306             job->setChannelId(roomId);
0307             mCurrentRocketChatAccount->restApi()->initializeRestApiJob(job);
0308             connect(job, &RocketChatRestApi::ChannelsConvertToTeamJob::channelConvertToTeamDone, this, &ChannelListView::slotChannelConvertToTeamDone);
0309             if (!job->start()) {
0310                 qCWarning(RUQOLAWIDGETS_LOG) << "Impossible to start ChannelsConvertToTeamJob job";
0311             }
0312             break;
0313         }
0314         case Room::RoomType::Private: {
0315             auto job = new RocketChatRestApi::GroupsConvertToTeamJob(this);
0316             job->setRoomId(roomId);
0317             mCurrentRocketChatAccount->restApi()->initializeRestApiJob(job);
0318             connect(job, &RocketChatRestApi::GroupsConvertToTeamJob::groupConvertToTeamDone, this, &ChannelListView::slotGroupConvertToTeamDone);
0319             if (!job->start()) {
0320                 qCWarning(RUQOLAWIDGETS_LOG) << "Impossible to start ChannelsConvertToTeamJob job";
0321             }
0322             break;
0323         }
0324         }
0325     }
0326 }
0327 
0328 void ChannelListView::slotChannelConvertToTeamDone(const QJsonObject &obj)
0329 {
0330     Q_UNUSED(obj)
0331     // qDebug() << " obj "<< obj;
0332     // TODO
0333 }
0334 
0335 void ChannelListView::slotGroupConvertToTeamDone(const QJsonObject &obj)
0336 {
0337     Q_UNUSED(obj)
0338     // qDebug() << " obj "<< obj;
0339     // TODO
0340 }
0341 
0342 void ChannelListView::slotMarkAsChannel(const QModelIndex &index, bool markAsRead)
0343 {
0344     const QString roomId = index.data(RoomModel::RoomId).toString();
0345     if (markAsRead) {
0346         mCurrentRocketChatAccount->markRoomAsRead(roomId);
0347     } else {
0348         mCurrentRocketChatAccount->markRoomAsUnRead(roomId);
0349     }
0350 }
0351 
0352 void ChannelListView::channelSelected(const QModelIndex &index)
0353 {
0354     if (!index.parent().isValid())
0355         return;
0356 
0357     const QString roomId = index.data(RoomModel::RoomId).toString();
0358     const QString roomName = index.data(RoomModel::RoomFName).toString();
0359     const auto roomType = index.data(RoomModel::RoomType).value<Room::RoomType>();
0360     const auto avatarInfo = index.data(RoomModel::RoomAvatarInfo).value<Utils::AvatarInfo>();
0361     ChannelSelectedInfo info;
0362     info.avatarInfo = avatarInfo;
0363     info.roomId = roomId;
0364     info.roomName = roomName;
0365     info.roomType = roomType;
0366     Q_EMIT roomSelected(info);
0367 }
0368 
0369 void ChannelListView::slotHideChannel(const QModelIndex &index, Room::RoomType roomType)
0370 {
0371     const QString roomId = index.data(RoomModel::RoomId).toString();
0372     mCurrentRocketChatAccount->hideRoom(roomId, roomType);
0373 }
0374 
0375 void ChannelListView::slotLeaveChannel(const QModelIndex &index, Room::RoomType roomType)
0376 {
0377     const QString roomId = index.data(RoomModel::RoomId).toString();
0378     mCurrentRocketChatAccount->leaveRoom(roomId, roomType);
0379 }
0380 
0381 void ChannelListView::slotChangeFavorite(const QModelIndex &index, bool isFavorite)
0382 {
0383     const QString roomId = index.data(RoomModel::RoomId).toString();
0384     mCurrentRocketChatAccount->changeFavorite(roomId, !isFavorite);
0385 }
0386 
0387 void ChannelListView::selectChannelRequested(const QString &channelId, const QString &messageId)
0388 {
0389     if (!selectChannelByRoomIdRequested(channelId)) {
0390         qCWarning(RUQOLAWIDGETS_LOG) << "Room not found:" << channelId;
0391     } else {
0392         if (!messageId.isEmpty()) {
0393             Q_EMIT selectMessageIdRequested(messageId);
0394         }
0395     }
0396 }
0397 
0398 bool ChannelListView::selectChannelByRoomIdOrRoomName(const QString &id, bool roomId)
0399 {
0400     if (id.isEmpty()) {
0401         return false;
0402     }
0403     Q_ASSERT(filterModel());
0404     const int nSections = filterModel()->rowCount();
0405     for (int sectionId = 0; sectionId < nSections; ++sectionId) {
0406         const auto section = filterModel()->index(sectionId, 0, {});
0407         const auto sectionSize = filterModel()->rowCount(section);
0408 
0409         for (int roomIdx = 0; roomIdx < sectionSize; ++roomIdx) {
0410             const auto roomModelIndex = filterModel()->index(roomIdx, 0, section);
0411             const auto identifier = roomId ? roomModelIndex.data(RoomModel::RoomId).toString() : roomModelIndex.data(RoomModel::RoomName).toString();
0412             if (identifier == id) {
0413                 selectionModel()->setCurrentIndex(roomModelIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
0414                 return true;
0415             }
0416         }
0417     }
0418     return false;
0419 }
0420 
0421 bool ChannelListView::selectChannelByRoomNameRequested(const QString &selectedRoomName)
0422 {
0423     return selectChannelByRoomIdOrRoomName(selectedRoomName, false);
0424 }
0425 
0426 bool ChannelListView::selectChannelByRoomIdRequested(const QString &identifier)
0427 {
0428     return selectChannelByRoomIdOrRoomName(identifier, true);
0429 }
0430 
0431 void ChannelListView::selectNextUnreadChannel()
0432 {
0433     selectNextChannel(Direction::Down, true);
0434 }
0435 
0436 void ChannelListView::selectNextChannel(Direction direction, bool switchToNextUnreadChannel)
0437 {
0438     Q_ASSERT(filterModel());
0439 
0440     const auto nSections = filterModel()->rowCount();
0441     if (nSections == 0) {
0442         // FIXME : switch to empty room widget ?
0443         return;
0444     }
0445 
0446     const QModelIndex initialIndex = selectionModel()->currentIndex();
0447     QModelIndex currentIndex = initialIndex;
0448 
0449     // nextIndex(invalid) → top or bottom
0450     // nextIndex(other) → above or below, invalid on overflow
0451     const auto nextIndex = [this, direction](const QModelIndex &index) {
0452         if (!index.isValid()) {
0453             switch (direction) {
0454             case Direction::Up: {
0455                 QModelIndex lastIndex = filterModel()->index(filterModel()->rowCount() - 1, 0);
0456                 while (filterModel()->rowCount(lastIndex) > 0) {
0457                     lastIndex = filterModel()->index(filterModel()->rowCount(lastIndex) - 1, 0, lastIndex);
0458                 }
0459                 return lastIndex;
0460             }
0461             case Direction::Down:
0462                 return filterModel()->index(0, 0, {});
0463             }
0464             Q_UNREACHABLE();
0465         }
0466 
0467         switch (direction) {
0468         case Direction::Up: {
0469             return indexAbove(index);
0470         }
0471         case Direction::Down: {
0472             return indexBelow(index);
0473         }
0474         }
0475         Q_UNREACHABLE();
0476     };
0477 
0478     const auto matchesFilter = [switchToNextUnreadChannel](const QModelIndex &index) {
0479         return index.isValid() && index.flags().testFlag(Qt::ItemIsSelectable) && (!switchToNextUnreadChannel || index.data(RoomModel::RoomAlert).toBool());
0480     };
0481 
0482     do {
0483         currentIndex = nextIndex(currentIndex);
0484     } while (currentIndex != initialIndex && !matchesFilter(currentIndex));
0485 
0486     if (currentIndex.isValid()) {
0487         selectionModel()->setCurrentIndex(currentIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
0488     }
0489 }
0490 
0491 void ChannelListView::slotRoomRemoved(const QString &roomId)
0492 {
0493     const auto currentlySelectedIndex = selectionModel()->currentIndex();
0494     const QString currentRoomId = currentlySelectedIndex.data(RoomModel::RoomId).toString();
0495     if (currentRoomId == roomId) {
0496         selectNextChannel();
0497     }
0498 }
0499 
0500 #include "moc_channellistview.cpp"