From 6ad89a4f6ae4802bea052f373a4d3a44cebeb083 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 4 Oct 2026 18:08:43 +0200 Subject: [PATCH] Elide Qt invitation links with consistent layout and full-address selection --- tools/chatgui/src/messagedelegate.cpp | 48 +++++++++++++++++++-------- tools/chatgui/src/messagelist.cpp | 9 +++++ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index efa98d28..6fc17689 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -27,18 +27,25 @@ #include /* Один layout для размеров, отрисовки и hit-test длинных invite-ссылок. */ -static qreal layoutInviteText(QTextLayout& layout, int width) { +static qreal layoutInviteText(QTextLayout& layout, int width, const QString& compactLink = {}) { + int linkStart = -1; + if (!compactLink.isEmpty() && layout.text().endsWith(compactLink)) { + linkStart = layout.text().length() - compactLink.length(); + const QString shortened = QFontMetrics(layout.font()).elidedText(compactLink, Qt::ElideRight, qMax(1, width - 1)); + layout.setText(layout.text().left(linkStart) + shortened); + } QTextOption option; option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); layout.setTextOption(option); static const QRegularExpression pattern(QStringLiteral("utun://[A-Za-z0-9+/]+={0,2}")); QVector formats; auto matches = pattern.globalMatch(layout.text()); - while (matches.hasNext()) { - auto match = matches.next(); + while (linkStart >= 0 || matches.hasNext()) { QTextLayout::FormatRange range; - range.start = match.capturedStart(); range.length = match.capturedLength(); + if (linkStart >= 0) { range.start = linkStart; range.length = layout.text().length() - linkStart; } + else { auto match = matches.next(); range.start = match.capturedStart(); range.length = match.capturedLength(); } range.format.setForeground(QColor(0x33, 0x90, 0xEC)); range.format.setFontUnderline(true); formats.append(range); + if (linkStart >= 0) break; } layout.setFormats(formats); layout.beginLayout(); @@ -61,14 +68,16 @@ QString MessageDelegate::inviteLinkAt(const QStyleOptionViewItem& option, const if (!QRect(QPoint(0, 0), geometry.textRect.size()).contains(local)) return {}; QString display = text; display.replace('\n', QChar(0x2028)); QTextLayout layout(display, option.font); - layoutInviteText(layout, geometry.textRect.width()); + const QString compactLink = index.data(MsgInviteLinkRole).toString(); + layoutInviteText(layout, geometry.textRect.width(), compactLink); for (int i = 0; i < layout.lineCount(); ++i) { const auto line = layout.lineAt(i); if (local.y() < line.y() || local.y() >= line.y() + line.height()) continue; if (local.x() < line.x() || local.x() >= line.x() + line.naturalTextWidth()) return {}; const int position = line.xToCursor(local.x(), QTextLine::CursorOnCharacter); for (const auto& range : layout.formats()) { - if (position >= range.start && position < range.start + range.length) return text.mid(range.start, range.length); + if (position >= range.start && position < range.start + range.length) + return compactLink.isEmpty() ? text.mid(range.start, range.length) : compactLink; } } return {}; @@ -314,12 +323,12 @@ QSize MessageDelegate::sizeHint(const QStyleOptionViewItem &option, return QSize(L.viewWidth, L.totalHeight); } -static int measureTextHeight(const QFont &font, const QString &text, int width) { +static int measureTextHeight(const QFont &font, const QString &text, int width, const QString& compactLink = {}) { if (text.isEmpty() || width <= 0) return 0; if (text.contains(QStringLiteral("utun://"))) { QString display = text; display.replace('\n', QChar(0x2028)); QTextLayout layout(display, font); - return (int)std::ceil(layoutInviteText(layout, width)); + return (int)std::ceil(layoutInviteText(layout, width, compactLink)); } QRect r = QFontMetrics(font).boundingRect(QRect(0, 0, width, 100000), Qt::AlignLeft | Qt::TextWordWrap, text); @@ -614,7 +623,7 @@ MessageDelegate::Layout MessageDelegate::calcLayout( qH = kQuotePadV + qOneLineH + (qText.isEmpty() ? 0 : 2 + qTextH) + kQuotePadV; } - int textH = measureTextHeight(option.font, text, textW); + int textH = measureTextHeight(option.font, text, textW, index.data(MsgInviteLinkRole).toString()); int authorH = fm.height(); int bubbleH = L.isOutgoing ? kPadTop + (hasQuote ? qH + kQuoteGap : 0) + textH + kStatusH + kPadBot @@ -1371,11 +1380,14 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio painter->setFont(textFont); QString msgText = index.data(MsgTextRole).toString(); + QString overlayText = msgText; const bool hasInvite = msgText.contains(QStringLiteral("utun://")); if (hasInvite) { QString display = msgText; display.replace('\n', QChar(0x2028)); QTextLayout layout(display, textFont); - layoutInviteText(layout, L.textRect.width()); + const QString compactLink = index.data(MsgInviteLinkRole).toString(); + layoutInviteText(layout, L.textRect.width(), compactLink); + overlayText = layout.text(); overlayText.replace(QChar(0x2028), '\n'); QVector selections; if (singleTextSel && cv) { auto cursorAt = [&](const QPoint& point) -> int { @@ -1383,14 +1395,22 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio const auto line = layout.lineAt(i); if (point.y() < line.y() + line.height()) return line.xToCursor(point.x()); } - return msgText.length(); + return layout.text().length(); }; const auto& selection = cv->textSelection(); int start = cursorAt(selection.clickPos - L.textRect.topLeft()); int end = cursorAt(selection.dragPos - L.textRect.topLeft()); if (start > end) qSwap(start, end); - if (start == end && end < msgText.length()) ++end; - const_cast(cv)->setTextSelChars(start, end); + if (start == end && end < layout.text().length()) ++end; + int copyStart = start, copyEnd = end; + if (!compactLink.isEmpty()) { + const int linkStart = msgText.length() - compactLink.length(); + if (end > linkStart) { + if (start >= linkStart) { start = linkStart; copyStart = linkStart; } + end = layout.text().length(); copyEnd = msgText.length(); + } + } + const_cast(cv)->setTextSelChars(copyStart, copyEnd); QTextLayout::FormatRange range; range.start = start; range.length = end - start; range.format.setBackground(QColor(0x33, 0x90, 0xEC, 60)); selections.append(range); @@ -1414,7 +1434,7 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio startChar, endChar); } - drawAnimatedEmojiOverlay(painter, textFont, msgText, + drawAnimatedEmojiOverlay(painter, textFont, overlayText, L.textRect.width(), L.textRect); drawStatusBar(painter, L, index, option); diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index 8cdd23df..e390b897 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -1151,6 +1151,14 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { qint64 ts = (qint64)message.value("ts").toDouble(); quint64 author = message.value("author").toString().toULongLong(); QString text = message.value("data").toString(); + const bool invite = message.value("ct").toString() == QStringLiteral("invite"); + const QString inviteLink = invite ? text : QString(); + if (invite) { + const QString groupName = message.value("tags").toObject().value("group_name").toString(); + text = tr("Приглашение в группу «%1»").arg(groupName) + '\n' + inviteLink; + GUI_DEBUG("invite PM: rendered conv=%s seq=%lld group_name_bytes=%d link_bytes=%d", qPrintable(m_currentChannelId), + (long long)seq, (int)groupName.toUtf8().size(), (int)inviteLink.size()); + } bool file = message.value("ct").toString() == "file"; QString state = message.value("media_status").toString(); QString status; @@ -1181,6 +1189,7 @@ void MessageList::onDmMessages(const uint8_t* data, int len) { item->setData(m_currentChannelId, MsgChannelIdRole); QString type = message.value("content_type").toString(QStringLiteral("text/plain")); item->setData(type, MsgContentTypeRole); + if (invite) item->setData(inviteLink, MsgInviteLinkRole); if (file) { QString path = message.value("media_path").toString(); item->setData(path, MsgVoiceFileRole);