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"