Files
neochat/src/timeline/messagedelegate.cpp
James Graham 7356a68f4c Hopefully this stops any crashes around QuickActions.qml and EmojiDialog.qml
Hopefully this stops any crashes around QuickActions.qml and EmojiDialog.qml. Best I can guess this is some race condition where QuickActions are deleted in the time it takes to instnatiate the EmojiDialog popup. I've also rearranged the updateQuickActions function to stop a possible race condition there.

BUG: 509484
2025-10-08 13:07:02 +00:00

697 lines
22 KiB
C++

// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "messagedelegate.h"
#include <algorithm>
#include <cmath>
MessageObjectIncubator::MessageObjectIncubator(std::function<void(QQuickItem *)> initialCallback,
std::function<void(MessageObjectIncubator *)> completedCallback,
std::function<void(MessageObjectIncubator *)> errorCallback)
: QQmlIncubator(QQmlIncubator::Asynchronous)
, m_initialCallback(initialCallback)
, m_completedCallback(completedCallback)
, m_errorCallback(errorCallback)
{
}
void MessageObjectIncubator::setInitialState(QObject *object)
{
auto item = qobject_cast<QQuickItem *>(object);
if (item) {
m_initialCallback(item);
}
}
void MessageObjectIncubator::statusChanged(QQmlIncubator::Status status)
{
if (status == QQmlIncubator::Error && m_errorCallback) {
m_errorCallback(this);
}
if (status == QQmlIncubator::Ready && m_completedCallback) {
m_completedCallback(this);
}
}
MessageDelegateBase::MessageDelegateBase(QQuickItem *parent)
: TimelineDelegate(parent)
{
m_contentSizeHelper.setParentItem(this);
setAcceptHoverEvents(true);
setPercentageValues();
connect(this, &MessageDelegateBase::leftPaddingChanged, this, &MessageDelegateBase::setContentPadding);
connect(this, &MessageDelegateBase::rightPaddingChanged, this, &MessageDelegateBase::setContentPadding);
connect(&m_contentSizeHelper, &DelegateSizeHelper::availableWidthChanged, this, &MessageDelegateBase::setContentPadding);
connect(&m_contentSizeHelper, &DelegateSizeHelper::availableWidthChanged, this, &MessageDelegateBase::maxContentWidthChanged);
connect(&m_contentSizeHelper, &DelegateSizeHelper::availableWidthChanged, this, &MessageDelegateBase::markAsDirty);
}
MessageDelegateBase::~MessageDelegateBase()
{
for (const auto &incubator : m_activeIncubators) {
incubator->clear();
delete incubator;
}
}
NeochatRoomMember *MessageDelegateBase::author() const
{
return m_author;
}
void MessageDelegateBase::setAuthor(NeochatRoomMember *author)
{
if (author == m_author) {
return;
}
m_author = author;
Q_EMIT authorChanged();
setContentPadding();
markAsDirty();
}
bool MessageDelegateBase::isThreaded() const
{
return m_isThreaded;
}
void MessageDelegateBase::setIsThreaded(bool isThreaded)
{
if (isThreaded == m_isThreaded) {
return;
}
m_isThreaded = isThreaded;
setAlwaysFillWidth(m_isThreaded || m_compactMode);
setPercentageValues(m_isThreaded || m_compactMode);
updateAvatar();
Q_EMIT isThreadedChanged();
}
void MessageDelegateBase::setCurveValues()
{
m_spacing = qreal(m_units->smallSpacing());
m_avatarSize = qreal(m_units->gridUnit() + m_units->largeSpacing() * 2);
m_sizeHelper.setLeftPadding(qreal(m_units->largeSpacing()));
setBaseRightPadding();
m_sizeHelper.setStartBreakpoint(qreal(m_units->gridUnit() * 46));
m_sizeHelper.setEndBreakpoint(qreal(m_units->gridUnit() * 66));
m_sizeHelper.setMaxWidth(qreal(m_units->gridUnit() * 60));
m_contentSizeHelper.setStartBreakpoint(qreal(m_units->gridUnit() * 25));
m_contentSizeHelper.setEndBreakpoint(qreal(m_units->gridUnit() * 40));
m_contentSizeHelper.setMaxWidth(qreal(m_units->gridUnit() * 60));
setContentPadding();
}
void MessageDelegateBase::setBaseRightPadding()
{
if (!m_units) {
return;
}
if (m_compactMode && width() > m_units->gridUnit() * 30) {
m_sizeHelper.setRightPadding(qreal(m_units->gridUnit() * 2 + m_units->largeSpacing()));
} else {
m_sizeHelper.setRightPadding(qreal(m_units->largeSpacing()));
}
}
void MessageDelegateBase::setPercentageValues(bool fillWidth)
{
if (fillWidth) {
m_contentSizeHelper.setStartPercentWidth(100);
m_contentSizeHelper.setEndPercentWidth(100);
} else {
m_contentSizeHelper.setStartPercentWidth(85);
m_contentSizeHelper.setEndPercentWidth(60);
}
}
void MessageDelegateBase::setContentPadding()
{
m_contentSizeHelper.setLeftPadding(m_sizeHelper.leftX() + (leaveAvatarSpace() ? m_avatarSize + m_spacing : 0));
m_contentSizeHelper.setRightPadding(m_sizeHelper.rightPadding());
}
qreal MessageDelegateBase::maxContentWidth() const
{
return m_isThreaded || m_alwaysFillWidth ? m_contentSizeHelper.maxAvailableWidth() : m_contentSizeHelper.availableWidth();
}
void MessageDelegateBase::cleanupIncubator(MessageObjectIncubator *incubator)
{
if (!incubator) {
return;
}
incubator->clear();
const auto it = std::find(m_activeIncubators.begin(), m_activeIncubators.end(), incubator);
if (it != m_activeIncubators.end()) {
m_activeIncubators.erase(it);
}
delete incubator;
}
void MessageDelegateBase::cleanupItem(QQuickItem *item)
{
if (!item) {
return;
}
item->setParentItem(nullptr);
item->disconnect(this);
item->deleteLater();
}
QQmlComponent *MessageDelegateBase::avatarComponent() const
{
return m_avatarComponent;
}
void MessageDelegateBase::setAvatarComponent(QQmlComponent *avatarComponent)
{
if (avatarComponent == m_avatarComponent) {
return;
}
m_avatarComponent = avatarComponent;
Q_EMIT avatarComponentChanged();
updateAvatar();
}
bool MessageDelegateBase::showAuthor() const
{
return m_showAuthor;
}
void MessageDelegateBase::setShowAuthor(bool showAuthor)
{
if (showAuthor == m_showAuthor) {
return;
}
m_showAuthor = showAuthor;
Q_EMIT showAuthorChanged();
updateAvatar();
}
bool MessageDelegateBase::enableAvatars() const
{
return m_enableAvatars;
}
void MessageDelegateBase::setEnableAvatars(bool enableAvatars)
{
if (enableAvatars == m_enableAvatars) {
return;
}
m_enableAvatars = enableAvatars;
Q_EMIT enableAvatarsChanged();
updateAvatar();
}
bool MessageDelegateBase::leaveAvatarSpace() const
{
return m_enableAvatars && !showMessageOnRight();
}
bool MessageDelegateBase::showAvatar() const
{
return m_enableAvatars && m_showAuthor && !showMessageOnRight();
}
void MessageDelegateBase::updateAvatar()
{
if (m_avatarComponent && showAvatar() && !m_avatarItem && !m_avatarIncubating) {
const auto avatarIncubator = new MessageObjectIncubator(
m_objectInitialCallback,
[this](MessageObjectIncubator *incubator) {
if (!incubator) {
return;
}
const auto avatarObject = qobject_cast<QQuickItem *>(incubator->object());
if (avatarObject) {
// The setting may have changed during the incubation period.
if (showAvatar()) {
m_avatarItem = avatarObject;
} else {
cleanupItem(avatarObject);
}
markAsDirty();
}
m_avatarIncubating = false;
cleanupIncubator(incubator);
},
m_errorCallback);
m_activeIncubators.push_back(avatarIncubator);
m_avatarComponent->create(*avatarIncubator, qmlContext(m_avatarComponent));
m_avatarIncubating = true;
} else if (!showAvatar() && m_avatarItem) {
cleanupItem(m_avatarItem);
markAsDirty();
}
}
QQmlComponent *MessageDelegateBase::sectionComponent() const
{
return m_sectionComponent;
}
void MessageDelegateBase::setSectionComponent(QQmlComponent *sectionComponent)
{
if (sectionComponent == m_sectionComponent) {
return;
}
m_sectionComponent = sectionComponent;
Q_EMIT sectionComponentChanged();
updateSection();
}
bool MessageDelegateBase::showSection() const
{
return m_showSection;
}
void MessageDelegateBase::setShowSection(bool showSection)
{
if (showSection == m_showSection) {
return;
}
m_showSection = showSection;
Q_EMIT showSectionChanged();
updateSection();
}
void MessageDelegateBase::updateSection()
{
if (m_sectionComponent && m_showSection && !m_sectionItem && !m_sectionIncubating) {
const auto sectionIncubator = new MessageObjectIncubator(
m_objectInitialCallback,
[this](MessageObjectIncubator *incubator) {
if (!incubator) {
return;
}
const auto sectionObject = qobject_cast<QQuickItem *>(incubator->object());
if (sectionObject) {
// The setting may have changed during the incubation period.
if (m_showSection) {
m_sectionItem = sectionObject;
} else {
cleanupItem(sectionObject);
}
markAsDirty();
}
m_sectionIncubating = false;
cleanupIncubator(incubator);
},
m_errorCallback);
m_activeIncubators.push_back(sectionIncubator);
m_sectionComponent->create(*sectionIncubator, qmlContext(m_sectionComponent));
m_sectionIncubating = true;
} else if (!m_showSection && m_sectionItem) {
cleanupItem(m_sectionItem);
markAsDirty();
}
}
QQmlComponent *MessageDelegateBase::readMarkerComponent() const
{
return m_readMarkerComponent;
}
void MessageDelegateBase::setReadMarkerComponent(QQmlComponent *readMarkerComponent)
{
if (readMarkerComponent == m_readMarkerComponent) {
return;
}
m_readMarkerComponent = readMarkerComponent;
Q_EMIT readMarkerComponentChanged();
updateReadMarker();
}
bool MessageDelegateBase::showReadMarkers() const
{
return m_showReadMarkers;
}
void MessageDelegateBase::setShowReadMarkers(bool showReadMarkers)
{
if (showReadMarkers == m_showReadMarkers) {
return;
}
m_showReadMarkers = showReadMarkers;
Q_EMIT showReadMarkersChanged();
updateReadMarker();
}
void MessageDelegateBase::updateReadMarker()
{
if (m_readMarkerComponent && m_showReadMarkers && !m_readMarkerItem && !m_readMarkerIncubating) {
const auto readMarkerIncubator = new MessageObjectIncubator(
m_objectInitialCallback,
[this](MessageObjectIncubator *incubator) {
if (!incubator) {
return;
}
const auto readMarkerObject = qobject_cast<QQuickItem *>(incubator->object());
if (readMarkerObject) {
if (m_showReadMarkers) {
m_readMarkerItem = readMarkerObject;
} else {
cleanupItem(readMarkerObject);
}
markAsDirty();
}
m_readMarkerIncubating = false;
cleanupIncubator(incubator);
},
m_errorCallback);
m_activeIncubators.push_back(readMarkerIncubator);
m_readMarkerComponent->create(*readMarkerIncubator, qmlContext(m_readMarkerComponent));
m_readMarkerIncubating = true;
} else if (!m_showReadMarkers && m_readMarkerItem) {
cleanupItem(m_readMarkerItem);
markAsDirty();
}
}
QQmlComponent *MessageDelegateBase::compactBackgroundComponent() const
{
return m_compactBackgroundComponent;
}
void MessageDelegateBase::setCompactBackgroundComponentt(QQmlComponent *compactBackgroundComponent)
{
if (compactBackgroundComponent == m_compactBackgroundComponent) {
return;
}
m_compactBackgroundComponent = compactBackgroundComponent;
Q_EMIT compactBackgroundComponentChanged();
updateBackground();
}
bool MessageDelegateBase::compactMode() const
{
return m_compactMode;
}
void MessageDelegateBase::setCompactMode(bool compactMode)
{
if (compactMode == m_compactMode) {
return;
}
m_compactMode = compactMode;
setAlwaysFillWidth(m_isThreaded || m_compactMode);
setPercentageValues(m_isThreaded || m_compactMode);
setBaseRightPadding();
Q_EMIT compactModeChanged();
Q_EMIT maxContentWidthChanged();
updateBackground();
updateQuickAction();
updateAvatar();
}
void MessageDelegateBase::updateBackground()
{
if (m_compactBackgroundComponent && m_compactMode && m_hovered && !m_compactBackgroundItem && !m_compactBackgroundIncubating) {
const auto compactBackgroundIncubator = new MessageObjectIncubator(
m_objectInitialCallback,
[this](MessageObjectIncubator *incubator) {
if (!incubator) {
return;
}
const auto compactBackgroundObject = qobject_cast<QQuickItem *>(incubator->object());
if (compactBackgroundObject) {
if (m_compactMode) {
m_compactBackgroundItem = compactBackgroundObject;
} else {
cleanupItem(compactBackgroundObject);
}
markAsDirty();
}
m_compactBackgroundIncubating = false;
cleanupIncubator(incubator);
},
m_errorCallback);
m_activeIncubators.push_back(compactBackgroundIncubator);
m_compactBackgroundComponent->create(*compactBackgroundIncubator, qmlContext(m_compactBackgroundComponent));
m_compactBackgroundIncubating = true;
} else if (m_compactBackgroundItem && !m_hovered) {
cleanupItem(m_compactBackgroundItem);
markAsDirty();
}
}
QQmlComponent *MessageDelegateBase::quickActionComponent() const
{
return m_quickActionComponent;
}
void MessageDelegateBase::setQuickActionComponent(QQmlComponent *quickActionComponent)
{
if (quickActionComponent == m_quickActionComponent) {
return;
}
m_quickActionComponent = quickActionComponent;
Q_EMIT quickActionComponentChanged();
updateQuickAction();
}
void MessageDelegateBase::updateQuickAction()
{
if (!m_hovered || m_compactMode) {
if (m_quickActionItem && (!m_quickActionItem->property("reacting").toBool() || m_compactMode)) {
cleanupItem(m_quickActionItem);
markAsDirty();
}
return;
}
if (!m_quickActionComponent || m_quickActionItem || m_quickActionIncubating) {
return;
}
m_quickActionIncubating = true;
const auto quickActionIncubator = new MessageObjectIncubator(
m_objectInitialCallback,
[this](MessageObjectIncubator *incubator) {
if (!incubator) {
return;
}
if (const auto quickActionObject = qobject_cast<QQuickItem *>(incubator->object())) {
if (!m_compactMode) {
m_quickActionItem = quickActionObject;
connect(m_quickActionItem, SIGNAL(reactingChanged()), this, SLOT(updateQuickAction()));
} else {
cleanupItem(quickActionObject);
}
markAsDirty();
}
cleanupIncubator(incubator);
m_quickActionIncubating = false;
},
m_errorCallback);
m_activeIncubators.push_back(quickActionIncubator);
m_quickActionComponent->create(*quickActionIncubator, qmlContext(m_quickActionComponent));
}
bool MessageDelegateBase::showLocalMessagesOnRight() const
{
return m_showLocalMessagesOnRight;
}
void MessageDelegateBase::setShowLocalMessagesOnRight(bool showLocalMessagesOnRight)
{
if (showLocalMessagesOnRight == m_showLocalMessagesOnRight) {
return;
}
m_showLocalMessagesOnRight = showLocalMessagesOnRight;
Q_EMIT showLocalMessagesOnRightChanged();
setContentPadding();
updateAvatar();
}
void MessageDelegateBase::updateImplicitHeight()
{
qreal implicitHeight = 0.0;
int numObj = 0;
if (m_showSection && m_sectionItem) {
implicitHeight += m_sectionItem->implicitHeight();
numObj++;
}
qreal avatarHeight = 0.0;
qreal contentHeight = 0.0;
qreal quickActionHeight = 0.0;
if (showAvatar() && m_avatarItem) {
m_avatarItem->setImplicitWidth(m_avatarSize);
m_avatarItem->setImplicitHeight(m_avatarSize);
avatarHeight = m_avatarItem->implicitHeight();
}
if (m_contentItem) {
contentHeight = m_contentItem->implicitHeight();
}
if (m_quickActionItem) {
quickActionHeight = m_quickActionItem->implicitHeight();
}
implicitHeight += std::max({avatarHeight, contentHeight, quickActionHeight});
if (avatarHeight > 0 || contentHeight > 0) {
numObj++;
}
if (m_showReadMarkers && m_readMarkerItem) {
implicitHeight += m_readMarkerItem->implicitHeight();
numObj++;
}
implicitHeight += (numObj - 1) * m_spacing;
implicitHeight += m_showAuthor ? m_spacing * 2 : m_spacing;
implicitHeight = std::ceil(implicitHeight);
setImplicitWidth(m_alwaysFillWidth ? m_sizeHelper.maxAvailableWidth() : m_sizeHelper.availableWidth());
setImplicitHeight(implicitHeight);
}
bool MessageDelegateBase::showMessageOnRight() const
{
return m_showLocalMessagesOnRight && !m_alwaysFillWidth && m_author && m_author->isLocalMember();
}
void MessageDelegateBase::resizeContent()
{
if (!isComponentComplete() || m_resizingContent) {
return;
}
m_isDirty = false;
m_resizingContent = true;
updateImplicitHeight();
qreal nextY = m_showAuthor ? m_spacing * 2 : m_spacing;
if (m_compactMode && m_compactBackgroundItem) {
m_compactBackgroundItem->setPosition(QPointF(std::ceil(m_sizeHelper.leftX() / 2), std::ceil(nextY / 2)));
m_compactBackgroundItem->setSize(
QSizeF(m_sizeHelper.availableWidth() + m_sizeHelper.rightPadding() - std::ceil(m_sizeHelper.leftPadding() / 2), implicitHeight()));
m_compactBackgroundItem->setZ(-1);
}
if (m_showSection && m_sectionItem) {
m_sectionItem->setPosition(QPointF(m_sizeHelper.leftX(), nextY));
m_sectionItem->setSize(QSizeF(m_sizeHelper.availableWidth(), m_sectionItem->implicitHeight()));
nextY += m_sectionItem->implicitHeight() + m_spacing;
}
qreal yAdd = 0.0;
if (showAvatar() && m_avatarItem) {
m_avatarItem->setPosition(QPointF(m_sizeHelper.leftX(), nextY));
m_avatarItem->setSize(QSizeF(m_avatarItem->implicitWidth(), m_avatarItem->implicitHeight()));
yAdd = m_avatarItem->implicitWidth();
}
if (m_contentItem) {
const auto contentItemWidth =
m_alwaysFillWidth ? m_contentSizeHelper.availableWidth() : std::min(m_contentItem->implicitWidth(), m_contentSizeHelper.availableWidth());
const auto contentX = showMessageOnRight() ? m_sizeHelper.rightX() - contentItemWidth - 1 : m_contentSizeHelper.leftPadding();
m_contentItem->setPosition(QPointF(contentX, nextY));
m_contentItem->setSize(QSizeF(contentItemWidth, m_contentItem->implicitHeight()));
yAdd = std::max(yAdd, m_contentItem->implicitHeight());
}
if (m_quickActionItem) {
const auto availableWidth = m_contentItem && showMessageOnRight() ? m_contentItem->x() - m_contentSizeHelper.leftPadding()
: m_sizeHelper.rightX() - m_contentItem->x() - m_contentItem->width() - m_spacing;
m_quickActionItem->setProperty("availableWidth", availableWidth);
const auto actionX = showMessageOnRight() && m_contentItem ? m_contentItem->x() - m_quickActionItem->implicitWidth() - m_spacing
: m_contentItem->x() + m_contentItem->width() + m_spacing;
const auto actionWidth = std::min(m_quickActionItem->implicitWidth(), availableWidth);
m_quickActionItem->setPosition(QPointF(actionX, nextY));
m_quickActionItem->setSize(QSizeF(actionWidth, m_quickActionItem->implicitHeight()));
yAdd = std::max(yAdd, m_quickActionItem->implicitHeight());
}
nextY += yAdd + m_spacing;
if (m_showReadMarkers && m_readMarkerItem) {
qreal extraSpacing = m_readMarkerItem->implicitWidth() < m_sizeHelper.availableWidth() - m_spacing ? m_spacing : 0;
qreal objectWidth = std::min(m_sizeHelper.availableWidth(), m_readMarkerItem->implicitWidth());
m_readMarkerItem->setPosition(QPointF(m_sizeHelper.rightX() - objectWidth - extraSpacing, nextY));
m_readMarkerItem->setSize(QSizeF(objectWidth, m_readMarkerItem->implicitHeight()));
}
m_resizingContent = false;
}
void MessageDelegateBase::hoverEnterEvent(QHoverEvent *event)
{
m_hovered = true;
Q_EMIT hoveredChanged();
event->setAccepted(true);
updateBackground();
updateQuickAction();
}
void MessageDelegateBase::hoverMoveEvent(QHoverEvent *event)
{
bool oldHovered = m_hovered;
m_hovered = contains(event->position());
if (oldHovered != m_hovered) {
Q_EMIT hoveredChanged();
}
event->setAccepted(true);
updateBackground();
updateQuickAction();
}
void MessageDelegateBase::hoverLeaveEvent(QHoverEvent *event)
{
m_hovered = false;
Q_EMIT hoveredChanged();
event->setAccepted(true);
updateBackground();
updateQuickAction();
}
bool MessageDelegateBase::isTemporaryHighlighted() const
{
return m_temporaryHighlightTimer && m_temporaryHighlightTimer->isActive();
}
void MessageDelegateBase::setIsTemporaryHighlighted(bool isTemporaryHighlighted)
{
if (!isTemporaryHighlighted) {
if (m_temporaryHighlightTimer) {
m_temporaryHighlightTimer->stop();
m_temporaryHighlightTimer->deleteLater();
Q_EMIT isTemporaryHighlightedChanged();
}
return;
}
if (!m_temporaryHighlightTimer) {
m_temporaryHighlightTimer = new QTimer(this);
}
m_temporaryHighlightTimer->start(1500);
connect(m_temporaryHighlightTimer, &QTimer::timeout, this, [this]() {
m_temporaryHighlightTimer->stop();
m_temporaryHighlightTimer->deleteLater();
Q_EMIT isTemporaryHighlightedChanged();
});
Q_EMIT isTemporaryHighlightedChanged();
}
bool MessageDelegateBase::hovered() const
{
return m_hovered;
}
#include "moc_messagedelegate.cpp"