486 lines
13 KiB
C++
486 lines
13 KiB
C++
/*
|
|
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::MarkedTextContext{
|
|
.session = &history->session(),
|
|
.customEmojiRepaint = 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));
|
|
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) {
|
|
auto senderWithOffset = topic.empty()
|
|
? TextWithTagOffset<lt_from>::FromString(sender)
|
|
: tr::lng_dialogs_text_from_in_topic(
|
|
tr::now,
|
|
lt_from,
|
|
{ sender },
|
|
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
|