File indexing completed on 2024-05-26 05:06:05

0001 /*
0002    SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
0003    SPDX-FileCopyrightText: 2024 Laurent Montel <montel@kde.org>
0004 
0005    SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "messagelistdelegate.h"
0009 #include "colorsandmessageviewstyle.h"
0010 #include "common/delegatepaintutil.h"
0011 #include "config-ruqola.h"
0012 #include "delegateutils/messagedelegateutils.h"
0013 #include "delegateutils/textselection.h"
0014 #include "messageattachmentdelegatehelperbase.h"
0015 #include "messageattachmentdelegatehelperfile.h"
0016 #include "messageattachmentdelegatehelperimage.h"
0017 #include "messageattachmentdelegatehelpersound.h"
0018 #include "messageattachmentdelegatehelpertext.h"
0019 #include "messageattachmentdelegatehelpervideo.h"
0020 #include "messagedelegatehelperconferencevideo.h"
0021 #include "messagedelegatehelperreactions.h"
0022 #include "messagedelegatehelpertext.h"
0023 #include "misc/avatarcachemanager.h"
0024 #include "misc/emoticonmenuwidget.h"
0025 #include "model/messagesmodel.h"
0026 #include "rocketchataccount.h"
0027 #include "room/delegate/messagelistlayout/messagelistcompactlayout.h"
0028 #include "room/delegate/messagelistlayout/messagelistcozylayout.h"
0029 #include "room/delegate/messagelistlayout/messagelistnormallayout.h"
0030 #if USE_SIZEHINT_CACHE_SUPPORT
0031 #include "ruqola_sizehint_cache_debug.h"
0032 #endif
0033 #include "messagedelegatehelperurlpreview.h"
0034 #include "ruqola_thread_message_widgets_debug.h"
0035 #include "ruqolaglobalconfig.h"
0036 #include "ruqolawidgets_debug.h"
0037 
0038 #include <QAbstractItemView>
0039 #include <QApplication>
0040 #include <QClipboard>
0041 #include <QListView>
0042 #include <QMouseEvent>
0043 #include <QPainter>
0044 #include <QScreen>
0045 #include <QToolTip>
0046 
0047 #include <KColorScheme>
0048 #include <KLocalizedString>
0049 #include <QMenu>
0050 
0051 MessageListDelegate::MessageListDelegate(RocketChatAccount *account, QListView *view)
0052     : QItemDelegate(view)
0053     , mEditedIcon(QIcon::fromTheme(QStringLiteral("document-edit")))
0054     , mRolesIcon(QIcon::fromTheme(QStringLiteral("documentinfo")))
0055     , mAddReactionIcon(QIcon::fromTheme(QStringLiteral("smiley-add")))
0056     , mFavoriteIcon(QIcon::fromTheme(QStringLiteral("favorite")))
0057     , mFollowingIcon(QIcon::fromTheme(QStringLiteral("notifications")))
0058     , mPinIcon(QIcon::fromTheme(QStringLiteral("pin")))
0059     , mTranslatedIcon(QIcon::fromTheme(QStringLiteral("languages"))) // TODO use another icon for it. But kde doesn't correct icon perhaps flags ?
0060     , mListView(view)
0061     , mTextSelectionImpl(new TextSelectionImpl)
0062     , mHelperText(new MessageDelegateHelperText(account, view, mTextSelectionImpl))
0063     , mHelperAttachmentImage(new MessageAttachmentDelegateHelperImage(account, view, mTextSelectionImpl))
0064     , mHelperAttachmentFile(new MessageAttachmentDelegateHelperFile(account, view, mTextSelectionImpl))
0065     , mHelperReactions(new MessageDelegateHelperReactions(account))
0066     , mHelperAttachmentVideo(new MessageAttachmentDelegateHelperVideo(account, view, mTextSelectionImpl))
0067     , mHelperAttachmentSound(new MessageAttachmentDelegateHelperSound(account, view, mTextSelectionImpl))
0068     , mHelperAttachmentText(new MessageAttachmentDelegateHelperText(account, view, mTextSelectionImpl))
0069     , mHelperConferenceVideo(new MessageDelegateHelperConferenceVideo(account, view, mTextSelectionImpl))
0070     , mHelperUrlPreview(new MessageDelegateHelperUrlPreview(account, view, mTextSelectionImpl))
0071     , mAvatarCacheManager(new AvatarCacheManager(Utils::AvatarType::User, this))
0072     , mMessageListLayoutBase(new MessageListCompactLayout(this))
0073 {
0074     mTextSelectionImpl->textSelection()->setMessageUrlHelperFactory(mHelperUrlPreview.data());
0075     mTextSelectionImpl->textSelection()->setTextHelperFactory(mHelperText.data());
0076     mTextSelectionImpl->textSelection()->setAttachmentFactories({mHelperAttachmentImage.data(),
0077                                                                  mHelperAttachmentFile.data(),
0078                                                                  mHelperAttachmentVideo.data(),
0079                                                                  mHelperAttachmentSound.data(),
0080                                                                  mHelperAttachmentText.data()});
0081     // Hardcode color otherwise in dark mode otherwise scheme.background(KColorScheme::NeutralBackground).color(); is not correct for text color.
0082     mEditColorMode = QColor(255, 170, 127);
0083     connect(&ColorsAndMessageViewStyle::self(), &ColorsAndMessageViewStyle::needToUpdateColors, this, &MessageListDelegate::slotUpdateColors);
0084     connect(&ColorsAndMessageViewStyle::self(), &ColorsAndMessageViewStyle::needUpdateMessageStyle, this, &MessageListDelegate::switchMessageLayout);
0085     slotUpdateColors();
0086     mSizeHintCache.setMaxEntries(32); // Enough ?
0087 }
0088 
0089 MessageListDelegate::~MessageListDelegate()
0090 {
0091     delete mTextSelectionImpl;
0092     delete mMessageListLayoutBase;
0093 }
0094 
0095 void MessageListDelegate::slotUpdateColors()
0096 {
0097     const KColorScheme scheme = ColorsAndMessageViewStyle::self().schemeView();
0098     mThreadedMessageBackgroundColor = ColorsAndMessageViewStyle::self().schemeWindow().background(KColorScheme::AlternateBackground).color();
0099     mOpenDiscussionColorMode = scheme.foreground(KColorScheme::LinkText).color();
0100     mReplyThreadColorMode = scheme.foreground(KColorScheme::NegativeText).color();
0101     mHoverHightlightColor = scheme.background(KColorScheme::AlternateBackground).color();
0102     Q_EMIT updateView();
0103 }
0104 
0105 MessageListLayoutBase::Layout MessageListDelegate::doLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const
0106 {
0107     return mMessageListLayoutBase->doLayout(option, index);
0108 }
0109 
0110 void MessageListDelegate::setRocketChatAccount(RocketChatAccount *rcAccount)
0111 {
0112     if (mRocketChatAccount) {
0113         disconnect(mRocketChatAccount, &RocketChatAccount::ownUserPreferencesChanged, this, &MessageListDelegate::switchMessageLayout);
0114         disconnect(mRocketChatAccount, &RocketChatAccount::privateSettingsChanged, this, &MessageListDelegate::slotPrivateSettingsChanged);
0115     }
0116     mRocketChatAccount = rcAccount;
0117     connect(mRocketChatAccount, &RocketChatAccount::ownUserPreferencesChanged, this, &MessageListDelegate::switchMessageLayout);
0118     connect(mRocketChatAccount, &RocketChatAccount::privateSettingsChanged, this, &MessageListDelegate::slotPrivateSettingsChanged);
0119 
0120     mAvatarCacheManager->setCurrentRocketChatAccount(mRocketChatAccount);
0121     // Switch messageLayout after set rocketchatAccount
0122     switchMessageLayout();
0123 
0124     mHelperText->setRocketChatAccount(mRocketChatAccount);
0125     mHelperAttachmentImage->setRocketChatAccount(mRocketChatAccount);
0126     mHelperAttachmentFile->setRocketChatAccount(mRocketChatAccount);
0127     mHelperReactions->setRocketChatAccount(mRocketChatAccount);
0128     mHelperAttachmentVideo->setRocketChatAccount(mRocketChatAccount);
0129     mHelperAttachmentSound->setRocketChatAccount(mRocketChatAccount);
0130     mHelperAttachmentText->setRocketChatAccount(mRocketChatAccount);
0131     mHelperConferenceVideo->setRocketChatAccount(mRocketChatAccount);
0132     mHelperUrlPreview->setRocketChatAccount(mRocketChatAccount);
0133 }
0134 
0135 QPixmap MessageListDelegate::makeAvatarPixmap(const QWidget *widget, const QModelIndex &index, int maxHeight) const
0136 {
0137     const QString emojiStr = index.data(MessagesModel::Emoji).toString();
0138     const auto info = index.data(MessagesModel::AvatarInfo).value<Utils::AvatarInfo>();
0139     if (emojiStr.isEmpty()) {
0140         const QString avatarUrl = index.data(MessagesModel::Avatar).toString();
0141         if (!avatarUrl.isEmpty()) {
0142             // qDebug() << " avatarUrl is not empty " << avatarUrl;
0143             return mAvatarCacheManager->makeAvatarUrlPixmap(widget, avatarUrl, maxHeight);
0144         } else {
0145             return mAvatarCacheManager->makeAvatarPixmap(widget, info, maxHeight);
0146         }
0147     } else {
0148         return mAvatarCacheManager->makeAvatarEmojiPixmap(emojiStr, widget, info, maxHeight);
0149     }
0150 }
0151 
0152 MessageBlockDelegateHelperBase *MessageListDelegate::blocksHelper(const Block &block) const
0153 {
0154     switch (block.blockType()) {
0155     case Block::BlockType::Unknown:
0156         qCWarning(RUQOLAWIDGETS_LOG) << "It's an unknown block ! It's a bug for sure";
0157         return nullptr;
0158     case Block::BlockType::VideoConf:
0159         return mHelperConferenceVideo.get();
0160     }
0161     return nullptr;
0162 }
0163 
0164 MessageAttachmentDelegateHelperBase *MessageListDelegate::attachmentsHelper(const MessageAttachment &msgAttach) const
0165 {
0166     switch (msgAttach.attachmentType()) {
0167     case MessageAttachment::Image:
0168         return mHelperAttachmentImage.data();
0169     case MessageAttachment::File:
0170         return mHelperAttachmentFile.data();
0171     case MessageAttachment::Video:
0172         return mHelperAttachmentVideo.data();
0173     case MessageAttachment::Audio:
0174         return mHelperAttachmentSound.data();
0175     case MessageAttachment::NormalText:
0176         return mHelperAttachmentText.data();
0177     case MessageAttachment::Unknown:
0178         qCWarning(RUQOLAWIDGETS_LOG) << "It's an unknown attachment ! It's a bug for sure";
0179         break;
0180     }
0181     return nullptr;
0182 }
0183 
0184 void MessageListDelegate::setSearchText(const QString &newSearchText)
0185 {
0186     bool needClearDocumentCache = false;
0187     if (mHelperText->searchText() != newSearchText) {
0188         mHelperText->setSearchText(newSearchText);
0189         mHelperText->clearTextDocumentCache();
0190         needClearDocumentCache = true;
0191     }
0192     if (mPreviewEmbed) {
0193         if (mHelperUrlPreview->searchText() != newSearchText) {
0194             mHelperUrlPreview->setSearchText(newSearchText);
0195             mHelperUrlPreview->clearTextDocumentCache();
0196             needClearDocumentCache = true;
0197         }
0198     }
0199     if (mHelperAttachmentText->searchText() != newSearchText) {
0200         mHelperAttachmentText->setSearchText(newSearchText);
0201         mHelperAttachmentText->clearTextDocumentCache();
0202         needClearDocumentCache = true;
0203     }
0204     if (mHelperAttachmentImage->searchText() != newSearchText) {
0205         mHelperAttachmentImage->setSearchText(newSearchText);
0206         mHelperAttachmentImage->clearTextDocumentCache();
0207         needClearDocumentCache = true;
0208     }
0209     if (mHelperAttachmentFile->searchText() != newSearchText) {
0210         mHelperAttachmentFile->setSearchText(newSearchText);
0211         mHelperAttachmentFile->clearTextDocumentCache();
0212         needClearDocumentCache = true;
0213     }
0214     if (mHelperAttachmentVideo->searchText() != newSearchText) {
0215         mHelperAttachmentVideo->setSearchText(newSearchText);
0216         mHelperAttachmentVideo->clearTextDocumentCache();
0217         needClearDocumentCache = true;
0218     }
0219     if (mHelperAttachmentSound->searchText() != newSearchText) {
0220         mHelperAttachmentSound->setSearchText(newSearchText);
0221         mHelperAttachmentSound->clearTextDocumentCache();
0222         needClearDocumentCache = true;
0223     }
0224     if (needClearDocumentCache) {
0225         mSizeHintCache.clear();
0226     }
0227 }
0228 
0229 void MessageListDelegate::drawLastSeenLine(QPainter *painter, qint64 displayLastSeenY, const QStyleOptionViewItem &option) const
0230 {
0231     const QPen origPen = painter->pen();
0232     const int lineY = displayLastSeenY;
0233     painter->setPen(Qt::red);
0234     painter->drawLine(option.rect.x(), lineY, option.rect.width(), lineY);
0235     painter->setPen(origPen);
0236 }
0237 
0238 MessageDelegateHelperReactions *MessageListDelegate::helperReactions() const
0239 {
0240     return mHelperReactions.data();
0241 }
0242 
0243 MessageDelegateHelperText *MessageListDelegate::helperText() const
0244 {
0245     return mHelperText.data();
0246 }
0247 
0248 void MessageListDelegate::drawModerationDate(QPainter *painter, const QModelIndex &index, const QStyleOptionViewItem &option, const QString &roomName) const
0249 {
0250     const QPen origPen = painter->pen();
0251     const qreal margin = MessageDelegateUtils::basicMargin();
0252     const QString dateAndRoomNameStr = QStringLiteral("%1 - %2").arg(index.data(MessagesModel::Date).toString(), roomName);
0253     const QSize dateSize = option.fontMetrics.size(Qt::TextSingleLine, dateAndRoomNameStr);
0254     const QRect dateAreaRect(option.rect.x(), option.rect.y(), option.rect.width(), dateSize.height()); // the whole row
0255     const QRect dateTextRect = QStyle::alignedRect(Qt::LayoutDirectionAuto, Qt::AlignCenter, dateSize, dateAreaRect);
0256     painter->drawText(dateTextRect, dateAndRoomNameStr);
0257     const int lineY = (dateAreaRect.top() + dateAreaRect.bottom()) / 2;
0258     QColor lightColor(painter->pen().color());
0259     lightColor.setAlpha(60);
0260     painter->setPen(lightColor);
0261     painter->drawLine(dateAreaRect.left(), lineY, dateTextRect.left() - margin, lineY);
0262     painter->drawLine(dateTextRect.right() + margin, lineY, dateAreaRect.right(), lineY);
0263     painter->setPen(origPen);
0264 }
0265 
0266 void MessageListDelegate::drawDate(QPainter *painter, const QModelIndex &index, const QStyleOptionViewItem &option, bool drawLastSeenLine) const
0267 {
0268     const QPen origPen = painter->pen();
0269     const qreal margin = MessageDelegateUtils::basicMargin();
0270     const QString dateStr = index.data(MessagesModel::Date).toString();
0271     const QSize dateSize = option.fontMetrics.size(Qt::TextSingleLine, dateStr);
0272     const QRect dateAreaRect(option.rect.x(), option.rect.y(), option.rect.width(), dateSize.height()); // the whole row
0273     const QRect dateTextRect = QStyle::alignedRect(Qt::LayoutDirectionAuto, Qt::AlignCenter, dateSize, dateAreaRect);
0274     painter->drawText(dateTextRect, dateStr);
0275     const int lineY = (dateAreaRect.top() + dateAreaRect.bottom()) / 2;
0276     if (drawLastSeenLine) {
0277         painter->setPen(Qt::red);
0278     } else {
0279         QColor lightColor(painter->pen().color());
0280         lightColor.setAlpha(60);
0281         painter->setPen(lightColor);
0282     }
0283     painter->drawLine(dateAreaRect.left(), lineY, dateTextRect.left() - margin, lineY);
0284     painter->drawLine(dateTextRect.right() + margin, lineY, dateAreaRect.right(), lineY);
0285     painter->setPen(origPen);
0286 }
0287 
0288 void MessageListDelegate::selectAll(const QStyleOptionViewItem &option, const QModelIndex &index)
0289 {
0290     Q_UNUSED(option);
0291     mTextSelectionImpl->textSelection()->selectMessage(index);
0292     mListView->update(index);
0293     MessageDelegateUtils::setClipboardSelection(mTextSelectionImpl->textSelection());
0294 }
0295 
0296 void MessageListDelegate::removeSizeHintCache(const QString &messageId)
0297 {
0298     mSizeHintCache.remove(messageId);
0299 }
0300 
0301 void MessageListDelegate::removeMessageCache(const Message *message)
0302 {
0303     const QString messageId = message->messageId();
0304     removeSizeHintCache(messageId);
0305     mHelperText->removeMessageCache(messageId);
0306 
0307     const auto attachments{message->attachments()};
0308     for (const auto &attachment : attachments) {
0309         const QString attachmentId = attachment.attachmentId();
0310         mHelperAttachmentImage->removeMessageCache(attachmentId);
0311         mHelperAttachmentFile->removeMessageCache(attachmentId);
0312         mHelperAttachmentVideo->removeMessageCache(attachmentId);
0313         mHelperAttachmentSound->removeMessageCache(attachmentId);
0314         mHelperAttachmentText->removeMessageCache(attachmentId);
0315     }
0316     if (mPreviewEmbed) {
0317         const auto messageUrls{message->urls()};
0318         for (const auto &url : messageUrls) {
0319             mHelperUrlPreview->removeMessageCache(url.urlId());
0320         }
0321     }
0322 }
0323 
0324 void MessageListDelegate::clearTextDocumentCache()
0325 {
0326     mSizeHintCache.clear();
0327     mHelperText->clearTextDocumentCache();
0328     mHelperAttachmentImage->clearTextDocumentCache();
0329     mHelperAttachmentFile->clearTextDocumentCache();
0330     mHelperAttachmentVideo->clearTextDocumentCache();
0331     mHelperAttachmentSound->clearTextDocumentCache();
0332     mHelperAttachmentText->clearTextDocumentCache();
0333     if (mPreviewEmbed) {
0334         mHelperUrlPreview->clearTextDocumentCache();
0335     }
0336 }
0337 
0338 void MessageListDelegate::clearSelection()
0339 {
0340     mTextSelectionImpl->textSelection()->clear();
0341 }
0342 
0343 QString MessageListDelegate::urlAt(const QStyleOptionViewItem &option, const QModelIndex &index, QPoint pos) const
0344 {
0345     const MessageListLayoutBase::Layout layout = doLayout(option, index);
0346     const auto messageRect = layout.textRect;
0347     QString url = mHelperText->urlAt(index, pos - messageRect.topLeft());
0348     if (url.isEmpty()) {
0349         const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0350         Q_ASSERT(message);
0351         const auto attachments = message->attachments();
0352         int i = 0;
0353         for (const MessageAttachment &msgAttach : attachments) {
0354             MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(msgAttach);
0355             url = helper->urlAt(option, msgAttach, layout.attachmentsRectList.at(i), pos);
0356             if (!url.isEmpty()) {
0357                 return url;
0358             }
0359             i++;
0360         }
0361 
0362         if (mPreviewEmbed) {
0363             const auto urlsMessage = message->urls();
0364             int messageUrlIndex = 0;
0365             for (const MessageUrl &messageUrl : urlsMessage) {
0366                 url = mHelperUrlPreview->urlAt(option, messageUrl, layout.messageUrlsRectList.at(messageUrlIndex), pos);
0367                 if (!url.isEmpty()) {
0368                     return url;
0369                 }
0370                 messageUrlIndex++;
0371             }
0372         }
0373     }
0374     return url;
0375 }
0376 
0377 bool MessageListDelegate::contextMenu(const QStyleOptionViewItem &option, const QModelIndex &index, const MessageListDelegate::MenuInfo &info)
0378 {
0379     const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0380     if (!message) {
0381         return false;
0382     }
0383 
0384     const MessageListLayoutBase::Layout layout = doLayout(option, index);
0385     if (layout.senderRect.contains(info.pos) && !layout.sameSenderAsPreviousMessage) {
0386         QMenu menu;
0387         auto userInfoAction = new QAction(QIcon::fromTheme(QStringLiteral("documentinfo")), i18n("User Info"), &menu);
0388         connect(userInfoAction, &QAction::triggered, this, [message, this]() {
0389             Q_EMIT showUserInfo(message->username());
0390         });
0391 
0392         if (info.editMode) {
0393             if (info.roomType != Room::RoomType::Direct) {
0394                 if (mRocketChatAccount->hasPermission(QStringLiteral("create-d"))) {
0395                     menu.addSeparator();
0396                     auto startPrivateConversationAction = new QAction(i18n("Start a Private Conversation"), &menu);
0397                     connect(startPrivateConversationAction, &QAction::triggered, this, [=]() {
0398                         Q_EMIT startPrivateConversation(message->username());
0399                     });
0400                     menu.addAction(startPrivateConversationAction);
0401                 }
0402             }
0403         }
0404 
0405         menu.addSeparator();
0406         menu.addAction(userInfoAction);
0407         menu.exec(info.globalPos);
0408         return true;
0409     }
0410     return false;
0411 }
0412 
0413 void MessageListDelegate::attachmentContextMenu(const QStyleOptionViewItem &option,
0414                                                 const QModelIndex &index,
0415                                                 const MessageListDelegate::MenuInfo &info,
0416                                                 QMenu *menu)
0417 {
0418     const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0419     if (!message) {
0420         return;
0421     }
0422     const MessageListLayoutBase::Layout layout = doLayout(option, index);
0423     const auto attachments = message->attachments();
0424     int i = 0;
0425     for (const MessageAttachment &msgAttach : attachments) {
0426         MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(msgAttach);
0427         if (helper->contextMenu(info.pos, info.globalPos, msgAttach, layout.attachmentsRectList.at(i), option, menu)) {
0428             return;
0429         }
0430         ++i;
0431     }
0432 }
0433 
0434 QString MessageListDelegate::selectedText() const
0435 {
0436     return mTextSelectionImpl->textSelection()->selectedText(TextSelection::Format::Text);
0437 }
0438 
0439 bool MessageListDelegate::hasSelection() const
0440 {
0441     return mTextSelectionImpl->textSelection()->hasSelection();
0442 }
0443 
0444 void MessageListDelegate::setShowThreadContext(bool b)
0445 {
0446     mHelperText->setShowThreadContext(b);
0447 }
0448 
0449 void MessageListDelegate::setEnableEmojiMenu(bool b)
0450 {
0451     mEmojiMenuEnabled = b;
0452 }
0453 
0454 void MessageListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0455 {
0456     painter->save();
0457 
0458     const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0459 
0460     const QColor goToMessageBackgroundColor{message->goToMessageBackgroundColor()};
0461     if (goToMessageBackgroundColor.isValid() && goToMessageBackgroundColor != QColor(Qt::transparent)) {
0462         painter->fillRect(option.rect, goToMessageBackgroundColor);
0463     } else if (message->isEditingMode()) {
0464         painter->fillRect(option.rect, mEditColorMode);
0465     } else if (message->hoverHighlight() && RuqolaGlobalConfig::self()->showHoverHighlights()) {
0466         painter->fillRect(option.rect, mHoverHightlightColor);
0467     } else if (mHelperText->showThreadContext() && !message->threadMessageId().isEmpty()) {
0468         painter->fillRect(option.rect, mThreadedMessageBackgroundColor);
0469     } else {
0470         drawBackground(painter, option, index);
0471     }
0472 
0473     const MessageListLayoutBase::Layout layout = doLayout(option, index);
0474 
0475     // Draw date if it differs from the previous message
0476     const bool displayLastSeenMessage = index.data(MessagesModel::DisplayLastSeenMessage).toBool();
0477     if (!message->moderationMessage().isEmpty()) {
0478         drawModerationDate(painter, index, option, message->moderationMessage().roomName());
0479     } else if (index.data(MessagesModel::DateDiffersFromPrevious).toBool()) {
0480         drawDate(painter, index, option, displayLastSeenMessage);
0481     } else if (displayLastSeenMessage) {
0482         drawLastSeenLine(painter, layout.displayLastSeenMessageY, option);
0483     }
0484 
0485     // Timestamp
0486     DelegatePaintUtil::drawLighterText(painter, layout.timeStampText, layout.timeStampPos);
0487     if (!isSystemMessage(message) && message->hoverHighlight() && mEmojiMenuEnabled) {
0488         mAddReactionIcon.paint(painter, layout.addReactionRect, Qt::AlignCenter);
0489     }
0490 
0491     // Message
0492     if (layout.textRect.isValid()) {
0493 #if 0
0494         painter->save();
0495         painter->setPen(QPen(Qt::red));
0496         painter->drawRect(layout.textRect);
0497         painter->restore();
0498 #endif
0499         mHelperText->draw(painter, layout.textRect, index, option);
0500     }
0501 
0502     // Draw the pixmap
0503     if (mRocketChatAccount->displayAvatars() && !layout.sameSenderAsPreviousMessage) {
0504 #if USE_ROUNDED_RECT_PIXMAP
0505         DelegatePaintUtil::createClipRoundedRectangle(painter, QRectF(layout.avatarPos, layout.avatarPixmap.size()), layout.avatarPos, layout.avatarPixmap);
0506 #else
0507         painter->drawPixmap(layout.avatarPos, layout.avatarPixmap);
0508 #endif
0509     }
0510 
0511     if (!layout.sameSenderAsPreviousMessage) {
0512         // Draw the sender
0513         const QFont oldFont = painter->font();
0514         painter->setFont(layout.senderFont);
0515         painter->drawText(layout.senderRect.x(), layout.baseLine, layout.senderText);
0516         painter->setFont(oldFont);
0517 
0518         // Draw the roles icon
0519         if (!index.data(MessagesModel::Roles).toString().isEmpty() && !mRocketChatAccount->hideRoles()) {
0520             mRolesIcon.paint(painter, layout.rolesIconRect);
0521         }
0522     }
0523 
0524     // Draw the edited icon
0525     if (message->wasEdited()) {
0526         mEditedIcon.paint(painter, layout.editedIconRect);
0527     }
0528     // Draw the favorite icon
0529     if (message->isStarred()) {
0530         mFavoriteIcon.paint(painter, layout.favoriteIconRect);
0531     }
0532     // Draw the pin icon
0533     if (message->isPinned()) {
0534         mPinIcon.paint(painter, layout.pinIconRect);
0535     }
0536     // Draw the following icon
0537     if (layout.messageIsFollowing) {
0538         mFollowingIcon.paint(painter, layout.followingIconRect);
0539     }
0540     // Draw translated string
0541     if (message->isAutoTranslated()) {
0542         mTranslatedIcon.paint(painter, layout.translatedIconRect);
0543     }
0544 
0545     if (MessageDelegateUtils::showIgnoreMessages(index)) {
0546         const QIcon hideShowIcon = QIcon::fromTheme(layout.showIgnoreMessage ? QStringLiteral("visibility") : QStringLiteral("hint"));
0547         hideShowIcon.paint(painter, layout.showIgnoredMessageIconRect);
0548     }
0549 
0550     // Attachments
0551     const auto attachments = message->attachments();
0552     int i = 0;
0553     for (const MessageAttachment &att : attachments) {
0554         const MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(att);
0555         if (helper) {
0556 #if 0
0557             painter->save();
0558             painter->setPen(QPen(Qt::green));
0559             painter->drawRect(layout.attachmentsRectList.at(i));
0560             painter->restore();
0561 #endif
0562             helper->draw(att, painter, layout.attachmentsRectList.at(i), index, option);
0563         }
0564         ++i;
0565     }
0566     // Blocks
0567 
0568     const auto blocks = message->blocks();
0569     int blockIndex = 0;
0570     for (const Block &block : blocks) {
0571         const MessageBlockDelegateHelperBase *helper = blocksHelper(block);
0572         if (helper) {
0573 #if 0
0574             painter->save();
0575             painter->setPen(QPen(Qt::red));
0576             painter->drawRect(layout.blocksRectList.at(blockIndex));
0577             painter->restore();
0578 #endif
0579             helper->draw(block, painter, layout.blocksRectList.at(blockIndex), index, option);
0580         }
0581         ++blockIndex;
0582     }
0583 
0584     if (mPreviewEmbed) {
0585         // Preview Url
0586         const QVector<MessageUrl> messageUrls = message->urls();
0587         int messageUrlIndex = 0;
0588         for (const MessageUrl &messageUrl : messageUrls) {
0589             if (messageUrl.hasPreviewUrl()) {
0590                 // qDebug() << "messageUrl  " << messageUrl;
0591                 mHelperUrlPreview.get()->draw(messageUrl, painter, layout.messageUrlsRectList.at(messageUrlIndex), index, option);
0592             }
0593             ++messageUrlIndex;
0594         }
0595     }
0596 
0597     // Reactions
0598     const QRect reactionsRect(layout.senderRect.x(), layout.reactionsY, layout.usableRect.width(), layout.reactionsHeight);
0599     mHelperReactions->draw(painter, reactionsRect, index, option);
0600 
0601     // Replies
0602     const int threadCount = message->threadCount();
0603     if (threadCount > 0) {
0604         const QString repliesText = i18np("1 reply", "%1 replies", threadCount);
0605         const QColor oldColor = painter->pen().color();
0606         painter->setPen(mReplyThreadColorMode);
0607         painter->drawText(layout.usableRect.x(), layout.repliesY + option.fontMetrics.ascent(), repliesText);
0608         painter->setPen(oldColor);
0609     }
0610     // Discussion
0611     if (!message->discussionRoomId().isEmpty()) {
0612         const QColor oldColor = painter->pen().color();
0613         const int discussionCount{message->discussionCount()};
0614         const QString discussionsText = (discussionCount > 0) ? i18np("1 message", "%1 messages", discussionCount) : i18n("No message yet");
0615         painter->setPen(mOpenDiscussionColorMode);
0616         painter->drawText(layout.usableRect.x(), layout.repliesY + layout.repliesHeight + option.fontMetrics.ascent(), discussionsText);
0617         // Note: pen still blue, currently relying on restore()
0618         painter->setPen(oldColor);
0619     }
0620 
0621     // drawFocus(painter, option, messageRect);
0622 
0623     // debug painter->drawRect(option.rect.adjusted(0, 0, -1, -1));
0624 
0625     painter->restore();
0626 }
0627 
0628 void MessageListDelegate::clearSizeHintCache()
0629 {
0630     mSizeHintCache.clear();
0631 }
0632 
0633 QString MessageListDelegate::cacheIdentifier(const QModelIndex &index) const
0634 {
0635     const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0636     Q_ASSERT(message);
0637     return message->messageId();
0638 }
0639 
0640 MessageDelegateHelperUrlPreview *MessageListDelegate::helperUrlPreview() const
0641 {
0642     return mHelperUrlPreview.get();
0643 }
0644 
0645 QSize MessageListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
0646 {
0647 #if USE_SIZEHINT_CACHE_SUPPORT
0648     const QString identifier = cacheIdentifier(index);
0649     auto it = mSizeHintCache.find(identifier);
0650     if (it != mSizeHintCache.end()) {
0651         const QSize result = it->value;
0652         qCDebug(RUQOLA_SIZEHINT_CACHE_LOG) << "MessageListDelegate: SizeHint found in cache: " << result;
0653         return result;
0654     }
0655 #endif
0656 
0657     const QSize size = mMessageListLayoutBase->sizeHint(option, index);
0658 #if USE_SIZEHINT_CACHE_SUPPORT
0659     if (!size.isEmpty()) {
0660         mSizeHintCache.insert(identifier, size);
0661     }
0662 #endif
0663     return size;
0664 }
0665 
0666 static void positionPopup(QPoint pos, QWidget *parentWindow, QWidget *popup)
0667 {
0668     const QRect screenRect = parentWindow->screen()->availableGeometry();
0669 
0670     const QSize popupSize{popup->sizeHint()};
0671     QRect popupRect(QPoint(pos.x() - popupSize.width(), pos.y() - popupSize.height()), popup->sizeHint());
0672     if (popupRect.top() < screenRect.top()) {
0673         popupRect.moveTop(screenRect.top());
0674     }
0675 
0676     if ((pos.x() + popupSize.width()) > (screenRect.x() + screenRect.width())) {
0677         popupRect.setX(screenRect.x() + screenRect.width() - popupSize.width());
0678     }
0679     if (pos.x() - popupSize.width() < screenRect.x()) {
0680         popupRect.setX(screenRect.x());
0681     }
0682 
0683     popup->setGeometry(popupRect);
0684 }
0685 
0686 bool MessageListDelegate::isSystemMessage(const Message *message) const
0687 {
0688     const Message::MessageType messageType = message->messageType();
0689     const bool isSystemMessage = (messageType == Message::System) || (messageType == Message::Information) || (messageType == Message::VideoConference);
0690     return isSystemMessage;
0691 }
0692 
0693 bool MessageListDelegate::mouseEvent(QEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
0694 {
0695     const QEvent::Type eventType = event->type();
0696     if (eventType == QEvent::MouseButtonRelease) {
0697         auto mev = static_cast<QMouseEvent *>(event);
0698         const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0699 
0700         const MessageListLayoutBase::Layout layout = doLayout(option, index);
0701 
0702         if (layout.addReactionRect.contains(mev->pos()) && !isSystemMessage(message) && mEmojiMenuEnabled) {
0703             auto mEmoticonMenuWidget = new EmoticonMenuWidget(mListView);
0704             mEmoticonMenuWidget->setWindowFlag(Qt::Popup);
0705             mEmoticonMenuWidget->setCurrentRocketChatAccount(mRocketChatAccount);
0706             mEmoticonMenuWidget->forceLineEditFocus();
0707             positionPopup(mev->globalPosition().toPoint(), mListView, mEmoticonMenuWidget);
0708             mEmoticonMenuWidget->show();
0709             connect(mEmoticonMenuWidget, &EmoticonMenuWidget::insertEmojiIdentifier, this, [=](const QString &id) {
0710                 mRocketChatAccount->reactOnMessage(message->messageId(), id, true /*add*/);
0711             });
0712             return true;
0713         }
0714 
0715         if (!message->reactions().isEmpty()) {
0716             const QRect reactionsRect(layout.senderRect.x(), layout.reactionsY, layout.usableRect.width(), layout.reactionsHeight);
0717             if (mHelperReactions->handleMouseEvent(mev, reactionsRect, option, message)) {
0718                 return true;
0719             }
0720         }
0721         if (message->threadCount() > 0) {
0722             qCDebug(RUQOLA_THREAD_MESSAGE_WIDGETS_LOG) << "Click on thread area";
0723             const QRect threadRect(layout.usableRect.x(), layout.repliesY, layout.usableRect.width(), layout.repliesHeight);
0724             if (threadRect.contains(mev->pos())) {
0725                 const QString threadMessagePreview = index.data(MessagesModel::ThreadMessagePreview).toString();
0726                 qCDebug(RUQOLA_THREAD_MESSAGE_WIDGETS_LOG) << "Click on thread area: " << message->messageId();
0727                 const bool threadIsFollowing = message->replies().contains(mRocketChatAccount->userId());
0728                 // We show current => use this message
0729                 const Message threadMessage = *message;
0730                 Q_EMIT mRocketChatAccount->openThreadRequested(message->messageId(),
0731                                                                threadMessagePreview.isEmpty() ? index.data(MessagesModel::MessageConvertedText).toString()
0732                                                                                               : threadMessagePreview,
0733                                                                threadIsFollowing,
0734                                                                threadMessage);
0735                 return true;
0736             }
0737         }
0738         if (!message->discussionRoomId().isEmpty()) {
0739             const QRect discussionRect(layout.usableRect.x(), layout.repliesY + layout.repliesHeight, layout.usableRect.width(), layout.discussionsHeight);
0740             if (discussionRect.contains(mev->pos())) {
0741                 // We need to fix rest api first
0742                 mRocketChatAccount->joinDiscussion(message->discussionRoomId(), QString());
0743                 return true;
0744             }
0745         }
0746         if (MessageDelegateUtils::showIgnoreMessages(index)) {
0747             if (layout.showIgnoredMessageIconRect.contains(mev->pos())) {
0748                 mHelperText->removeMessageCache(message->messageId());
0749                 auto model = const_cast<QAbstractItemModel *>(index.model());
0750                 model->setData(index, !layout.showIgnoreMessage, MessagesModel::ShowIgnoredMessage);
0751                 return true;
0752             }
0753         }
0754         if (mHelperText->handleMouseEvent(mev, layout.textRect, option, index)) {
0755             return true;
0756         }
0757 
0758         const auto attachments = message->attachments();
0759         int i = 0;
0760         for (const MessageAttachment &att : attachments) {
0761             MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(att);
0762             if (helper && helper->handleMouseEvent(att, mev, layout.attachmentsRectList.at(i), option, index)) {
0763                 return true;
0764             }
0765             ++i;
0766         }
0767 
0768         const auto blocks = message->blocks();
0769         int blockIndex = 0;
0770         for (const Block &block : blocks) {
0771             MessageBlockDelegateHelperBase *helper = blocksHelper(block);
0772             if (helper && helper->handleMouseEvent(block, mev, layout.blocksRectList.at(blockIndex), option, index)) {
0773                 return true;
0774             }
0775             ++blockIndex;
0776         }
0777         if (mPreviewEmbed) {
0778             const auto messageUrls = message->urls();
0779             int messageUrlsIndex = 0;
0780             for (const MessageUrl &url : messageUrls) {
0781                 if (mHelperUrlPreview->handleMouseEvent(url, mev, layout.messageUrlsRectList.at(messageUrlsIndex), option, index)) {
0782                     return true;
0783                 }
0784                 ++messageUrlsIndex;
0785             }
0786         }
0787     } else if (eventType == QEvent::MouseButtonPress || eventType == QEvent::MouseMove || eventType == QEvent::MouseButtonDblClick) {
0788         auto mev = static_cast<QMouseEvent *>(event);
0789         if (mev->buttons() & Qt::LeftButton) {
0790             const MessageListLayoutBase::Layout layout = doLayout(option, index);
0791             if (mHelperText->handleMouseEvent(mev, layout.textRect, option, index)) {
0792                 return true;
0793             }
0794 
0795             const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0796             const auto attachments = message->attachments();
0797             int i = 0;
0798             for (const MessageAttachment &att : attachments) {
0799                 MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(att);
0800                 if (helper && helper->handleMouseEvent(att, mev, layout.attachmentsRectList.at(i), option, index)) {
0801                     return true;
0802                 }
0803                 ++i;
0804             }
0805             if (mPreviewEmbed) {
0806                 const auto messageUrls = message->urls();
0807                 int messageUrlsIndex = 0;
0808                 for (const MessageUrl &url : messageUrls) {
0809                     if (mHelperUrlPreview->handleMouseEvent(url, mev, layout.messageUrlsRectList.at(messageUrlsIndex), option, index)) {
0810                         return true;
0811                     }
0812                     ++messageUrlsIndex;
0813                 }
0814             }
0815         }
0816     }
0817     return false;
0818 }
0819 
0820 bool MessageListDelegate::maybeStartDrag(QMouseEvent *event, const QStyleOptionViewItem &option, const QModelIndex &index)
0821 {
0822     const MessageListLayoutBase::Layout layout = doLayout(option, index);
0823     if (mHelperText->maybeStartDrag(event, layout.textRect, option, index)) {
0824         return true;
0825     }
0826 
0827     const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0828     {
0829         const auto attachments = message->attachments();
0830         int i = 0;
0831         for (const MessageAttachment &att : attachments) {
0832             MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(att);
0833             if (helper && helper->maybeStartDrag(att, event, layout.attachmentsRectList.at(i), option, index)) {
0834                 return true;
0835             }
0836             ++i;
0837         }
0838     }
0839     {
0840         if (mPreviewEmbed) {
0841             const auto urls = message->urls();
0842             int i = 0;
0843             for (const MessageUrl &url : urls) {
0844                 if (mHelperUrlPreview->maybeStartDrag(url, event, layout.messageUrlsRectList.at(i), option, index)) {
0845                     return true;
0846                 }
0847                 ++i;
0848             }
0849         }
0850     }
0851 
0852     return false;
0853 }
0854 
0855 bool MessageListDelegate::helpEvent(QHelpEvent *helpEvent, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index)
0856 {
0857     if (helpEvent->type() == QEvent::ToolTip) {
0858         const Message *message = index.data(MessagesModel::MessagePointer).value<Message *>();
0859         if (!message) {
0860             // tooltip was requested in an empty space below the last message, nothing to do
0861             return false;
0862         }
0863 
0864         const MessageListLayoutBase::Layout layout = doLayout(option, index);
0865         if (!message->reactions().isEmpty()) {
0866             const QRect reactionsRect(layout.senderRect.x(), layout.reactionsY, layout.usableRect.width(), layout.reactionsHeight);
0867             if (mHelperReactions->handleHelpEvent(helpEvent, view, reactionsRect, option, message)) {
0868                 return true;
0869             }
0870         }
0871         const QPoint helpEventPos{helpEvent->pos()};
0872         if (layout.senderRect.contains(helpEventPos)) {
0873             QString tooltip = message->name();
0874 
0875             if (mRocketChatAccount && mRocketChatAccount->useRealName() && !tooltip.isEmpty()) {
0876                 tooltip = QLatin1Char('@') + message->username();
0877             }
0878 
0879             if (!tooltip.isEmpty()) {
0880                 QToolTip::showText(helpEvent->globalPos(), tooltip, view);
0881                 return true;
0882             }
0883         }
0884         if (layout.rolesIconRect.contains(helpEventPos)) {
0885             const QString tooltip = index.data(MessagesModel::Roles).toString();
0886             QToolTip::showText(helpEvent->globalPos(), tooltip, view);
0887             return true;
0888         }
0889         if (layout.editedIconRect.contains(helpEventPos)) {
0890             const QString tooltip = index.data(MessagesModel::EditedToolTip).toString();
0891             QToolTip::showText(helpEvent->globalPos(), tooltip, view);
0892             return true;
0893         }
0894         if (layout.followingIconRect.contains(helpEventPos)) {
0895             QToolTip::showText(helpEvent->globalPos(), i18n("Following"), view);
0896             return true;
0897         }
0898         if (layout.pinIconRect.contains(helpEventPos)) {
0899             QToolTip::showText(helpEvent->globalPos(), i18n("Message has been pinned"), view);
0900             return true;
0901         }
0902         if (layout.favoriteIconRect.contains(helpEventPos)) {
0903             QToolTip::showText(helpEvent->globalPos(), i18n("Message has been starred"), view);
0904             return true;
0905         }
0906         if (layout.textRect.contains(helpEvent->pos()) && mHelperText->handleHelpEvent(helpEvent, layout.textRect, index)) {
0907             return true;
0908         }
0909         // Attachments
0910         const auto attachments = message->attachments();
0911         int i = 0;
0912         for (const MessageAttachment &att : attachments) {
0913             MessageAttachmentDelegateHelperBase *helper = attachmentsHelper(att);
0914             if (helper) {
0915                 if (layout.attachmentsRectList.at(i).contains(helpEventPos)
0916                     && helper->handleHelpEvent(helpEvent, layout.attachmentsRectList.at(i), att, option)) {
0917                     return true;
0918                 }
0919             }
0920             ++i;
0921         }
0922 
0923         // Block
0924         const auto blocks = message->blocks();
0925         int blockIndex = 0;
0926         for (const Block &block : blocks) {
0927             MessageBlockDelegateHelperBase *helper = blocksHelper(block);
0928             if (helper) {
0929                 if (layout.blocksRectList.at(blockIndex).contains(helpEventPos)
0930                     && helper->handleHelpEvent(helpEvent, layout.blocksRectList.at(blockIndex), block, option)) {
0931                     return true;
0932                 }
0933             }
0934             ++blockIndex;
0935         }
0936 
0937         if (mPreviewEmbed) {
0938             // messageurls
0939             const auto messageUrls = message->urls();
0940             int messageUrlsIndex = 0;
0941             for (const MessageUrl &url : messageUrls) {
0942                 if (layout.messageUrlsRectList.at(messageUrlsIndex).contains(helpEventPos)
0943                     && mHelperUrlPreview->handleHelpEvent(helpEvent, layout.messageUrlsRectList.at(messageUrlsIndex), url, option)) {
0944                     return true;
0945                 }
0946                 ++messageUrlsIndex;
0947             }
0948         }
0949         if (layout.timeStampRect.contains(helpEvent->pos())) {
0950             const QString dateStr = index.data(MessagesModel::Date).toString();
0951             QToolTip::showText(helpEvent->globalPos(), dateStr, view);
0952             return true;
0953         }
0954     }
0955     return false;
0956 }
0957 
0958 void MessageListDelegate::slotPrivateSettingsChanged()
0959 {
0960     mPreviewEmbed = mRocketChatAccount ? mRocketChatAccount->previewEmbed() : true;
0961 }
0962 
0963 void MessageListDelegate::switchMessageLayout()
0964 {
0965     clearSizeHintCache();
0966     mAvatarCacheManager->clearCache();
0967     delete mMessageListLayoutBase;
0968     switch (RuqolaGlobalConfig::self()->messageStyle()) {
0969     case RuqolaGlobalConfig::EnumMessageStyle::Normal:
0970         mMessageListLayoutBase = new MessageListNormalLayout(this);
0971         break;
0972     case RuqolaGlobalConfig::EnumMessageStyle::Cozy:
0973         mMessageListLayoutBase = new MessageListCozyLayout(this);
0974         break;
0975     case RuqolaGlobalConfig::EnumMessageStyle::Compact:
0976         mMessageListLayoutBase = new MessageListCompactLayout(this);
0977         break;
0978     default:
0979         qCWarning(RUQOLAWIDGETS_LOG) << "Invalid Message Layout type " << RuqolaGlobalConfig::self()->messageStyle();
0980         mMessageListLayoutBase = new MessageListCompactLayout(this);
0981         break;
0982     }
0983     mMessageListLayoutBase->setRocketChatAccount(mRocketChatAccount);
0984     mPreviewEmbed = mRocketChatAccount ? mRocketChatAccount->previewEmbed() : true;
0985     Q_EMIT updateView();
0986 }
0987 
0988 #include "moc_messagelistdelegate.cpp"