File indexing completed on 2024-12-01 04:37:01

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 "messagelistview.h"
0008 #include "administratordialog/moderationconsole/moderationmessageinfodialog.h"
0009 #include "chat/followmessagejob.h"
0010 #include "chat/unfollowmessagejob.h"
0011 
0012 #include "moderation/moderationdismissreportsjob.h"
0013 
0014 #include "connection.h"
0015 #include "delegate/messagelistdelegate.h"
0016 #include "dialogs/directchannelinfodialog.h"
0017 #include "dialogs/reportmessagedialog.h"
0018 #include "moderation/moderationreportsjob.h"
0019 #include "rocketchataccount.h"
0020 #include "room.h"
0021 #include "roomutil.h"
0022 #include "ruqola.h"
0023 #include "ruqola_translatemessage_debug.h"
0024 #include "ruqolawidgets_debug.h"
0025 #include "selectedmessagebackgroundanimation.h"
0026 #include "threadwidget/threadmessagedialog.h"
0027 
0028 #include <KLocalizedString>
0029 #include <KMessageBox>
0030 
0031 #include <QApplication>
0032 #include <QClipboard>
0033 #include <QIcon>
0034 #include <QKeyEvent>
0035 #include <QMenu>
0036 #include <QPainter>
0037 #include <QScrollBar>
0038 
0039 #include "config-ruqola.h"
0040 
0041 #if HAVE_TEXT_TRANSLATOR
0042 #include "translatetext/translatetextjob.h"
0043 #include "translatetext/translatorenginemanager.h"
0044 #include <texttranslator_version.h>
0045 
0046 #include <TextTranslator/TranslatorMenu>
0047 #endif
0048 
0049 MessageListView::MessageListView(RocketChatAccount *account, Mode mode, QWidget *parent)
0050     : MessageListViewBase(parent)
0051     , mMode(mode)
0052     , mMessageListDelegate(new MessageListDelegate(account, this))
0053     , mCurrentRocketChatAccount(account)
0054 {
0055     if (mCurrentRocketChatAccount) {
0056         mMessageListDelegate->setRocketChatAccount(mCurrentRocketChatAccount);
0057     }
0058     mMessageListDelegate->setShowThreadContext(mMode != Mode::ThreadEditing);
0059     mMessageListDelegate->setEnableEmojiMenu(mMode != Mode::Moderation);
0060     setItemDelegate(mMessageListDelegate);
0061 
0062     connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &MessageListView::slotVerticalScrollbarChanged);
0063 
0064     // ensure the scrolling behavior isn't jumpy
0065     // we always single step by roughly one line
0066     const auto lineHeight = fontMetrics().height() + 10;
0067     verticalScrollBar()->setSingleStep(lineHeight);
0068     // the page step depends on the height of the viewport and needs to be reset when the range changes
0069     // as Qt would otherwise overwrite it internally. We apparently need a queued connection too to ensure our value is set
0070     connect(verticalScrollBar(), &QScrollBar::rangeChanged, this, &MessageListView::updateVerticalPageStep, Qt::QueuedConnection);
0071     updateVerticalPageStep();
0072     connect(mMessageListDelegate, &MessageListDelegate::showUserInfo, this, &MessageListView::slotShowUserInfo);
0073     connect(mMessageListDelegate, &MessageListDelegate::startPrivateConversation, this, &MessageListView::slotStartPrivateConversation);
0074     connect(mMessageListDelegate, &MessageListDelegate::updateView, this, &MessageListView::slotUpdateView);
0075     connect(this, &MessageListView::needToClearSizeHintCache, mMessageListDelegate, &MessageListDelegate::clearSizeHintCache);
0076 }
0077 
0078 MessageListView::~MessageListView() = default;
0079 
0080 void MessageListView::wheelEvent(QWheelEvent *e)
0081 {
0082     const int y = e->angleDelta().y();
0083     if (y > 0) {
0084         if (verticalScrollBar()->value() <= 1) {
0085             Q_EMIT loadHistoryRequested();
0086         }
0087     }
0088     MessageListViewBase::wheelEvent(e);
0089 }
0090 
0091 void MessageListView::paintEvent(QPaintEvent *e)
0092 {
0093     if (mRoom && (mRoom->numberMessages() == 0)) {
0094         QPainter p(viewport());
0095 
0096         QFont font = p.font();
0097         font.setItalic(true);
0098         p.setFont(font);
0099         if (mRoom->channelType() == Room::RoomType::Direct) {
0100             p.drawText(QRect(0, 0, width(), height()), Qt::AlignHCenter | Qt::AlignTop, i18n("You have joined a new direct message"));
0101         } else {
0102             p.drawText(QRect(0, 0, width(), height()), Qt::AlignHCenter | Qt::AlignTop, i18n("Start of conversation"));
0103         }
0104     } else {
0105         QListView::paintEvent(e);
0106     }
0107 }
0108 
0109 void MessageListView::slotUpdateView()
0110 {
0111     viewport()->update();
0112 }
0113 
0114 void MessageListView::setRoom(Room *room)
0115 {
0116     if (mRoom) {
0117         disconnect(mRoom, &Room::lastSeenChanged, this, &MessageListView::slotUpdateView);
0118         mMessageListDelegate->clearSelection();
0119     }
0120     mRoom = room;
0121     if (mRoom) {
0122         connect(mRoom, &Room::lastSeenChanged, this, &MessageListView::slotUpdateView);
0123     }
0124 }
0125 
0126 void MessageListView::slotVerticalScrollbarChanged(int value)
0127 {
0128     if (value == 0) {
0129         Q_EMIT loadHistoryRequested();
0130         // Perhaps finding a better method.
0131         verticalScrollBar()->setValue(1); // If we are at 0 we can't continue to load history
0132     }
0133 }
0134 
0135 void MessageListView::goToMessage(const QString &messageId)
0136 {
0137     auto messageModel = qobject_cast<MessagesModel *>(model());
0138     Q_ASSERT(messageModel);
0139     const QModelIndex index = messageModel->indexForMessage(messageId);
0140     if (index.isValid()) {
0141         scrollTo(index);
0142     } else {
0143         qCWarning(RUQOLAWIDGETS_LOG) << "Message not found:" << messageId;
0144     }
0145 }
0146 
0147 void MessageListView::setChannelSelected(Room *room)
0148 {
0149     auto oldModel = qobject_cast<MessagesModel *>(model());
0150     if (oldModel) {
0151         oldModel->deactivate();
0152     }
0153     setRoom(room);
0154     const QString roomId = room->roomId();
0155     mCurrentRocketChatAccount->switchingToRoom(roomId);
0156     MessagesModel *model = mCurrentRocketChatAccount->messageModelForRoom(roomId);
0157     setModel(model);
0158     model->activate();
0159 }
0160 
0161 void MessageListView::setModel(QAbstractItemModel *newModel)
0162 {
0163     QAbstractItemModel *oldModel = model();
0164     if (oldModel) {
0165         disconnect(oldModel, nullptr, this, nullptr);
0166     }
0167     QListView::setModel(newModel);
0168     connect(newModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &MessageListView::checkIfAtBottom);
0169     connect(newModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MessageListView::checkIfAtBottom);
0170     connect(newModel, &QAbstractItemModel::modelAboutToBeReset, this, &MessageListView::checkIfAtBottom);
0171     // Connect to rangeChanged rather than rowsInserted/rowsRemoved/modelReset.
0172     // This way it also catches the case of an item changing height (e.g. after async image loading)
0173     connect(verticalScrollBar(), &QScrollBar::rangeChanged, this, &MessageListView::maybeScrollToBottom);
0174 
0175     connect(newModel, &QAbstractItemModel::rowsInserted, this, &MessageListView::modelChanged);
0176     connect(newModel, &QAbstractItemModel::rowsRemoved, this, &MessageListView::modelChanged);
0177     connect(newModel, &QAbstractItemModel::modelReset, this, &MessageListView::modelChanged);
0178     // Clear document cache when message is updated otherwise image description is not up to date
0179     connect(newModel, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &topLeft, const QModelIndex &, const QVector<int> &roles) {
0180         if (roles.contains(MessagesModel::OriginalMessageOrAttachmentDescription)) {
0181             const Message *message = topLeft.data(MessagesModel::MessagePointer).value<Message *>();
0182             if (message) {
0183                 mMessageListDelegate->removeMessageCache(message);
0184             }
0185         } else if (roles.contains(MessagesModel::DisplayUrlPreview) || roles.contains(MessagesModel::DisplayAttachment)) {
0186             const Message *message = topLeft.data(MessagesModel::MessagePointer).value<Message *>();
0187             if (message) {
0188                 mMessageListDelegate->removeSizeHintCache(message->messageId());
0189             }
0190         }
0191     });
0192 
0193     scrollToBottom();
0194 }
0195 
0196 void MessageListView::handleKeyPressEvent(QKeyEvent *ev)
0197 {
0198     const int key = ev->key();
0199     if (key == Qt::Key_Up || key == Qt::Key_Down || key == Qt::Key_PageDown || key == Qt::Key_PageUp) {
0200         // QListView/QAIV PageUp/PageDown moves the current item, first inside visible bounds
0201         // before it triggers scrolling around. Let's just let the scrollarea handle it,
0202         // since we don't show the current item.
0203         QAbstractScrollArea::keyPressEvent(ev);
0204         ev->accept();
0205     } else if (ev->modifiers() & Qt::ControlModifier) {
0206         if (key == Qt::Key_Home) {
0207             scrollToTop();
0208             ev->accept();
0209         } else if (key == Qt::Key_End) {
0210             scrollToBottom();
0211             ev->accept();
0212         }
0213     }
0214 }
0215 
0216 void MessageListView::createTranslorMenu()
0217 {
0218 #if HAVE_TEXT_TRANSLATOR
0219     if (!mTranslatorMenu) {
0220         mTranslatorMenu = new TextTranslator::TranslatorMenu(this);
0221         connect(mTranslatorMenu, &TextTranslator::TranslatorMenu::translate, this, &MessageListView::slotTranslate);
0222         connect(Ruqola::self(), &Ruqola::translatorMenuChanged, this, [this]() {
0223             TranslatorEngineManager::self()->translatorConfigChanged();
0224             mTranslatorMenu->updateMenu();
0225         });
0226     }
0227 #endif
0228 }
0229 
0230 void MessageListView::contextMenuEvent(QContextMenuEvent *event)
0231 {
0232     //    if (!mRoom) {
0233     //        return;
0234     //    }
0235     const QModelIndex index = indexAt(event->pos());
0236     if (!index.isValid()) {
0237         if (Ruqola::self()->debug()) {
0238             QMenu menu(this);
0239             addDebugMenu(menu, index);
0240             menu.exec(event->globalPos());
0241         }
0242         return;
0243     }
0244 
0245     auto options = listViewOptions();
0246     options.rect = visualRect(index);
0247     options.index = index;
0248     MessageListDelegate::MenuInfo info;
0249     info.editMode = (mMode == Mode::Editing);
0250     info.globalPos = event->globalPos();
0251     info.pos = viewport()->mapFromGlobal(event->globalPos());
0252     info.roomType = mRoom ? mRoom->channelType() : Room::RoomType::Unknown;
0253     if (mMessageListDelegate->contextMenu(options, index, info)) {
0254         return;
0255     }
0256     const auto messageType = index.data(MessagesModel::MessageType).value<Message::MessageType>();
0257     const bool isSystemMessage = (messageType == Message::System) || (messageType == Message::Information) || (messageType == Message::VideoConference);
0258     QMenu menu(this);
0259     if (isSystemMessage) {
0260         if (Ruqola::self()->debug()) {
0261             addDebugMenu(menu, index);
0262             menu.exec(event->globalPos());
0263         }
0264         return;
0265     }
0266     mMessageListDelegate->attachmentContextMenu(options, index, info, &menu);
0267     const bool isNotOwnerOfMessage = (index.data(MessagesModel::UserId).toString() != mCurrentRocketChatAccount->userId());
0268 
0269     auto copyAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Message"), &menu);
0270     copyAction->setShortcut(QKeySequence::Copy);
0271     connect(copyAction, &QAction::triggered, this, [this, index]() {
0272         copyMessageToClipboard(index);
0273     });
0274     QAction *setPinnedMessage = nullptr;
0275     if (mCurrentRocketChatAccount->allowMessagePinningEnabled() && mRoom && mRoom->allowToPinMessage()) {
0276         const bool isPinned = index.data(MessagesModel::Pinned).toBool();
0277         setPinnedMessage = new QAction(QIcon::fromTheme(QStringLiteral("pin")), isPinned ? i18n("Unpin Message") : i18n("Pin Message"), &menu);
0278         connect(setPinnedMessage, &QAction::triggered, this, [this, isPinned, index]() {
0279             slotSetPinnedMessage(index, isPinned);
0280         });
0281     }
0282     QAction *setAsFavoriteAction = nullptr;
0283     if (mCurrentRocketChatAccount->allowMessageStarringEnabled()) {
0284         const bool isStarred = index.data(MessagesModel::Starred).toBool();
0285         setAsFavoriteAction =
0286             new QAction(QIcon::fromTheme(QStringLiteral("favorite")), isStarred ? i18n("Remove as Favorite") : i18n("Set as Favorite"), &menu);
0287         connect(setAsFavoriteAction, &QAction::triggered, this, [this, isStarred, index]() {
0288             slotSetAsFavorite(index, isStarred);
0289         });
0290     }
0291     QAction *deleteAction = nullptr;
0292     if (index.data(MessagesModel::CanDeleteMessage).toBool()) {
0293         deleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete"), &menu);
0294         connect(deleteAction, &QAction::triggered, this, [this, index]() {
0295             slotDeleteMessage(index);
0296         });
0297     }
0298 
0299     auto selectAllAction = new QAction(i18n("Select All"), &menu);
0300     connect(selectAllAction, &QAction::triggered, this, [this, index]() {
0301         slotSelectAll(index);
0302     });
0303 
0304     auto markMessageAsUnReadAction = new QAction(i18n("Mark Message As Unread"), &menu);
0305     connect(markMessageAsUnReadAction, &QAction::triggered, this, [this, index]() {
0306         slotMarkMessageAsUnread(index);
0307     });
0308 
0309     auto showFullThreadAction = new QAction(i18n("Show Full Thread"), &menu);
0310     connect(showFullThreadAction, &QAction::triggered, this, [this, index]() {
0311         slotShowFullThread(index);
0312     });
0313 
0314     auto editAction = new QAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit"), &menu);
0315     connect(editAction, &QAction::triggered, this, [this, index]() {
0316         slotEditMessage(index);
0317     });
0318 
0319     auto quoteAction = new QAction(QIcon::fromTheme(QStringLiteral("format-text-blockquote")), i18n("Quote"), &menu);
0320     connect(quoteAction, &QAction::triggered, this, [this, index]() {
0321         slotQuoteMessage(index);
0322     });
0323 
0324     auto copyLinkToMessageAction = new QAction(i18n("Copy Link To Message"), &menu); // TODO add icon
0325     connect(copyLinkToMessageAction, &QAction::triggered, this, [this, index]() {
0326         slotCopyLinkToMessage(index);
0327     });
0328 
0329     const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0330 
0331     const QString threadMessageId = index.data(MessagesModel::ThreadMessageId).toString();
0332     const bool messageIsFollowing = threadMessageId.isEmpty() ? message->replies().contains(mCurrentRocketChatAccount->userId())
0333                                                               : index.data(MessagesModel::ThreadMessageFollowed).toBool();
0334 
0335     const auto followingToMessageAction =
0336         new QAction(messageIsFollowing ? QIcon::fromTheme(QStringLiteral("notifications-disabled")) : QIcon::fromTheme(QStringLiteral("notifications")),
0337                     messageIsFollowing ? i18n("Unfollow Message") : i18n("Follow Message"),
0338                     &menu);
0339     connect(followingToMessageAction, &QAction::triggered, this, [this, index, messageIsFollowing]() {
0340         slotFollowMessage(index, messageIsFollowing);
0341     });
0342 
0343     auto copyUrlAction = [&]() -> QAction * {
0344         auto options = listViewOptions();
0345         options.rect = visualRect(index);
0346         options.index = index;
0347         const QString url = mMessageListDelegate->urlAt(options, index, viewport()->mapFromGlobal(event->globalPos()));
0348         if (url.isEmpty() || url.startsWith(QStringLiteral("ruqola:/")))
0349             return nullptr;
0350         auto action = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy URL"), &menu);
0351         connect(action, &QAction::triggered, this, [url]() {
0352             QGuiApplication::clipboard()->setText(url);
0353         });
0354         return action;
0355     }();
0356 
0357     auto userInfoActions = [&]() -> QList<QAction *> {
0358         QList<QAction *> listActions;
0359         auto options = listViewOptions();
0360         options.rect = visualRect(index);
0361         options.index = index;
0362         QString url = mMessageListDelegate->urlAt(options, index, viewport()->mapFromGlobal(event->globalPos()));
0363         if (url.isEmpty())
0364             return {};
0365         if (url.startsWith(QLatin1String("ruqola:/user/"))) {
0366             url.remove(QStringLiteral("ruqola:/user/"));
0367             if (!RoomUtil::validUser(url)) {
0368                 return {};
0369             }
0370         } else {
0371             return {};
0372         }
0373         auto action = new QAction(QIcon::fromTheme(QStringLiteral("documentinfo")), i18n("User Info"), &menu);
0374         connect(action, &QAction::triggered, this, [url, this]() {
0375             slotShowUserInfo(url);
0376         });
0377         listActions.append(action);
0378         if (info.editMode) {
0379             if (info.roomType != Room::RoomType::Direct) {
0380                 if (mCurrentRocketChatAccount->hasPermission(QStringLiteral("create-d"))) {
0381                     auto startPrivateConversationAction = new QAction(i18n("Start a Private Conversation"), &menu);
0382                     connect(startPrivateConversationAction, &QAction::triggered, this, [this, url]() {
0383                         slotStartPrivateConversation(url);
0384                     });
0385                     auto separator = new QAction(&menu);
0386                     separator->setSeparator(true);
0387                     listActions.append(separator);
0388                     listActions.append(startPrivateConversationAction);
0389                 }
0390             }
0391         }
0392         return listActions;
0393     }();
0394 
0395     switch (mMode) {
0396     case Mode::Editing: {
0397         auto startDiscussion = new QAction(i18n("Start a Discussion"), &menu);
0398         connect(startDiscussion, &QAction::triggered, this, [this, index]() {
0399             slotStartDiscussion(index);
0400         });
0401         menu.addAction(startDiscussion);
0402         menu.addSeparator();
0403         if (mCurrentRocketChatAccount->threadsEnabled()) {
0404             auto replyInThreadAction = new QAction(i18n("Reply in Thread"), &menu);
0405             connect(replyInThreadAction, &QAction::triggered, this, [this, index]() {
0406                 slotReplyInThread(index);
0407             });
0408             menu.addAction(replyInThreadAction);
0409 
0410             const QString threadMessageId = index.data(MessagesModel::ThreadMessageId).toString();
0411             const int threadMessageCount = index.data(MessagesModel::ThreadCount).toInt();
0412             if (!threadMessageId.isEmpty() || threadMessageCount > 0) {
0413                 menu.addSeparator();
0414                 menu.addAction(showFullThreadAction);
0415             }
0416         }
0417         menu.addSeparator();
0418         menu.addAction(quoteAction);
0419         menu.addSeparator();
0420         if (setPinnedMessage) {
0421             menu.addAction(setPinnedMessage);
0422         }
0423         if (setAsFavoriteAction) {
0424             menu.addAction(setAsFavoriteAction);
0425         }
0426         menu.addSeparator();
0427 
0428         if (index.data(MessagesModel::CanEditMessage).toBool()) {
0429             menu.addAction(editAction);
0430             menu.addSeparator();
0431         }
0432         menu.addAction(copyAction);
0433         if (copyUrlAction) {
0434             menu.addAction(copyUrlAction);
0435         }
0436         menu.addAction(copyLinkToMessageAction);
0437         menu.addSeparator();
0438         menu.addAction(selectAllAction);
0439 
0440         menu.addSeparator();
0441         if (isNotOwnerOfMessage) {
0442             menu.addAction(markMessageAsUnReadAction);
0443         }
0444 
0445         menu.addSeparator();
0446         menu.addAction(followingToMessageAction);
0447 
0448 #if HAVE_TEXT_TRANSLATOR
0449         createTranslorMenu();
0450         if (!mTranslatorMenu->isEmpty()) {
0451             menu.addSeparator();
0452             mTranslatorMenu->setModelIndex(index);
0453             menu.addMenu(mTranslatorMenu->menu());
0454         }
0455 #endif
0456 
0457         if (deleteAction) {
0458             menu.addSeparator();
0459             menu.addAction(deleteAction);
0460         }
0461         if (mCurrentRocketChatAccount->hasAutotranslateSupport() || !message->localTranslation().isEmpty()) {
0462             createSeparator(menu);
0463             const bool isTranslated = message->showTranslatedMessage();
0464             auto translateAction = new QAction(isTranslated ? i18n("Show Original Message") : i18n("Translate Message"), &menu);
0465             connect(translateAction, &QAction::triggered, this, [this, index, isTranslated]() {
0466                 slotTranslateMessage(index, !isTranslated);
0467             });
0468             menu.addAction(translateAction);
0469         }
0470         break;
0471     }
0472     case Mode::ThreadEditing: {
0473         if (setPinnedMessage) {
0474             menu.addAction(setPinnedMessage);
0475         }
0476         if (setAsFavoriteAction) {
0477             menu.addAction(setAsFavoriteAction);
0478         }
0479 
0480         menu.addSeparator();
0481         menu.addAction(quoteAction);
0482         menu.addSeparator();
0483         menu.addAction(copyAction);
0484         if (copyUrlAction) {
0485             menu.addAction(copyUrlAction);
0486         }
0487         menu.addAction(copyLinkToMessageAction);
0488         menu.addSeparator();
0489         menu.addAction(selectAllAction);
0490         if (isNotOwnerOfMessage) {
0491             menu.addAction(markMessageAsUnReadAction);
0492             menu.addSeparator();
0493         }
0494         if (index.data(MessagesModel::CanEditMessage).toBool()) {
0495             menu.addSeparator();
0496             menu.addAction(editAction);
0497         }
0498 
0499         if (deleteAction) {
0500             menu.addSeparator();
0501             menu.addAction(deleteAction);
0502         }
0503         break;
0504     }
0505     case Mode::Moderation: {
0506         auto showReportInfo = new QAction(i18n("View Reports"), &menu); // Add icon
0507         connect(showReportInfo, &QAction::triggered, this, [this, message]() {
0508             const auto messageId = message->messageId();
0509             const auto job = new RocketChatRestApi::ModerationReportsJob(this);
0510             job->setMessageId(messageId);
0511             mCurrentRocketChatAccount->restApi()->initializeRestApiJob(job);
0512             connect(job, &RocketChatRestApi::ModerationReportsJob::moderationReportsDone, this, [this](const QJsonObject &obj) {
0513                 ModerationReportInfos infos;
0514                 infos.parseModerationReportInfos(obj);
0515                 slotShowReportInfo(std::move(infos));
0516             });
0517             if (!job->start()) {
0518                 qCWarning(RUQOLAWIDGETS_LOG) << "Impossible to start ModerationReportInfoJob job";
0519             }
0520         });
0521         menu.addAction(showReportInfo);
0522         menu.addSeparator();
0523         if (copyUrlAction) {
0524             menu.addAction(copyUrlAction);
0525         }
0526         menu.addSeparator();
0527         menu.addAction(selectAllAction);
0528         menu.addSeparator();
0529         auto dismissReports = new QAction(i18n("Dismiss Reports"), &menu); // Add icon
0530         connect(dismissReports, &QAction::triggered, this, [this, message]() {
0531             const auto messageId = message->messageId();
0532             const auto job = new RocketChatRestApi::ModerationDismissReportsJob(this);
0533             job->setMessageId(messageId);
0534             mCurrentRocketChatAccount->restApi()->initializeRestApiJob(job);
0535             connect(job, &RocketChatRestApi::ModerationDismissReportsJob::moderationDismissReportsDone, this, []() {
0536                 // TODO
0537                 qDebug() << " RocketChatRestApi::ModerationDismissReportsJob::moderationDismissReportsDone ";
0538                 // TODO update element!
0539             });
0540             if (!job->start()) {
0541                 qCWarning(RUQOLAWIDGETS_LOG) << "Impossible to start ModerationReportInfoJob job";
0542             }
0543         });
0544 
0545         break;
0546     }
0547     case Mode::Viewing: {
0548 #if 0
0549         if (setPinnedMessage) {
0550             menu.addAction(setPinnedMessage);
0551         }
0552 #endif
0553         if (setAsFavoriteAction) {
0554             menu.addAction(setAsFavoriteAction);
0555             menu.addSeparator();
0556         }
0557         menu.addAction(copyAction);
0558         if (copyUrlAction) {
0559             menu.addAction(copyUrlAction);
0560         }
0561         menu.addAction(copyLinkToMessageAction);
0562         menu.addSeparator();
0563         menu.addAction(selectAllAction);
0564 #if 0
0565         createTranslorMenu();
0566         if (!mTranslatorMenu->isEmpty()) {
0567             menu.addSeparator();
0568             mTranslatorMenu->setModelIndex(index);
0569             menu.addMenu(mTranslatorMenu->menu());
0570         }
0571 #endif
0572         menu.addSeparator();
0573         auto goToMessageAction = new QAction(i18n("Go to Message"), &menu); // Add icon
0574         connect(goToMessageAction, &QAction::triggered, this, [this, index, message]() {
0575             const QString messageId = message->messageId();
0576             const QString messageDateTimeUtc = index.data(MessagesModel::DateTimeUtc).toString();
0577             Q_EMIT goToMessageRequested(messageId, messageDateTimeUtc);
0578         });
0579         menu.addAction(goToMessageAction);
0580         break;
0581     }
0582     }
0583 
0584     if (mMessageListDelegate->hasSelection()) {
0585         addTextPlugins(&menu, mMessageListDelegate->selectedText());
0586     }
0587 
0588 #if HAVE_TEXT_TO_SPEECH
0589     createSeparator(menu);
0590     auto speakAction = menu.addAction(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech")), i18n("Speak Text"));
0591     connect(speakAction, &QAction::triggered, this, [this, index]() {
0592         slotTextToSpeech(index);
0593     });
0594 #endif
0595 
0596     if (mMode != Mode::Moderation && isNotOwnerOfMessage) {
0597         createSeparator(menu);
0598         auto reportMessageAction = new QAction(QIcon::fromTheme(QStringLiteral("messagebox_warning")), i18n("Report Message"), &menu);
0599         connect(reportMessageAction, &QAction::triggered, this, [this, index]() {
0600             slotReportMessage(index);
0601         });
0602         menu.addAction(reportMessageAction);
0603     }
0604     if (!userInfoActions.isEmpty()) {
0605         menu.addSeparator();
0606         for (auto action : userInfoActions) {
0607             menu.addAction(action);
0608         }
0609     }
0610     if (Ruqola::self()->debug()) {
0611         addDebugMenu(menu, index);
0612     }
0613     if (!menu.actions().isEmpty()) {
0614         menu.exec(event->globalPos());
0615     }
0616 }
0617 
0618 void MessageListView::addDebugMenu(QMenu &menu, const QModelIndex &index)
0619 {
0620     if (!mRoom) {
0621         return;
0622     }
0623     createSeparator(menu);
0624     if (index.isValid()) {
0625         auto debugMessageAction = new QAction(QStringLiteral("Dump Message"), &menu); // Don't translate it.
0626         connect(debugMessageAction, &QAction::triggered, this, [this, index]() {
0627             slotDebugMessage(index);
0628         });
0629         menu.addAction(debugMessageAction);
0630         createSeparator(menu);
0631     }
0632     auto debugRoomAction = new QAction(QStringLiteral("Dump Room"), &menu); // Don't translate it.
0633     connect(debugRoomAction, &QAction::triggered, this, [this]() {
0634         // Dump info about room => don't use qCDebug here.
0635         qDebug() << " mRoom " << *mRoom;
0636     });
0637     menu.addAction(debugRoomAction);
0638 }
0639 
0640 bool MessageListView::maybeStartDrag(QMouseEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
0641 {
0642     return mMessageListDelegate->maybeStartDrag(event, option, index);
0643 }
0644 
0645 bool MessageListView::mouseEvent(QMouseEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
0646 {
0647     return mMessageListDelegate->mouseEvent(event, option, index);
0648 }
0649 
0650 void MessageListView::createSeparator(QMenu &menu)
0651 {
0652     if (!menu.isEmpty()) {
0653         menu.addSeparator();
0654     }
0655 }
0656 
0657 void MessageListView::slotSelectAll(const QModelIndex &index)
0658 {
0659     mMessageListDelegate->selectAll(listViewOptions(), index);
0660 }
0661 
0662 void MessageListView::slotTranslateMessage(const QModelIndex &index, bool checked)
0663 {
0664     auto model = const_cast<QAbstractItemModel *>(index.model());
0665     model->setData(index, checked, MessagesModel::ShowTranslatedMessage);
0666 }
0667 
0668 void MessageListView::slotDebugMessage(const QModelIndex &index)
0669 {
0670     const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0671     // Show debug output.
0672     qDebug() << " message " << *message << " MessageConvertedText " << index.data(MessagesModel::MessageConvertedText).toString();
0673 }
0674 
0675 void MessageListView::setCurrentRocketChatAccount(RocketChatAccount *currentRocketChatAccount)
0676 {
0677     mCurrentRocketChatAccount = currentRocketChatAccount;
0678     mMessageListDelegate->setRocketChatAccount(mCurrentRocketChatAccount);
0679 }
0680 
0681 void MessageListView::slotFollowMessage(const QModelIndex &index, bool messageIsFollowing)
0682 {
0683     const QString messageId = index.data(MessagesModel::MessageId).toString();
0684     if (messageIsFollowing) {
0685         auto job = new RocketChatRestApi::UnFollowMessageJob(this);
0686         job->setMessageId(messageId);
0687         mCurrentRocketChatAccount->restApi()->initializeRestApiJob(job);
0688         // connect(job, &RocketChatRestApi::FollowMessageJob::followMessageDone, this, &UsersInRoleWidget::slotAddUsersToRoleDone);
0689         if (!job->start()) {
0690             qCWarning(RUQOLAWIDGETS_LOG) << "Impossible to start UnFollowMessageJob job";
0691         }
0692     } else {
0693         auto job = new RocketChatRestApi::FollowMessageJob(this);
0694         job->setMessageId(messageId);
0695         mCurrentRocketChatAccount->restApi()->initializeRestApiJob(job);
0696         // connect(job, &RocketChatRestApi::UnFollowMessageJob::followMessageDone, this, &UsersInRoleWidget::slotAddUsersToRoleDone);
0697         if (!job->start()) {
0698             qCWarning(RUQOLAWIDGETS_LOG) << "Impossible to start FollowMessageJob job";
0699         }
0700     }
0701 }
0702 
0703 void MessageListView::slotCopyLinkToMessage(const QModelIndex &index)
0704 {
0705     const QString messageId = index.data(MessagesModel::MessageId).toString();
0706     const QString permalink = generatePermalink(messageId);
0707     QClipboard *clip = QApplication::clipboard();
0708     clip->setText(permalink, QClipboard::Clipboard);
0709 }
0710 
0711 QString MessageListView::generatePermalink(const QString &messageId) const
0712 {
0713     if (!mRoom) {
0714         return {};
0715     }
0716     QString permalink = mCurrentRocketChatAccount->serverUrl() + QLatin1Char('/') + RoomUtil::generatePermalink(messageId, mRoom->name(), mRoom->channelType());
0717     if (!permalink.startsWith(QStringLiteral("https://"))) {
0718         permalink.prepend(QStringLiteral("https://"));
0719     }
0720     return permalink;
0721 }
0722 
0723 void MessageListView::slotQuoteMessage(const QModelIndex &index)
0724 {
0725     const QString messageId = index.data(MessagesModel::MessageId).toString();
0726     QString text = index.data(MessagesModel::OriginalMessage).toString();
0727     const QString permalink = generatePermalink(messageId);
0728     // qDebug() << " permalink " << permalink;
0729     if (text.length() > 80) {
0730         text = text.left(80) + QStringLiteral("...");
0731     }
0732     Q_EMIT quoteMessageRequested(permalink, text);
0733 }
0734 
0735 void MessageListView::slotEditMessage(const QModelIndex &index)
0736 {
0737     const QString text = index.data(MessagesModel::OriginalMessageOrAttachmentDescription).toString();
0738     const QString messageId = index.data(MessagesModel::MessageId).toString();
0739     Q_EMIT editMessageRequested(messageId, text);
0740 }
0741 
0742 void MessageListView::slotShowFullThread(const QModelIndex &index)
0743 {
0744     const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0745     const QString threadMessageId = message->threadMessageId();
0746     QString threadMessagePreview = index.data(MessagesModel::ThreadMessagePreview).toString();
0747     const bool threadIsFollowing = threadMessageId.isEmpty() ? message->replies().contains(mCurrentRocketChatAccount->userId())
0748                                                              : index.data(MessagesModel::ThreadMessageFollowed).toBool();
0749     QString messageId = threadMessageId;
0750     if (threadMessageId.isEmpty()) {
0751         messageId = message->messageId();
0752         if (threadMessagePreview.isEmpty()) {
0753             threadMessagePreview = index.data(MessagesModel::MessageConvertedText).toString();
0754         }
0755     }
0756     auto dlg = new ThreadMessageDialog(mCurrentRocketChatAccount, this);
0757     ThreadMessageWidget::ThreadMessageInfo info;
0758     info.threadMessageId = messageId;
0759     info.threadMessagePreview = threadMessagePreview;
0760     info.threadIsFollowing = threadIsFollowing;
0761     info.room = mRoom;
0762     const Message tm = index.data(MessagesModel::ThreadMessage).value<Message>();
0763     info.messageThread = tm;
0764     dlg->setThreadMessageInfo(info);
0765     dlg->show();
0766 }
0767 
0768 void MessageListView::slotMarkMessageAsUnread(const QModelIndex &index)
0769 {
0770     const QString messageId = index.data(MessagesModel::MessageId).toString();
0771     mCurrentRocketChatAccount->markMessageAsUnReadFrom(messageId);
0772 }
0773 
0774 void MessageListView::slotDeleteMessage(const QModelIndex &index)
0775 {
0776     if (KMessageBox::ButtonCode::PrimaryAction
0777         == KMessageBox::questionTwoActions(this,
0778                                            i18n("Do you want to delete this message?"),
0779                                            i18nc("@title", "Delete Message"),
0780                                            KStandardGuiItem::del(),
0781                                            KStandardGuiItem::cancel())) {
0782         const QString messageId = index.data(MessagesModel::MessageId).toString();
0783         mCurrentRocketChatAccount->deleteMessage(messageId, mRoom->roomId());
0784     }
0785 }
0786 
0787 void MessageListView::slotTextToSpeech(const QModelIndex &index)
0788 {
0789     QString message = mMessageListDelegate->selectedText();
0790     if (message.isEmpty()) {
0791         message = index.data(MessagesModel::OriginalMessage).toString();
0792     }
0793     if (!message.isEmpty()) {
0794         Q_EMIT textToSpeech(message);
0795     }
0796 }
0797 
0798 void MessageListView::slotReportMessage(const QModelIndex &index)
0799 {
0800     QPointer<ReportMessageDialog> dlg = new ReportMessageDialog(this);
0801     const QString message = index.data(MessagesModel::OriginalMessage).toString();
0802     dlg->setPreviewMessage(message);
0803     if (dlg->exec()) {
0804         const QString messageId = index.data(MessagesModel::MessageId).toString();
0805         mCurrentRocketChatAccount->reportMessage(messageId, dlg->message());
0806     }
0807     delete dlg;
0808 }
0809 
0810 void MessageListView::slotSetAsFavorite(const QModelIndex &index, bool isStarred)
0811 {
0812     const QString messageId = index.data(MessagesModel::MessageId).toString();
0813     mCurrentRocketChatAccount->starMessage(messageId, !isStarred);
0814 }
0815 
0816 void MessageListView::slotSetPinnedMessage(const QModelIndex &index, bool isPinned)
0817 {
0818     const QString messageId = index.data(MessagesModel::MessageId).toString();
0819     mCurrentRocketChatAccount->pinMessage(messageId, !isPinned);
0820 }
0821 
0822 void MessageListView::slotStartPrivateConversation(const QString &userName)
0823 {
0824     Q_EMIT createPrivateConversation(userName);
0825 }
0826 
0827 void MessageListView::slotStartDiscussion(const QModelIndex &index)
0828 {
0829     const QString message = index.data(MessagesModel::OriginalMessage).toString();
0830     const QString messageId = index.data(MessagesModel::MessageId).toString();
0831     Q_EMIT createNewDiscussion(messageId, message);
0832 }
0833 
0834 QString MessageListView::selectedText(const QModelIndex &index)
0835 {
0836     QString message = mMessageListDelegate->selectedText();
0837     if (message.isEmpty()) {
0838         if (!index.isValid()) {
0839             return {};
0840         }
0841         message = index.data(MessagesModel::OriginalMessage).toString();
0842     }
0843     return message;
0844 }
0845 
0846 void MessageListView::clearTextDocumentCache()
0847 {
0848     mMessageListDelegate->clearTextDocumentCache();
0849 }
0850 
0851 void MessageListView::scrollTo(const QModelIndex &index, QAbstractItemView::ScrollHint hint)
0852 {
0853     disconnect(verticalScrollBar(), &QScrollBar::valueChanged, this, &MessageListView::slotVerticalScrollbarChanged);
0854     QListView::scrollTo(index, hint);
0855     connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &MessageListView::slotVerticalScrollbarChanged);
0856     addSelectedMessageBackgroundAnimation(index);
0857 }
0858 
0859 void MessageListView::addSelectedMessageBackgroundAnimation(const QModelIndex &index)
0860 {
0861     auto messageModel = qobject_cast<MessagesModel *>(model());
0862     if (messageModel) {
0863         auto animation = new SelectedMessageBackgroundAnimation(messageModel, this);
0864         animation->setModelIndex(index);
0865         animation->start();
0866     } else {
0867         qCWarning(RUQOLAWIDGETS_LOG) << " message model empty";
0868     }
0869 }
0870 
0871 void MessageListView::setSearchText(const QString &str)
0872 {
0873     mMessageListDelegate->setSearchText(str);
0874 }
0875 
0876 MessageListView::Mode MessageListView::mode() const
0877 {
0878     return mMode;
0879 }
0880 
0881 void MessageListView::slotReplyInThread(const QModelIndex &index)
0882 {
0883     const QString messageId = index.data(MessagesModel::MessageId).toString();
0884     const QString threadPreview = index.data(MessagesModel::OriginalMessage).toString();
0885     Q_EMIT replyInThreadRequested(messageId, threadPreview);
0886 }
0887 
0888 void MessageListView::slotShowUserInfo(const QString &userName)
0889 {
0890     DirectChannelInfoDialog dlg(mCurrentRocketChatAccount, this);
0891     dlg.setUserName(userName);
0892     dlg.setRoles(mCurrentRocketChatAccount->roleInfo());
0893     dlg.exec();
0894 }
0895 
0896 void MessageListView::slotTranslate(const QString &from, const QString &to, const QPersistentModelIndex &modelIndex)
0897 {
0898 #if HAVE_TEXT_TRANSLATOR
0899     if (modelIndex.isValid()) {
0900         const QString originalMessage = modelIndex.data(MessagesModel::OriginalMessage).toString();
0901         if (!originalMessage.isEmpty()) {
0902             qCDebug(RUQOLA_TRANSLATEMESSAGE_LOG) << " originalMessage " << originalMessage;
0903             qCDebug(RUQOLA_TRANSLATEMESSAGE_LOG) << " from " << from << " to " << to;
0904             TranslateTextJob::TranslateInfo info;
0905             info.from = from;
0906             info.to = to;
0907             info.inputText = originalMessage;
0908             auto job = new TranslateTextJob(this);
0909             job->setInfo(info);
0910             connect(job, &TranslateTextJob::translateDone, this, [this, modelIndex, job](const QString &str) {
0911                 auto messageModel = qobject_cast<MessagesModel *>(model());
0912                 qCDebug(RUQOLA_TRANSLATEMESSAGE_LOG) << " modelIndex " << modelIndex;
0913                 // qCDebug(RUQOLA_TRANSLATEMESSAGE_LOG) << " messageModel " << messageModel;
0914                 messageModel->setData(modelIndex, str, MessagesModel::LocalTranslation);
0915                 qCDebug(RUQOLA_TRANSLATEMESSAGE_LOG) << " translated string :" << str;
0916                 job->deleteLater();
0917             });
0918             connect(job, &TranslateTextJob::translateFailed, this, [this, job](const QString &errorMessage) {
0919                 KMessageBox::error(this, errorMessage, i18n("Translator Error"));
0920                 job->deleteLater();
0921             });
0922             job->translate();
0923         }
0924     }
0925 #else
0926     Q_UNUSED(from)
0927     Q_UNUSED(to)
0928     Q_UNUSED(modelIndex)
0929 #endif
0930 }
0931 
0932 void MessageListView::slotShowReportInfo(const ModerationReportInfos &info)
0933 {
0934     ModerationMessageInfoDialog dlg(mCurrentRocketChatAccount, this);
0935     dlg.setReportInfos(info);
0936     dlg.exec();
0937 }
0938 #include "moc_messagelistview.cpp"