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 "channellistwidget.h"
0008 #include "channellistview.h"
0009 #include "model/roomfilterproxymodel.h"
0010 #include "room/roomutil.h"
0011 #include "ruqolawidgets_debug.h"
0012 
0013 #include "accountmanager.h"
0014 #include "rocketchataccount.h"
0015 #include "ruqola.h"
0016 #include "ruqolautils.h"
0017 
0018 #include <KLocalizedString>
0019 #include <QAction>
0020 #include <QKeyEvent>
0021 #include <QLineEdit>
0022 #include <QVBoxLayout>
0023 
0024 ChannelListWidget::ChannelListWidget(QWidget *parent)
0025     : QWidget(parent)
0026     , mChannelView(new ChannelListView(this))
0027     , mSearchRoomLineEdit(new QLineEdit(this))
0028 {
0029     auto mainLayout = new QVBoxLayout(this);
0030     mainLayout->setObjectName(QStringLiteral("mainlayout"));
0031     mainLayout->setSpacing(0);
0032     mainLayout->setContentsMargins({});
0033 
0034     mChannelView->setObjectName(QStringLiteral("mChannelView"));
0035     mChannelView->setProperty("_breeze_force_frame", false);
0036     mainLayout->addWidget(mChannelView);
0037     connect(mChannelView, &ChannelListView::selectMessageIdRequested, this, &ChannelListWidget::selectMessageIdRequested);
0038     connect(mChannelView, &ChannelListView::roomPressed, this, &ChannelListWidget::roomPressed);
0039     connect(mChannelView, &ChannelListView::roomSelected, this, [this](const ChannelListView::ChannelSelectedInfo &roomInfo) {
0040         // retain focus on the search line edit when this is triggering the room change
0041         const auto wasFocused = mSearchRoomLineEdit->hasFocus();
0042 
0043         Q_EMIT roomSelected(roomInfo);
0044 
0045         if (wasFocused) {
0046             mSearchRoomLineEdit->setFocus();
0047         }
0048     });
0049 
0050     // dummy action just for getting the icon)
0051     mSearchRoomLineEdit->addAction(QIcon::fromTheme(QStringLiteral("view-filter")), QLineEdit::LeadingPosition);
0052     mSearchRoomLineEdit->setObjectName(QStringLiteral("mSearchRoom"));
0053     mSearchRoomLineEdit->setPlaceholderText(i18n("Filter channels (CTRL + K)"));
0054     mSearchRoomLineEdit->setClearButtonEnabled(true);
0055     mSearchRoomLineEdit->installEventFilter(this);
0056     mSearchRoomLineEdit->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge}));
0057     mSearchRoomLineEdit->setMinimumHeight(36); // match the default size of the message text field
0058     mainLayout->addWidget(mSearchRoomLineEdit);
0059     connect(mSearchRoomLineEdit, &QLineEdit::textChanged, this, &ChannelListWidget::slotSearchRoomTextChanged);
0060 
0061     // BEGIN: Actions
0062     auto searchRoomAction = new QAction(i18n("Search Channels"), this);
0063     searchRoomAction->setShortcut(Qt::CTRL | Qt::Key_K);
0064     connect(searchRoomAction, &QAction::triggered, this, [this]() {
0065         mSearchRoomLineEdit->setFocus();
0066     });
0067     addAction(searchRoomAction); // TODO: Add to MainWindow's action collection instead?
0068 
0069     auto previousRoomAction = new QAction(i18n("Previous Channel"), this);
0070     previousRoomAction->setShortcut(Qt::CTRL | Qt::Key_Up);
0071     connect(previousRoomAction, &QAction::triggered, this, [this]() {
0072         mChannelView->selectNextChannel(ChannelListView::Direction::Up);
0073         mSearchRoomLineEdit->clear();
0074     });
0075     addAction(previousRoomAction); // TODO: Add to MainWindow's action collection instead?
0076 
0077     auto nextRoomAction = new QAction(i18n("Next Channel"), this);
0078     nextRoomAction->setShortcut(Qt::CTRL | Qt::Key_Down);
0079     connect(nextRoomAction, &QAction::triggered, this, [this]() {
0080         mChannelView->selectNextChannel(ChannelListView::Direction::Down);
0081         mSearchRoomLineEdit->clear();
0082     });
0083     addAction(nextRoomAction); // TODO: Add to MainWindow's action collection instead?
0084     // END: Actions
0085 }
0086 
0087 ChannelListWidget::~ChannelListWidget() = default;
0088 
0089 void ChannelListWidget::clearFilterChannel()
0090 {
0091     if (auto *model = mChannelView->filterModel()) {
0092         model->setFilterString(QString());
0093         mSearchRoomLineEdit->clear();
0094     }
0095 }
0096 
0097 void ChannelListWidget::setCurrentRocketChatAccount(RocketChatAccount *account)
0098 {
0099     clearFilterChannel();
0100     if (mCurrentRocketChatAccount) {
0101         disconnect(mCurrentRocketChatAccount, nullptr, this, nullptr);
0102     }
0103     mCurrentRocketChatAccount = account;
0104     connect(mCurrentRocketChatAccount, &RocketChatAccount::accountInitialized, this, &ChannelListWidget::slotAccountInitialized);
0105     connect(mCurrentRocketChatAccount, &RocketChatAccount::openLinkRequested, this, &ChannelListWidget::slotOpenLinkRequested);
0106     connect(mCurrentRocketChatAccount, &RocketChatAccount::openTeamNameRequested, this, &ChannelListWidget::slotOpenTeamRequested);
0107     connect(mCurrentRocketChatAccount, &RocketChatAccount::selectRoomByRoomNameRequested, mChannelView, &ChannelListView::selectChannelByRoomNameRequested);
0108     connect(mCurrentRocketChatAccount, &RocketChatAccount::selectRoomByRoomIdRequested, mChannelView, &ChannelListView::selectChannelRequested);
0109     connect(mCurrentRocketChatAccount, &RocketChatAccount::selectChannelAndMessage, this, &ChannelListWidget::slotSelectMessageRequested);
0110 
0111     mChannelView->setCurrentRocketChatAccount(account);
0112 }
0113 
0114 ChannelListView *ChannelListWidget::channelListView() const
0115 {
0116     return mChannelView;
0117 }
0118 
0119 bool ChannelListWidget::eventFilter(QObject *object, QEvent *event)
0120 {
0121     if (object == mSearchRoomLineEdit && event->type() == QEvent::KeyPress) {
0122         const auto keyEvent = static_cast<QKeyEvent *>(event);
0123         const int keyValue = keyEvent->key();
0124         if (keyValue == Qt::Key_Return || keyValue == Qt::Key_Enter) {
0125             // The search line edit wants to restore focus to itself, but Enter is supposed to
0126             // explicitly switch to the message line edit, so distract it from doing that
0127             setFocus();
0128             applyChannelSelection();
0129         } else if (keyValue == Qt::Key_Up || keyValue == Qt::Key_Down) {
0130             mChannelView->selectNextChannel(keyValue == Qt::Key_Up ? ChannelListView::Direction::Up : ChannelListView::Direction::Down);
0131             return true; // eat event
0132         } else if (keyValue == Qt::Key_Escape) {
0133             mSearchRoomLineEdit->clear();
0134         }
0135     }
0136 
0137     return QWidget::eventFilter(object, event);
0138 }
0139 
0140 void ChannelListWidget::slotAccountInitialized()
0141 {
0142     mChannelView->selectChannelRequested(mCurrentRocketChatAccount->settings()->lastSelectedRoom(), QString());
0143 }
0144 
0145 void ChannelListWidget::slotSearchRoomTextChanged()
0146 {
0147     mChannelView->filterModel()->setFilterString(mSearchRoomLineEdit->text());
0148 }
0149 
0150 void ChannelListWidget::slotOpenTeamRequested(const QString &identifier)
0151 {
0152     const QModelIndex selectedIndex = mChannelView->selectionModel()->currentIndex();
0153     if (selectedIndex.isValid()) {
0154         const QString currentRoomId = selectedIndex.data(RoomModel::RoomId).toString();
0155         if (identifier == currentRoomId) {
0156             return;
0157         }
0158     }
0159     if (!mChannelView->selectChannelByRoomIdRequested(identifier)) {
0160         mCurrentRocketChatAccount->openChannel(identifier, RocketChatAccount::ChannelTypeInfo::RoomId);
0161     }
0162 }
0163 
0164 void ChannelListWidget::slotSelectMessageRequested(const QString &messageId,
0165                                                    const QString &roomId,
0166                                                    ParseMessageUrlUtils::RoomIdType roomType,
0167                                                    ParseMessageUrlUtils::ChannelType channelType)
0168 {
0169     switch (roomType) {
0170     case ParseMessageUrlUtils::RoomIdType::Unknown:
0171         qCWarning(RUQOLAWIDGETS_LOG) << "Room type undefined!";
0172         break;
0173     case ParseMessageUrlUtils::RoomIdType::RoomId: {
0174         const QModelIndex selectedIndex = mChannelView->selectionModel()->currentIndex();
0175         if (selectedIndex.isValid()) {
0176             const QString currentRoomId = selectedIndex.data(RoomModel::RoomId).toString();
0177             if (roomId == currentRoomId) {
0178                 Q_EMIT selectMessageIdRequested(messageId);
0179                 return;
0180             }
0181             switch (channelType) {
0182             case ParseMessageUrlUtils::ChannelType::Channel: {
0183                 if (!mChannelView->selectChannelByRoomIdRequested(roomId)) {
0184                     mCurrentRocketChatAccount->openChannel(roomId, RocketChatAccount::ChannelTypeInfo::RoomId);
0185                     // TODO implement scroll to message
0186                 } else {
0187                     Q_EMIT selectMessageIdRequested(messageId);
0188                 }
0189                 break;
0190             }
0191             case ParseMessageUrlUtils::ChannelType::Group: {
0192                 // TODO ?
0193                 if (!mChannelView->selectChannelByRoomIdRequested(roomId)) {
0194                     mCurrentRocketChatAccount->openChannel(roomId, RocketChatAccount::ChannelTypeInfo::RoomId);
0195                     // TODO implement scroll to message
0196                 } else {
0197                     Q_EMIT selectMessageIdRequested(messageId);
0198                 }
0199                 break;
0200             }
0201             case ParseMessageUrlUtils::ChannelType::Direct: {
0202                 if (!mChannelView->selectChannelByRoomIdRequested(roomId)) {
0203                     // TODO add support for roomId or roomName
0204                     // mCurrentRocketChatAccount->openDirectChannel(roomId /*, RocketChatAccount::ChannelTypeInfo::RoomId*/);
0205                     // Workaround RC 4.7.x where openDirectChannel doesn't accept userId as direct open channel REST API
0206                     mCurrentRocketChatAccount->ddp()->openDirectChannel(roomId);
0207                     // TODO implement scroll to message
0208                 } else {
0209                     Q_EMIT selectMessageIdRequested(messageId);
0210                 }
0211                 break;
0212             }
0213             case ParseMessageUrlUtils::ChannelType::Unknown: {
0214                 qCWarning(RUQOLAWIDGETS_LOG) << "ChannelType undefined!";
0215                 break;
0216             }
0217             }
0218         }
0219         break;
0220     }
0221     case ParseMessageUrlUtils::RoomIdType::RoomName: {
0222         const QModelIndex selectedIndex = mChannelView->selectionModel()->currentIndex();
0223         if (selectedIndex.isValid()) {
0224             const QString currentRoomName = selectedIndex.data(RoomModel::RoomName).toString();
0225             if (roomId == currentRoomName) {
0226                 Q_EMIT selectMessageIdRequested(messageId);
0227                 return;
0228             }
0229             switch (channelType) {
0230             case ParseMessageUrlUtils::ChannelType::Channel: {
0231                 if (!mChannelView->selectChannelByRoomNameRequested(roomId)) {
0232                     mCurrentRocketChatAccount->openChannel(roomId, RocketChatAccount::ChannelTypeInfo::RoomName);
0233                     // TODO implement scroll to message
0234                 } else {
0235                     Q_EMIT selectMessageIdRequested(messageId);
0236                 }
0237                 break;
0238             }
0239             case ParseMessageUrlUtils::ChannelType::Direct: {
0240                 if (!mChannelView->selectChannelByRoomNameRequested(roomId)) {
0241                     // TODO add support for roomId or roomName
0242                     mCurrentRocketChatAccount->openDirectChannel(roomId /*, RocketChatAccount::ChannelTypeInfo::RoomName*/);
0243                 } else {
0244                     Q_EMIT selectMessageIdRequested(messageId);
0245                 }
0246                 break;
0247             }
0248             case ParseMessageUrlUtils::ChannelType::Group: {
0249                 if (!mChannelView->selectChannelByRoomNameRequested(roomId)) {
0250                     // TODO add support for roomId or roomName
0251                     mCurrentRocketChatAccount->openDirectChannel(roomId /*, RocketChatAccount::ChannelTypeInfo::RoomName*/);
0252                 } else {
0253                     Q_EMIT selectMessageIdRequested(messageId);
0254                 }
0255                 break;
0256             }
0257             case ParseMessageUrlUtils::ChannelType::Unknown: {
0258                 qCWarning(RUQOLAWIDGETS_LOG) << "ChannelType undefined!";
0259                 break;
0260             }
0261             }
0262         }
0263         break;
0264     }
0265     }
0266 }
0267 
0268 void ChannelListWidget::slotOpenLinkRequested(const QString &link)
0269 {
0270     // qDebug() << " void ChannelListWidget::slotOpenLinkRequested(const QString &link)" << link;
0271     if (link.startsWith(QLatin1String("ruqola:"))) {
0272         const QString roomOrUserId = RuqolaUtils::self()->extractRoomUserFromUrl(link);
0273         const QModelIndex selectedIndex = mChannelView->selectionModel()->currentIndex();
0274         if (selectedIndex.isValid()) {
0275             const QString currentRoomId = selectedIndex.data(RoomModel::RoomId).toString();
0276             if (roomOrUserId == currentRoomId) {
0277                 return;
0278             }
0279         }
0280         if (link.startsWith(QLatin1String("ruqola:/room/"))) {
0281             if (!mChannelView->selectChannelByRoomIdRequested(roomOrUserId)) {
0282                 mCurrentRocketChatAccount->openChannel(roomOrUserId, RocketChatAccount::ChannelTypeInfo::RoomId);
0283             }
0284         } else if (link.startsWith(QLatin1String("ruqola:/user/"))) {
0285             if (!RoomUtil::validUser(roomOrUserId)) {
0286                 return;
0287             }
0288             if (!mChannelView->selectChannelByRoomIdRequested(roomOrUserId)) {
0289                 if (roomOrUserId != mCurrentRocketChatAccount->userName()) {
0290                     if (mCurrentRocketChatAccount->hasPermission(QStringLiteral("create-d"))) {
0291                         // Workaround RC 4.7.x where openDirectChannel doesn't accept userId as direct open channel REST API
0292                         mCurrentRocketChatAccount->ddp()->openDirectChannel(roomOrUserId);
0293                     }
0294 
0295                     // mCurrentRocketChatAccount->openDirectChannel(roomOrUserId);
0296                 }
0297             }
0298         } else if (link == QLatin1String("ruqola:/jitsicall/")) {
0299             const QModelIndex jitsiSelectedIndex = mChannelView->selectionModel()->currentIndex();
0300             if (jitsiSelectedIndex.isValid()) {
0301                 const QString roomId = jitsiSelectedIndex.data(RoomModel::RoomId).toString();
0302                 mCurrentRocketChatAccount->joinJitsiConfCall(roomId);
0303             }
0304         }
0305     } else {
0306         ParseMessageUrlUtils parseUrl;
0307         if (parseUrl.parseUrl(link)) {
0308             if (Ruqola::self()->accountManager()->showMessage(parseUrl)) {
0309                 return;
0310             }
0311         }
0312         RuqolaUtils::self()->openUrl(link);
0313     }
0314 }
0315 
0316 void ChannelListWidget::setLayoutSpacing(int spacing)
0317 {
0318     layout()->setSpacing(spacing);
0319 }
0320 
0321 void ChannelListWidget::applyChannelSelection()
0322 {
0323     const auto selectedIndex = mChannelView->selectionModel()->currentIndex();
0324     if (selectedIndex.isValid()) {
0325         mChannelView->channelSelected(selectedIndex);
0326         mSearchRoomLineEdit->clear();
0327     }
0328 }
0329 
0330 #include "moc_channellistwidget.cpp"