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"