/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/history_view_service_message.h" #include "history/view/media/history_view_media.h" #include "history/view/history_view_cursor_state.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_components.h" #include "history/history_item_helpers.h" #include "data/data_abstract_structure.h" #include "data/data_chat.h" #include "data/data_channel.h" #include "info/profile/info_profile_cover.h" #include "ui/chat/chat_style.h" #include "ui/text/text_options.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "mainwidget.h" #include "menu/menu_ttl_validator.h" #include "data/data_forum_topic.h" #include "lang/lang_keys.h" #include "styles/style_chat.h" #include "styles/style_info.h" namespace HistoryView { namespace { TextParseOptions EmptyLineOptions = { TextParseMultiline, // flags 4096, // maxw 1, // maxh Qt::LayoutDirectionAuto, // lang-dependent }; enum CircleMask { NormalMask = 0x00, InvertedMask = 0x01, }; enum CircleMaskMultiplier { MaskMultiplier = 0x04, }; enum CornerVerticalSide { CornerTop = 0x00, CornerBottom = 0x02, }; enum CornerHorizontalSide { CornerLeft = 0x00, CornerRight = 0x01, }; enum class SideStyle { Rounded, Plain, Inverted, }; // Returns amount of pixels already painted vertically (so you can skip them in the complex rect shape). int PaintBubbleSide( Painter &p, not_null st, int x, int y, int width, SideStyle style, CornerVerticalSide side) { if (style == SideStyle::Rounded) { const auto &corners = st->serviceBgCornersNormal(); const auto left = corners.p[(side == CornerTop) ? 0 : 2]; const auto leftWidth = left.width() / cIntRetinaFactor(); p.drawPixmap(x, y, left); const auto right = corners.p[(side == CornerTop) ? 1 : 3]; const auto rightWidth = right.width() / cIntRetinaFactor(); p.drawPixmap(x + width - rightWidth, y, right); const auto cornerHeight = left.height() / cIntRetinaFactor(); p.fillRect( x + leftWidth, y, width - leftWidth - rightWidth, cornerHeight, st->msgServiceBg()); return cornerHeight; } else if (style == SideStyle::Inverted) { // CornerLeft and CornerRight are inverted in the top part. const auto &corners = st->serviceBgCornersInverted(); const auto left = corners.p[(side == CornerTop) ? 1 : 2]; const auto leftWidth = left.width() / cIntRetinaFactor(); p.drawPixmap(x - leftWidth, y, left); const auto right = corners.p[(side == CornerTop) ? 0 : 3]; p.drawPixmap(x + width, y, right); } return 0; } void PaintBubblePart( Painter &p, not_null st, int x, int y, int width, int height, SideStyle topStyle, SideStyle bottomStyle, bool forceShrink = false) { if ((topStyle == SideStyle::Inverted) || (bottomStyle == SideStyle::Inverted) || forceShrink) { width -= Ui::HistoryServiceMsgInvertedShrink() * 2; x += Ui::HistoryServiceMsgInvertedShrink(); } if (int skip = PaintBubbleSide(p, st, x, y, width, topStyle, CornerTop)) { y += skip; height -= skip; } int bottomSize = 0; if (bottomStyle == SideStyle::Rounded) { bottomSize = Ui::HistoryServiceMsgRadius(); } else if (bottomStyle == SideStyle::Inverted) { bottomSize = Ui::HistoryServiceMsgInvertedRadius(); } const auto skip = PaintBubbleSide( p, st, x, y + height - bottomSize, width, bottomStyle, CornerBottom); if (skip) { height -= skip; } p.fillRect(x, y, width, height, st->msgServiceBg()); } void PaintPreparedDate( Painter &p, const style::color &bg, const Ui::CornersPixmaps &corners, const style::color &fg, const QString &dateText, int dateTextWidth, int y, int w, bool chatWide) { int left = st::msgServiceMargin.left(); const auto maxwidth = chatWide ? std::min(w, WideChatWidth()) : w; w = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); left += (w - dateTextWidth - st::msgServicePadding.left() - st::msgServicePadding.right()) / 2; int height = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom(); ServiceMessagePainter::PaintBubble( p, bg, corners, QRect( left, y + st::msgServiceMargin.top(), dateTextWidth + st::msgServicePadding.left() + st::msgServicePadding.left(), height)); p.setFont(st::msgServiceFont); p.setPen(fg); p.drawText( left + st::msgServicePadding.left(), (y + st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->ascent), dateText); } bool NeedAboutGroup(not_null history) { if (const auto chat = history->peer->asChat()) { return chat->amCreator(); } else if (const auto channel = history->peer->asMegagroup()) { return channel->amCreator(); } return false; } void SetText(Ui::Text::String &text, const QString &content) { text.setText(st::serviceTextStyle, content, EmptyLineOptions); } } // namespace int WideChatWidth() { return st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left(); } void ServiceMessagePainter::PaintDate( Painter &p, not_null st, const QDateTime &date, int y, int w, bool chatWide) { PaintDate( p, st, langDayOfMonthFull(date.date()), y, w, chatWide); } void ServiceMessagePainter::PaintDate( Painter &p, not_null st, const QString &dateText, int y, int w, bool chatWide) { PaintDate( p, st, dateText, st::msgServiceFont->width(dateText), y, w, chatWide); } void ServiceMessagePainter::PaintDate( Painter &p, not_null st, const QString &dateText, int dateTextWidth, int y, int w, bool chatWide) { PaintPreparedDate( p, st->msgServiceBg(), st->serviceBgCornersNormal(), st->msgServiceFg(), dateText, dateTextWidth, y, w, chatWide); } void ServiceMessagePainter::PaintDate( Painter &p, const style::color &bg, const Ui::CornersPixmaps &corners, const style::color &fg, const QString &dateText, int dateTextWidth, int y, int w, bool chatWide) { PaintPreparedDate( p, bg, corners, fg, dateText, dateTextWidth, y, w, chatWide); } void ServiceMessagePainter::PaintBubble( Painter &p, not_null st, QRect rect) { PaintBubble(p, st->msgServiceBg(), st->serviceBgCornersNormal(), rect); } void ServiceMessagePainter::PaintBubble( Painter &p, const style::color &bg, const Ui::CornersPixmaps &corners, QRect rect) { Ui::FillRoundRect(p, rect, bg, corners); } void ServiceMessagePainter::PaintComplexBubble( Painter &p, not_null st, int left, int width, const Ui::Text::String &text, const QRect &textRect) { const auto lineWidths = CountLineWidths(text, textRect); int y = st::msgServiceMargin.top(), previousRichWidth = 0; bool previousShrink = false, forceShrink = false; SideStyle topStyle = SideStyle::Rounded, bottomStyle; for (int i = 0, count = lineWidths.size(); i < count; ++i) { const auto lineWidth = lineWidths[i]; if (i + 1 < count) { const auto nextLineWidth = lineWidths[i + 1]; if (nextLineWidth > lineWidth) { bottomStyle = SideStyle::Inverted; } else if (nextLineWidth < lineWidth) { bottomStyle = SideStyle::Rounded; } else { bottomStyle = SideStyle::Plain; } } else { bottomStyle = SideStyle::Rounded; } auto richWidth = lineWidth + st::msgServicePadding.left() + st::msgServicePadding.right(); auto richHeight = st::msgServiceFont->height; if (topStyle == SideStyle::Rounded) { richHeight += st::msgServicePadding.top(); } else if (topStyle == SideStyle::Inverted) { richHeight -= st::msgServicePadding.bottom(); } if (bottomStyle == SideStyle::Rounded) { richHeight += st::msgServicePadding.bottom(); } else if (bottomStyle == SideStyle::Inverted) { richHeight -= st::msgServicePadding.top(); } forceShrink = previousShrink && (richWidth == previousRichWidth); PaintBubblePart( p, st, left + ((width - richWidth) / 2), y, richWidth, richHeight, topStyle, bottomStyle, forceShrink); y += richHeight; previousShrink = forceShrink || (topStyle == SideStyle::Inverted) || (bottomStyle == SideStyle::Inverted); previousRichWidth = richWidth; if (bottomStyle == SideStyle::Inverted) { topStyle = SideStyle::Rounded; } else if (bottomStyle == SideStyle::Rounded) { topStyle = SideStyle::Inverted; } else { topStyle = SideStyle::Plain; } } } QVector ServiceMessagePainter::CountLineWidths( const Ui::Text::String &text, const QRect &textRect) { const auto linesCount = qMax( textRect.height() / st::msgServiceFont->height, 1); auto result = QVector(); result.reserve(linesCount); text.countLineWidths(textRect.width(), &result); const auto minDelta = 2 * (Ui::HistoryServiceMsgRadius() + Ui::HistoryServiceMsgInvertedRadius() - Ui::HistoryServiceMsgInvertedShrink()); for (int i = 0, count = result.size(); i != count; ++i) { auto width = qMax(result[i], 0); if (i > 0) { const auto widthBefore = result[i - 1]; if (width < widthBefore && width + minDelta > widthBefore) { width = widthBefore; } } if (i + 1 < count) { const auto widthAfter = result[i + 1]; if (width < widthAfter && width + minDelta > widthAfter) { width = widthAfter; } } if (width > result[i]) { result[i] = width; if (i > 0) { int widthBefore = result[i - 1]; if (widthBefore != width && widthBefore < width + minDelta && widthBefore + minDelta > width) { i -= 2; } } } } return result; } Service::Service( not_null delegate, not_null data, Element *replacing) : Element(delegate, data, replacing, Flag::ServiceMessage) { } QRect Service::innerGeometry() const { return countGeometry(); } QRect Service::countGeometry() const { auto result = QRect(0, 0, width(), height()); if (delegate()->elementIsChatWide()) { result.setWidth(qMin(result.width(), st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left())); } return result.marginsRemoved(st::msgServiceMargin); } QSize Service::performCountCurrentSize(int newWidth) { auto newHeight = displayedDateHeight(); if (const auto bar = Get()) { newHeight += bar->height(); } if (isHidden()) { return { newWidth, newHeight }; } const auto media = this->media(); if (media && data()->isUserpicSuggestion()) { newHeight = st::msgServiceMargin.top() + media->resizeGetHeight(newWidth) + st::msgServiceMargin.bottom(); } else if (!text().isEmpty()) { auto contentWidth = newWidth; if (delegate()->elementIsChatWide()) { accumulate_min(contentWidth, st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); } contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; } auto nwidth = qMax(contentWidth - st::msgServicePadding.left() - st::msgServicePadding.right(), 0); newHeight += (contentWidth >= maxWidth()) ? minHeight() : textHeightFor(nwidth); newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom() + st::msgServiceMargin.top() + st::msgServiceMargin.bottom(); if (media) { newHeight += st::msgServiceMargin.top() + media->resizeGetHeight(media->maxWidth()); } } return { newWidth, newHeight }; } QSize Service::performCountOptimalSize() { validateText(); if (const auto media = this->media()) { media->initDimensions(); if (data()->isUserpicSuggestion()) { return { media->maxWidth(), media->minHeight() }; } } auto maxWidth = text().maxWidth() + st::msgServicePadding.left() + st::msgServicePadding.right(); auto minHeight = text().minHeight(); return { maxWidth, minHeight }; } bool Service::isHidden() const { return Element::isHidden(); } int Service::marginTop() const { return st::msgServiceMargin.top(); } int Service::marginBottom() const { return st::msgServiceMargin.bottom(); } void Service::draw(Painter &p, const PaintContext &context) const { auto g = countGeometry(); if (g.width() < 1) { return; } const auto &margin = st::msgServiceMargin; const auto st = context.st; auto height = this->height() - margin.top() - margin.bottom(); auto dateh = 0; auto unreadbarh = 0; auto clip = context.clip; if (auto date = Get()) { dateh = date->height(); p.translate(0, dateh); clip.translate(0, -dateh); height -= dateh; } if (const auto bar = Get()) { unreadbarh = bar->height(); if (clip.intersects(QRect(0, 0, width(), unreadbarh))) { bar->paint( p, context, 0, width(), delegate()->elementIsChatWide()); } p.translate(0, unreadbarh); clip.translate(0, -unreadbarh); height -= unreadbarh; } if (isHidden()) { if (auto skiph = dateh + unreadbarh) { p.translate(0, -skiph); } return; } paintHighlight(p, context, height); p.setTextPalette(st->serviceTextPalette()); const auto media = this->media(); const auto onlyMedia = (media && data()->isUserpicSuggestion()); if (!onlyMedia) { if (media) { height -= margin.top() + media->height(); } const auto trect = QRect(g.left(), margin.top(), g.width(), height) - st::msgServicePadding; ServiceMessagePainter::PaintComplexBubble( p, context.st, g.left(), g.width(), text(), trect); p.setBrush(Qt::NoBrush); p.setPen(st->msgServiceFg()); p.setFont(st::msgServiceFont); prepareCustomEmojiPaint(p, context, text()); text().draw(p, { .position = trect.topLeft(), .availableWidth = trect.width(), .align = style::al_top, .palette = &st->serviceTextPalette(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, .paused = context.paused, .selection = context.selection, .fullWidthSelection = false, }); } if (media) { const auto left = margin.left() + (g.width() - media->maxWidth()) / 2; const auto top = margin.top() + (onlyMedia ? 0 : (height + margin.top())); p.translate(left, top); media->draw(p, context.translated(-left, -top).withSelection({})); p.translate(-left, -top); } if (auto skiph = dateh + unreadbarh) { p.translate(0, -skiph); } } PointState Service::pointState(QPoint point) const { const auto media = this->media(); auto g = countGeometry(); if (g.width() < 1 || isHidden()) { return PointState::Outside; } if (const auto dateh = displayedDateHeight()) { g.setTop(g.top() + dateh); } if (const auto bar = Get()) { g.setTop(g.top() + bar->height()); } if (media) { const auto centerPadding = (g.width() - media->width()) / 2; const auto r = g - QMargins(centerPadding, 0, centerPadding, 0); if (!r.contains(point)) { g.setHeight(g.height() - (st::msgServiceMargin.top() + media->height())); } } return g.contains(point) ? PointState::Inside : PointState::Outside; } TextState Service::textState(QPoint point, StateRequest request) const { const auto item = data(); const auto media = this->media(); const auto onlyMedia = (media && data()->isUserpicSuggestion()); auto result = TextState(item); auto g = countGeometry(); if (g.width() < 1 || isHidden()) { return result; } if (const auto dateh = displayedDateHeight()) { point.setY(point.y() - dateh); g.setHeight(g.height() - dateh); } if (const auto bar = Get()) { auto unreadbarh = bar->height(); point.setY(point.y() - unreadbarh); g.setHeight(g.height() - unreadbarh); } if (onlyMedia) { return media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->maxWidth()) / 2, st::msgServiceMargin.top()), request); } else if (media) { g.setHeight(g.height() - (st::msgServiceMargin.top() + media->height())); } auto trect = g.marginsAdded(-st::msgServicePadding); if (trect.contains(point)) { auto textRequest = request.forText(); textRequest.align = style::al_center; result = TextState(item, text().getState( point - trect.topLeft(), trect.width(), textRequest)); if (!result.link && result.cursor == CursorState::Text && g.contains(point)) { if (const auto gamescore = item->Get()) { result.link = gamescore->lnk; } else if (const auto payment = item->Get()) { result.link = payment->invoiceLink; } else if (const auto call = item->Get()) { const auto peer = history()->peer; if (PeerHasThisCall(peer, call->id).value_or(false)) { result.link = call->link; } } else if (const auto theme = item->Get()) { result.link = theme->link; } else if (const auto ttl = item->Get()) { if (TTLMenu::TTLValidator(nullptr, history()->peer).can()) { result.link = ttl->link; } } } } else if (media) { result = media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->maxWidth()) / 2, st::msgServiceMargin.top() + g.height() + st::msgServiceMargin.top()), request); } return result; } void Service::updatePressed(QPoint point) { } TextForMimeData Service::selectedText(TextSelection selection) const { return text().toTextForMimeData(selection); } TextSelection Service::adjustSelection( TextSelection selection, TextSelectType type) const { return text().adjustSelection(selection, type); } EmptyPainter::EmptyPainter(not_null history) : _history(history) , _header(st::msgMinWidth) , _text(st::msgMinWidth) { if (NeedAboutGroup(_history)) { fillAboutGroup(); } } EmptyPainter::EmptyPainter( not_null topic, Fn paused, Fn update) : _history(topic->history()) , _topic(topic) , _icon( std::make_unique( topic, paused, update, st::msgServiceFg)) , _header(st::msgMinWidth) , _text(st::msgMinWidth) { fillAboutTopic(); } EmptyPainter::~EmptyPainter() = default; void EmptyPainter::fillAboutGroup() { const auto phrases = { tr::lng_group_about1(tr::now), tr::lng_group_about2(tr::now), tr::lng_group_about3(tr::now), tr::lng_group_about4(tr::now), }; SetText(_header, tr::lng_group_about_header(tr::now)); SetText(_text, tr::lng_group_about_text(tr::now)); for (const auto &text : phrases) { _phrases.emplace_back(st::msgMinWidth); SetText(_phrases.back(), text); } } void EmptyPainter::fillAboutTopic() { SetText(_header, _topic->my() ? tr::lng_forum_topic_created_title_my(tr::now) : tr::lng_forum_topic_created_title(tr::now)); SetText(_text, _topic->my() ? tr::lng_forum_topic_created_body_my(tr::now) : tr::lng_forum_topic_created_body(tr::now)); } void EmptyPainter::paint( Painter &p, not_null st, int width, int height) { if (_phrases.empty() && _text.isEmpty()) { return; } constexpr auto kMaxTextLines = 3; const auto maxPhraseWidth = _phrases.empty() ? 0 : ranges::max_element( _phrases, ranges::less(), &Ui::Text::String::maxWidth)->maxWidth(); const auto &font = st::serviceTextStyle.font; const auto maxBubbleWidth = width - 2 * st::historyGroupAboutMargin; const auto padding = st::historyGroupAboutPadding; const auto bubbleWidth = std::min( maxBubbleWidth, std::max({ maxPhraseWidth + st::historyGroupAboutBulletSkip, _header.maxWidth(), _text.maxWidth() }) + padding.left() + padding.right()); const auto innerWidth = bubbleWidth - padding.left() - padding.right(); const auto textHeight = [&](const Ui::Text::String &text) { return std::min( text.countHeight(innerWidth), kMaxTextLines * font->height); }; const auto iconHeight = _icon ? st::infoTopicCover.photo.size.height() : 0; const auto bubbleHeight = padding.top() + (_icon ? (iconHeight + st::historyGroupAboutHeaderSkip) : 0) + textHeight(_header) + st::historyGroupAboutHeaderSkip + textHeight(_text) + st::historyGroupAboutTextSkip + ranges::accumulate(_phrases, 0, ranges::plus(), textHeight) + st::historyGroupAboutSkip * std::max(int(_phrases.size()) - 1, 0) + padding.bottom(); const auto bubbleLeft = (width - bubbleWidth) / 2; const auto bubbleTop = (height - bubbleHeight) / 2; ServiceMessagePainter::PaintBubble( p, st->msgServiceBg(), st->serviceBgCornersNormal(), QRect(bubbleLeft, bubbleTop, bubbleWidth, bubbleHeight)); p.setPen(st->msgServiceFg()); p.setBrush(st->msgServiceFg()); const auto left = bubbleLeft + padding.left(); auto top = bubbleTop + padding.top(); if (_icon) { _icon->paintInRect( p, QRect(bubbleLeft, top, bubbleWidth, iconHeight)); top += iconHeight + st::historyGroupAboutHeaderSkip; } _header.drawElided( p, left, top, innerWidth, kMaxTextLines, style::al_top); top += textHeight(_header) + st::historyGroupAboutHeaderSkip; _text.drawElided( p, left, top, innerWidth, kMaxTextLines, _topic ? style::al_top : style::al_topleft); top += textHeight(_text) + st::historyGroupAboutTextSkip; for (const auto &text : _phrases) { p.setPen(st->msgServiceFg()); text.drawElided( p, left + st::historyGroupAboutBulletSkip, top, innerWidth, kMaxTextLines); PainterHighQualityEnabler hq(p); p.setPen(Qt::NoPen); p.drawEllipse( left, top + (font->height - st::mediaUnreadSize) / 2, st::mediaUnreadSize, st::mediaUnreadSize); top += textHeight(text) + st::historyGroupAboutSkip; } } } // namespace HistoryView