| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- /*
- 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 "dialogs/ui/dialogs_message_view.h"
- #include "history/history.h"
- #include "history/history_item.h"
- #include "history/view/history_view_item_preview.h"
- #include "main/main_session.h"
- #include "dialogs/dialogs_three_state_icon.h"
- #include "dialogs/ui/dialogs_layout.h"
- #include "dialogs/ui/dialogs_topics_view.h"
- #include "ui/effects/spoiler_mess.h"
- #include "ui/text/text_options.h"
- #include "ui/text/text_utilities.h"
- #include "ui/painter.h"
- #include "ui/power_saving.h"
- #include "core/ui_integration.h"
- #include "lang/lang_keys.h"
- #include "lang/lang_text_entity.h"
- #include "styles/style_dialogs.h"
- namespace {
- constexpr auto kEmojiLoopCount = 2;
- template <ushort kTag>
- struct TextWithTagOffset {
- TextWithTagOffset(TextWithEntities text) : text(std::move(text)) {
- }
- TextWithTagOffset(QString text) : text({ std::move(text) }) {
- }
- static TextWithTagOffset FromString(const QString &text) {
- return { { text } };
- }
- TextWithEntities text;
- int offset = -1;
- };
- } // namespace
- namespace Lang {
- template <ushort kTag>
- struct ReplaceTag<TextWithTagOffset<kTag>> {
- static TextWithTagOffset<kTag> Call(
- TextWithTagOffset<kTag> &&original,
- ushort tag,
- const TextWithTagOffset<kTag> &replacement);
- };
- template <ushort kTag>
- TextWithTagOffset<kTag> ReplaceTag<TextWithTagOffset<kTag>>::Call(
- TextWithTagOffset<kTag> &&original,
- ushort tag,
- const TextWithTagOffset<kTag> &replacement) {
- const auto replacementPosition = FindTagReplacementPosition(
- original.text.text,
- tag);
- if (replacementPosition < 0) {
- return std::move(original);
- }
- original.text = ReplaceTag<TextWithEntities>::Replace(
- std::move(original.text),
- replacement.text,
- replacementPosition);
- if (tag == kTag) {
- original.offset = replacementPosition;
- } else if (original.offset > replacementPosition) {
- constexpr auto kReplaceCommandLength = 4;
- const auto replacementSize = replacement.text.text.size();
- original.offset += replacementSize - kReplaceCommandLength;
- }
- return std::move(original);
- }
- } // namespace Lang
- namespace Dialogs::Ui {
- TextWithEntities DialogsPreviewText(TextWithEntities text) {
- auto result = Ui::Text::Filtered(
- std::move(text),
- {
- EntityType::Pre,
- EntityType::Code,
- EntityType::Spoiler,
- EntityType::StrikeOut,
- EntityType::Underline,
- EntityType::Italic,
- EntityType::CustomEmoji,
- EntityType::Colorized,
- });
- for (auto &entity : result.entities) {
- if (entity.type() == EntityType::Pre) {
- entity = EntityInText(
- EntityType::Code,
- entity.offset(),
- entity.length());
- } else if (entity.type() == EntityType::Colorized
- && !entity.data().isEmpty()) {
- // Drop 'data' so that only link-color colorization takes place.
- entity = EntityInText(
- EntityType::Colorized,
- entity.offset(),
- entity.length());
- }
- }
- return result;
- }
- struct MessageView::LoadingContext {
- std::any context;
- rpl::lifetime lifetime;
- };
- MessageView::MessageView()
- : _senderCache(st::dialogsTextWidthMin)
- , _textCache(st::dialogsTextWidthMin) {
- }
- MessageView::~MessageView() = default;
- void MessageView::itemInvalidated(not_null<const HistoryItem*> item) {
- if (_textCachedFor == item.get()) {
- _textCachedFor = nullptr;
- }
- }
- bool MessageView::dependsOn(not_null<const HistoryItem*> item) const {
- return (_textCachedFor == item.get());
- }
- bool MessageView::prepared(
- not_null<const HistoryItem*> item,
- Data::Forum *forum) const {
- return (_textCachedFor == item.get())
- && (!forum
- || (_topics
- && _topics->forum() == forum
- && _topics->prepared()));
- }
- void MessageView::prepare(
- not_null<const HistoryItem*> item,
- Data::Forum *forum,
- Fn<void()> customEmojiRepaint,
- ToPreviewOptions options) {
- if (!forum) {
- _topics = nullptr;
- } else if (!_topics || _topics->forum() != forum) {
- _topics = std::make_unique<TopicsView>(forum);
- _topics->prepare(item->topicRootId(), customEmojiRepaint);
- } else if (!_topics->prepared()) {
- _topics->prepare(item->topicRootId(), customEmojiRepaint);
- }
- if (_textCachedFor == item.get()) {
- return;
- }
- options.existing = &_imagesCache;
- options.ignoreTopic = true;
- options.spoilerLoginCode = true;
- auto preview = item->toPreview(options);
- _leftIcon = (preview.icon == ItemPreview::Icon::ForwardedMessage)
- ? &st::dialogsMiniForward
- : (preview.icon == ItemPreview::Icon::ReplyToStory)
- ? &st::dialogsMiniReplyStory
- : nullptr;
- const auto hasImages = !preview.images.empty();
- const auto history = item->history();
- const auto context = Core::TextContext({
- .session = &history->session(),
- .repaint = customEmojiRepaint,
- .customEmojiLoopLimit = kEmojiLoopCount,
- });
- const auto senderTill = (preview.arrowInTextPosition > 0)
- ? preview.arrowInTextPosition
- : preview.imagesInTextPosition;
- if ((hasImages || _leftIcon) && senderTill > 0) {
- auto sender = Text::Mid(preview.text, 0, senderTill);
- TextUtilities::Trim(sender);
- _senderCache.setMarkedText(
- st::dialogsTextStyle,
- std::move(sender),
- DialogTextOptions());
- preview.text = Text::Mid(preview.text, senderTill);
- } else {
- _senderCache = { st::dialogsTextWidthMin };
- }
- TextUtilities::Trim(preview.text);
- auto textToCache = DialogsPreviewText(std::move(preview.text));
- _hasPlainLinkAtBegin = !textToCache.entities.empty()
- && (textToCache.entities.front().type() == EntityType::Colorized);
- _textCache.setMarkedText(
- st::dialogsTextStyle,
- std::move(textToCache),
- DialogTextOptions(),
- context);
- _textCachedFor = item;
- _imagesCache = std::move(preview.images);
- if (!ranges::any_of(_imagesCache, &ItemPreviewImage::hasSpoiler)) {
- _spoiler = nullptr;
- } else if (!_spoiler) {
- _spoiler = std::make_unique<SpoilerAnimation>(customEmojiRepaint);
- }
- if (preview.loadingContext.has_value()) {
- if (!_loadingContext) {
- _loadingContext = std::make_unique<LoadingContext>();
- item->history()->session().downloaderTaskFinished(
- ) | rpl::start_with_next([=] {
- _textCachedFor = nullptr;
- }, _loadingContext->lifetime);
- }
- _loadingContext->context = std::move(preview.loadingContext);
- } else {
- _loadingContext = nullptr;
- }
- }
- bool MessageView::isInTopicJump(int x, int y) const {
- return _topics && _topics->isInTopicJumpArea(x, y);
- }
- void MessageView::addTopicJumpRipple(
- QPoint origin,
- not_null<TopicJumpCache*> topicJumpCache,
- Fn<void()> updateCallback) {
- if (_topics) {
- _topics->addTopicJumpRipple(
- origin,
- topicJumpCache,
- std::move(updateCallback));
- }
- }
- void MessageView::stopLastRipple() {
- if (_topics) {
- _topics->stopLastRipple();
- }
- }
- void MessageView::clearRipple() {
- if (_topics) {
- _topics->clearRipple();
- }
- }
- int MessageView::countWidth() const {
- auto result = 0;
- if (!_senderCache.isEmpty()) {
- result += _senderCache.maxWidth();
- if (!_imagesCache.empty()) {
- result += st::dialogsMiniPreviewSkip
- + st::dialogsMiniPreviewRight;
- }
- }
- if (!_imagesCache.empty()) {
- result += (_imagesCache.size()
- * (st::dialogsMiniPreview + st::dialogsMiniPreviewSkip))
- + st::dialogsMiniPreviewRight;
- }
- return result + _textCache.maxWidth();
- }
- void MessageView::paint(
- Painter &p,
- const QRect &geometry,
- const PaintContext &context) const {
- if (geometry.isEmpty()) {
- return;
- }
- p.setFont(st::dialogsTextFont);
- p.setPen(context.active
- ? st::dialogsTextFgActive
- : context.selected
- ? st::dialogsTextFgOver
- : st::dialogsTextFg);
- const auto withTopic = _topics && context.st->topicsHeight;
- const auto palette = &(withTopic
- ? (context.active
- ? st::dialogsTextPaletteInTopicActive
- : context.selected
- ? st::dialogsTextPaletteInTopicOver
- : st::dialogsTextPaletteInTopic)
- : (context.active
- ? st::dialogsTextPaletteActive
- : context.selected
- ? st::dialogsTextPaletteOver
- : st::dialogsTextPalette));
- auto rect = geometry;
- const auto checkJump = withTopic && !context.active;
- const auto jump1 = checkJump ? _topics->jumpToTopicWidth() : 0;
- if (jump1) {
- paintJumpToLast(p, rect, context, jump1);
- } else if (_topics) {
- _topics->clearTopicJumpGeometry();
- }
- if (withTopic) {
- _topics->paint(p, rect, context);
- rect.setTop(rect.top() + context.st->topicsHeight);
- }
- auto finalRight = rect.x() + rect.width();
- if (jump1) {
- rect.setWidth(rect.width() - st::forumDialogJumpArrowSkip);
- finalRight -= st::forumDialogJumpArrowSkip;
- }
- const auto pausedSpoiler = context.paused
- || On(PowerSaving::kChatSpoiler);
- if (!_senderCache.isEmpty()) {
- _senderCache.draw(p, {
- .position = rect.topLeft(),
- .availableWidth = rect.width(),
- .palette = palette,
- .elisionHeight = rect.height(),
- });
- rect.setLeft(rect.x() + _senderCache.maxWidth());
- if (!_imagesCache.empty() && !_leftIcon) {
- const auto skip = st::dialogsMiniPreviewSkip
- + st::dialogsMiniPreviewRight;
- rect.setLeft(rect.x() + skip);
- }
- }
- if (_leftIcon) {
- const auto &icon = ThreeStateIcon(
- _leftIcon->icon,
- context.active,
- context.selected);
- const auto w = (icon.width());
- if (rect.width() > w) {
- if (_hasPlainLinkAtBegin && !context.active) {
- icon.paint(
- p,
- rect.topLeft(),
- rect.width(),
- palette->linkFg->c);
- } else {
- icon.paint(p, rect.topLeft(), rect.width());
- }
- rect.setLeft(rect.x()
- + w
- + (_imagesCache.empty()
- ? _leftIcon->skipText
- : _leftIcon->skipMedia));
- }
- }
- for (const auto &image : _imagesCache) {
- const auto w = st::dialogsMiniPreview + st::dialogsMiniPreviewSkip;
- if (rect.width() < w) {
- break;
- }
- const auto mini = QRect(
- rect.x(),
- rect.y() + st::dialogsMiniPreviewTop,
- st::dialogsMiniPreview,
- st::dialogsMiniPreview);
- if (!image.data.isNull()) {
- p.drawImage(mini, image.data);
- if (image.hasSpoiler()) {
- const auto frame = DefaultImageSpoiler().frame(
- _spoiler->index(context.now, pausedSpoiler));
- if (image.isEllipse()) {
- const auto radius = st::dialogsMiniPreview / 2;
- static auto mask = Images::CornersMask(radius);
- FillSpoilerRect(
- p,
- mini,
- Images::CornersMaskRef(mask),
- frame,
- _cornersCache);
- } else {
- FillSpoilerRect(p, mini, frame);
- }
- }
- }
- rect.setLeft(rect.x() + w);
- }
- if (!_imagesCache.empty()) {
- rect.setLeft(rect.x() + st::dialogsMiniPreviewRight);
- }
- // Style of _textCache.
- static const auto ellipsisWidth = st::dialogsTextStyle.font->width(
- kQEllipsis);
- if (rect.width() > ellipsisWidth) {
- _textCache.draw(p, {
- .position = rect.topLeft(),
- .availableWidth = rect.width(),
- .palette = palette,
- .spoiler = Text::DefaultSpoilerCache(),
- .now = context.now,
- .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
- .pausedSpoiler = pausedSpoiler,
- .elisionHeight = rect.height(),
- });
- rect.setLeft(rect.x() + _textCache.maxWidth());
- }
- if (jump1) {
- const auto position = st::forumDialogJumpArrowPosition
- + QPoint((rect.width() > 0) ? rect.x() : finalRight, rect.y());
- (context.selected
- ? st::forumDialogJumpArrowOver
- : st::forumDialogJumpArrow).paint(p, position, context.width);
- }
- }
- void MessageView::paintJumpToLast(
- Painter &p,
- const QRect &rect,
- const PaintContext &context,
- int width1) const {
- if (!context.topicJumpCache) {
- _topics->clearTopicJumpGeometry();
- return;
- }
- const auto width2 = countWidth() + st::forumDialogJumpArrowSkip;
- const auto geometry = FillJumpToLastBg(p, {
- .st = context.st,
- .corners = (context.selected
- ? &context.topicJumpCache->over
- : &context.topicJumpCache->corners),
- .geometry = rect,
- .bg = (context.selected
- ? st::dialogsRippleBg
- : st::dialogsBgOver),
- .width1 = width1,
- .width2 = width2,
- });
- if (context.topicJumpSelected) {
- p.setOpacity(0.1);
- FillJumpToLastPrepared(p, {
- .st = context.st,
- .corners = &context.topicJumpCache->selected,
- .bg = st::dialogsTextFg,
- .prepared = geometry,
- });
- p.setOpacity(1.);
- }
- if (!_topics->changeTopicJumpGeometry(geometry)) {
- auto color = st::dialogsTextFg->c;
- color.setAlpha(color.alpha() / 10);
- if (color.alpha() > 0) {
- _topics->paintRipple(p, 0, 0, context.width, &color);
- }
- }
- }
- HistoryView::ItemPreview PreviewWithSender(
- HistoryView::ItemPreview &&preview,
- const QString &sender,
- TextWithEntities topic) {
- const auto wrappedSender = st::wrap_rtl(sender);
- auto senderWithOffset = topic.empty()
- ? TextWithTagOffset<lt_from>::FromString(wrappedSender)
- : tr::lng_dialogs_text_from_in_topic(
- tr::now,
- lt_from,
- { wrappedSender },
- lt_topic,
- std::move(topic),
- TextWithTagOffset<lt_from>::FromString);
- auto wrappedWithOffset = tr::lng_dialogs_text_from_wrapped(
- tr::now,
- lt_from,
- std::move(senderWithOffset.text),
- TextWithTagOffset<lt_from>::FromString);
- const auto wrappedSize = wrappedWithOffset.text.text.size();
- auto fullWithOffset = tr::lng_dialogs_text_with_from(
- tr::now,
- lt_from_part,
- Ui::Text::Colorized(std::move(wrappedWithOffset.text)),
- lt_message,
- std::move(preview.text),
- TextWithTagOffset<lt_from_part>::FromString);
- preview.text = std::move(fullWithOffset.text);
- preview.arrowInTextPosition = (fullWithOffset.offset < 0
- || wrappedWithOffset.offset < 0
- || senderWithOffset.offset < 0)
- ? -1
- : (fullWithOffset.offset
- + wrappedWithOffset.offset
- + senderWithOffset.offset
- + sender.size());
- preview.imagesInTextPosition = (fullWithOffset.offset < 0)
- ? 0
- : (fullWithOffset.offset + wrappedSize);
- return std::move(preview);
- }
- } // namespace Dialogs::Ui
|