init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
/*
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/media/history_view_birthday_suggestion.h"
#include "boxes/star_gift_box.h"
#include "chat_helpers/stickers_lottie.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_birthday.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "data/data_star_gift.h"
#include "history/view/media/history_view_media_generic.h"
#include "history/view/media/history_view_premium_gift.h"
#include "history/view/media/history_view_unique_gift.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_element.h"
#include "history/history.h"
#include "history/history_item.h"
#include "info/peer_gifts/info_peer_gifts_common.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_credits_graphics.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/rect.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_credits.h"
namespace HistoryView {
[[nodiscard]] auto GenerateSuggetsBirthdayMedia(
not_null<Element*> parent,
Element *replacing,
Data::Birthday birthday)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
return [=](
not_null<MediaGeneric*> media,
Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto session = &media->parent()->history()->session();
const auto document = ChatHelpers::GenerateLocalTgsSticker(
session,
u"cake"_q);
const auto sticker = [=] {
using Tag = ChatHelpers::StickerLottieSize;
return StickerInBubblePart::Data{
.sticker = document,
.size = st::birthdaySuggestStickerSize,
.cacheTag = Tag::ChatIntroHelloSticker,
.stopOnLastFrame = true,
};
};
push(std::make_unique<StickerInBubblePart>(
parent,
replacing,
sticker,
st::birthdaySuggestStickerPadding));
const auto from = media->parent()->data()->from();
const auto isSelf = (from->id == from->session().userPeerId());
const auto peer = isSelf ? media->parent()->history()->peer : from;
push(std::make_unique<MediaGenericTextPart>(
(isSelf
? tr::lng_action_suggested_birthday_me
: tr::lng_action_suggested_birthday)(
tr::now,
lt_user,
TextWithEntities{ peer->shortName() },
tr::marked),
st::birthdaySuggestTextPadding));
push(std::make_unique<BirthdayTable>(
birthday,
(isSelf
? st::birthdaySuggestTableLastPadding
: st::birthdaySuggestTablePadding)));
if (!isSelf) {
auto link = std::make_shared<LambdaClickHandler>([=](
ClickContext context) {
Core::App().openInternalUrl(
(u"internal:edit_birthday:suggestion_"_q
+ QString::number(birthday.serialize())),
context.other);
});
push(MakeGenericButtonPart(
tr::lng_sticker_premium_view(tr::now),
st::chatUniqueButtonPadding,
[=] { parent->repaint(); },
std::move(link)));
}
};
}
BirthdayTable::BirthdayTable(Data::Birthday birthday, QMargins margins)
: _margins(margins) {
const auto push = [&](QString label, QString value) {
_parts.push_back({
.label = Ui::Text::String(st::defaultTextStyle, label),
.value = Ui::Text::String(
st::defaultTextStyle,
tr::bold(value)),
});
};
push(tr::lng_date_input_day(tr::now), QString::number(birthday.day()));
push(
tr::lng_date_input_month(tr::now),
Lang::Month(birthday.month())(tr::now));
if (const auto year = birthday.year()) {
push(tr::lng_date_input_year(tr::now), QString::number(year));
}
}
void BirthdayTable::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
const auto top = _margins.top();
const auto palette = &context.st->serviceTextPalette();
const auto paint = [&](
const Ui::Text::String &text,
int left,
int yskip = 0) {
text.draw(p, {
.position = { left, top + yskip},
.outerWidth = outerWidth,
.availableWidth = text.maxWidth(),
.palette = palette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.elisionLines = 1,
});
};
p.setPen(context.st->msgServiceFg()->c);
for (const auto &part : _parts) {
p.setOpacity(0.7);
paint(part.label, part.labelLeft);
p.setOpacity(1.);
paint(
part.value,
part.valueLeft,
st::normalFont->height + st::birthdaySuggestTableSkip);
}
}
TextState BirthdayTable::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
return {};
}
QSize BirthdayTable::countOptimalSize() {
auto width = 0;
for (const auto &part : _parts) {
width += std::max(part.label.maxWidth(), part.value.maxWidth());
}
width += st::normalFont->spacew * (_parts.size() - 1);
const auto height = st::normalFont->height * 2
+ st::birthdaySuggestTableSkip;
return {
_margins.left() + width + _margins.right(),
_margins.top() + height + _margins.bottom(),
};
}
QSize BirthdayTable::countCurrentSize(int newWidth) {
auto available = newWidth - _margins.left() - _margins.right();
for (const auto &part : _parts) {
available -= std::max(part.label.maxWidth(), part.value.maxWidth());
}
const auto skip = available / int(_parts.size() + 1);
auto left = _margins.left() + skip;
for (auto &part : _parts) {
auto full = std::max(part.label.maxWidth(), part.value.maxWidth());
part.labelLeft = left + (full - part.label.maxWidth()) / 2;
part.valueLeft = left + (full - part.value.maxWidth()) / 2;
left += full + skip;
}
return { newWidth, minHeight() };
}
} // namespace HistoryView

View File

@@ -0,0 +1,70 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media_generic.h"
class Painter;
namespace Data {
class MediaGiftBox;
struct UniqueGift;
class Birthday;
} // namespace Data
namespace Ui {
struct ChatPaintContext;
} // namespace Ui
namespace HistoryView {
class Element;
class MediaGeneric;
class MediaGenericPart;
[[nodiscard]] auto GenerateSuggetsBirthdayMedia(
not_null<Element*> parent,
Element *replacing,
Data::Birthday birthday)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
class BirthdayTable final : public MediaGenericPart {
public:
BirthdayTable(Data::Birthday birthday, QMargins margins);
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
TextState textState(
QPoint point,
StateRequest request,
int outerWidth) const override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
private:
struct Part {
Ui::Text::String label;
Ui::Text::String value;
int labelLeft = 0;
int valueLeft = 0;
};
std::vector<Part> _parts;
QMargins _margins;
Fn<QColor(const PaintContext &)> _labelColor;
Fn<QColor(const PaintContext &)> _valueColor;
};
} // namespace HistoryView

View File

@@ -0,0 +1,138 @@
/*
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/media/history_view_call.h"
#include "lang/lang_keys.h"
#include "ui/chat/chat_style.h"
#include "ui/text/format_values.h"
#include "ui/painter.h"
#include "layout/layout_selection.h" // FullSelection
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "calls/calls_instance.h"
#include "data/data_media_types.h"
#include "data/data_user.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
using State = Data::CallState;
[[nodiscard]] int ComputeDuration(State state, int duration) {
return (state != State::Missed && state != State::Busy)
? duration
: 0;
}
} // namespace
Call::Call(
not_null<Element*> parent,
not_null<Data::Call*> call)
: Media(parent)
, _duration(ComputeDuration(call->state, call->duration))
, _state(call->state)
, _conference(call->conferenceId != 0)
, _video(call->video) {
const auto item = parent->data();
_text = Data::MediaCall::Text(item, _state, _conference, _video);
_status = QLocale().toString(
parent->dateTime().time(),
QLocale::ShortFormat);
if (_duration) {
_status = tr::lng_call_duration_info(
tr::now,
lt_time,
_status,
lt_duration,
Ui::FormatDurationWords(_duration));
}
}
QSize Call::countOptimalSize() {
const auto user = _parent->history()->peer->asUser();
const auto conference = _conference;
const auto video = _video;
const auto contextId = _parent->data()->fullId();
const auto id = _parent->data()->id;
_link = std::make_shared<LambdaClickHandler>([=](ClickContext context) {
if (conference) {
const auto my = context.other.value<ClickHandlerContext>();
const auto weak = my.sessionWindow;
if (const auto strong = weak.get()) {
QSize();
strong->resolveConferenceCall(id, contextId);
}
} else if (user) {
Core::App().calls().startOutgoingCall(user, video);
}
});
auto maxWidth = st::historyCallWidth;
auto minHeight = st::historyCallHeight;
if (!isBubbleTop()) {
minHeight -= st::msgFileTopMinus;
}
return { maxWidth, minHeight };
}
void Call::draw(Painter &p, const PaintContext &context) const {
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;
auto paintw = width();
const auto stm = context.messageStyle();
accumulate_min(paintw, maxWidth());
auto nameleft = 0, nametop = 0, statustop = 0;
auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;
nameleft = st::historyCallLeft;
nametop = st::historyCallTop - topMinus;
statustop = st::historyCallStatusTop - topMinus;
p.setFont(st::semiboldFont);
p.setPen(stm->historyFileNameFg);
p.drawTextLeft(nameleft, nametop, paintw, _text);
auto statusleft = nameleft;
auto missed = (_state == State::Missed) || (_state == State::Busy);
const auto &arrow = missed
? stm->historyCallArrowMissed
: stm->historyCallArrow;
arrow.paint(p, statusleft + st::historyCallArrowPosition.x(), statustop + st::historyCallArrowPosition.y(), paintw);
statusleft += arrow.width() + st::historyCallStatusSkip;
p.setFont(st::normalFont);
p.setPen(stm->mediaFg);
p.drawTextLeft(statusleft, statustop, paintw, _status);
const auto &icon = _video
? stm->historyCallCameraIcon
: _conference
? stm->historyCallGroupIcon
: stm->historyCallIcon;
icon.paint(p, paintw - st::historyCallIconPosition.x() - icon.width(), st::historyCallIconPosition.y() - topMinus, paintw);
}
TextState Call::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
if (QRect(0, 0, width(), height()).contains(point)) {
result.link = _link;
return result;
}
return result;
}
} // namespace HistoryView

View File

@@ -0,0 +1,59 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
namespace Data {
enum class CallState : char;
struct Call;
} // namespace Data
namespace HistoryView {
class Call : public Media {
public:
Call(
not_null<Element*> parent,
not_null<Data::Call*> call);
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
return true;
}
bool dragItemByHandler(const ClickHandlerPtr &p) const override {
return false;
}
bool needsBubble() const override {
return true;
}
bool customInfoLayout() const override {
return true;
}
private:
using State = Data::CallState;
QSize countOptimalSize() override;
const int _duration = 0;
const State _state = {};
const bool _conference = false;
const bool _video = false;
QString _text;
QString _status;
ClickHandlerPtr _link;
};
} // namespace HistoryView

View File

@@ -0,0 +1,660 @@
/*
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/media/history_view_contact.h"
#include "boxes/add_contact_box.h"
#include "core/click_handler_types.h" // ClickHandlerContext
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/history_item_components.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_reply.h"
#include "history/view/media/history_view_media_common.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/chat/chat_style.h"
#include "ui/empty_userpic.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/rect.h"
#include "ui/text/format_values.h" // Ui::FormatPhone
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h" // Ui::Text::Wrapped.
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
namespace HistoryView {
namespace {
class ContactClickHandler final : public LambdaClickHandler {
public:
using LambdaClickHandler::LambdaClickHandler;
void setDragText(const QString &t) {
_dragText = t;
}
QString dragText() const override final {
return _dragText;
}
private:
QString _dragText;
};
ClickHandlerPtr SendMessageClickHandler(not_null<PeerData*> peer) {
const auto clickHandlerPtr = std::make_shared<ContactClickHandler>([peer](
ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
if (controller->session().uniqueId()
!= peer->session().uniqueId()) {
return;
}
controller->showPeerHistory(
peer->id,
Window::SectionShow::Way::Forward);
}
});
if (const auto user = peer->asUser()) {
clickHandlerPtr->setDragText(user->phone().isEmpty()
? peer->name()
: Ui::FormatPhone(user->phone()));
}
return clickHandlerPtr;
}
ClickHandlerPtr AddContactClickHandler(not_null<HistoryItem*> item) {
const auto session = &item->history()->session();
const auto sharedContact = [=, fullId = item->fullId()] {
if (const auto item = session->data().message(fullId)) {
if (const auto media = item->media()) {
return media->sharedContact();
}
}
return (const Data::SharedContact *)nullptr;
};
const auto clickHandlerPtr = std::make_shared<ContactClickHandler>([=](
ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
if (controller->session().uniqueId() != session->uniqueId()) {
return;
}
if (const auto contact = sharedContact()) {
controller->show(Box<AddContactBox>(
session,
contact->firstName,
contact->lastName,
contact->phoneNumber));
}
}
});
if (const auto contact = sharedContact()) {
clickHandlerPtr->setDragText(Ui::FormatPhone(contact->phoneNumber));
}
return clickHandlerPtr;
}
[[nodiscard]] Fn<void(not_null<Ui::GenericBox*>)> VcardBoxFactory(
const Data::SharedContact::VcardItems &vcardItems) {
if (vcardItems.empty()) {
return nullptr;
}
return [=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_contact_details_title());
const auto &stL = st::proxyApplyBoxLabel;
const auto &stSubL = st::boxDividerLabel;
const auto add = [&](const QString &s, tr::phrase<> phrase) {
if (!s.isEmpty()) {
const auto label = box->addRow(
object_ptr<Ui::FlatLabel>(box, s, stL));
box->addRow(object_ptr<Ui::FlatLabel>(box, phrase(), stSubL));
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
return label;
}
return (Ui::FlatLabel*)(nullptr);
};
for (const auto &[type, value] : vcardItems) {
using Type = Data::SharedContact::VcardItemType;
const auto isPhoneType = (type == Type::Phone)
|| (type == Type::PhoneMain)
|| (type == Type::PhoneHome)
|| (type == Type::PhoneMobile)
|| (type == Type::PhoneWork)
|| (type == Type::PhoneOther);
const auto typePhrase = (type == Type::Phone)
? tr::lng_contact_details_phone
: (type == Type::PhoneMain)
? tr::lng_contact_details_phone_main
: (type == Type::PhoneHome)
? tr::lng_contact_details_phone_home
: (type == Type::PhoneMobile)
? tr::lng_contact_details_phone_mobile
: (type == Type::PhoneWork)
? tr::lng_contact_details_phone_work
: (type == Type::PhoneOther)
? tr::lng_contact_details_phone_other
: (type == Type::Email)
? tr::lng_contact_details_email
: (type == Type::Address)
? tr::lng_contact_details_address
: (type == Type::Url)
? tr::lng_contact_details_url
: (type == Type::Note)
? tr::lng_contact_details_note
: (type == Type::Birthday)
? tr::lng_contact_details_birthday
: (type == Type::Organization)
? tr::lng_contact_details_organization
: tr::lng_payments_info_name;
if (const auto label = add(value, typePhrase)) {
const auto copyText = isPhoneType
? tr::lng_profile_copy_phone
: (type == Type::Email)
? tr::lng_context_copy_email
: (type == Type::Url)
? tr::lng_context_copy_link
: (type == Type::Name)
? tr::lng_profile_copy_fullname
: tr::lng_context_copy_text;
label->setContextCopyText(copyText(tr::now));
if (type == Type::Email) {
label->setMarkedText(
Ui::Text::Wrapped({ value }, EntityType::Email));
} else if (type == Type::Url) {
label->setMarkedText(
Ui::Text::Wrapped({ value }, EntityType::Url));
} else if (isPhoneType) {
label->setText(Ui::FormatPhone(value));
}
using Request = Ui::FlatLabel::ContextMenuRequest;
label->setContextMenuHook([=](Request r) {
label->fillContextMenu(r.link
? r
: Request{ .menu = r.menu, .fullSelection = true });
});
}
}
{
const auto inner = box->verticalLayout();
if (inner->count() > 2) {
delete inner->widgetAt(inner->count() - 1);
delete inner->widgetAt(inner->count() - 1);
}
}
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
};
}
} // namespace
Contact::Contact(
not_null<Element*> parent,
const Data::SharedContact &data)
: Media(parent)
, _st(st::historyPagePreview)
, _pixh(st::contactsPhotoSize)
, _userId(data.userId)
, _vcardBoxFactory(VcardBoxFactory(data.vcardItems)) {
history()->owner().registerContactView(data.userId, parent);
_nameLine.setText(
st::webPageTitleStyle,
tr::lng_full_name(
tr::now,
lt_first_name,
data.firstName,
lt_last_name,
data.lastName).trimmed(),
Ui::WebpageTextTitleOptions());
_phoneLine.setText(
st::webPageDescriptionStyle,
Ui::FormatPhone(data.phoneNumber),
Ui::WebpageTextTitleOptions());
}
Contact::~Contact() {
history()->owner().unregisterContactView(_userId, _parent);
if (!_userpic.null()) {
_userpic = {};
_parent->checkHeavyPart();
}
}
void Contact::updateSharedContactUserId(UserId userId) {
if (_userId != userId) {
history()->owner().unregisterContactView(_userId, _parent);
_userId = userId;
history()->owner().registerContactView(_userId, _parent);
}
}
QSize Contact::countOptimalSize() {
_contact = _userId
? _parent->data()->history()->owner().userLoaded(_userId)
: nullptr;
if (_contact) {
_contact->loadUserpic();
} else {
const auto full = _nameLine.toString();
_photoEmpty = std::make_unique<Ui::EmptyUserpic>(
Ui::EmptyUserpic::UserpicColor(Data::DecideColorIndex(_userId
? peerFromUser(_userId)
: Data::FakePeerIdForJustName(full))),
full);
}
const auto vcardBoxFactory = _vcardBoxFactory;
_buttons.clear();
if (_contact) {
const auto message = tr::lng_contact_send_message(tr::now).toUpper();
_buttons.push_back({
message,
st::semiboldFont->width(message),
SendMessageClickHandler(_contact),
});
if (!_contact->isContact()) {
const auto add = tr::lng_contact_add(tr::now).toUpper();
_buttons.push_back({
add,
st::semiboldFont->width(add),
AddContactClickHandler(_parent->data()),
});
}
_mainButton.link = _buttons.front().link;
} else if (vcardBoxFactory) {
const auto view = tr::lng_contact_details_button(tr::now).toUpper();
_buttons.push_back({
view,
st::semiboldFont->width(view),
AddContactClickHandler(_parent->data()),
});
}
if (vcardBoxFactory) {
_mainButton.link = std::make_shared<LambdaClickHandler>([=](
const ClickContext &context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
controller->uiShow()->show(Box(vcardBoxFactory));
}
});
}
const auto padding = inBubblePadding() + innerMargin();
const auto full = Rect(currentSize());
const auto outer = full - inBubblePadding();
const auto inner = outer - innerMargin();
const auto lineLeft = inner.left() + _pixh + inner.left() - outer.left();
const auto lineHeight = UnitedLineHeight();
auto maxWidth = _parent->skipBlockWidth();
auto minHeight = 0;
auto textMinHeight = 0;
if (!_nameLine.isEmpty()) {
accumulate_max(maxWidth, lineLeft + _nameLine.maxWidth());
textMinHeight += 1 * lineHeight;
}
if (!_phoneLine.isEmpty()) {
accumulate_max(maxWidth, lineLeft + _phoneLine.maxWidth());
textMinHeight += 1 * lineHeight;
}
minHeight = std::max(textMinHeight, st::contactsPhotoSize);
if (!_buttons.empty()) {
auto buttonsWidth = rect::m::sum::h(st::historyPageButtonPadding);
for (const auto &button : _buttons) {
buttonsWidth += button.width;
}
accumulate_max(maxWidth, buttonsWidth);
}
maxWidth += rect::m::sum::h(padding);
minHeight += rect::m::sum::v(padding);
return { maxWidth, minHeight };
}
void Contact::draw(Painter &p, const PaintContext &context) const {
if (width() < rect::m::sum::h(st::msgPadding) + 1) {
return;
}
const auto st = context.st;
const auto stm = context.messageStyle();
const auto full = Rect(currentSize());
const auto outer = full - inBubblePadding();
const auto inner = outer - innerMargin();
auto tshift = inner.top();
const auto selected = context.selected();
const auto view = parent();
const auto colorIndex = _contact
? _contact->colorIndex()
: Data::DecideColorIndex(
Data::FakePeerIdForJustName(_nameLine.toString()));
const auto &colorCollectible = _contact
? _contact->colorCollectible()
: nullptr;
const auto colorPattern = colorCollectible
? st->collectiblePatternIndex(colorCollectible)
: st->colorPatternIndex(colorIndex);
const auto useColorCollectible = colorCollectible && !context.outbg;
const auto useColorIndex = !context.outbg;
const auto cache = useColorCollectible
? st->collectibleReplyCache(selected, colorCollectible).get()
: useColorIndex
? st->coloredReplyCache(selected, colorIndex).get()
: stm->replyCache[colorPattern].get();
const auto backgroundEmojiId = _contact
? _contact->backgroundEmojiId()
: DocumentId();
const auto backgroundEmojiData = backgroundEmojiId
? st->backgroundEmojiData(backgroundEmojiId, colorCollectible).get()
: nullptr;
const auto backgroundEmojiCache = !backgroundEmojiData
? nullptr
: useColorCollectible
? &backgroundEmojiData->collectibleCaches[colorCollectible]
: &backgroundEmojiData->caches[Ui::BackgroundEmojiData::CacheIndex(
selected,
context.outbg,
true,
useColorIndex ? (colorIndex + 1) : 0)];
Ui::Text::ValidateQuotePaintCache(*cache, _st);
Ui::Text::FillQuotePaint(p, outer, *cache, _st);
if (backgroundEmojiData) {
ValidateBackgroundEmoji(
backgroundEmojiId,
colorCollectible,
backgroundEmojiData,
backgroundEmojiCache,
cache,
view);
if (!backgroundEmojiCache->frames[0].isNull()) {
const auto end = rect::bottom(inner) + _st.padding.bottom();
const auto r = outer
- QMargins(0, 0, 0, rect::bottom(outer) - end);
FillBackgroundEmoji(
p,
r,
false,
*backgroundEmojiCache,
backgroundEmojiData->firstGiftFrame);
}
}
if (_mainButton.ripple) {
_mainButton.ripple->paint(
p,
outer.x(),
outer.y(),
width(),
&cache->bg);
if (_mainButton.ripple->empty()) {
_mainButton.ripple = nullptr;
}
}
{
const auto left = inner.left();
const auto top = tshift;
if (_userId) {
if (_contact) {
const auto was = !_userpic.null();
_contact->paintUserpic(p, _userpic, left, top, _pixh);
if (!was && !_userpic.null()) {
history()->owner().registerHeavyViewPart(_parent);
}
} else {
_photoEmpty->paintCircle(p, left, top, _pixh, _pixh);
}
} else {
_photoEmpty->paintCircle(p, left, top, _pixh, _pixh);
}
if (context.selected()) {
auto hq = PainterHighQualityEnabler(p);
p.setBrush(p.textPalette().selectOverlay);
p.setPen(Qt::NoPen);
p.drawEllipse(left, top, _pixh, _pixh);
}
}
const auto lineHeight = UnitedLineHeight();
const auto lineLeft = inner.left() + _pixh + inner.left() - outer.left();
const auto lineWidth = rect::right(inner) - lineLeft;
{
p.setPen(cache->icon);
p.setTextPalette(useColorCollectible
? st->collectibleTextPalette(selected, colorCollectible)
: useColorIndex
? st->coloredTextPalette(selected, colorIndex)
: stm->semiboldPalette);
const auto endskip = _nameLine.hasSkipBlock()
? _parent->skipBlockWidth()
: 0;
_nameLine.drawLeftElided(
p,
lineLeft,
tshift,
lineWidth,
width(),
1,
style::al_left,
0,
-1,
endskip,
false,
context.selection);
tshift += lineHeight;
p.setTextPalette(stm->textPalette);
}
p.setPen(stm->historyTextFg);
{
tshift += st::lineWidth * 3; // Additional skip.
const auto endskip = _phoneLine.hasSkipBlock()
? _parent->skipBlockWidth()
: 0;
_phoneLine.drawLeftElided(
p,
lineLeft,
tshift,
lineWidth,
width(),
1,
style::al_left,
0,
-1,
endskip,
false,
toTitleSelection(context.selection));
tshift += 1 * lineHeight;
}
if (!_buttons.empty()) {
p.setFont(st::semiboldFont);
p.setPen(cache->icon);
const auto end = rect::bottom(inner) + _st.padding.bottom();
const auto line = st::historyPageButtonLine;
auto color = cache->icon;
color.setAlphaF(color.alphaF() * 0.3);
const auto top = end + st::historyPageButtonPadding.top();
const auto buttonWidth = inner.width() / float64(_buttons.size());
p.fillRect(inner.x(), end, inner.width(), line, color);
for (auto i = 0; i < _buttons.size(); i++) {
const auto &button = _buttons[i];
const auto left = inner.x() + i * buttonWidth;
if (button.ripple) {
button.ripple->paint(p, left, end, buttonWidth, &cache->bg);
if (button.ripple->empty()) {
_buttons[i].ripple = nullptr;
}
}
p.drawText(
left + (buttonWidth - button.width) / 2,
top + st::semiboldFont->ascent,
button.text);
}
}
}
TextState Contact::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
const auto full = Rect(currentSize());
const auto outer = full - inBubblePadding();
const auto inner = outer - innerMargin();
_lastPoint = point;
if (!hasSingleLink()) {
const auto end = rect::bottom(inner) + _st.padding.bottom();
const auto bWidth = inner.width() / float64(_buttons.size());
const auto bHeight = rect::bottom(outer) - end;
for (auto i = 0; i < _buttons.size(); i++) {
const auto left = inner.x() + i * bWidth;
if (QRectF(left, end, bWidth, bHeight).contains(point)) {
result.link = _buttons[i].link;
return result;
}
}
}
if (outer.contains(point)) {
result.link = _mainButton.link;
return result;
}
return result;
}
void Contact::unloadHeavyPart() {
_userpic = {};
}
bool Contact::hasHeavyPart() const {
return !_userpic.null();
}
bool Contact::hasSingleLink() const {
return (_buttons.size() > 1)
? false
: (_buttons.size() == 1 && _buttons.front().link == _mainButton.link)
? true
: (_buttons.empty() && _mainButton.link);
}
void Contact::clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) {
const auto full = Rect(currentSize());
const auto outer = full - inBubblePadding();
const auto inner = outer - innerMargin();
const auto end = rect::bottom(inner) + _st.padding.bottom();
if ((_lastPoint.y() < end) || hasSingleLink()) {
if (p != _mainButton.link) {
return;
}
if (pressed) {
if (!_mainButton.ripple) {
const auto owner = &parent()->history()->owner();
_mainButton.ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
outer.size(),
_st.radius),
[=] { owner->requestViewRepaint(parent()); });
}
_mainButton.ripple->add(_lastPoint - outer.topLeft());
} else if (_mainButton.ripple) {
_mainButton.ripple->lastStop();
}
return;
} else if (_buttons.empty()) {
return;
}
const auto bWidth = inner.width() / float64(_buttons.size());
const auto bHeight = rect::bottom(outer) - end;
for (auto i = 0; i < _buttons.size(); i++) {
const auto &button = _buttons[i];
if (p != button.link) {
continue;
}
if (pressed) {
if (!button.ripple) {
const auto owner = &parent()->history()->owner();
_buttons[i].ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::MaskByDrawer(
QSize(bWidth, bHeight),
false,
[=](QPainter &p) {
p.drawRect(0, 0, bWidth, bHeight);
}),
[=] { owner->requestViewRepaint(parent()); });
}
button.ripple->add(_lastPoint
- QPoint(inner.x() + i * bWidth, end));
} else if (button.ripple) {
button.ripple->lastStop();
}
}
}
QMargins Contact::inBubblePadding() const {
return {
st::msgPadding.left(),
isBubbleTop() ? st::msgPadding.left() : 0,
st::msgPadding.right(),
isBubbleBottom() ? (st::msgPadding.left() + bottomInfoPadding()) : 0
};
}
QMargins Contact::innerMargin() const {
const auto button = _buttons.empty() ? 0 : st::historyPageButtonHeight;
return _st.padding + QMargins(0, 0, 0, button);
}
int Contact::bottomInfoPadding() const {
if (!isBubbleBottom()) {
return 0;
}
auto result = st::msgDateFont->height;
// We use padding greater than st::msgPadding.bottom() in the
// bottom of the bubble so that the left line looks pretty.
// but if we have bottom skip because of the info display
// we don't need that additional padding so we replace it
// back with st::msgPadding.bottom() instead of left().
result += st::msgPadding.bottom() - st::msgPadding.left();
return result;
}
TextSelection Contact::toTitleSelection(TextSelection selection) const {
return UnshiftItemSelection(selection, _nameLine);
}
TextSelection Contact::toDescriptionSelection(TextSelection selection) const {
return UnshiftItemSelection(toTitleSelection(selection), _phoneLine);
}
} // namespace HistoryView

View File

@@ -0,0 +1,99 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
#include "ui/userpic_view.h"
namespace Data {
struct SharedContact;
} // namespace Data
namespace Ui {
class EmptyUserpic;
class GenericBox;
class RippleAnimation;
} // namespace Ui
namespace HistoryView {
class Contact final : public Media {
public:
Contact(
not_null<Element*> parent,
const Data::SharedContact &data);
~Contact();
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const override {
return true;
}
bool dragItemByHandler(const ClickHandlerPtr &p) const override {
return true;
}
bool needsBubble() const override {
return true;
}
bool customInfoLayout() const override {
return false;
}
// Should be called only by Data::Session.
void updateSharedContactUserId(UserId userId) override;
void unloadHeavyPart() override;
bool hasHeavyPart() const override;
private:
QSize countOptimalSize() override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &p, bool pressed) override;
[[nodiscard]] QMargins inBubblePadding() const;
[[nodiscard]] QMargins innerMargin() const;
[[nodiscard]] int bottomInfoPadding() const;
[[nodiscard]] TextSelection toTitleSelection(
TextSelection selection) const;
[[nodiscard]] TextSelection toDescriptionSelection(
TextSelection selection) const;
[[nodiscard]] bool hasSingleLink() const;
const style::QuoteStyle &_st;
const int _pixh;
UserId _userId = 0;
UserData *_contact = nullptr;
Ui::Text::String _nameLine;
Ui::Text::String _phoneLine;
Fn<void(not_null<Ui::GenericBox*>)> _vcardBoxFactory;
struct Button {
QString text;
int width = 0;
ClickHandlerPtr link;
mutable std::unique_ptr<Ui::RippleAnimation> ripple;
};
std::vector<Button> _buttons;
Button _mainButton;
std::unique_ptr<Ui::EmptyUserpic> _photoEmpty;
mutable Ui::PeerUserpicView _userpic;
mutable QPoint _lastPoint;
};
} // namespace HistoryView

View File

@@ -0,0 +1,333 @@
/*
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/media/history_view_custom_emoji.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/history_view_element.h"
#include "history/history.h"
#include "history/history_item.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/stickers/data_custom_emoji.h"
#include "main/main_session.h"
#include "chat_helpers/stickers_emoji_pack.h"
#include "chat_helpers/stickers_lottie.h"
#include "ui/chat/chat_style.h"
#include "ui/text/text_isolated_emoji.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
using SizeTag = Data::CustomEmojiManager::SizeTag;
using LottieSize = ChatHelpers::StickerLottieSize;
using CustomPtr = std::unique_ptr<Ui::Text::CustomEmoji>;
using StickerPtr = std::unique_ptr<Sticker>;
struct CustomEmojiSizeInfo {
LottieSize tag = LottieSize::MessageHistory;
float64 scale = 1.;
};
[[nodiscard]] const base::flat_map<int, CustomEmojiSizeInfo> &SizesInfo() {
// size = i->second.scale * Sticker::EmojiSize().width()
// CustomEmojiManager::SizeTag caching uses first ::EmojiInteraction-s.
using Info = CustomEmojiSizeInfo;
static auto result = base::flat_map<int, Info>{
{ 1, Info{ LottieSize::EmojiInteractionReserved7, 1. } },
{ 2, Info{ LottieSize::EmojiInteractionReserved6, 0.7 } },
{ 3, Info{ LottieSize::EmojiInteractionReserved5, 0.52 } },
};
return result;
}
[[nodiscard]] SizeTag EmojiSize(int dimension) {
return (dimension == 4 || dimension == 5)
? SizeTag::Isolated
: (dimension == 6 || dimension == 7)
? SizeTag::Large
: SizeTag::Normal;
}
} //namespace
CustomEmoji::CustomEmoji(
not_null<Element*> parent,
const Ui::Text::OnlyCustomEmoji &emoji)
: _parent(parent) {
Expects(!emoji.lines.empty());
const auto owner = &parent->history()->owner();
const auto manager = &owner->customEmojiManager();
const auto max = ranges::max_element(
emoji.lines,
std::less<>(),
&std::vector<Ui::Text::OnlyCustomEmoji::Item>::size);
const auto dimension = int(std::max(emoji.lines.size(), max->size()));
const auto &sizes = SizesInfo();
const auto i = sizes.find(dimension);
const auto useCustomEmoji = (i == end(sizes));
const auto tag = EmojiSize(dimension);
_singleSize = !useCustomEmoji
? int(base::SafeRound(
i->second.scale * Sticker::EmojiSize().width()))
: (Data::FrameSizeFromTag(tag) / style::DevicePixelRatio());
if (!useCustomEmoji) {
_cachingTag = i->second.tag;
}
for (const auto &line : emoji.lines) {
_lines.emplace_back();
for (const auto &element : line) {
if (useCustomEmoji) {
_lines.back().push_back(
manager->create(
element.entityData,
[=] { parent->customEmojiRepaint(); },
tag));
} else {
const auto &data = element.entityData;
const auto id = Data::ParseCustomEmojiData(data);
const auto document = owner->document(id);
if (document->sticker()) {
_lines.back().push_back(createStickerPart(document));
} else {
_lines.back().push_back(id);
manager->resolve(id, listener());
_resolving = true;
}
}
}
}
}
void CustomEmoji::customEmojiResolveDone(not_null<DocumentData*> document) {
if (!document->sticker()) {
return;
}
_resolving = false;
const auto id = document->id;
for (auto &line : _lines) {
for (auto &entry : line) {
if (entry == id) {
entry = createStickerPart(document);
} else if (v::is<DocumentId>(entry)) {
_resolving = true;
}
}
}
}
std::unique_ptr<Sticker> CustomEmoji::createStickerPart(
not_null<DocumentData*> document) const {
const auto skipPremiumEffect = false;
auto result = std::make_unique<Sticker>(
_parent,
document,
skipPremiumEffect);
result->initSize(_singleSize);
result->setCustomCachingTag(_cachingTag);
result->setCustomEmojiPart();
return result;
}
void CustomEmoji::refreshInteractionLink() {
if (_lines.size() != 1 || _lines.front().size() != 1) {
return;
}
const auto &pack = _parent->history()->session().emojiStickersPack();
const auto version = pack.animationsVersion();
if (_animationsCheckVersion == version) {
return;
}
_animationsCheckVersion = version;
if (pack.hasAnimationsFor(_parent->data())) {
const auto weak = base::make_weak(this);
_interactionLink = std::make_shared<LambdaClickHandler>([weak] {
if (const auto that = weak.get()) {
that->interactionLinkClicked();
}
});
} else {
_interactionLink = nullptr;
}
}
ClickHandlerPtr CustomEmoji::link() {
refreshInteractionLink();
return _interactionLink;
}
void CustomEmoji::interactionLinkClicked() {
const auto &entry = _lines.front().front();
if (const auto sticker = std::get_if<StickerPtr>(&entry)) {
if ((*sticker)->ready()) {
_parent->delegate()->elementStartInteraction(_parent);
}
}
}
CustomEmoji::~CustomEmoji() {
if (_hasHeavyPart) {
unloadHeavyPart();
_parent->checkHeavyPart();
}
if (_resolving) {
const auto owner = &_parent->history()->owner();
owner->customEmojiManager().unregisterListener(listener());
}
}
QSize CustomEmoji::countOptimalSize() {
Expects(!_lines.empty());
const auto max = ranges::max_element(
_lines,
std::less<>(),
&std::vector<LargeCustomEmoji>::size);
return {
_singleSize * int(max->size()),
_singleSize * int(_lines.size()),
};
}
QSize CustomEmoji::countCurrentSize(int newWidth) {
const auto perRow = std::max(newWidth / _singleSize, 1);
auto width = 0;
auto height = 0;
for (const auto &line : _lines) {
const auto count = int(line.size());
accumulate_max(width, std::min(perRow, count) * _singleSize);
height += std::max((count + perRow - 1) / perRow, 1) * _singleSize;
}
return { width, height };
}
void CustomEmoji::draw(
Painter &p,
const PaintContext &context,
const QRect &r) {
_parent->clearCustomEmojiRepaint();
auto x = r.x();
auto y = r.y();
const auto perRow = std::max(r.width() / _singleSize, 1);
for (auto &line : _lines) {
const auto count = int(line.size());
const auto rows = std::max((count + perRow - 1) / perRow, 1);
for (auto row = 0; row != rows; ++row) {
for (auto column = 0; column != perRow; ++column) {
const auto index = row * perRow + column;
if (index >= count) {
break;
}
paintElement(p, x, y, line[index], context);
x += _singleSize;
}
x = r.x();
y += _singleSize;
}
}
}
void CustomEmoji::paintElement(
Painter &p,
int x,
int y,
LargeCustomEmoji &element,
const PaintContext &context) {
if (const auto sticker = std::get_if<StickerPtr>(&element)) {
paintSticker(p, x, y, sticker->get(), context);
} else if (const auto custom = std::get_if<CustomPtr>(&element)) {
paintCustom(p, x, y, custom->get(), context);
}
}
void CustomEmoji::paintSticker(
Painter &p,
int x,
int y,
not_null<Sticker*> sticker,
const PaintContext &context) {
sticker->draw(p, context, { QPoint(x, y), sticker->countOptimalSize() });
}
void CustomEmoji::paintCustom(
Painter &p,
int x,
int y,
not_null<Ui::Text::CustomEmoji*> emoji,
const PaintContext &context) {
if (!_hasHeavyPart) {
_hasHeavyPart = true;
_parent->history()->owner().registerHeavyViewPart(_parent);
}
//const auto preview = context.imageStyle()->msgServiceBg->c;
auto &textst = context.st->messageStyle(false, false);
const auto paused = context.paused || On(PowerSaving::kEmojiChat);
if (context.selected()) {
const auto factor = style::DevicePixelRatio();
const auto size = QSize(_singleSize, _singleSize) * factor;
if (_selectedFrame.size() != size) {
_selectedFrame = QImage(
size,
QImage::Format_ARGB32_Premultiplied);
_selectedFrame.setDevicePixelRatio(factor);
}
_selectedFrame.fill(Qt::transparent);
auto q = QPainter(&_selectedFrame);
emoji->paint(q, {
.textColor = textst.historyTextFg->c,
.now = context.now,
.paused = paused,
});
q.end();
_selectedFrame = Images::Colored(
std::move(_selectedFrame),
context.st->msgStickerOverlay()->c);
p.drawImage(x, y, _selectedFrame);
} else {
emoji->paint(p, {
.textColor = textst.historyTextFg->c,
.now = context.now,
.position = { x, y },
.paused = paused,
});
}
}
bool CustomEmoji::alwaysShowOutTimestamp() {
return (_lines.size() == 1) && _lines.back().size() > 3;
}
bool CustomEmoji::hasHeavyPart() const {
return _hasHeavyPart;
}
void CustomEmoji::unloadHeavyPart() {
if (!_hasHeavyPart) {
return;
}
const auto unload = [&](const LargeCustomEmoji &element) {
if (const auto sticker = std::get_if<StickerPtr>(&element)) {
(*sticker)->unloadHeavyPart();
} else if (const auto custom = std::get_if<CustomPtr>(&element)) {
(*custom)->unload();
}
};
_hasHeavyPart = false;
for (const auto &line : _lines) {
for (const auto &element : line) {
unload(element);
}
}
}
} // namespace HistoryView

View File

@@ -0,0 +1,104 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media_unwrapped.h"
#include "data/stickers/data_custom_emoji.h"
#include "base/weak_ptr.h"
namespace Ui::Text {
struct OnlyCustomEmoji;
} // namespace Ui::Text
namespace Stickers {
struct LargeEmojiImage;
} // namespace Stickers
namespace ChatHelpers {
enum class StickerLottieSize : uint8;
} // namespace ChatHelpers
namespace HistoryView {
class Sticker;
using LargeCustomEmoji = std::variant<
DocumentId,
std::unique_ptr<Sticker>,
std::unique_ptr<Ui::Text::CustomEmoji>>;
class CustomEmoji final
: public UnwrappedMedia::Content
, public base::has_weak_ptr
, private Data::CustomEmojiManager::Listener {
public:
CustomEmoji(
not_null<Element*> parent,
const Ui::Text::OnlyCustomEmoji &emoji);
~CustomEmoji();
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
void draw(
Painter &p,
const PaintContext &context,
const QRect &r) override;
ClickHandlerPtr link() override;
bool alwaysShowOutTimestamp() override;
bool hasTextForCopy() const override {
return true;
}
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
private:
void paintElement(
Painter &p,
int x,
int y,
LargeCustomEmoji &element,
const PaintContext &context);
void paintSticker(
Painter &p,
int x,
int y,
not_null<Sticker*> sticker,
const PaintContext &context);
void paintCustom(
Painter &p,
int x,
int y,
not_null<Ui::Text::CustomEmoji*> emoji,
const PaintContext &context);
[[nodiscard]] not_null<Data::CustomEmojiManager::Listener*> listener() {
return this;
}
void customEmojiResolveDone(not_null<DocumentData*> document) override;
[[nodiscard]] std::unique_ptr<Sticker> createStickerPart(
not_null<DocumentData*> document) const;
void refreshInteractionLink();
void interactionLinkClicked();
const not_null<Element*> _parent;
std::vector<std::vector<LargeCustomEmoji>> _lines;
ClickHandlerPtr _interactionLink;
QImage _selectedFrame;
int _singleSize = 0;
int _animationsCheckVersion = -1;
ChatHelpers::StickerLottieSize _cachingTag = {};
bool _hasHeavyPart = false;
bool _resolving = false;
};
} // namespace HistoryView

View File

@@ -0,0 +1,89 @@
/*
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/media/history_view_dice.h"
#include "data/data_session.h"
#include "chat_helpers/stickers_dice_pack.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/view/history_view_element.h"
#include "main/main_session.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
[[nodiscard]] DocumentData *Lookup(
not_null<Element*> view,
const QString &emoji,
int value) {
const auto &session = view->history()->session();
return session.diceStickersPacks().lookup(emoji, value);
}
} // namespace
Dice::Dice(not_null<Element*> parent, not_null<Data::MediaDice*> dice)
: _parent(parent)
, _dice(dice)
, _link(dice->makeHandler()) {
if (const auto document = Lookup(parent, dice->emoji(), 0)) {
const auto skipPremiumEffect = false;
_start.emplace(parent, document, skipPremiumEffect);
_start->setDiceIndex(_dice->emoji(), 0);
}
_showLastFrame = _parent->data()->Has<HistoryMessageForwarded>();
if (_showLastFrame) {
_drawingEnd = true;
}
}
Dice::~Dice() = default;
QSize Dice::countOptimalSize() {
return _start ? _start->countOptimalSize() : Sticker::EmojiSize();
}
ClickHandlerPtr Dice::link() {
return _link;
}
void Dice::draw(Painter &p, const PaintContext &context, const QRect &r) {
if (!_start) {
if (const auto document = Lookup(_parent, _dice->emoji(), 0)) {
const auto skipPremiumEffect = false;
_start.emplace(_parent, document, skipPremiumEffect);
_start->setDiceIndex(_dice->emoji(), 0);
_start->initSize();
}
}
if (const auto value = _end ? 0 : _dice->value()) {
if (const auto document = Lookup(_parent, _dice->emoji(), value)) {
const auto skipPremiumEffect = false;
_end.emplace(_parent, document, skipPremiumEffect);
_end->setDiceIndex(_dice->emoji(), value);
_end->initSize();
}
}
if (!_end) {
_drawingEnd = false;
}
if (_drawingEnd) {
_end->draw(p, context, r);
} else if (_start) {
_start->draw(p, context, r);
if (_end
&& _end->readyToDrawAnimationFrame()
&& _start->atTheEnd()) {
_drawingEnd = true;
}
}
}
} // namespace HistoryView

View File

@@ -0,0 +1,56 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media_unwrapped.h"
#include "history/view/media/history_view_sticker.h"
namespace Data {
class MediaDice;
} // namespace Data
namespace HistoryView {
class Dice final : public UnwrappedMedia::Content {
public:
Dice(not_null<Element*> parent, not_null<Data::MediaDice*> dice);
~Dice();
QSize countOptimalSize() override;
void draw(
Painter &p,
const PaintContext &context,
const QRect &r) override;
ClickHandlerPtr link() override;
bool hasHeavyPart() const override {
return (_start ? _start->hasHeavyPart() : false)
|| (_end ? _end->hasHeavyPart() : false);
}
void unloadHeavyPart() override {
if (_start) {
_start->unloadHeavyPart();
}
if (_end) {
_end->unloadHeavyPart();
}
}
private:
const not_null<Element*> _parent;
const not_null<Data::MediaDice*> _dice;
ClickHandlerPtr _link;
std::optional<Sticker> _start;
std::optional<Sticker> _end;
mutable bool _showLastFrame = false;
mutable bool _drawingEnd = false;
};
} // namespace HistoryView

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,191 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_file.h"
#include "base/runtime_composer.h"
struct HistoryDocumentNamed;
struct HistoryDocumentThumbed;
namespace Data {
class DocumentMedia;
} // namespace Data
namespace Ui::Text {
class String;
} // namespace Ui::Text
namespace HistoryView {
using TtlPaintCallback = Fn<void(QPainter&, QRect, QColor)>;
class Document final
: public File
, public RuntimeComposer<Document> {
public:
Document(
not_null<Element*> parent,
not_null<HistoryItem*> realParent,
not_null<DocumentData*> document);
~Document();
bool hideMessageText() const override {
return false;
}
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
void updatePressed(QPoint point) override;
[[nodiscard]] TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const override;
uint16 fullSelectionLength() const override;
bool hasTextForCopy() const override;
TextForMimeData selectedText(TextSelection selection) const override;
SelectedQuote selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote(
const SelectedQuote &quote) const override;
bool uploading() const override;
DocumentData *getDocument() const override {
return _data;
}
void hideSpoilers() override;
bool needsBubble() const override {
return true;
}
bool customInfoLayout() const override {
return false;
}
QMargins bubbleMargins() const override;
QSize sizeForGroupingOptimal(int maxWidth, bool last) const override;
QSize sizeForGrouping(int width) const override;
void drawGrouped(
Painter &p,
const PaintContext &context,
const QRect &geometry,
RectParts sides,
Ui::BubbleRounding rounding,
float64 highlightOpacity,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const override;
TextState getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const override;
bool voiceProgressAnimationCallback(crl::time now);
void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
void refreshParentId(not_null<HistoryItem*> realParent) override;
void parentTextUpdated() override;
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
protected:
float64 dataProgress() const override;
bool dataFinished() const override;
bool dataLoaded() const override;
private:
enum class LayoutMode {
Full,
Grouped,
};
void draw(
Painter &p,
const PaintContext &context,
int width,
LayoutMode mode,
Ui::BubbleRounding outsideRounding) const;
[[nodiscard]] TextState textState(
QPoint point,
QSize layout,
StateRequest request,
LayoutMode mode) const;
void ensureDataMediaCreated() const;
[[nodiscard]] Ui::Text::String createCaption() const;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
void refreshCaption(bool last);
void createComponents();
void fillNamedFromData(not_null<HistoryDocumentNamed*> named);
[[nodiscard]] Ui::BubbleRounding thumbRounding(
LayoutMode mode,
Ui::BubbleRounding outsideRounding) const;
void validateThumbnail(
not_null<const HistoryDocumentThumbed*> thumbed,
int size,
Ui::BubbleRounding rounding) const;
void setStatusSize(int64 newSize, TimeId realDuration = 0) const;
bool updateStatusText() const; // returns showPause
[[nodiscard]] int thumbedLinkMaxWidth() const;
[[nodiscard]] bool downloadInCorner() const;
void drawCornerDownload(
Painter &p,
const PaintContext &context,
LayoutMode mode) const;
[[nodiscard]] TextState cornerDownloadTextState(
QPoint point,
StateRequest request,
LayoutMode mode) const;
not_null<DocumentData*> _data;
mutable std::shared_ptr<Data::DocumentMedia> _dataMedia;
mutable QImage _iconCache;
mutable QImage _cornerDownloadCache;
class TooltipFilename {
public:
void setElided(bool value);
void setMoused(bool value);
void setTooltipText(QString text);
void updateTooltipForLink(ClickHandler *link);
void updateTooltipForState(TextState &state) const;
private:
ClickHandler *_lastLink = nullptr;
bool _elided = false;
bool _moused = false;
bool _stale = false;
QString _tooltip;
};
mutable TooltipFilename _tooltipFilename;
TtlPaintCallback _drawTtl;
bool _transcribedRound = false;
};
bool DrawThumbnailAsSongCover(
Painter &p,
const style::color &colored,
const std::shared_ptr<Data::DocumentMedia> &dataMedia,
const QRect &rect,
bool selected = false);
rpl::producer<> TTLVoiceStops(FullMsgId fullId);
} // namespace HistoryView

View File

@@ -0,0 +1,143 @@
/*
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/media/history_view_file.h"
#include "lang/lang_keys.h"
#include "ui/text/format_values.h"
#include "history/history_item.h"
#include "history/history.h"
#include "history/view/history_view_element.h"
#include "data/data_document.h"
#include "data/data_file_click_handler.h"
#include "data/data_session.h"
#include "styles/style_chat.h"
namespace HistoryView {
bool File::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const {
return p == _openl || p == _savel || p == _cancell;
}
bool File::dragItemByHandler(const ClickHandlerPtr &p) const {
return p == _openl || p == _savel || p == _cancell;
}
void File::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
if (p == _savel || p == _cancell) {
if (active && !dataLoaded()) {
ensureAnimation();
_animation->a_thumbOver.start([=] { repaint(); }, 0., 1., st::msgFileOverDuration);
} else if (!active && _animation && !dataLoaded()) {
_animation->a_thumbOver.start([=] { repaint(); }, 1., 0., st::msgFileOverDuration);
}
}
}
void File::clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) {
repaint();
}
void File::setLinks(
FileClickHandlerPtr &&openl,
FileClickHandlerPtr &&savel,
FileClickHandlerPtr &&cancell) {
_openl = std::move(openl);
_savel = std::move(savel);
_cancell = std::move(cancell);
}
void File::refreshParentId(not_null<HistoryItem*> realParent) {
const auto contextId = realParent->fullId();
if (_openl) {
_openl->setMessageId(contextId);
}
if (_savel) {
_savel->setMessageId(contextId);
}
if (_cancell) {
_cancell->setMessageId(contextId);
}
}
void File::setStatusSize(
int64 newSize,
int64 fullSize,
TimeId duration,
TimeId realDuration) const {
_statusSize = newSize;
if (_statusSize == Ui::FileStatusSizeReady) {
_statusText = (duration >= 0) ? Ui::FormatDurationAndSizeText(duration, fullSize) : (duration < -1 ? Ui::FormatGifAndSizeText(fullSize) : Ui::FormatSizeText(fullSize));
} else if (_statusSize == Ui::FileStatusSizeLoaded) {
_statusText = (duration >= 0) ? Ui::FormatDurationText(duration) : (duration < -1 ? u"GIF"_q : Ui::FormatSizeText(fullSize));
} else if (_statusSize == Ui::FileStatusSizeFailed) {
_statusText = tr::lng_attach_failed(tr::now);
} else if (_statusSize >= 0) {
_statusText = Ui::FormatDownloadText(_statusSize, fullSize);
} else {
_statusText = Ui::FormatPlayedText(-_statusSize - 1, realDuration);
}
}
void File::radialAnimationCallback(crl::time now) const {
const auto updated = [&] {
return _animation->radial.update(
dataProgress(),
dataFinished(),
now);
}();
if (!anim::Disabled() || updated) {
repaint();
}
if (!_animation->radial.animating()) {
checkAnimationFinished();
}
}
void File::ensureAnimation() const {
if (!_animation) {
_animation = std::make_unique<AnimationData>([=](crl::time now) {
radialAnimationCallback(now);
});
}
}
void File::checkAnimationFinished() const {
if (_animation && !_animation->a_thumbOver.animating() && !_animation->radial.animating()) {
if (dataLoaded()) {
_animation.reset();
}
}
}
void File::setDocumentLinks(
not_null<DocumentData*> document,
not_null<HistoryItem*> realParent,
Fn<bool()> openHook) {
const auto context = realParent->fullId();
setLinks(
std::make_shared<DocumentOpenClickHandler>(
document,
crl::guard(this, [=](FullMsgId id) {
if (!openHook || !openHook()) {
_parent->delegate()->elementOpenDocument(document, id);
}
}),
context),
std::make_shared<DocumentSaveClickHandler>(document, context),
std::make_shared<DocumentCancelClickHandler>(
document,
crl::guard(this, [=](FullMsgId id) {
_parent->delegate()->elementCancelUpload(id);
}),
context));
}
File::~File() = default;
} // namespace HistoryView

View File

@@ -0,0 +1,110 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
class FileClickHandler;
namespace HistoryView {
class File : public Media {
public:
File(
not_null<Element*> parent,
not_null<HistoryItem*> realParent)
: Media(parent)
, _realParent(realParent) {
}
[[nodiscard]] bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const override;
[[nodiscard]] bool dragItemByHandler(
const ClickHandlerPtr &p) const override;
void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
void refreshParentId(not_null<HistoryItem*> realParent) override;
[[nodiscard]] bool allowsFastShare() const override {
return true;
}
~File();
protected:
using FileClickHandlerPtr = std::shared_ptr<FileClickHandler>;
not_null<HistoryItem*> _realParent;
FileClickHandlerPtr _openl, _savel, _cancell;
void setLinks(
FileClickHandlerPtr &&openl,
FileClickHandlerPtr &&savel,
FileClickHandlerPtr &&cancell);
void setDocumentLinks(
not_null<DocumentData*> document,
not_null<HistoryItem*> realParent,
Fn<bool()> openHook = nullptr);
// >= 0 will contain download / upload string, _statusSize = loaded bytes
// < 0 will contain played string, _statusSize = -(seconds + 1) played
// 0xFFFFFFF0LL will contain status for not yet downloaded file
// 0xFFFFFFF1LL will contain status for already downloaded file
// 0xFFFFFFF2LL will contain status for failed to download / upload file
mutable int64 _statusSize = 0;
mutable QString _statusText;
// duration = -1 - no duration, duration = -2 - "GIF" duration
void setStatusSize(int64 newSize, int64 fullSize, TimeId duration, TimeId realDuration) const;
void radialAnimationCallback(crl::time now) const;
void ensureAnimation() const;
void checkAnimationFinished() const;
bool isRadialAnimation() const {
if (_animation) {
if (_animation->radial.animating()) {
return true;
}
checkAnimationFinished();
}
return false;
}
bool isThumbAnimation() const {
if (_animation) {
if (_animation->a_thumbOver.animating()) {
return true;
}
checkAnimationFinished();
}
return false;
}
virtual float64 dataProgress() const = 0;
virtual bool dataFinished() const = 0;
virtual bool dataLoaded() const = 0;
struct AnimationData {
template <typename Callback>
AnimationData(Callback &&radialCallback)
: radial(std::forward<Callback>(radialCallback)) {
}
Ui::Animations::Simple a_thumbOver;
Ui::RadialAnimation radial;
};
mutable std::unique_ptr<AnimationData> _animation;
};
} // namespace HistoryView

View File

@@ -0,0 +1,548 @@
/*
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/media/history_view_game.h"
#include "lang/lang_keys.h"
#include "history/history_item_components.h"
#include "history/history.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/media/history_view_media_common.h"
#include "ui/item_text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/cached_round_corners.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "core/ui_integration.h"
#include "data/data_session.h"
#include "data/data_game.h"
#include "data/data_media_types.h"
#include "styles/style_chat.h"
namespace HistoryView {
Game::Game(
not_null<Element*> parent,
not_null<GameData*> data,
const TextWithEntities &consumed)
: Media(parent)
, _st(st::historyPagePreview)
, _data(data)
, _title(st::msgMinWidth - _st.padding.left() - _st.padding.right())
, _description(st::msgMinWidth - _st.padding.left() - _st.padding.right()) {
if (!consumed.text.isEmpty()) {
const auto context = Core::TextContext({
.session = &history()->session(),
.repaint = [=] { _parent->customEmojiRepaint(); },
});
_description.setMarkedText(
st::webPageDescriptionStyle,
consumed,
Ui::ItemTextOptions(parent->data()),
context);
}
history()->owner().registerGameView(_data, _parent);
}
QSize Game::countOptimalSize() {
auto lineHeight = UnitedLineHeight();
const auto item = _parent->data();
if (!_openl && item->isRegular()) {
const auto row = 0;
const auto column = 0;
_openl = std::make_shared<ReplyMarkupClickHandler>(
&item->history()->owner(),
row,
column,
item->fullId());
}
auto title = TextUtilities::SingleLine(_data->title);
// init attach
if (!_attach) {
_attach = CreateAttach(
_parent,
_data->document,
_data->document ? nullptr : _data->photo);
}
// init strings
if (_description.isEmpty() && !_data->description.isEmpty()) {
auto text = _data->description;
if (!text.isEmpty()) {
auto marked = TextWithEntities { text };
auto parseFlags = TextParseLinks | TextParseMultiline;
TextUtilities::ParseEntities(marked, parseFlags);
_description.setMarkedText(
st::webPageDescriptionStyle,
marked,
Ui::WebpageTextDescriptionOptions());
if (!_attach) {
_description.updateSkipBlock(
_parent->skipBlockWidth(),
_parent->skipBlockHeight());
}
}
}
if (_title.isEmpty() && !title.isEmpty()) {
_title.setText(
st::webPageTitleStyle,
title,
Ui::WebpageTextTitleOptions());
}
// init dimensions
auto skipBlockWidth = _parent->skipBlockWidth();
auto maxWidth = skipBlockWidth;
auto minHeight = 0;
auto titleMinHeight = _title.isEmpty() ? 0 : lineHeight;
// enable any count of lines in game description / message
auto descMaxLines = 4096;
auto descriptionMinHeight = _description.isEmpty() ? 0 : qMin(_description.minHeight(), descMaxLines * lineHeight);
if (!_title.isEmpty()) {
accumulate_max(maxWidth, _title.maxWidth());
minHeight += titleMinHeight;
}
if (!_description.isEmpty()) {
accumulate_max(maxWidth, _description.maxWidth());
minHeight += descriptionMinHeight;
}
if (_attach) {
auto attachAtTop = !_titleLines && !_descriptionLines;
if (!attachAtTop) minHeight += st::mediaInBubbleSkip;
_attach->initDimensions();
QMargins bubble(_attach->bubbleMargins());
auto maxMediaWidth = _attach->maxWidth() - bubble.left() - bubble.right();
if (isBubbleBottom() && _attach->customInfoLayout()) {
maxMediaWidth += skipBlockWidth;
}
accumulate_max(maxWidth, maxMediaWidth);
minHeight += _attach->minHeight() - bubble.top() - bubble.bottom();
}
auto padding = inBubblePadding() + innerMargin();
maxWidth += padding.left() + padding.right();
minHeight += padding.top() + padding.bottom();
if (!_gameTagWidth) {
_gameTagWidth = st::msgDateFont->width(tr::lng_game_tag(tr::now).toUpper());
}
return { maxWidth, minHeight };
}
void Game::refreshParentId(not_null<HistoryItem*> realParent) {
if (_openl) {
_openl->setMessageId(realParent->fullId());
}
if (_attach) {
_attach->refreshParentId(realParent);
}
}
QSize Game::countCurrentSize(int newWidth) {
accumulate_min(newWidth, maxWidth());
const auto padding = inBubblePadding() + innerMargin();
auto innerWidth = newWidth - padding.left() - padding.right();
// enable any count of lines in game description / message
auto linesMax = 4096;
auto lineHeight = UnitedLineHeight();
auto newHeight = 0;
if (_title.isEmpty()) {
_titleLines = 0;
} else {
if (_title.countHeight(innerWidth) < 2 * st::webPageTitleFont->height) {
_titleLines = 1;
} else {
_titleLines = 2;
}
newHeight += _titleLines * lineHeight;
}
if (_description.isEmpty()) {
_descriptionLines = 0;
} else {
auto descriptionHeight = _description.countHeight(innerWidth);
if (descriptionHeight < (linesMax - _titleLines) * st::webPageDescriptionFont->height) {
_descriptionLines = (descriptionHeight / st::webPageDescriptionFont->height);
} else {
_descriptionLines = (linesMax - _titleLines);
}
newHeight += _descriptionLines * lineHeight;
}
if (_attach) {
auto attachAtTop = !_titleLines && !_descriptionLines;
if (!attachAtTop) newHeight += st::mediaInBubbleSkip;
QMargins bubble(_attach->bubbleMargins());
_attach->resizeGetHeight(innerWidth + bubble.left() + bubble.right());
newHeight += _attach->height() - bubble.top() - bubble.bottom();
}
newHeight += padding.top() + padding.bottom();
return { newWidth, newHeight };
}
TextSelection Game::toDescriptionSelection(
TextSelection selection) const {
return UnshiftItemSelection(selection, _title);
}
TextSelection Game::fromDescriptionSelection(
TextSelection selection) const {
return ShiftItemSelection(selection, _title);
}
void Game::draw(Painter &p, const PaintContext &context) const {
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
return;
}
const auto st = context.st;
const auto sti = context.imageStyle();
const auto stm = context.messageStyle();
const auto bubble = _attach ? _attach->bubbleMargins() : QMargins();
const auto full = QRect(0, 0, width(), height());
auto outer = full.marginsRemoved(inBubblePadding());
auto inner = outer.marginsRemoved(innerMargin());
auto tshift = inner.top();
auto paintw = inner.width();
const auto selected = context.selected();
const auto colorIndex = parent()->contentColorIndex();
const auto &colorCollectible = parent()->contentColorCollectible();
const auto colorPattern = colorCollectible
? st->collectiblePatternIndex(colorCollectible)
: st->colorPatternIndex(colorIndex);
const auto useColorCollectible = colorCollectible && !context.outbg;
const auto useColorIndex = !context.outbg;
const auto cache = useColorCollectible
? st->collectibleReplyCache(selected, colorCollectible).get()
: useColorIndex
? st->coloredReplyCache(selected, colorIndex).get()
: stm->replyCache[colorPattern].get();
Ui::Text::ValidateQuotePaintCache(*cache, _st);
Ui::Text::FillQuotePaint(p, outer, *cache, _st);
if (_ripple) {
_ripple->paint(p, outer.x(), outer.y(), width(), &cache->bg);
if (_ripple->empty()) {
_ripple = nullptr;
}
}
auto lineHeight = UnitedLineHeight();
if (_titleLines) {
p.setPen(cache->icon);
p.setTextPalette(useColorCollectible
? st->collectibleTextPalette(selected, colorCollectible)
: useColorIndex
? st->coloredTextPalette(selected, colorIndex)
: stm->semiboldPalette);
auto endskip = 0;
if (_title.hasSkipBlock()) {
endskip = _parent->skipBlockWidth();
}
_title.drawLeftElided(
p,
inner.left(),
tshift,
paintw,
width(),
_titleLines,
style::al_left,
0,
-1,
endskip,
false,
context.selection);
tshift += _titleLines * lineHeight;
p.setTextPalette(stm->textPalette);
}
if (_descriptionLines) {
p.setPen(stm->historyTextFg);
auto endskip = 0;
if (_description.hasSkipBlock()) {
endskip = _parent->skipBlockWidth();
}
_parent->prepareCustomEmojiPaint(p, context, _description);
_description.draw(p, {
.position = { inner.left(), tshift },
.outerWidth = width(),
.availableWidth = paintw,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.selection = toDescriptionSelection(context.selection),
.elisionHeight = _descriptionLines * lineHeight,
.elisionRemoveFromEnd = endskip,
.useFullWidth = true,
});
tshift += _descriptionLines * lineHeight;
}
if (_attach) {
auto attachAtTop = !_titleLines && !_descriptionLines;
if (!attachAtTop) tshift += st::mediaInBubbleSkip;
auto attachLeft = inner.left() - bubble.left();
auto attachTop = tshift - bubble.top();
if (rtl()) attachLeft = width() - attachLeft - _attach->width();
p.translate(attachLeft, attachTop);
_attach->draw(p, context.translated(
-attachLeft,
-attachTop
).withSelection(context.selected()
? FullSelection
: TextSelection()));
auto pixwidth = _attach->width();
auto pixheight = _attach->height();
auto gameW = _gameTagWidth + 2 * st::msgDateImgPadding.x();
auto gameH = st::msgDateFont->height + 2 * st::msgDateImgPadding.y();
auto gameX = pixwidth - st::msgDateImgDelta - gameW;
auto gameY = pixheight - st::msgDateImgDelta - gameH;
Ui::FillRoundRect(p, style::rtlrect(gameX, gameY, gameW, gameH, pixwidth), sti->msgDateImgBg, sti->msgDateImgBgCorners);
p.setFont(st::msgDateFont);
p.setPen(st->msgDateImgFg());
p.drawTextLeft(gameX + st::msgDateImgPadding.x(), gameY + st::msgDateImgPadding.y(), pixwidth, tr::lng_game_tag(tr::now).toUpper());
p.translate(-attachLeft, -attachTop);
}
}
TextState Game::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
return result;
}
const auto bubble = _attach ? _attach->bubbleMargins() : QMargins();
const auto full = QRect(0, 0, width(), height());
auto outer = full.marginsRemoved(inBubblePadding());
auto inner = outer.marginsRemoved(innerMargin());
auto tshift = inner.top();
auto paintw = inner.width();
auto symbolAdd = 0;
auto lineHeight = UnitedLineHeight();
if (_titleLines) {
if (point.y() >= tshift && point.y() < tshift + _titleLines * lineHeight) {
Ui::Text::StateRequestElided titleRequest = request.forText();
titleRequest.lines = _titleLines;
result = TextState(_parent, _title.getStateElidedLeft(
point - QPoint(inner.left(), tshift),
paintw,
width(),
titleRequest));
} else if (point.y() >= tshift + _titleLines * lineHeight) {
symbolAdd += _title.length();
}
tshift += _titleLines * lineHeight;
}
if (_descriptionLines) {
if (point.y() >= tshift && point.y() < tshift + _descriptionLines * lineHeight) {
Ui::Text::StateRequestElided descriptionRequest = request.forText();
descriptionRequest.lines = _descriptionLines;
result = TextState(_parent, _description.getStateElidedLeft(
point - QPoint(inner.left(), tshift),
paintw,
width(),
descriptionRequest));
} else if (point.y() >= tshift + _descriptionLines * lineHeight) {
symbolAdd += _description.length();
}
tshift += _descriptionLines * lineHeight;
}
if (_attach) {
auto attachAtTop = !_titleLines && !_descriptionLines;
if (!attachAtTop) tshift += st::mediaInBubbleSkip;
auto attachLeft = inner.left() - bubble.left();
auto attachTop = tshift - bubble.top();
if (rtl()) attachLeft = width() - attachLeft - _attach->width();
if (QRect(attachLeft, tshift, _attach->width(), inner.top() + inner.height() - tshift).contains(point)) {
if (_attach->isReadyForOpen()) {
if (_parent->data()->isHistoryEntry()) {
result.link = _openl;
}
} else {
result = _attach->textState(point - QPoint(attachLeft, attachTop), request);
}
}
}
if (_parent->data()->isHistoryEntry()) {
if (!result.link && outer.contains(point)) {
result.link = _openl;
}
}
_lastPoint = point - outer.topLeft();
result.symbol += symbolAdd;
return result;
}
TextSelection Game::adjustSelection(TextSelection selection, TextSelectType type) const {
if (!_descriptionLines || selection.to <= _title.length()) {
return _title.adjustSelection(selection, type);
}
auto descriptionSelection = _description.adjustSelection(toDescriptionSelection(selection), type);
if (selection.from >= _title.length()) {
return fromDescriptionSelection(descriptionSelection);
}
auto titleSelection = _title.adjustSelection(selection, type);
return { titleSelection.from, fromDescriptionSelection(descriptionSelection).to };
}
void Game::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
if (_attach) {
_attach->clickHandlerActiveChanged(p, active);
}
}
void Game::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
if (p == _openl) {
if (pressed) {
if (!_ripple) {
const auto full = QRect(0, 0, width(), height());
const auto outer = full.marginsRemoved(inBubblePadding());
_ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
outer.size(),
_st.radius),
[=] { repaint(); });
}
_ripple->add(_lastPoint);
} else if (_ripple) {
_ripple->lastStop();
}
}
if (_attach) {
_attach->clickHandlerPressedChanged(p, pressed);
}
}
bool Game::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const {
return _attach && _attach->toggleSelectionByHandlerClick(p);
}
bool Game::allowTextSelectionByHandler(const ClickHandlerPtr &p) const {
return (p == _openl);
}
bool Game::dragItemByHandler(const ClickHandlerPtr &p) const {
return _attach && _attach->dragItemByHandler(p);
}
TextForMimeData Game::selectedText(TextSelection selection) const {
auto titleResult = _title.toTextForMimeData(selection);
auto descriptionResult = _description.toTextForMimeData(
toDescriptionSelection(selection));
if (titleResult.empty()) {
return descriptionResult;
} else if (descriptionResult.empty()) {
return titleResult;
}
return titleResult.append('\n').append(std::move(descriptionResult));
}
void Game::playAnimation(bool autoplay) {
if (_attach) {
if (autoplay) {
_attach->autoplayAnimation();
} else {
_attach->playAnimation();
}
}
}
QMargins Game::inBubblePadding() const {
return {
st::msgPadding.left(),
isBubbleTop() ? st::msgPadding.left() : st::mediaInBubbleSkip,
st::msgPadding.right(),
(isBubbleBottom()
? (st::msgPadding.left() + bottomInfoPadding())
: st::mediaInBubbleSkip),
};
}
QMargins Game::innerMargin() const {
return _st.padding;
}
int Game::bottomInfoPadding() const {
if (!isBubbleBottom()) {
return 0;
}
auto result = st::msgDateFont->height;
// we use padding greater than st::msgPadding.bottom() in the
// bottom of the bubble so that the left line looks pretty.
// but if we have bottom skip because of the info display
// we don't need that additional padding so we replace it
// back with st::msgPadding.bottom() instead of left().
result += st::msgPadding.bottom() - st::msgPadding.left();
return result;
}
void Game::parentTextUpdated() {
if (const auto media = _parent->data()->media()) {
const auto consumed = media->consumedMessageText();
if (!consumed.text.isEmpty()) {
const auto context = Core::TextContext({
.session = &history()->session(),
.repaint = [=] { _parent->customEmojiRepaint(); },
});
_description.setMarkedText(
st::webPageDescriptionStyle,
consumed,
Ui::ItemTextOptions(_parent->data()),
context);
} else {
_description = Ui::Text::String(st::msgMinWidth
- _st.padding.left()
- _st.padding.right());
}
history()->owner().requestViewResize(_parent);
}
}
bool Game::hasHeavyPart() const {
return _attach ? _attach->hasHeavyPart() : false;
}
void Game::unloadHeavyPart() {
if (_attach) {
_attach->unloadHeavyPart();
}
_description.unloadPersistentAnimation();
}
Game::~Game() {
history()->owner().unregisterGameView(_data, _parent);
}
} // namespace HistoryView

View File

@@ -0,0 +1,120 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
class ReplyMarkupClickHandler;
namespace Ui {
class RippleAnimation;
} // namespace Ui
namespace HistoryView {
class Game : public Media {
public:
Game(
not_null<Element*> parent,
not_null<GameData*> data,
const TextWithEntities &consumed);
void refreshParentId(not_null<HistoryItem*> realParent) override;
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
[[nodiscard]] TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const override;
uint16 fullSelectionLength() const override {
return _title.length() + _description.length();
}
bool hasTextForCopy() const override {
return false; // we do not add _title and _description in FullSelection text copy.
}
bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const override;
bool allowTextSelectionByHandler(
const ClickHandlerPtr &p) const override;
bool dragItemByHandler(const ClickHandlerPtr &p) const override;
TextForMimeData selectedText(TextSelection selection) const override;
void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
PhotoData *getPhoto() const override {
return _attach ? _attach->getPhoto() : nullptr;
}
DocumentData *getDocument() const override {
return _attach ? _attach->getDocument() : nullptr;
}
void stopAnimation() override {
if (_attach) _attach->stopAnimation();
}
void checkAnimation() override {
if (_attach) _attach->checkAnimation();
}
not_null<GameData*> game() {
return _data;
}
bool needsBubble() const override {
return true;
}
bool customInfoLayout() const override {
return false;
}
bool allowsFastShare() const override {
return true;
}
Media *attach() const {
return _attach.get();
}
void parentTextUpdated() override;
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
~Game();
private:
void playAnimation(bool autoplay) override;
[[nodiscard]] QSize countOptimalSize() override;
[[nodiscard]] QSize countCurrentSize(int newWidth) override;
[[nodiscard]] TextSelection toDescriptionSelection(
TextSelection selection) const;
[[nodiscard]] TextSelection fromDescriptionSelection(
TextSelection selection) const;
[[nodiscard]] QMargins inBubblePadding() const;
[[nodiscard]] QMargins innerMargin() const;
[[nodiscard]] int bottomInfoPadding() const;
const style::QuoteStyle &_st;
const not_null<GameData*> _data;
std::shared_ptr<ReplyMarkupClickHandler> _openl;
std::unique_ptr<Media> _attach;
mutable std::unique_ptr<Ui::RippleAnimation> _ripple;
mutable QPoint _lastPoint;
int _gameTagWidth = 0;
int _descriptionLines = 0;
int _titleLines = 0;
Ui::Text::String _title;
Ui::Text::String _description;
};
} // namespace HistoryView

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,246 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_file.h"
#include "media/streaming/media_streaming_common.h"
class Image;
struct HistoryMessageVia;
struct HistoryMessageReply;
struct HistoryMessageForwarded;
class Painter;
class PhotoData;
namespace Data {
class DocumentMedia;
class PhotoMedia;
} // namespace Data
namespace Media {
namespace View {
class PlaybackProgress;
} // namespace View
} // namespace Media
namespace Media {
namespace Streaming {
class Instance;
struct Update;
struct Information;
enum class Error;
} // namespace Streaming
} // namespace Media
namespace HistoryView {
class Photo;
class Reply;
class TranscribeButton;
using TtlRoundPaintCallback = Fn<void(
QPainter&,
QRect,
const PaintContext &context)>;
class Gif final : public File {
public:
Gif(
not_null<Element*> parent,
not_null<HistoryItem*> realParent,
not_null<DocumentData*> document,
bool spoiler);
~Gif();
bool hideMessageText() const override;
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) override;
bool uploading() const override;
DocumentData *getDocument() const override {
return _data;
}
bool fullFeaturedGrouped(RectParts sides) const;
QSize sizeForGroupingOptimal(int maxWidth, bool last) const override;
QSize sizeForGrouping(int width) const override;
void drawGrouped(
Painter &p,
const PaintContext &context,
const QRect &geometry,
RectParts sides,
Ui::BubbleRounding rounding,
float64 highlightOpacity,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const override;
TextState getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const override;
void stopAnimation() override;
void checkAnimation() override;
void drawSpoilerTag(
Painter &p,
QRect rthumb,
const PaintContext &context,
Fn<QImage()> generateBackground) const override;
ClickHandlerPtr spoilerTagLink() const override;
QImage spoilerTagBackground() const override;
void hideSpoilers() override;
bool needsBubble() const override;
bool unwrapped() const override;
bool customInfoLayout() const override {
return true;
}
QRect contentRectForReactions() const override;
std::optional<int> reactionButtonCenterOverride() const override;
QPoint resolveCustomInfoRightBottom() const override;
QString additionalInfoString() const override;
bool skipBubbleTail() const override {
return isRoundedInBubbleBottom();
}
bool isReadyForOpen() const override;
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
bool enforceBubbleWidth() const override;
[[nodiscard]] static bool CanPlayInline(not_null<DocumentData*> document);
private:
struct Streamed;
void validateVideoThumbnail() const;
[[nodiscard]] QSize countThumbSize(int &inOutWidthMax) const;
[[nodiscard]] int adjustHeightForLessCrop(
QSize dimensions,
QSize current) const;
float64 dataProgress() const override;
bool dataFinished() const override;
bool dataLoaded() const override;
void ensureDataMediaCreated() const;
void dataMediaCreated() const;
[[nodiscard]] bool autoplayEnabled() const;
[[nodiscard]] bool autoplayUnderCursor() const;
[[nodiscard]] bool underCursor() const;
void playAnimation(bool autoplay) override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
QSize videoSize() const;
::Media::Streaming::Instance *activeRoundStreamed() const;
Streamed *activeOwnStreamed() const;
::Media::Streaming::Instance *activeCurrentStreamed() const;
::Media::View::PlaybackProgress *videoPlayback() const;
void createStreamedPlayer();
void checkStreamedIsStarted() const;
void startStreamedPlayer() const;
void setStreamed(std::unique_ptr<Streamed> value);
void handleStreamingUpdate(::Media::Streaming::Update &&update);
void handleStreamingError(::Media::Streaming::Error &&error);
void streamingReady(::Media::Streaming::Information &&info);
void repaintStreamedContent();
void ensureTranscribeButton() const;
void paintTranscribe(
Painter &p,
int x,
int y,
bool right,
const PaintContext &context) const;
void paintTimestampMark(
Painter &p,
QRect rthumb,
std::optional<Ui::BubbleRounding> rounding) const;
[[nodiscard]] bool needInfoDisplay() const;
[[nodiscard]] bool needCornerStatusDisplay() const;
[[nodiscard]] int additionalWidth(
const Reply *reply,
const HistoryMessageVia *via,
const HistoryMessageForwarded *forwarded) const;
[[nodiscard]] int additionalWidth() const;
[[nodiscard]] bool isUnwrapped() const;
void validateThumbCache(
QSize outer,
bool isEllipse,
std::optional<Ui::BubbleRounding> rounding) const;
[[nodiscard]] QImage prepareThumbCache(QSize outer) const;
void validateSpoilerImageCache(
QSize outer,
std::optional<Ui::BubbleRounding> rounding) const;
void validateGroupedCache(
const QRect &geometry,
Ui::BubbleRounding rounding,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const;
void setStatusSize(int64 newSize) const;
void updateStatusText() const;
[[nodiscard]] QSize sizeForAspectRatio() const;
void validateRoundingMask(QSize size) const;
[[nodiscard]] bool downloadInCorner() const;
void drawCornerStatus(
Painter &p,
const PaintContext &context,
QPoint position) const;
[[nodiscard]] TextState cornerStatusTextState(
QPoint point,
StateRequest request,
QPoint position) const;
[[nodiscard]] ClickHandlerPtr currentVideoLink() const;
void togglePollingStory(bool enabled) const;
TtlRoundPaintCallback _drawTtl;
const not_null<DocumentData*> _data;
PhotoData *_videoCover = nullptr;
const FullStoryId _storyId;
std::unique_ptr<Streamed> _streamed;
const std::unique_ptr<MediaSpoiler> _spoiler;
mutable std::unique_ptr<MediaSpoilerTag> _spoilerTag;
mutable std::unique_ptr<TranscribeButton> _transcribe;
mutable std::shared_ptr<Data::DocumentMedia> _dataMedia;
mutable std::shared_ptr<Data::PhotoMedia> _videoCoverMedia;
mutable std::unique_ptr<Image> _videoThumbnailFrame;
QString _downloadSize;
mutable QImage _thumbCache;
mutable QImage _roundingMask;
mutable crl::time _videoPosition = 0;
mutable TimeId _videoTimestamp = 0;
mutable std::optional<Ui::BubbleRounding> _thumbCacheRounding;
mutable bool _thumbCacheBlurred : 1 = false;
mutable bool _thumbIsEllipse : 1 = false;
mutable bool _pollingStory : 1 = false;
mutable bool _purchasedPriceTag : 1 = false;
mutable bool _smallGroupPart : 1 = false;
const bool _sensitiveSpoiler : 1 = false;
const bool _hasVideoCover : 1 = false;
};
} // namespace HistoryView

View File

@@ -0,0 +1,314 @@
/*
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/media/history_view_giveaway.h"
#include "base/unixtime.h"
#include "boxes/gift_premium_box.h"
#include "chat_helpers/stickers_gift_box_pack.h"
#include "chat_helpers/stickers_dice_pack.h"
#include "countries/countries_instance.h"
#include "data/data_channel.h"
#include "data/data_media_types.h"
#include "history/view/media/history_view_media_generic.h"
#include "history/view/history_view_element.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/effects/credits_graphics.h"
#include "ui/text/text_utilities.h"
#include "styles/style_chat.h"
namespace HistoryView {
constexpr auto kOutlineRatio = 0.85;
auto GenerateGiveawayStart(
not_null<Element*> parent,
not_null<Data::GiveawayStart*> data)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
return [=](
not_null<MediaGeneric*> media,
Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto months = data->months;
const auto quantity = data->quantity;
using Data = StickerWithBadgePart::Data;
const auto sticker = [=] {
const auto &session = parent->history()->session();
auto &packs = session.giftBoxStickersPacks();
return Data{
.sticker = packs.lookup(months),
.size = st::msgServiceGiftBoxStickerSize,
.stopOnLastFrame = true,
};
};
push(std::make_unique<StickerWithBadgePart>(
parent,
nullptr,
sticker,
st::chatGiveawayStickerPadding,
data->credits
? QString::number(data->credits)
: tr::lng_prizes_badge(
tr::now,
lt_amount,
QString::number(quantity)),
data->credits
? Ui::CreditsWhiteDoubledIcon(
st::chatGiveawayCreditsIconHeight,
kOutlineRatio)
: QImage(),
data->credits
? std::make_optional(st::creditsBg3->c)
: std::nullopt));
auto pushText = [&](
TextWithEntities text,
QMargins margins = {},
const base::flat_map<uint16, ClickHandlerPtr> &links = {}) {
push(std::make_unique<MediaGenericTextPart>(
std::move(text),
margins,
st::defaultTextStyle,
links));
};
pushText(
tr::bold(
tr::lng_prizes_title(tr::now, lt_count, quantity)),
st::chatGiveawayPrizesTitleMargin);
if (!data->additionalPrize.isEmpty()) {
pushText(
tr::lng_prizes_additional(
tr::now,
lt_count,
quantity,
lt_prize,
TextWithEntities{ data->additionalPrize },
tr::rich),
st::chatGiveawayPrizesMargin);
push(std::make_unique<TextDelimeterPart>(
tr::lng_prizes_additional_with(tr::now),
st::chatGiveawayPrizesWithPadding));
}
pushText((data->credits && (quantity == 1))
? tr::lng_prizes_credits_about_single(
tr::now,
lt_amount,
tr::lng_prizes_credits_about_amount(
tr::now,
lt_count,
data->credits,
tr::rich),
tr::rich)
: (data->credits && (quantity > 1))
? tr::lng_prizes_credits_about(
tr::now,
lt_count,
quantity,
lt_amount,
tr::lng_prizes_credits_about_amount(
tr::now,
lt_count,
data->credits,
tr::rich),
tr::rich)
: tr::lng_prizes_about(
tr::now,
lt_count,
quantity,
lt_duration,
tr::bold(GiftDuration(months * 30)),
tr::rich),
st::chatGiveawayPrizesMargin);
pushText(
tr::bold(tr::lng_prizes_participants(tr::now)),
st::chatGiveawayPrizesTitleMargin);
const auto hasChannel = ranges::any_of(
data->channels,
&ChannelData::isBroadcast);
const auto hasGroup = ranges::any_of(
data->channels,
&ChannelData::isMegagroup);
const auto mixed = (hasChannel && hasGroup);
pushText({ (data->all
? (mixed
? tr::lng_prizes_participants_all_mixed
: hasGroup
? tr::lng_prizes_participants_all_group
: tr::lng_prizes_participants_all)
: (mixed
? tr::lng_prizes_participants_new_mixed
: hasGroup
? tr::lng_prizes_participants_new_group
: tr::lng_prizes_participants_new))(
tr::now,
lt_count,
data->channels.size()),
}, st::chatGiveawayParticipantsMargin);
auto list = ranges::views::all(
data->channels
) | ranges::views::transform([](not_null<ChannelData*> channel) {
return not_null<PeerData*>(channel);
}) | ranges::to_vector;
push(std::make_unique<PeerBubbleListPart>(
parent,
std::move(list)));
const auto &instance = Countries::Instance();
auto countries = QStringList();
for (const auto &country : data->countries) {
const auto name = instance.countryNameByISO2(country);
const auto flag = instance.flagEmojiByISO2(country);
countries.push_back(flag + QChar(0xA0) + name);
}
if (const auto count = countries.size()) {
auto united = countries.front();
for (auto i = 1; i != count; ++i) {
united = ((i + 1 == count)
? tr::lng_prizes_countries_and_last
: tr::lng_prizes_countries_and_one)(
tr::now,
lt_countries,
united,
lt_country,
countries[i]);
}
pushText({
tr::lng_prizes_countries(tr::now, lt_countries, united),
}, st::chatGiveawayPrizesMargin);
}
pushText(
tr::bold(tr::lng_prizes_date(tr::now)),
(countries.empty()
? st::chatGiveawayNoCountriesTitleMargin
: st::chatGiveawayPrizesMargin));
pushText({
langDateTime(base::unixtime::parse(data->untilDate)),
}, st::chatGiveawayEndDateMargin);
};
}
auto GenerateGiveawayResults(
not_null<Element*> parent,
not_null<Data::GiveawayResults*> data)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
return [=](
not_null<MediaGeneric*> media,
Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto quantity = data->winnersCount;
using Data = StickerWithBadgePart::Data;
const auto sticker = [=] {
const auto &session = parent->history()->session();
auto &packs = session.diceStickersPacks();
const auto &emoji = Stickers::DicePacks::kPartyPopper;
return Data{
.sticker = packs.lookup(emoji, 0),
.skipTop = st::chatGiveawayWinnersTopSkip,
.size = st::maxAnimatedEmojiSize,
.stopOnLastFrame = true,
};
};
push(std::make_unique<StickerWithBadgePart>(
parent,
nullptr,
sticker,
st::chatGiveawayStickerPadding,
data->credits
? QString::number(data->credits)
: tr::lng_prizes_badge(
tr::now,
lt_amount,
QString::number(quantity)),
data->credits
? Ui::CreditsWhiteDoubledIcon(
st::chatGiveawayCreditsIconHeight,
kOutlineRatio)
: QImage(),
data->credits
? std::make_optional(st::creditsBg3->c)
: std::nullopt));
auto pushText = [&](
TextWithEntities text,
QMargins margins = {},
const base::flat_map<uint16, ClickHandlerPtr> &links = {}) {
push(std::make_unique<MediaGenericTextPart>(
std::move(text),
margins,
st::defaultTextStyle,
links));
};
const auto isSingleWinner = (data->winnersCount == 1);
pushText(
(isSingleWinner
? tr::lng_prizes_results_title_one
: tr::lng_prizes_results_title)(tr::now, tr::bold),
st::chatGiveawayPrizesTitleMargin);
const auto showGiveawayHandler = JumpToMessageClickHandler(
data->channel,
data->launchId,
parent->data()->fullId());
pushText(
tr::lng_prizes_results_about(
tr::now,
lt_count,
quantity,
lt_link,
tr::link(tr::lng_prizes_results_link(tr::now)),
tr::rich),
st::chatGiveawayPrizesMargin,
{ { 1, showGiveawayHandler } });
pushText(
(isSingleWinner
? tr::lng_prizes_results_winner
: tr::lng_prizes_results_winners)(tr::now, tr::bold),
st::chatGiveawayPrizesTitleMargin);
push(std::make_unique<PeerBubbleListPart>(
parent,
data->winners));
if (data->winnersCount > data->winners.size()) {
pushText(
tr::bold(tr::lng_prizes_results_more(
tr::now,
lt_count,
data->winnersCount - data->winners.size())),
st::chatGiveawayNoCountriesTitleMargin);
}
pushText({ (data->credits && isSingleWinner)
? tr::lng_prizes_credits_results_one(
tr::now,
lt_count,
data->credits)
: (data->credits && !isSingleWinner)
? tr::lng_prizes_credits_results_all(
tr::now,
lt_count,
data->credits)
: data->unclaimedCount
? tr::lng_prizes_results_some(tr::now)
: isSingleWinner
? tr::lng_prizes_results_one(tr::now)
: tr::lng_prizes_results_all(tr::now)
}, st::chatGiveawayEndDateMargin);
};
}
} // namespace HistoryView

View File

@@ -0,0 +1,35 @@
/*
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
*/
#pragma once
namespace Data {
struct GiveawayStart;
struct GiveawayResults;
} // namespace Data
namespace HistoryView {
class Element;
class MediaGeneric;
class MediaGenericPart;
[[nodiscard]] auto GenerateGiveawayStart(
not_null<Element*> parent,
not_null<Data::GiveawayStart*> data)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
[[nodiscard]] auto GenerateGiveawayResults(
not_null<Element*> parent,
not_null<Data::GiveawayResults*> data)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
} // namespace HistoryView

View File

@@ -0,0 +1,412 @@
/*
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/media/history_view_invoice.h"
#include "lang/lang_keys.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/media/history_view_photo.h"
#include "history/view/media/history_view_media_common.h"
#include "ui/item_text_options.h"
#include "ui/chat/chat_style.h"
#include "ui/text/format_values.h"
#include "ui/cached_round_corners.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "data/data_media_types.h"
#include "styles/style_chat.h"
namespace HistoryView {
Invoice::Invoice(
not_null<Element*> parent,
not_null<Data::Invoice*> invoice)
: Media(parent)
, _title(st::msgMinWidth)
, _description(st::msgMinWidth)
, _status(st::msgMinWidth) {
fillFromData(invoice);
}
void Invoice::fillFromData(not_null<Data::Invoice*> invoice) {
const auto isCreditsCurrency = false;
if (invoice->photo && !isCreditsCurrency) {
const auto spoiler = false;
_attach = std::make_unique<Photo>(
_parent,
_parent->data(),
invoice->photo,
spoiler);
} else {
_attach = nullptr;
}
auto labelText = [&] {
if (invoice->receiptMsgId) {
if (invoice->isTest) {
return tr::lng_payments_receipt_label_test(tr::now);
}
return tr::lng_payments_receipt_label(tr::now);
} else if (invoice->isTest) {
return tr::lng_payments_invoice_label_test(tr::now);
}
return tr::lng_payments_invoice_label(tr::now);
};
auto statusText = TextWithEntities {
Ui::FillAmountAndCurrency(invoice->amount, invoice->currency),
EntitiesInText()
};
statusText.entities.push_back({
EntityType::Bold,
0,
int(statusText.text.size()) });
statusText.text += ' ' + labelText().toUpper();
if (isCreditsCurrency) {
statusText = {};
}
_status.setMarkedText(
st::defaultTextStyle,
statusText,
Ui::ItemTextOptions(_parent->data()));
_receiptMsgId = invoice->receiptMsgId;
// init strings
if (!invoice->description.empty()) {
_description.setMarkedText(
st::webPageDescriptionStyle,
invoice->description,
Ui::WebpageTextDescriptionOptions());
}
if (!invoice->title.isEmpty()) {
_title.setText(
st::webPageTitleStyle,
invoice->title,
Ui::WebpageTextTitleOptions());
}
}
QSize Invoice::countOptimalSize() {
auto lineHeight = UnitedLineHeight();
if (_attach) {
if (_status.hasSkipBlock()) {
_status.removeSkipBlock();
}
} else {
_status.updateSkipBlock(
_parent->skipBlockWidth(),
_parent->skipBlockHeight());
}
// init dimensions
auto skipBlockWidth = _parent->skipBlockWidth();
auto maxWidth = skipBlockWidth;
auto minHeight = 0;
auto titleMinHeight = _title.isEmpty() ? 0 : lineHeight;
// enable any count of lines in game description / message
auto descMaxLines = 4096;
auto descriptionMinHeight = _description.isEmpty() ? 0 : qMin(_description.minHeight(), descMaxLines * lineHeight);
if (!_title.isEmpty()) {
accumulate_max(maxWidth, _title.maxWidth());
minHeight += titleMinHeight;
}
if (!_description.isEmpty()) {
accumulate_max(maxWidth, _description.maxWidth());
minHeight += descriptionMinHeight;
}
if (_attach) {
auto attachAtTop = _title.isEmpty() && _description.isEmpty();
if (!attachAtTop) minHeight += st::mediaInBubbleSkip;
_attach->initDimensions();
auto bubble = _attach->bubbleMargins();
auto maxMediaWidth = _attach->maxWidth() - bubble.left() - bubble.right();
if (isBubbleBottom() && _attach->customInfoLayout()) {
maxMediaWidth += skipBlockWidth;
}
accumulate_max(maxWidth, maxMediaWidth);
minHeight += _attach->minHeight() - bubble.top() - bubble.bottom();
} else {
accumulate_max(maxWidth, _status.maxWidth());
minHeight += st::mediaInBubbleSkip + _status.minHeight();
}
auto padding = inBubblePadding();
maxWidth += padding.left() + padding.right();
minHeight += padding.top() + padding.bottom();
return { maxWidth, minHeight };
}
QSize Invoice::countCurrentSize(int newWidth) {
accumulate_min(newWidth, maxWidth());
auto innerWidth = newWidth - st::msgPadding.left() - st::msgPadding.right();
auto lineHeight = UnitedLineHeight();
auto newHeight = 0;
if (_title.isEmpty()) {
_titleHeight = 0;
} else {
if (_title.countHeight(innerWidth) < 2 * st::webPageTitleFont->height) {
_titleHeight = lineHeight;
} else {
_titleHeight = 2 * lineHeight;
}
newHeight += _titleHeight;
}
if (_description.isEmpty()) {
_descriptionHeight = 0;
} else {
_descriptionHeight = _description.countHeight(innerWidth);
newHeight += _descriptionHeight;
}
if (_attach) {
auto attachAtTop = !_title.isEmpty() && _description.isEmpty();
if (!attachAtTop) newHeight += st::mediaInBubbleSkip;
QMargins bubble(_attach->bubbleMargins());
_attach->resizeGetHeight(innerWidth + bubble.left() + bubble.right());
newHeight += _attach->height() - bubble.top() - bubble.bottom();
if (isBubbleBottom() && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > innerWidth + bubble.left() + bubble.right()) {
newHeight += bottomInfoPadding();
}
} else {
newHeight += st::mediaInBubbleSkip + _status.countHeight(innerWidth);
}
auto padding = inBubblePadding();
newHeight += padding.top() + padding.bottom();
return { newWidth, newHeight };
}
TextSelection Invoice::toDescriptionSelection(
TextSelection selection) const {
return UnshiftItemSelection(selection, _title);
}
TextSelection Invoice::fromDescriptionSelection(
TextSelection selection) const {
return ShiftItemSelection(selection, _title);
}
void Invoice::refreshParentId(not_null<HistoryItem*> realParent) {
if (_attach) {
_attach->refreshParentId(realParent);
}
}
void Invoice::draw(Painter &p, const PaintContext &context) const {
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;
auto paintw = width();
const auto st = context.st;
const auto sti = context.imageStyle();
const auto stm = context.messageStyle();
auto &semibold = stm->msgServiceFg;
QMargins bubble(_attach ? _attach->bubbleMargins() : QMargins());
auto padding = inBubblePadding();
auto tshift = padding.top();
paintw -= padding.left() + padding.right();
auto lineHeight = UnitedLineHeight();
if (_titleHeight) {
p.setPen(semibold);
p.setTextPalette(stm->semiboldPalette);
auto endskip = 0;
if (_title.hasSkipBlock()) {
endskip = _parent->skipBlockWidth();
}
_title.drawLeftElided(p, padding.left(), tshift, paintw, width(), _titleHeight / lineHeight, style::al_left, 0, -1, endskip, false, context.selection);
tshift += _titleHeight;
p.setTextPalette(stm->textPalette);
}
if (_descriptionHeight) {
p.setPen(stm->historyTextFg);
_parent->prepareCustomEmojiPaint(p, context, _description);
_description.draw(p, {
.position = { padding.left(), tshift },
.outerWidth = width(),
.availableWidth = paintw,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.selection = toDescriptionSelection(context.selection),
.useFullWidth = true,
});
tshift += _descriptionHeight;
}
if (_attach) {
auto attachAtTop = !_titleHeight && !_descriptionHeight;
if (!attachAtTop) tshift += st::mediaInBubbleSkip;
auto attachLeft = padding.left() - bubble.left();
auto attachTop = tshift - bubble.top();
if (rtl()) attachLeft = width() - attachLeft - _attach->width();
p.translate(attachLeft, attachTop);
_attach->draw(p, context.translated(
-attachLeft,
-attachTop
).withSelection(context.selected()
? FullSelection
: TextSelection()));
auto pixwidth = _attach->width();
auto available = _status.maxWidth();
auto statusW = available + 2 * st::msgDateImgPadding.x();
auto statusH = st::msgDateFont->height + 2 * st::msgDateImgPadding.y();
auto statusX = st::msgDateImgDelta;
auto statusY = st::msgDateImgDelta;
Ui::FillRoundRect(p, style::rtlrect(statusX, statusY, statusW, statusH, pixwidth), sti->msgDateImgBg, sti->msgDateImgBgCorners);
p.setFont(st::msgDateFont);
p.setPen(st->msgDateImgFg());
_status.drawLeftElided(p, statusX + st::msgDateImgPadding.x(), statusY + st::msgDateImgPadding.y(), available, pixwidth);
p.translate(-attachLeft, -attachTop);
} else {
p.setPen(stm->historyTextFg);
_status.drawLeft(p, padding.left(), tshift + st::mediaInBubbleSkip, paintw, width());
}
}
TextState Invoice::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
return result;
}
auto paintw = width();
QMargins bubble(_attach ? _attach->bubbleMargins() : QMargins());
auto padding = inBubblePadding();
auto tshift = padding.top();
auto bshift = padding.bottom();
if (isBubbleBottom() && _attach && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > paintw + bubble.left() + bubble.right()) {
bshift += bottomInfoPadding();
}
paintw -= padding.left() + padding.right();
auto lineHeight = UnitedLineHeight();
auto symbolAdd = 0;
if (_titleHeight) {
if (point.y() >= tshift && point.y() < tshift + _titleHeight) {
Ui::Text::StateRequestElided titleRequest = request.forText();
titleRequest.lines = _titleHeight / lineHeight;
result = TextState(_parent, _title.getStateElidedLeft(
point - QPoint(padding.left(), tshift),
paintw,
width(),
titleRequest));
} else if (point.y() >= tshift + _titleHeight) {
symbolAdd += _title.length();
}
tshift += _titleHeight;
}
if (_descriptionHeight) {
if (point.y() >= tshift && point.y() < tshift + _descriptionHeight) {
result = TextState(_parent, _description.getStateLeft(
point - QPoint(padding.left(), tshift),
paintw,
width(),
request.forText()));
} else if (point.y() >= tshift + _descriptionHeight) {
symbolAdd += _description.length();
}
tshift += _descriptionHeight;
}
if (_attach) {
auto attachAtTop = !_titleHeight && !_descriptionHeight;
if (!attachAtTop) tshift += st::mediaInBubbleSkip;
auto attachLeft = padding.left() - bubble.left();
auto attachTop = tshift - bubble.top();
if (rtl()) attachLeft = width() - attachLeft - _attach->width();
if (QRect(attachLeft, tshift, _attach->width(), height() - tshift - bshift).contains(point)) {
result = _attach->textState(point - QPoint(attachLeft, attachTop), request);
}
}
result.symbol += symbolAdd;
return result;
}
TextSelection Invoice::adjustSelection(TextSelection selection, TextSelectType type) const {
if (!_descriptionHeight || selection.to <= _title.length()) {
return _title.adjustSelection(selection, type);
}
auto descriptionSelection = _description.adjustSelection(toDescriptionSelection(selection), type);
if (selection.from >= _title.length()) {
return fromDescriptionSelection(descriptionSelection);
}
auto titleSelection = _title.adjustSelection(selection, type);
return { titleSelection.from, fromDescriptionSelection(descriptionSelection).to };
}
void Invoice::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
if (_attach) {
_attach->clickHandlerActiveChanged(p, active);
}
}
void Invoice::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
if (_attach) {
_attach->clickHandlerPressedChanged(p, pressed);
}
}
bool Invoice::hasHeavyPart() const {
return _attach ? _attach->hasHeavyPart() : false;
}
void Invoice::unloadHeavyPart() {
if (_attach) {
_attach->unloadHeavyPart();
}
_description.unloadPersistentAnimation();
}
TextForMimeData Invoice::selectedText(TextSelection selection) const {
auto titleResult = _title.toTextForMimeData(selection);
auto descriptionResult = _description.toTextForMimeData(
toDescriptionSelection(selection));
if (titleResult.empty()) {
return descriptionResult;
} else if (descriptionResult.empty()) {
return titleResult;
}
return titleResult.append('\n').append(std::move(descriptionResult));
}
QMargins Invoice::inBubblePadding() const {
auto lshift = st::msgPadding.left();
auto rshift = st::msgPadding.right();
auto bshift = isBubbleBottom() ? st::msgPadding.top() : st::mediaInBubbleSkip;
auto tshift = isBubbleTop() ? st::msgPadding.bottom() : st::mediaInBubbleSkip;
return QMargins(lshift, tshift, rshift, bshift);
}
int Invoice::bottomInfoPadding() const {
if (!isBubbleBottom()) return 0;
auto result = st::msgDateFont->height;
return result;
}
} // namespace HistoryView

View File

@@ -0,0 +1,102 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
namespace Data {
struct Invoice;
} // namespace Data
namespace HistoryView {
class Invoice : public Media {
public:
Invoice(
not_null<Element*> parent,
not_null<Data::Invoice*> invoice);
void refreshParentId(not_null<HistoryItem*> realParent) override;
MsgId getReceiptMsgId() const {
return _receiptMsgId;
}
QString getTitle() const {
return _title.toString();
}
bool aboveTextByDefault() const override {
return false;
}
bool hideMessageText() const override {
return false;
}
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
[[nodiscard]] TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const override;
uint16 fullSelectionLength() const override {
return _title.length() + _description.length();
}
bool hasTextForCopy() const override {
return false; // we do not add _title and _description in FullSelection text copy.
}
bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
return _attach && _attach->toggleSelectionByHandlerClick(p);
}
bool dragItemByHandler(const ClickHandlerPtr &p) const override {
return _attach && _attach->dragItemByHandler(p);
}
TextForMimeData selectedText(TextSelection selection) const override;
void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
bool needsBubble() const override {
return true;
}
bool customInfoLayout() const override {
return false;
}
Media *attach() const {
return _attach.get();
}
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
private:
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
void fillFromData(not_null<Data::Invoice*> invoice);
TextSelection toDescriptionSelection(TextSelection selection) const;
TextSelection fromDescriptionSelection(TextSelection selection) const;
QMargins inBubblePadding() const;
int bottomInfoPadding() const;
std::unique_ptr<Media> _attach;
int _titleHeight = 0;
int _descriptionHeight = 0;
Ui::Text::String _title;
Ui::Text::String _description;
Ui::Text::String _status;
MsgId _receiptMsgId = 0;
};
} // namespace HistoryView

View File

@@ -0,0 +1,187 @@
/*
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/media/history_view_large_emoji.h"
#include "main/main_session.h"
#include "chat_helpers/stickers_emoji_pack.h"
#include "history/view/history_view_element.h"
#include "history/history_item.h"
#include "history/history.h"
#include "ui/image/image.h"
#include "ui/chat/chat_style.h"
#include "ui/painter.h"
#include "data/data_session.h"
#include "data/data_file_origin.h"
#include "data/stickers/data_custom_emoji.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
using Stickers::LargeEmojiImage;
using ImagePtr = std::shared_ptr<Stickers::LargeEmojiImage>;
using CustomPtr = std::unique_ptr<Ui::Text::CustomEmoji>;
auto ResolveImages(
not_null<Main::Session*> session,
Fn<void()> customEmojiRepaint,
const Ui::Text::IsolatedEmoji &emoji)
-> std::array<LargeEmojiMedia, Ui::Text::kIsolatedEmojiLimit> {
const auto single = [&](Ui::Text::IsolatedEmoji::Item item)
-> LargeEmojiMedia {
if (const auto regular = std::get_if<EmojiPtr>(&item)) {
return session->emojiStickersPack().image(*regular);
} else if (const auto custom = std::get_if<QString>(&item)) {
return session->data().customEmojiManager().create(
*custom,
customEmojiRepaint,
Data::CustomEmojiManager::SizeTag::Isolated);
}
return v::null;
};
return { {
single(emoji.items[0]),
single(emoji.items[1]),
single(emoji.items[2]) } };
}
} // namespace
LargeEmoji::LargeEmoji(
not_null<Element*> parent,
const Ui::Text::IsolatedEmoji &emoji)
: _parent(parent)
, _images(ResolveImages(
&parent->history()->session(),
[=] { parent->customEmojiRepaint(); },
emoji)) {
}
LargeEmoji::~LargeEmoji() {
if (_hasHeavyPart) {
unloadHeavyPart();
_parent->checkHeavyPart();
}
}
QSize LargeEmoji::countOptimalSize() {
using namespace rpl::mappers;
const auto count = _images.size()
- ranges::count(_images, LargeEmojiMedia());
const auto single = LargeEmojiImage::Size() / style::DevicePixelRatio();
const auto skip = st::largeEmojiSkip - 2 * st::largeEmojiOutline;
const auto inner = count * single.width() + (count - 1) * skip;
const auto &padding = st::largeEmojiPadding;
_size = QSize(
padding.left() + inner + padding.right(),
padding.top() + single.height() + padding.bottom());
return _size;
}
void LargeEmoji::draw(
Painter &p,
const PaintContext &context,
const QRect &r) {
_parent->clearCustomEmojiRepaint();
const auto &padding = st::largeEmojiPadding;
auto x = r.x() + (r.width() - _size.width()) / 2 + padding.left();
const auto y = r.y() + (r.height() - _size.height()) / 2 + padding.top();
const auto skip = st::largeEmojiSkip - 2 * st::largeEmojiOutline;
const auto size = LargeEmojiImage::Size() / style::DevicePixelRatio();
const auto selected = context.selected();
if (!selected) {
_selectedFrame = QImage();
}
for (const auto &media : _images) {
if (const auto image = std::get_if<ImagePtr>(&media)) {
if (const auto &prepared = (*image)->image) {
const auto colored = selected
? &context.st->msgStickerOverlay()
: nullptr;
p.drawPixmap(
x,
y,
prepared->pix(size, { .colored = colored }));
} else if ((*image)->load) {
(*image)->load();
}
} else if (const auto custom = std::get_if<CustomPtr>(&media)) {
paintCustom(p, x, y, custom->get(), context);
} else {
continue;
}
x += size.width() + skip;
}
}
void LargeEmoji::paintCustom(
QPainter &p,
int x,
int y,
not_null<Ui::Text::CustomEmoji*> emoji,
const PaintContext &context) {
if (!_hasHeavyPart) {
_hasHeavyPart = true;
_parent->history()->owner().registerHeavyViewPart(_parent);
}
const auto inner = st::largeEmojiSize + 2 * st::largeEmojiOutline;
const auto outer = Ui::Text::AdjustCustomEmojiSize(inner);
const auto skip = (inner - outer) / 2;
//const auto preview = context.imageStyle()->msgServiceBg->c;
auto &textst = context.st->messageStyle(false, false);
if (context.selected()) {
const auto factor = style::DevicePixelRatio();
const auto size = QSize(outer, outer) * factor;
if (_selectedFrame.size() != size) {
_selectedFrame = QImage(
size,
QImage::Format_ARGB32_Premultiplied);
_selectedFrame.setDevicePixelRatio(factor);
}
_selectedFrame.fill(Qt::transparent);
auto q = QPainter(&_selectedFrame);
emoji->paint(q, {
.textColor = textst.historyTextFg->c,
.now = context.now,
.paused = context.paused,
});
q.end();
_selectedFrame = Images::Colored(
std::move(_selectedFrame),
context.st->msgStickerOverlay()->c);
p.drawImage(x + skip, y + skip, _selectedFrame);
} else {
emoji->paint(p, {
.textColor = textst.historyTextFg->c,
.now = context.now,
.position = { x + skip, y + skip },
.paused = context.paused,
});
}
}
bool LargeEmoji::hasHeavyPart() const {
return _hasHeavyPart;
}
void LargeEmoji::unloadHeavyPart() {
if (_hasHeavyPart) {
_hasHeavyPart = false;
for (auto &media : _images) {
if (const auto custom = std::get_if<CustomPtr>(&media)) {
(*custom)->unload();
}
}
}
}
} // namespace HistoryView

View File

@@ -0,0 +1,67 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media_unwrapped.h"
#include "ui/text/text_isolated_emoji.h"
namespace Stickers {
struct LargeEmojiImage;
} // namespace Stickers
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace HistoryView {
using LargeEmojiMedia = std::variant<
v::null_t,
std::shared_ptr<Stickers::LargeEmojiImage>,
std::unique_ptr<Ui::Text::CustomEmoji>>;
class LargeEmoji final : public UnwrappedMedia::Content {
public:
LargeEmoji(
not_null<Element*> parent,
const Ui::Text::IsolatedEmoji &emoji);
~LargeEmoji();
QSize countOptimalSize() override;
void draw(
Painter &p,
const PaintContext &context,
const QRect &r) override;
bool alwaysShowOutTimestamp() override {
return true;
}
bool hasTextForCopy() const override {
return true;
}
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
private:
void paintCustom(
QPainter &p,
int x,
int y,
not_null<Ui::Text::CustomEmoji*> emoji,
const PaintContext &context);
const not_null<Element*> _parent;
const std::array<LargeEmojiMedia, Ui::Text::kIsolatedEmojiLimit> _images;
QImage _selectedFrame;
QSize _size;
bool _hasHeavyPart = false;
};
} // namespace HistoryView

View File

@@ -0,0 +1,742 @@
/*
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/media/history_view_location.h"
#include "base/unixtime.h"
#include "history/history.h"
#include "history/history_item_components.h"
#include "history/history_item.h"
#include "history/history_location_manager.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "lang/lang_keys.h"
#include "ui/chat/chat_style.h"
#include "ui/image/image.h"
#include "ui/text/text_options.h"
#include "ui/cached_round_corners.h"
#include "ui/painter.h"
#include "data/data_session.h"
#include "data/data_file_origin.h"
#include "data/data_cloud_file.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
constexpr auto kUntilOffPeriod = std::numeric_limits<TimeId>::max();
constexpr auto kLiveElapsedPartOpacity = 0.2;
[[nodiscard]] TimeId ResolveUpdateDate(not_null<Element*> view) {
const auto item = view->data();
const auto edited = item->Get<HistoryMessageEdited>();
return edited ? edited->date : item->date();
}
[[nodiscard]] QString RemainingTimeText(
not_null<Element*> view,
TimeId period) {
if (period == kUntilOffPeriod) {
return QString(1, QChar(0x221E));
}
const auto elapsed = base::unixtime::now() - view->data()->date();
const auto remaining = std::clamp(period - elapsed, 0, period);
if (remaining < 10) {
return tr::lng_seconds_tiny(tr::now, lt_count, remaining);
} else if (remaining < 600) {
return tr::lng_minutes_tiny(tr::now, lt_count, remaining / 60);
} else if (remaining < 3600) {
return QString::number(remaining / 60);
} else if (remaining < 86400) {
return tr::lng_hours_tiny(tr::now, lt_count, remaining / 3600);
}
return tr::lng_days_tiny(tr::now, lt_count, remaining / 86400);
}
[[nodiscard]] float64 RemainingTimeProgress(
not_null<Element*> view,
TimeId period) {
if (period == kUntilOffPeriod) {
return 1.;
} else if (period < 1) {
return 0.;
}
const auto elapsed = base::unixtime::now() - view->data()->date();
return std::clamp(period - elapsed, 0, period) / float64(period);
}
} // namespace
struct Location::Live {
explicit Live(TimeId period) : period(period) {
}
base::Timer updateStatusTimer;
base::Timer updateRemainingTimer;
QImage previous;
QImage previousCache;
Ui::BubbleRounding previousRounding;
Ui::Animations::Simple crossfade;
TimeId period = 0;
int thumbnailHeight = 0;
};
Location::Location(
not_null<Element*> parent,
not_null<Data::CloudImage*> data,
Data::LocationPoint point,
Element *replacing,
TimeId livePeriod)
: Media(parent)
, _data(data)
, _live(CreateLiveTracker(parent, livePeriod))
, _title(st::msgMinWidth)
, _description(st::msgMinWidth)
, _link(std::make_shared<LocationClickHandler>(point)) {
if (_live) {
_title.setText(
st::webPageTitleStyle,
tr::lng_live_location(tr::now),
Ui::WebpageTextTitleOptions());
_live->updateStatusTimer.setCallback([=] {
updateLiveStatus();
checkLiveFinish();
});
_live->updateRemainingTimer.setCallback([=] {
checkLiveFinish();
});
updateLiveStatus();
if (const auto media = replacing ? replacing->media() : nullptr) {
_live->previous = media->locationTakeImage();
if (!_live->previous.isNull()) {
history()->owner().registerHeavyViewPart(_parent);
}
}
}
}
Location::Location(
not_null<Element*> parent,
not_null<Data::CloudImage*> data,
Data::LocationPoint point,
const QString &title,
const QString &description)
: Media(parent)
, _data(data)
, _title(st::msgMinWidth)
, _description(st::msgMinWidth)
, _link(std::make_shared<LocationClickHandler>(point)) {
if (!title.isEmpty()) {
_title.setText(
st::webPageTitleStyle,
title,
Ui::WebpageTextTitleOptions());
}
if (!description.isEmpty()) {
_description.setMarkedText(
st::webPageDescriptionStyle,
TextUtilities::ParseEntities(
description,
TextParseLinks | TextParseMultiline),
Ui::WebpageTextDescriptionOptions());
}
}
Location::~Location() {
if (hasHeavyPart()) {
unloadHeavyPart();
_parent->checkHeavyPart();
}
}
void Location::checkLiveFinish() {
Expects(_live != nullptr);
const auto now = base::unixtime::now();
const auto item = _parent->data();
const auto start = item->date();
if (_live->period != kUntilOffPeriod && now - start >= _live->period) {
const auto had = hasHeavyPart();
_live = nullptr;
if (had && !hasHeavyPart()) {
_parent->checkHeavyPart();
}
item->history()->owner().requestViewResize(_parent);
} else {
_parent->repaint();
}
}
std::unique_ptr<Location::Live> Location::CreateLiveTracker(
not_null<Element*> parent,
TimeId period) {
if (!period) {
return nullptr;
}
const auto now = base::unixtime::now();
const auto date = parent->data()->date();
return (now < date || now - date < period)
? std::make_unique<Live>(period)
: nullptr;
}
void Location::updateLiveStatus() {
const auto date = ResolveUpdateDate(_parent);
const auto now = base::unixtime::now();
const auto elapsed = now - date;
auto next = TimeId();
const auto text = [&] {
if (elapsed < 60) {
next = 60 - elapsed;
return tr::lng_live_location_now(tr::now);
} else if (const auto minutes = elapsed / 60; minutes < 60) {
next = 60 - (elapsed % 60);
return tr::lng_live_location_minutes(tr::now, lt_count, minutes);
} else if (const auto hours = elapsed / 3600; hours < 12) {
next = 3600 - (elapsed % 3600);
return tr::lng_live_location_hours(tr::now, lt_count, hours);
}
const auto dateFull = base::unixtime::parse(date);
const auto nowFull = base::unixtime::parse(now);
const auto nextTomorrow = [&] {
const auto tomorrow = nowFull.date().addDays(1);
next = nowFull.secsTo(QDateTime(tomorrow, QTime(0, 0)));
};
const auto locale = QLocale();
const auto format = QLocale::ShortFormat;
if (dateFull.date() == nowFull.date()) {
nextTomorrow();
const auto time = locale.toString(dateFull.time(), format);
return tr::lng_live_location_today(tr::now, lt_time, time);
} else if (dateFull.date().addDays(1) == nowFull.date()) {
nextTomorrow();
const auto time = locale.toString(dateFull.time(), format);
return tr::lng_live_location_yesterday(tr::now, lt_time, time);
}
return tr::lng_live_location_date_time(
tr::now,
lt_date,
locale.toString(dateFull.date(), format),
lt_time,
locale.toString(dateFull.time(), format));
}();
_description.setMarkedText(
st::webPageDescriptionStyle,
{ text },
Ui::WebpageTextDescriptionOptions());
if (next > 0 && next < 86400) {
_live->updateStatusTimer.callOnce(next * crl::time(1000));
}
}
QImage Location::locationTakeImage() {
if (_media && !_media->isNull()) {
return *_media;
} else if (_live && !_live->previous.isNull()) {
return _live->previous;
}
return {};
}
void Location::unloadHeavyPart() {
_media = nullptr;
if (_live) {
_live->previous = QImage();
}
}
bool Location::hasHeavyPart() const {
return (_media != nullptr) || (_live && !_live->previous.isNull());
}
void Location::ensureMediaCreated() const {
if (_media) {
return;
}
_media = _data->createView();
_data->load(&history()->session(), _parent->data()->fullId());
history()->owner().registerHeavyViewPart(_parent);
}
QSize Location::countOptimalSize() {
auto tw = fullWidth();
auto th = fullHeight();
if (tw > st::maxMediaSize) {
th = (st::maxMediaSize * th) / tw;
tw = st::maxMediaSize;
}
auto minWidth = std::clamp(
_parent->minWidthForMedia(),
st::minPhotoSize,
st::maxMediaSize);
auto maxWidth = qMax(tw, minWidth);
auto minHeight = qMax(th, st::minPhotoSize);
if (_parent->hasBubble()) {
if (!_title.isEmpty()) {
minHeight += qMin(_title.countHeight(maxWidth - st::msgPadding.left() - st::msgPadding.right()), 2 * st::webPageTitleFont->height);
}
if (!_description.isEmpty()) {
minHeight += qMin(_description.countHeight(maxWidth - st::msgPadding.left() - st::msgPadding.right()), 3 * st::webPageDescriptionFont->height);
}
if (!_title.isEmpty() || !_description.isEmpty()) {
minHeight += st::mediaInBubbleSkip;
if (_live) {
if (isBubbleBottom()) {
minHeight += st::msgPadding.bottom();
}
} else {
if (isBubbleTop()) {
minHeight += st::msgPadding.top();
}
}
}
}
return { maxWidth, minHeight };
}
QSize Location::countCurrentSize(int newWidth) {
accumulate_min(newWidth, maxWidth());
auto tw = fullWidth();
auto th = fullHeight();
if (tw > st::maxMediaSize) {
th = (st::maxMediaSize * th) / tw;
tw = st::maxMediaSize;
}
auto newHeight = th;
if (tw > newWidth) {
newHeight = (newWidth * newHeight / tw);
} else {
newWidth = tw;
}
auto minWidth = std::clamp(
_parent->minWidthForMedia(),
st::minPhotoSize,
std::min(newWidth, st::maxMediaSize));
accumulate_max(newWidth, minWidth);
accumulate_max(newHeight, st::minPhotoSize);
if (_live) {
_live->thumbnailHeight = newHeight;
}
if (_parent->hasBubble()) {
if (!_title.isEmpty()) {
newHeight += qMin(_title.countHeight(newWidth - st::msgPadding.left() - st::msgPadding.right()), st::webPageTitleFont->height * 2);
}
if (!_description.isEmpty()) {
newHeight += qMin(_description.countHeight(newWidth - st::msgPadding.left() - st::msgPadding.right()), st::webPageDescriptionFont->height * 3);
}
if (!_title.isEmpty() || !_description.isEmpty()) {
newHeight += st::mediaInBubbleSkip;
if (_live) {
if (isBubbleBottom()) {
newHeight += st::msgPadding.bottom();
}
} else {
if (isBubbleTop()) {
newHeight += st::msgPadding.top();
}
}
}
}
return { newWidth, newHeight };
}
TextSelection Location::toDescriptionSelection(
TextSelection selection) const {
return UnshiftItemSelection(selection, _title);
}
TextSelection Location::fromDescriptionSelection(
TextSelection selection) const {
return ShiftItemSelection(selection, _title);
}
void Location::draw(Painter &p, const PaintContext &context) const {
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
return;
}
auto paintx = 0, painty = 0, paintw = width(), painth = height();
bool bubble = _parent->hasBubble();
const auto st = context.st;
const auto stm = context.messageStyle();
const auto hasText = !_title.isEmpty() || !_description.isEmpty();
const auto rounding = adjustedBubbleRounding(_live
? RectPart::FullBottom
: hasText
? RectPart::FullTop
: RectPart());
const auto paintText = [&] {
if (_live) {
painty += st::mediaInBubbleSkip;
} else if (!hasText) {
return;
} else if (isBubbleTop()) {
painty += st::msgPadding.top();
}
auto textw = width() - st::msgPadding.left() - st::msgPadding.right();
p.setPen(stm->historyTextFg);
if (!_title.isEmpty()) {
_title.drawLeftElided(p, paintx + st::msgPadding.left(), painty, textw, width(), 2, style::al_left, 0, -1, 0, false, context.selection);
painty += qMin(_title.countHeight(textw), 2 * st::webPageTitleFont->height);
}
if (!_description.isEmpty()) {
if (_live) {
p.setPen(stm->msgDateFg);
}
_description.drawLeftElided(p, paintx + st::msgPadding.left(), painty, textw, width(), 3, style::al_left, 0, -1, 0, false, toDescriptionSelection(context.selection));
painty += qMin(_description.countHeight(textw), 3 * st::webPageDescriptionFont->height);
}
if (!_live) {
painty += st::mediaInBubbleSkip;
painth -= painty;
}
};
if (!_live) {
paintText();
}
const auto thumbh = _live ? _live->thumbnailHeight : painth;
auto rthumb = QRect(paintx, painty, paintw, thumbh);
if (!bubble) {
fillImageShadow(p, rthumb, rounding, context);
}
ensureMediaCreated();
validateImageCache(rthumb.size(), rounding);
const auto paintPrevious = _live && !_live->previous.isNull();
auto opacity = _imageCache.isNull() ? 0. : 1.;
if (paintPrevious) {
opacity = _live->crossfade.value(opacity);
if (opacity < 1.) {
p.drawImage(rthumb.topLeft(), _live->previousCache);
if (opacity > 0.) {
p.setOpacity(opacity);
}
}
}
if (!_imageCache.isNull() && opacity > 0.) {
p.drawImage(rthumb.topLeft(), _imageCache);
if (opacity < 1.) {
p.setOpacity(1.);
}
} else if (!bubble && !paintPrevious) {
Ui::PaintBubble(
p,
Ui::SimpleBubble{
.st = context.st,
.geometry = rthumb,
.pattern = context.bubblesPattern,
.patternViewport = context.viewport,
.outerWidth = width(),
.selected = context.selected(),
.outbg = context.outbg,
.rounding = rounding,
});
}
const auto paintMarker = [&](const style::icon &icon) {
icon.paint(
p,
rthumb.x() + ((rthumb.width() - icon.width()) / 2),
rthumb.y() + (rthumb.height() / 2) - icon.height(),
width());
};
paintMarker(st->historyMapPoint());
paintMarker(st->historyMapPointInner());
if (context.selected()) {
fillImageOverlay(p, rthumb, rounding, context);
}
if (_live) {
painty += _live->thumbnailHeight;
painth -= _live->thumbnailHeight;
paintLiveRemaining(p, context, { paintx, painty, paintw, painth });
paintText();
} else if (_parent->media() == this) {
auto fullRight = paintx + paintw;
auto fullBottom = height();
_parent->drawInfo(
p,
context,
fullRight,
fullBottom,
paintx * 2 + paintw,
InfoDisplayType::Image);
if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {
auto fastShareLeft = _parent->hasRightLayout()
? (paintx - size->width() - st::historyFastShareLeft)
: (fullRight + st::historyFastShareLeft);
auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
_parent->drawRightAction(p, context, fastShareLeft, fastShareTop, 2 * paintx + paintw);
}
}
}
void Location::paintLiveRemaining(
QPainter &p,
const PaintContext &context,
QRect bottom) const {
const auto size = st::liveLocationRemainingSize;
const auto skip = (bottom.height() - size) / 2;
const auto rect = QRect(
bottom.x() + bottom.width() - size - skip,
bottom.y() + skip,
size,
size);
auto hq = PainterHighQualityEnabler(p);
const auto stm = context.messageStyle();
const auto color = stm->msgServiceFg->c;
const auto untiloff = (_live->period == kUntilOffPeriod);
const auto progress = RemainingTimeProgress(_parent, _live->period);
const auto part = 1. / 360;
const auto full = (progress >= 1. - part);
auto elapsed = color;
if (!full) {
elapsed.setAlphaF(elapsed.alphaF() * kLiveElapsedPartOpacity);
}
auto pen = QPen(elapsed);
const auto stroke = style::ConvertScaleExact(2.);
pen.setWidthF(stroke);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
p.drawEllipse(rect);
if (untiloff) {
stm->liveLocationLongIcon.paintInCenter(p, rect);
} else {
if (!full && progress > part) {
auto pen = QPen(color);
pen.setWidthF(stroke);
p.setPen(pen);
p.drawArc(
rect,
arc::kQuarterLength,
int(base::SafeRound(arc::kFullLength * progress)));
}
p.setPen(stm->msgServiceFg);
p.setFont(st::semiboldFont);
const auto text = RemainingTimeText(_parent, _live->period);
p.drawText(rect, text, style::al_center);
const auto each = std::clamp(_live->period / 360, 1, 86400);
_live->updateRemainingTimer.callOnce(each * crl::time(1000));
}
}
void Location::validateImageCache(
QSize outer,
Ui::BubbleRounding rounding) const {
Expects(_media != nullptr);
if (_live && !_live->previous.isNull()) {
validateImageCache(
_live->previous,
_live->previousCache,
_live->previousRounding,
outer,
rounding);
}
validateImageCache(
*_media,
_imageCache,
_imageCacheRounding,
outer,
rounding);
checkLiveCrossfadeStart();
}
void Location::checkLiveCrossfadeStart() const {
if (!_live
|| _live->previous.isNull()
|| !_media
|| _media->isNull()
|| _live->crossfade.animating()) {
return;
}
_live->crossfade.start([=] {
if (!_live->crossfade.animating()) {
_live->previous = QImage();
_live->previousCache = QImage();
}
_parent->repaint();
}, 0., 1., st::fadeWrapDuration);
}
void Location::validateImageCache(
const QImage &source,
QImage &cache,
Ui::BubbleRounding &cacheRounding,
QSize outer,
Ui::BubbleRounding rounding) const {
if (source.isNull()) {
return;
}
const auto ratio = style::DevicePixelRatio();
if (cache.size() == (outer * ratio) && cacheRounding == rounding) {
return;
}
cache = Images::Round(
source.scaled(
outer * ratio,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation),
MediaRoundingMask(rounding));
cache.setDevicePixelRatio(ratio);
cacheRounding = rounding;
}
TextState Location::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
auto symbolAdd = 0;
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
return result;
}
auto paintx = 0, painty = 0, paintw = width(), painth = height();
bool bubble = _parent->hasBubble();
auto checkText = [&] {
if (_live) {
painty += st::mediaInBubbleSkip;
} else if (_title.isEmpty() && _description.isEmpty()) {
return false;
} else if (isBubbleTop()) {
painty += st::msgPadding.top();
}
auto textw = width() - st::msgPadding.left() - st::msgPadding.right();
if (!_title.isEmpty()) {
auto titleh = qMin(_title.countHeight(textw), 2 * st::webPageTitleFont->height);
if (point.y() >= painty && point.y() < painty + titleh) {
result = TextState(_parent, _title.getStateLeft(
point - QPoint(paintx + st::msgPadding.left(), painty),
textw,
width(),
request.forText()));
return true;
} else if (point.y() >= painty + titleh) {
symbolAdd += _title.length();
}
painty += titleh;
}
if (!_description.isEmpty()) {
auto descriptionh = qMin(_description.countHeight(textw), 3 * st::webPageDescriptionFont->height);
if (point.y() >= painty && point.y() < painty + descriptionh) {
result = TextState(_parent, _description.getStateLeft(
point - QPoint(paintx + st::msgPadding.left(), painty),
textw,
width(),
request.forText()));
result.symbol += symbolAdd;
return true;
} else if (point.y() >= painty + descriptionh) {
symbolAdd += _description.length();
}
painty += descriptionh;
}
if (!_title.isEmpty() || !_description.isEmpty()) {
painty += st::mediaInBubbleSkip;
}
painth -= painty;
return false;
};
if (!_live && checkText()) {
return result;
}
const auto thumbh = _live ? _live->thumbnailHeight : painth;
if (QRect(paintx, painty, paintw, thumbh).contains(point) && _data) {
result.link = _link;
}
if (_live) {
painty += _live->thumbnailHeight;
painth -= _live->thumbnailHeight;
if (checkText()) {
return result;
}
} else if (_parent->media() == this) {
auto fullRight = paintx + paintw;
auto fullBottom = height();
const auto bottomInfoResult = _parent->bottomInfoTextState(
fullRight,
fullBottom,
point,
InfoDisplayType::Image);
if (bottomInfoResult.link
|| bottomInfoResult.cursor != CursorState::None
|| bottomInfoResult.customTooltip) {
return bottomInfoResult;
}
if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {
auto fastShareLeft = _parent->hasRightLayout()
? (paintx - size->width() - st::historyFastShareLeft)
: (fullRight + st::historyFastShareLeft);
auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
if (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) {
result.link = _parent->rightActionLink(point
- QPoint(fastShareLeft, fastShareTop));
}
}
}
result.symbol += symbolAdd;
return result;
}
TextSelection Location::adjustSelection(TextSelection selection, TextSelectType type) const {
if (_description.isEmpty() || selection.to <= _title.length()) {
return _title.adjustSelection(selection, type);
}
auto descriptionSelection = _description.adjustSelection(toDescriptionSelection(selection), type);
if (selection.from >= _title.length()) {
return fromDescriptionSelection(descriptionSelection);
}
auto titleSelection = _title.adjustSelection(selection, type);
return { titleSelection.from, fromDescriptionSelection(descriptionSelection).to };
}
TextForMimeData Location::selectedText(TextSelection selection) const {
auto titleResult = _title.toTextForMimeData(selection);
auto descriptionResult = _description.toTextForMimeData(
toDescriptionSelection(selection));
if (titleResult.empty()) {
return descriptionResult;
} else if (descriptionResult.empty()) {
return titleResult;
}
return titleResult.append('\n').append(std::move(descriptionResult));
}
bool Location::needsBubble() const {
if (!_title.isEmpty() || !_description.isEmpty()) {
return true;
}
const auto item = _parent->data();
return item->repliesAreComments()
|| item->externalReply()
|| item->viaBot()
|| _parent->displayReply()
|| _parent->displayForwardedFrom()
|| _parent->displayFromName()
|| _parent->displayedTopicButton();
}
QPoint Location::resolveCustomInfoRightBottom() const {
const auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x());
const auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y());
return QPoint(width() - skipx, height() - skipy);
}
int Location::fullWidth() const {
return st::locationSize.width();
}
int Location::fullHeight() const {
return st::locationSize.height();
}
} // namespace HistoryView

View File

@@ -0,0 +1,121 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
#include "data/data_location.h"
namespace Data {
class CloudImage;
} // namespace Data
namespace HistoryView {
class Location : public Media {
public:
Location(
not_null<Element*> parent,
not_null<Data::CloudImage*> data,
Data::LocationPoint point,
Element *replacing = nullptr,
TimeId livePeriod = 0);
Location(
not_null<Element*> parent,
not_null<Data::CloudImage*> data,
Data::LocationPoint point,
const QString &title,
const QString &description);
~Location();
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
[[nodiscard]] TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const override;
uint16 fullSelectionLength() const override {
return _title.length() + _description.length();
}
bool hasTextForCopy() const override {
return !_title.isEmpty() || !_description.isEmpty();
}
bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
return p == _link;
}
bool dragItemByHandler(const ClickHandlerPtr &p) const override {
return p == _link;
}
TextForMimeData selectedText(TextSelection selection) const override;
bool needsBubble() const override;
bool customInfoLayout() const override {
return true;
}
QPoint resolveCustomInfoRightBottom() const override;
bool skipBubbleTail() const override {
return isRoundedInBubbleBottom();
}
QImage locationTakeImage() override;
void unloadHeavyPart() override;
bool hasHeavyPart() const override;
private:
struct Live;
[[nodiscard]] static std::unique_ptr<Live> CreateLiveTracker(
not_null<Element*> parent,
TimeId period);
void ensureMediaCreated() const;
void validateImageCache(
QSize outer,
Ui::BubbleRounding rounding) const;
void validateImageCache(
const QImage &source,
QImage &cache,
Ui::BubbleRounding &cacheRounding,
QSize outer,
Ui::BubbleRounding rounding) const;
void paintLiveRemaining(
QPainter &p,
const PaintContext &context,
QRect bottom) const;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
[[nodiscard]] TextSelection toDescriptionSelection(
TextSelection selection) const;
[[nodiscard]] TextSelection fromDescriptionSelection(
TextSelection selection) const;
[[nodiscard]] int fullWidth() const;
[[nodiscard]] int fullHeight() const;
void checkLiveCrossfadeStart() const;
void updateLiveStatus();
void checkLiveFinish();
const not_null<Data::CloudImage*> _data;
mutable std::unique_ptr<Live> _live;
mutable std::shared_ptr<QImage> _media;
Ui::Text::String _title, _description;
ClickHandlerPtr _link;
mutable QImage _imageCache;
mutable Ui::BubbleRounding _imageCacheRounding;
};
} // namespace HistoryView

View File

@@ -0,0 +1,653 @@
/*
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/media/history_view_media.h"
#include "boxes/send_credits_box.h" // CreditsEmoji.
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_text_helper.h"
#include "history/view/media/history_view_media_common.h"
#include "history/view/media/history_view_media_spoiler.h"
#include "history/view/media/history_view_sticker.h"
#include "storage/storage_shared_media.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
#include "lang/lang_keys.h"
#include "ui/item_text_options.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/message_bubble.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/image/image_prepare.h"
#include "ui/cached_round_corners.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/text/text_utilities.h"
#include "core/ui_integration.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h" // mediaMenuIconStealth.
namespace HistoryView {
namespace {
[[nodiscard]] TimeId TimeFromMatch(
QStringView hours,
QStringView minutes1,
QStringView minutes2,
QStringView seconds) {
auto ok1 = true;
auto ok2 = true;
auto ok3 = true;
auto minutes = minutes1.toString();
minutes += minutes2;
const auto value1 = (hours.isEmpty() ? 0 : hours.toInt(&ok1));
const auto value2 = minutes.toInt(&ok2);
const auto value3 = seconds.toInt(&ok3);
const auto ok = ok1 && ok2 && ok3;
return (ok && value3 < 60 && (hours.isEmpty() || value2 < 60))
? (value1 * 3600 + value2 * 60 + value3)
: -1;
}
} // namespace
TimeId DurationForTimestampLinks(not_null<DocumentData*> document) {
if (!document->isVideoFile()
&& !document->isSong()
&& !document->isVoiceMessage()) {
return TimeId(0);
}
return std::max(document->duration(), crl::time(0)) / 1000;
}
QString TimestampLinkBase(
not_null<DocumentData*> document,
FullMsgId context) {
return QString(
"media_timestamp?base=doc%1_%2_%3&t="
).arg(document->id).arg(context.peer.value).arg(context.msg.bare);
}
TimeId DurationForTimestampLinks(not_null<WebPageData*> webpage) {
if (!webpage->collage.items.empty()) {
return 0;
} else if (const auto document = webpage->document) {
return DurationForTimestampLinks(document);
} else if (webpage->type != WebPageType::Video
|| webpage->siteName != u"YouTube"_q) {
return TimeId(0);
} else if (webpage->duration > 0) {
return webpage->duration;
}
constexpr auto kMaxYouTubeTimestampDuration = 100 * 60 * TimeId(60);
return kMaxYouTubeTimestampDuration;
}
QString TimestampLinkBase(
not_null<WebPageData*> webpage,
FullMsgId context) {
const auto url = webpage->url;
if (url.isEmpty()) {
return QString();
}
auto parts = url.split(QChar('#'));
const auto base = parts[0];
parts.pop_front();
const auto use = [&] {
const auto query = base.indexOf(QChar('?'));
if (query < 0) {
return base + QChar('?');
}
auto params = base.mid(query + 1).split(QChar('&'));
for (auto i = params.begin(); i != params.end();) {
if (i->startsWith("t=")) {
i = params.erase(i);
} else {
++i;
}
}
return base.mid(0, query)
+ (params.empty() ? "?" : ("?" + params.join(QChar('&')) + "&"));
}();
return "url:"
+ use
+ "t="
+ (parts.empty() ? QString() : ("#" + parts.join(QChar('#'))));
}
TextWithEntities AddTimestampLinks(
TextWithEntities text,
TimeId duration,
const QString &base) {
if (base.isEmpty()) {
return text;
}
static const auto expression = QRegularExpression(
"(?<![^\\s\\(\\)\"\\,\\.\\-])"
"(?:(?:(\\d{1,2}):)?(\\d))?(\\d):(\\d\\d)"
"(?![^\\s\\(\\)\",\\.\\-\\+])");
const auto &string = text.text;
auto offset = 0;
while (true) {
const auto m = expression.match(string, offset);
if (!m.hasMatch()) {
break;
}
const auto from = m.capturedStart();
const auto till = from + m.capturedLength();
offset = till;
const auto time = TimeFromMatch(
m.capturedView(1),
m.capturedView(2),
m.capturedView(3),
m.capturedView(4));
if (time < 0 || time > duration) {
continue;
}
auto &entities = text.entities;
auto i = ranges::lower_bound(
entities,
from,
std::less<>(),
&EntityInText::offset);
while (i != entities.end()
&& i->offset() < till
&& i->type() == EntityType::Spoiler) {
++i;
}
if (i != entities.end() && i->offset() < till) {
continue;
}
const auto intersects = [&](const EntityInText &entity) {
return (entity.offset() + entity.length() > from)
&& (entity.type() != EntityType::Spoiler);
};
auto j = std::make_reverse_iterator(i);
const auto e = std::make_reverse_iterator(entities.begin());
if (std::find_if(j, e, intersects) != e) {
continue;
}
entities.insert(
i,
EntityInText(
EntityType::CustomUrl,
from,
till - from,
("internal:" + base + QString::number(time))));
}
return text;
}
Storage::SharedMediaTypesMask Media::sharedMediaTypes() const {
return {};
}
bool Media::allowTextSelectionByHandler(
const ClickHandlerPtr &handler) const {
return false;
}
not_null<Element*> Media::parent() const {
return _parent;
}
not_null<History*> Media::history() const {
return _parent->history();
}
SelectedQuote Media::selectedQuote(TextSelection selection) const {
return {};
}
QSize Media::countCurrentSize(int newWidth) {
return QSize(qMin(newWidth, maxWidth()), minHeight());
}
bool Media::hasPurchasedTag() const {
if (const auto media = parent()->data()->media()) {
if (const auto invoice = media->invoice()) {
if (invoice->isPaidMedia && !invoice->extendedMedia.empty()) {
const auto photo = invoice->extendedMedia.front()->photo();
return !photo || !photo->extendedMediaPreview();
}
}
}
return false;
}
void Media::drawPurchasedTag(
Painter &p,
QRect outer,
const PaintContext &context) const {
const auto purchased = parent()->enforcePurchasedTag();
if (purchased->text.isEmpty()) {
const auto item = parent()->data();
const auto media = item->media();
const auto invoice = media ? media->invoice() : nullptr;
const auto amount = invoice ? invoice->amount : 0;
if (!amount) {
return;
}
auto text = Ui::Text::Colorized(Ui::CreditsEmojiSmall());
text.append(Lang::FormatCountDecimal(amount));
purchased->text.setMarkedText(
st::defaultTextStyle,
text,
kMarkupTextOptions);
}
const auto st = context.st;
const auto sti = context.imageStyle();
const auto &padding = st::purchasedTagPadding;
auto right = outer.x() + outer.width();
auto top = outer.y();
right -= st::msgDateImgDelta + padding.right();
top += st::msgDateImgDelta + padding.top();
const auto size = QSize(
purchased->text.maxWidth(),
st::normalFont->height);
const auto tagX = right - size.width();
const auto tagY = top;
const auto tagW = padding.left() + size.width() + padding.right();
const auto tagH = padding.top() + size.height() + padding.bottom();
Ui::FillRoundRect(
p,
tagX - padding.left(),
tagY - padding.top(),
tagW,
tagH,
sti->msgDateImgBg,
sti->msgDateImgBgCorners);
p.setPen(st->msgDateImgFg());
purchased->text.draw(p, {
.position = { tagX, tagY },
.outerWidth = width(),
.availableWidth = size.width(),
.palette = &st->priceTagTextPalette(),
});
}
void Media::fillImageShadow(
QPainter &p,
QRect rect,
Ui::BubbleRounding rounding,
const PaintContext &context) const {
const auto sti = context.imageStyle();
auto corners = Ui::CornersPixmaps();
const auto choose = [&](int index) -> QPixmap {
using Corner = Ui::BubbleCornerRounding;
switch (rounding[index]) {
case Corner::Large: return sti->msgShadowCornersLarge.p[index];
case Corner::Small: return sti->msgShadowCornersSmall.p[index];
}
return QPixmap();
};
corners.p[2] = choose(2);
corners.p[3] = choose(3);
Ui::FillRoundShadow(p, rect, sti->msgShadow, corners);
}
void Media::fillImageOverlay(
QPainter &p,
QRect rect,
std::optional<Ui::BubbleRounding> rounding,
const PaintContext &context) const {
using Radius = Ui::CachedCornerRadius;
const auto &st = context.st;
if (!rounding) {
Ui::FillComplexOverlayRect(
p,
rect,
st->msgSelectOverlay(),
st->msgSelectOverlayCorners(Radius::Small));
return;
}
using Corner = Ui::BubbleCornerRounding;
auto corners = Ui::CornersPixmaps();
const auto lookup = [&](Corner corner) {
switch (corner) {
case Corner::None: return Radius::kCount;
case Corner::Small: return Radius::BubbleSmall;
case Corner::Large: return Radius::BubbleLarge;
}
Unexpected("Corner value in Document::fillThumbnailOverlay.");
};
for (auto i = 0; i != 4; ++i) {
const auto radius = lookup((*rounding)[i]);
corners.p[i] = (radius == Radius::kCount)
? QPixmap()
: st->msgSelectOverlayCorners(radius).p[i];
}
Ui::FillComplexOverlayRect(p, rect, st->msgSelectOverlay(), corners);
}
void Media::fillImageSpoiler(
QPainter &p,
not_null<MediaSpoiler*> spoiler,
QRect rect,
const PaintContext &context) const {
if (!spoiler->animation) {
spoiler->animation = std::make_unique<Ui::SpoilerAnimation>([=] {
_parent->customEmojiRepaint();
});
history()->owner().registerHeavyViewPart(_parent);
}
_parent->clearCustomEmojiRepaint();
const auto pausedSpoiler = context.paused
|| On(PowerSaving::kChatSpoiler);
Ui::FillSpoilerRect(
p,
rect,
MediaRoundingMask(spoiler->backgroundRounding),
Ui::DefaultImageSpoiler().frame(
spoiler->animation->index(context.now, pausedSpoiler)),
spoiler->cornerCache);
}
void Media::drawSpoilerTag(
Painter &p,
not_null<MediaSpoiler*> spoiler,
std::unique_ptr<MediaSpoilerTag> &tag,
QRect rthumb,
const PaintContext &context,
Fn<QImage()> generateBackground) const {
if (!tag) {
setupSpoilerTag(tag);
if (!tag) {
return;
}
}
const auto revealed = spoiler->revealAnimation.value(
spoiler->revealed ? 1. : 0.);
if (revealed == 1.) {
return;
}
p.setOpacity(1. - revealed);
const auto st = context.st;
const auto darken = st->msgDateImgBg()->c;
const auto fg = st->msgDateImgFg()->c;
const auto star = st->creditsBg1()->c;
if (tag->cache.isNull()
|| tag->darken != darken
|| tag->fg != fg
|| tag->star != star) {
const auto ratio = style::DevicePixelRatio();
auto bg = generateBackground();
if (bg.isNull()) {
bg = QImage(ratio, ratio, QImage::Format_ARGB32_Premultiplied);
bg.fill(Qt::black);
}
auto text = Ui::Text::String();
auto iconSkip = 0;
if (tag->sensitive) {
text.setText(
st::semiboldTextStyle,
tr::lng_sensitive_tag(tr::now));
iconSkip = st::mediaMenuIconStealth.width() * 1.4;
} else {
auto price = Ui::Text::Colorized(Ui::CreditsEmoji());
price.append(Lang::FormatCountDecimal(tag->price));
text.setMarkedText(
st::semiboldTextStyle,
tr::lng_paid_price(
tr::now,
lt_price,
price,
tr::marked),
kMarkupTextOptions);
}
const auto width = iconSkip + text.maxWidth();
const auto inner = QRect(0, 0, width, text.minHeight());
const auto outer = inner.marginsAdded(st::paidTagPadding);
const auto size = outer.size();
const auto radius = std::min(size.width(), size.height()) / 2;
auto cache = QImage(
size * ratio,
QImage::Format_ARGB32_Premultiplied);
cache.setDevicePixelRatio(ratio);
cache.fill(Qt::black);
auto p = Painter(&cache);
auto hq = PainterHighQualityEnabler(p);
p.drawImage(
QRect(
(size.width() - rthumb.width()) / 2,
(size.height() - rthumb.height()) / 2,
rthumb.width(),
rthumb.height()),
bg);
p.fillRect(QRect(QPoint(), size), darken);
p.setPen(fg);
p.setTextPalette(st->priceTagTextPalette());
if (iconSkip) {
st::mediaMenuIconStealth.paint(
p,
-outer.x(),
(size.height() - st::mediaMenuIconStealth.height()) / 2,
size.width(),
fg);
}
text.draw(p, iconSkip - outer.x(), -outer.y(), width);
p.end();
tag->darken = darken;
tag->fg = fg;
tag->cache = Images::Round(
std::move(cache),
Images::CornersMask(radius));
}
const auto &cache = tag->cache;
const auto size = cache.size() / cache.devicePixelRatio();
const auto left = rthumb.x() + (rthumb.width() - size.width()) / 2;
const auto top = rthumb.y() + (rthumb.height() - size.height()) / 2;
p.drawImage(left, top, cache);
if (context.selected()) {
auto hq = PainterHighQualityEnabler(p);
const auto radius = std::min(size.width(), size.height()) / 2;
p.setPen(Qt::NoPen);
p.setBrush(st->msgSelectOverlay());
p.drawRoundedRect(
QRect(left, top, size.width(), size.height()),
radius,
radius);
}
p.setOpacity(1.);
}
void Media::setupSpoilerTag(std::unique_ptr<MediaSpoilerTag> &tag) const {
const auto item = parent()->data();
if (item->isMediaSensitive()) {
tag = std::make_unique<MediaSpoilerTag>();
tag->sensitive = 1;
return;
}
const auto media = parent()->data()->media();
const auto invoice = media ? media->invoice() : nullptr;
if (const auto price = invoice->isPaidMedia ? invoice->amount : 0) {
tag = std::make_unique<MediaSpoilerTag>();
tag->price = price;
}
}
ClickHandlerPtr Media::spoilerTagLink(
not_null<MediaSpoiler*> spoiler,
std::unique_ptr<MediaSpoilerTag> &tag) const {
const auto item = parent()->data();
if (!item->isRegular() || spoiler->revealed) {
return nullptr;
} else if (!tag) {
setupSpoilerTag(tag);
if (!tag) {
return nullptr;
}
}
if (!tag->link) {
tag->link = tag->sensitive
? MakeSensitiveMediaLink(spoiler->link, item)
: MakePaidMediaLink(item);
}
return tag->link;
}
void Media::createSpoilerLink(not_null<MediaSpoiler*> spoiler) {
const auto weak = base::make_weak(this);
spoiler->link = std::make_shared<LambdaClickHandler>([weak, spoiler](
const ClickContext &context) {
const auto button = context.button;
const auto media = weak.get();
if (button != Qt::LeftButton || !media || spoiler->revealed) {
return;
}
const auto view = media->parent();
spoiler->revealed = true;
spoiler->revealAnimation.start([=] {
view->repaint();
}, 0., 1., st::fadeWrapDuration);
view->repaint();
media->history()->owner().registerShownSpoiler(view);
});
}
void Media::repaint() const {
_parent->repaint();
}
Ui::Text::String Media::createCaption(not_null<HistoryItem*> item) const {
if (item->emptyText()) {
return {};
}
const auto minResizeWidth = st::minPhotoSize
- st::msgPadding.left()
- st::msgPadding.right();
auto result = Ui::Text::String(minResizeWidth);
const auto context = Core::TextContext({
.session = &history()->session(),
.repaint = [=] { _parent->customEmojiRepaint(); },
});
result.setMarkedText(
st::messageTextStyle,
item->translatedTextWithLocalEntities(),
Ui::ItemTextOptions(item),
context);
InitElementTextPart(_parent, result);
if (const auto width = _parent->skipBlockWidth()) {
result.updateSkipBlock(width, _parent->skipBlockHeight());
}
return result;
}
TextSelection Media::skipSelection(TextSelection selection) const {
return UnshiftItemSelection(selection, fullSelectionLength());
}
TextSelection Media::unskipSelection(TextSelection selection) const {
return ShiftItemSelection(selection, fullSelectionLength());
}
auto Media::getBubbleSelectionIntervals(
TextSelection selection) const
-> std::vector<Ui::BubbleSelectionInterval> {
return {};
}
bool Media::usesBubblePattern(const PaintContext &context) const {
return _parent->usesBubblePattern(context);
}
PointState Media::pointState(QPoint point) const {
return QRect(0, 0, width(), height()).contains(point)
? PointState::Inside
: PointState::Outside;
}
std::unique_ptr<StickerPlayer> Media::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return nullptr;
}
QImage Media::locationTakeImage() {
return QImage();
}
std::vector<Media::TodoTaskInfo> Media::takeTasksInfo() {
return {};
}
TextState Media::getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const {
Unexpected("Grouping method call.");
}
Ui::BubbleRounding Media::adjustedBubbleRounding(RectParts square) const {
auto result = bubbleRounding();
using Corner = Ui::BubbleCornerRounding;
const auto adjust = [&](bool round, Corner already, RectPart corner) {
return (already == Corner::Tail || !round || (square & corner))
? Corner::None
: already;
};
const auto top = isBubbleTop();
const auto bottom = isRoundedInBubbleBottom();
result.topLeft = adjust(top, result.topLeft, RectPart::TopLeft);
result.topRight = adjust(top, result.topRight, RectPart::TopRight);
result.bottomLeft = adjust(
bottom,
result.bottomLeft,
RectPart::BottomLeft);
result.bottomRight = adjust(
bottom,
result.bottomRight,
RectPart::BottomRight);
return result;
}
HistoryItem *Media::itemForText() const {
return _parent->data();
}
bool Media::isRoundedInBubbleBottom() const {
return isBubbleBottom()
&& !_parent->data()->repliesAreComments()
&& !_parent->data()->externalReply();
}
Images::CornersMaskRef MediaRoundingMask(
std::optional<Ui::BubbleRounding> rounding) {
using Radius = Ui::CachedCornerRadius;
if (!rounding) {
return Images::CornersMaskRef(Ui::CachedCornersMasks(Radius::Small));
}
using Corner = Ui::BubbleCornerRounding;
auto result = Images::CornersMaskRef();
const auto &small = Ui::CachedCornersMasks(Radius::BubbleSmall);
const auto &large = Ui::CachedCornersMasks(Radius::BubbleLarge);
for (auto i = 0; i != 4; ++i) {
switch ((*rounding)[i]) {
case Corner::Small: result.p[i] = &small[i]; break;
case Corner::Large: result.p[i] = &large[i]; break;
}
}
return result;
}
} // namespace HistoryView

View File

@@ -0,0 +1,439 @@
/*
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
*/
#pragma once
#include "history/view/history_view_object.h"
#include "ui/chat/message_bubble.h"
#include "ui/rect_part.h"
class History;
struct HistoryMessageEdited;
struct TextSelection;
namespace base {
template <typename Enum>
class enum_mask;
} // namespace base
namespace Storage {
enum class SharedMediaType : signed char;
using SharedMediaTypesMask = base::enum_mask<SharedMediaType>;
} // namespace Storage
namespace Lottie {
struct ColorReplacements;
} // namespace Lottie
namespace Ui {
struct BubbleSelectionInterval;
struct ChatPaintContext;
class SpoilerAnimation;
} // namespace Ui
namespace Images {
struct CornersMaskRef;
} // namespace Images
namespace HistoryView {
enum class PointState : char;
enum class CursorState : char;
enum class InfoDisplayType : char;
struct TextState;
struct StateRequest;
struct MediaSpoiler;
struct MediaSpoilerTag;
class StickerPlayer;
class Element;
struct SelectedQuote;
using PaintContext = Ui::ChatPaintContext;
enum class MediaInBubbleState : uchar {
None,
Top,
Middle,
Bottom,
};
[[nodiscard]] TimeId DurationForTimestampLinks(
not_null<DocumentData*> document);
[[nodiscard]] QString TimestampLinkBase(
not_null<DocumentData*> document,
FullMsgId context);
[[nodiscard]] TimeId DurationForTimestampLinks(
not_null<WebPageData*> webpage);
[[nodiscard]] QString TimestampLinkBase(
not_null<WebPageData*> webpage,
FullMsgId context);
[[nodiscard]] TextWithEntities AddTimestampLinks(
TextWithEntities text,
TimeId duration,
const QString &base);
struct PaidInformation {
int messages = 0;
int stars = 0;
explicit operator bool() const {
return stars != 0;
}
};
class Media : public Object, public base::has_weak_ptr {
public:
explicit Media(not_null<Element*> parent) : _parent(parent) {
}
[[nodiscard]] not_null<Element*> parent() const;
[[nodiscard]] not_null<History*> history() const;
[[nodiscard]] virtual TextForMimeData selectedText(
TextSelection selection) const {
return {};
}
[[nodiscard]] virtual SelectedQuote selectedQuote(
TextSelection selection) const;
[[nodiscard]] virtual TextSelection selectionFromQuote(
const SelectedQuote &quote) const {
return {};
}
[[nodiscard]] virtual bool isDisplayed() const {
return true;
}
virtual void updateNeedBubbleState() {
}
[[nodiscard]] virtual bool hasTextForCopy() const {
return false;
}
[[nodiscard]] virtual bool aboveTextByDefault() const {
return true;
}
[[nodiscard]] virtual HistoryItem *itemForText() const;
[[nodiscard]] virtual bool hideMessageText() const {
return true;
}
[[nodiscard]] virtual bool hideServiceText() const {
return false;
}
[[nodiscard]] virtual bool hideFromName() const {
return false;
}
[[nodiscard]] virtual bool allowsFastShare() const {
return false;
}
[[nodiscard]] virtual auto paidInformation() const
-> std::optional<PaidInformation> {
return {};
}
virtual void refreshParentId(not_null<HistoryItem*> realParent) {
}
virtual void drawHighlight(
Painter &p,
const PaintContext &context,
int top) const {
}
virtual void draw(Painter &p, const PaintContext &context) const = 0;
[[nodiscard]] virtual PointState pointState(QPoint point) const;
[[nodiscard]] virtual TextState textState(
QPoint point,
StateRequest request) const = 0;
virtual void updatePressed(QPoint point) {
}
[[nodiscard]] virtual Storage::SharedMediaTypesMask sharedMediaTypes() const;
// if we are in selecting items mode perhaps we want to
// toggle selection instead of activating the pressed link
[[nodiscard]] virtual bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const = 0;
[[nodiscard]] virtual bool allowTextSelectionByHandler(
const ClickHandlerPtr &p) const;
[[nodiscard]] virtual TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const {
return selection;
}
[[nodiscard]] virtual uint16 fullSelectionLength() const {
return 0;
}
[[nodiscard]] TextSelection skipSelection(
TextSelection selection) const;
[[nodiscard]] TextSelection unskipSelection(
TextSelection selection) const;
[[nodiscard]] virtual auto getBubbleSelectionIntervals(
TextSelection selection) const
-> std::vector<Ui::BubbleSelectionInterval>;
// if we press and drag this link should we drag the item
[[nodiscard]] virtual bool dragItemByHandler(
const ClickHandlerPtr &p) const = 0;
virtual void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
}
virtual void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
}
[[nodiscard]] virtual bool uploading() const {
return false;
}
[[nodiscard]] virtual PhotoData *getPhoto() const {
return nullptr;
}
[[nodiscard]] virtual DocumentData *getDocument() const {
return nullptr;
}
void playAnimation() {
playAnimation(false);
}
void autoplayAnimation() {
playAnimation(true);
}
virtual void stopAnimation() {
}
virtual void stickerClearLoopPlayed() {
}
virtual std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements);
virtual QImage locationTakeImage();
struct TodoTaskInfo {
int id = 0;
PeerData *completedBy = nullptr;
TimeId completionDate = TimeId();
};
virtual std::vector<TodoTaskInfo> takeTasksInfo();
virtual void checkAnimation() {
}
[[nodiscard]] virtual QSize sizeForGroupingOptimal(
int maxWidth,
bool last) const {
Unexpected("Grouping method call.");
}
[[nodiscard]] virtual QSize sizeForGrouping(int width) const {
Unexpected("Grouping method call.");
}
virtual void drawGrouped(
Painter &p,
const PaintContext &context,
const QRect &geometry,
RectParts sides,
Ui::BubbleRounding rounding,
float64 highlightOpacity,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const {
Unexpected("Grouping method call.");
}
[[nodiscard]] virtual TextState getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const;
virtual void drawSpoilerTag(
Painter &p,
QRect rthumb,
const PaintContext &context,
Fn<QImage()> generateBackground) const {
Unexpected("Spoiler tag method call.");
}
[[nodiscard]] virtual ClickHandlerPtr spoilerTagLink() const {
Unexpected("Spoiler tag method call.");
}
[[nodiscard]] virtual QImage spoilerTagBackground() const {
Unexpected("Spoiler tag method call.");
}
[[nodiscard]] virtual bool animating() const {
return false;
}
virtual void hideSpoilers() {
}
[[nodiscard]] virtual bool needsBubble() const = 0;
[[nodiscard]] virtual bool unwrapped() const {
return false;
}
[[nodiscard]] virtual bool customInfoLayout() const = 0;
[[nodiscard]] virtual QRect contentRectForReactions() const {
return QRect(0, 0, width(), height());
}
[[nodiscard]] virtual auto reactionButtonCenterOverride() const
-> std::optional<int> {
return std::nullopt;
}
[[nodiscard]] virtual QPoint resolveCustomInfoRightBottom() const {
return QPoint();
}
[[nodiscard]] virtual QMargins bubbleMargins() const {
return QMargins();
}
[[nodiscard]] virtual bool overrideEditedDate() const {
return false;
}
[[nodiscard]] virtual HistoryMessageEdited *displayedEditBadge() const {
Unexpected("displayedEditBadge() on non-grouped media.");
}
// An attach media in a web page can provide an
// additional text to be displayed below the attach.
// For example duration / progress for video messages.
[[nodiscard]] virtual QString additionalInfoString() const {
return QString();
}
void setInBubbleState(MediaInBubbleState state) {
_inBubbleState = state;
}
[[nodiscard]] MediaInBubbleState inBubbleState() const {
return _inBubbleState;
}
void setBubbleRounding(Ui::BubbleRounding rounding) {
_bubbleRounding = rounding;
}
[[nodiscard]] Ui::BubbleRounding bubbleRounding() const {
return _bubbleRounding;
}
[[nodiscard]] Ui::BubbleRounding adjustedBubbleRounding(
RectParts square = {}) const;
[[nodiscard]] bool isBubbleTop() const {
return (_inBubbleState == MediaInBubbleState::Top)
|| (_inBubbleState == MediaInBubbleState::None);
}
[[nodiscard]] bool isBubbleBottom() const {
return (_inBubbleState == MediaInBubbleState::Bottom)
|| (_inBubbleState == MediaInBubbleState::None);
}
[[nodiscard]] bool isRoundedInBubbleBottom() const;
[[nodiscard]] virtual bool skipBubbleTail() const {
return false;
}
// Sometimes webpages can force the bubble to fit their size instead of
// allowing message text to be as wide as possible (like wallpapers).
[[nodiscard]] virtual bool enforceBubbleWidth() const {
return false;
}
// Sometimes click on media in message is overloaded by the message:
// (for example it can open a link or a game instead of opening media)
// But the overloading click handler should be used only when media
// is already loaded (not a photo or GIF waiting for load with auto
// load being disabled - in such case media should handle the click).
[[nodiscard]] virtual bool isReadyForOpen() const {
return true;
}
struct BubbleRoll {
float64 rotate = 0.;
float64 scale = 1.;
explicit operator bool() const {
return (rotate != 0.) || (scale != 1.);
}
};
[[nodiscard]] virtual BubbleRoll bubbleRoll() const {
return BubbleRoll();
}
[[nodiscard]] virtual QMargins bubbleRollRepaintMargins() const {
return QMargins();
}
virtual void paintBubbleFireworks(
Painter &p,
const QRect &bubble,
crl::time ms) const {
}
[[nodiscard]] virtual bool customHighlight() const {
return false;
}
virtual bool hasHeavyPart() const {
return false;
}
virtual void unloadHeavyPart() {
}
// Should be called only by Data::Session.
virtual void updateSharedContactUserId(UserId userId) {
}
virtual void parentTextUpdated() {
}
virtual bool consumeHorizontalScroll(QPoint position, int delta) {
return false;
}
[[nodiscard]] bool hasPurchasedTag() const;
void drawPurchasedTag(
Painter &p,
QRect outer,
const PaintContext &context) const;
virtual ~Media() = default;
protected:
[[nodiscard]] QSize countCurrentSize(int newWidth) override;
[[nodiscard]] Ui::Text::String createCaption(
not_null<HistoryItem*> item) const;
virtual void playAnimation(bool autoplay) {
}
[[nodiscard]] bool usesBubblePattern(const PaintContext &context) const;
void fillImageShadow(
QPainter &p,
QRect rect,
Ui::BubbleRounding rounding,
const PaintContext &context) const;
void fillImageOverlay(
QPainter &p,
QRect rect,
std::optional<Ui::BubbleRounding> rounding, // nullopt if in WebPage.
const PaintContext &context) const;
void fillImageSpoiler(
QPainter &p,
not_null<MediaSpoiler*> spoiler,
QRect rect,
const PaintContext &context) const;
void drawSpoilerTag(
Painter &p,
not_null<MediaSpoiler*> spoiler,
std::unique_ptr<MediaSpoilerTag> &tag,
QRect rthumb,
const PaintContext &context,
Fn<QImage()> generateBackground) const;
void setupSpoilerTag(std::unique_ptr<MediaSpoilerTag> &tag) const;
[[nodiscard]] ClickHandlerPtr spoilerTagLink(
not_null<MediaSpoiler*> spoiler,
std::unique_ptr<MediaSpoilerTag> &tag) const;
void createSpoilerLink(not_null<MediaSpoiler*> spoiler);
void repaint() const;
const not_null<Element*> _parent;
MediaInBubbleState _inBubbleState = MediaInBubbleState::None;
Ui::BubbleRounding _bubbleRounding;
};
[[nodiscard]] Images::CornersMaskRef MediaRoundingMask(
std::optional<Ui::BubbleRounding> rounding);
} // namespace HistoryView

View File

@@ -0,0 +1,604 @@
/*
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/media/history_view_media_common.h"
#include "api/api_sensitive_content.h"
#include "api/api_views.h"
#include "apiwrap.h"
#include "inline_bots/bot_attach_web_view.h"
#include "ui/boxes/confirm_box.h"
#include "ui/layers/generic_box.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/painter.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_wall_paper.h"
#include "data/data_media_types.h"
#include "data/data_user.h"
#include "history/view/history_view_element.h"
#include "history/view/media/history_view_media_grouped.h"
#include "history/view/media/history_view_photo.h"
#include "history/view/media/history_view_gif.h"
#include "history/view/media/history_view_document.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_theme_document.h"
#include "history/history_item.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "mainwindow.h"
#include "media/streaming/media_streaming_utility.h"
#include "payments/payments_checkout_process.h"
#include "payments/payments_non_panel_process.h"
#include "settings/settings_common.h"
#include "webrtc/webrtc_environment.h"
#include "webview/webview_interface.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace HistoryView {
namespace {
constexpr auto kMediaUnlockedTooltipDuration = 5 * crl::time(1000);
const auto kVerifyAgeAboutPrefix = "cloud_lng_age_verify_about_";
rpl::producer<TextWithEntities> AgeVerifyAbout(
not_null<Main::Session*> session) {
const auto appConfig = &session->appConfig();
return rpl::single(
rpl::empty
) | rpl::then(
Lang::Updated()
) | rpl::map([=] {
const auto country = appConfig->ageVerifyCountry().toLower();
const auto age = appConfig->ageVerifyMinAge();
const auto [shift, string] = Lang::Plural(
Lang::kPluralKeyBaseForCloudValue,
age,
lt_count);
const auto postfixes = {
"#zero",
"#one",
"#two",
"#few",
"#many",
"#other"
};
Assert(shift >= 0 && shift < postfixes.size());
const auto postfix = *(begin(postfixes) + shift);
return tr::rich(Lang::GetNonDefaultValue(
kVerifyAgeAboutPrefix + country.toUtf8() + postfix
).replace(u"{count}"_q, string));
});
}
[[nodiscard]] object_ptr<Ui::RpWidget> AgeVerifyIcon(
not_null<QWidget*> parent) {
const auto padding = st::settingsAgeVerifyIconPadding;
const auto full = st::settingsAgeVerifyIcon.size().grownBy(padding);
auto result = object_ptr<Ui::RpWidget>(parent);
const auto raw = result.data();
raw->resize(full);
raw->paintRequest() | rpl::on_next([=] {
auto p = QPainter(raw);
const auto x = (raw->width() - full.width()) / 2;
auto hq = PainterHighQualityEnabler(p);
p.setBrush(st::windowBgActive);
p.setPen(Qt::NoPen);
const auto inner = QRect(QPoint(x, 0), full);
p.drawEllipse(inner);
st::settingsAgeVerifyIcon.paintInCenter(p, inner);
}, raw->lifetime());
return result;
}
} // namespace
void PaintInterpolatedIcon(
QPainter &p,
const style::icon &a,
const style::icon &b,
float64 b_ratio,
QRect rect) {
PainterHighQualityEnabler hq(p);
p.save();
p.translate(rect.center());
p.setOpacity(b_ratio);
p.scale(b_ratio, b_ratio);
b.paintInCenter(p, rect.translated(-rect.center()));
p.restore();
p.save();
p.translate(rect.center());
p.setOpacity(1. - b_ratio);
p.scale(1. - b_ratio, 1. - b_ratio);
a.paintInCenter(p, rect.translated(-rect.center()));
p.restore();
}
std::unique_ptr<Media> CreateAttach(
not_null<Element*> parent,
DocumentData *document,
PhotoData *photo) {
return CreateAttach(parent, document, photo, {}, {});
}
std::unique_ptr<Media> CreateAttach(
not_null<Element*> parent,
DocumentData *document,
PhotoData *photo,
const std::vector<std::unique_ptr<Data::Media>> &collage,
const QString &webpageUrl) {
if (!collage.empty()) {
return std::make_unique<GroupedMedia>(parent, collage);
} else if (document) {
const auto spoiler = false;
if (document->sticker()) {
const auto skipPremiumEffect = true;
return std::make_unique<UnwrappedMedia>(
parent,
std::make_unique<Sticker>(
parent,
document,
skipPremiumEffect));
} else if (document->isAnimation() || document->isVideoFile()) {
return std::make_unique<Gif>(
parent,
parent->data(),
document,
spoiler);
} else if (document->isWallPaper() || document->isTheme()) {
return std::make_unique<ThemeDocument>(
parent,
document,
ThemeDocument::ParamsFromUrl(webpageUrl));
}
return std::make_unique<Document>(parent, parent->data(), document);
} else if (photo) {
const auto spoiler = false;
return std::make_unique<Photo>(
parent,
parent->data(),
photo,
spoiler);
} else if (const auto params = ThemeDocument::ParamsFromUrl(webpageUrl)) {
return std::make_unique<ThemeDocument>(parent, nullptr, params);
}
return nullptr;
}
int UnitedLineHeight() {
return std::max(st::semiboldFont->height, st::normalFont->height);
}
QImage PrepareWithBlurredBackground(
QSize outer,
::Media::Streaming::ExpandDecision resize,
Image *large,
Image *blurred) {
return PrepareWithBlurredBackground(
outer,
resize,
large ? large->original() : QImage(),
blurred ? blurred->original() : QImage());
}
QImage PrepareWithBlurredBackground(
QSize outer,
::Media::Streaming::ExpandDecision resize,
QImage large,
QImage blurred) {
const auto ratio = style::DevicePixelRatio();
if (resize.expanding) {
return Images::Prepare(std::move(large), resize.result * ratio, {
.outer = outer,
});
}
auto background = QImage(
outer * ratio,
QImage::Format_ARGB32_Premultiplied);
background.setDevicePixelRatio(ratio);
if (blurred.isNull()) {
background.fill(Qt::black);
if (large.isNull()) {
return background;
}
}
auto p = QPainter(&background);
if (!blurred.isNull()) {
using namespace ::Media::Streaming;
FillBlurredBackground(p, outer, std::move(blurred));
}
if (!large.isNull()) {
auto image = large.scaled(
resize.result * ratio,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
image.setDevicePixelRatio(ratio);
p.drawImage(
(outer.width() - resize.result.width()) / 2,
(outer.height() - resize.result.height()) / 2,
image);
}
p.end();
return background;
}
QSize CountDesiredMediaSize(QSize original) {
return DownscaledSize(
style::ConvertScale(original),
{ st::maxMediaSize, st::maxMediaSize });
}
QSize CountMediaSize(QSize desired, int newWidth) {
Expects(!desired.isEmpty());
return (desired.width() <= newWidth)
? desired
: NonEmptySize(
desired.scaled(newWidth, desired.height(), Qt::KeepAspectRatio));
}
QSize CountPhotoMediaSize(
QSize desired,
int newWidth,
int maxWidth) {
const auto media = CountMediaSize(desired, qMin(newWidth, maxWidth));
return (media.height() <= newWidth)
? media
: NonEmptySize(
media.scaled(media.width(), newWidth, Qt::KeepAspectRatio));
}
void ShowPaidMediaUnlockedToast(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item) {
const auto media = item->media();
const auto invoice = media ? media->invoice() : nullptr;
if (!invoice || !invoice->isPaidMedia) {
return;
}
const auto sender = item->originalSender();
const auto broadcast = (sender && sender->isBroadcast())
? sender
: item->history()->peer.get();
const auto user = item->viaBot()
? item->viaBot()
: item->originalSender()
? item->originalSender()->asUser()
: nullptr;
auto text = tr::lng_credits_media_done_title(
tr::now,
tr::bold
).append('\n').append(user
? tr::lng_credits_media_done_text_user(
tr::now,
lt_count,
invoice->amount,
lt_user,
tr::bold(user->shortName()),
tr::rich)
: tr::lng_credits_media_done_text(
tr::now,
lt_count,
invoice->amount,
lt_chat,
tr::bold(broadcast->name()),
tr::rich));
controller->showToast(std::move(text), kMediaUnlockedTooltipDuration);
}
ClickHandlerPtr MakePaidMediaLink(not_null<HistoryItem*> item) {
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get();
const auto weak = my.sessionWindow;
const auto itemId = item->fullId();
const auto session = &item->history()->session();
using Result = Payments::CheckoutResult;
const auto done = crl::guard(session, [=](Result result) {
if (result != Result::Paid) {
return;
} else if (const auto item = session->data().message(itemId)) {
session->api().views().pollExtendedMedia(item, true);
if (const auto strong = weak.get()) {
ShowPaidMediaUnlockedToast(strong, item);
}
}
});
const auto reactivate = controller
? crl::guard(
controller,
[=](auto) { controller->widget()->activate(); })
: Fn<void(Payments::CheckoutResult)>();
const auto credits = Payments::IsCreditsInvoice(item);
const auto nonPanelPaymentFormProcess = (controller && credits)
? Payments::ProcessNonPanelPaymentFormFactory(controller, done)
: nullptr;
Payments::CheckoutProcess::Start(
item,
Payments::Mode::Payment,
reactivate,
nonPanelPaymentFormProcess);
});
}
void ShowAgeVerification(
std::shared_ptr<Ui::Show> show,
not_null<UserData*> bot,
Fn<void()> reveal) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setNoContentMargin(true);
box->setStyle(st::settingsAgeVerifyBox);
box->setWidth(st::boxWideWidth);
box->addRow(AgeVerifyIcon(box), st::settingsAgeVerifyIconMargin);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_age_verify_title(),
st::settingsAgeVerifyTitle),
st::boxRowPadding + st::settingsAgeVerifyMargin,
style::al_top);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
AgeVerifyAbout(&bot->session()),
st::settingsAgeVerifyText),
st::boxRowPadding + st::settingsAgeVerifyMargin,
style::al_top);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_age_verify_here(tr::rich),
st::settingsAgeVerifyText),
st::boxRowPadding + st::settingsAgeVerifyMargin,
style::al_top);
const auto weak = QPointer<Ui::GenericBox>(box);
const auto done = crl::guard(&bot->session(), [=](int age) {
const auto min = bot->session().appConfig().ageVerifyMinAge();
if (age >= min) {
reveal();
bot->session().api().sensitiveContent().update(true);
} else {
show->showToast({
.title = tr::lng_age_verify_sorry_title(tr::now),
.text = { tr::lng_age_verify_sorry_text(tr::now) },
.duration = Ui::Toast::kDefaultDuration * 3,
});
}
if (const auto strong = weak.data()) {
strong->closeBox();
}
});
const auto button = box->addButton(tr::lng_age_verify_button(), [=] {
bot->session().attachWebView().open({
.bot = bot,
.parentShow = box->uiShow(),
.context = { .maySkipConfirmation = true },
.source = InlineBots::WebViewSourceAgeVerification{
.done = done,
},
});
});
box->widthValue(
) | rpl::on_next([=](int width) {
const auto &padding = st::settingsAgeVerifyBox.buttonPadding;
button->resizeToWidth(width
- padding.left()
- padding.right());
button->moveToLeft(padding.left(), padding.top());
}, button->lifetime());
const auto close = Ui::CreateChild<Ui::IconButton>(
box.get(),
st::boxTitleClose);
close->setClickedCallback([=] {
box->closeBox();
});
box->widthValue(
) | rpl::on_next([=](int width) {
close->moveToRight(0, 0);
}, box->lifetime());
crl::on_main(close, [=] { close->raise(); });
}));
}
void ShowAgeVerificationMobile(
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_age_verify_title());
box->setWidth(st::boxWideWidth);
const auto size = st::settingsCloudPasswordIconSize;
auto icon = Settings::CreateLottieIcon(
box->verticalLayout(),
{
.name = u"phone"_q,
.sizeOverride = { size, size },
},
st::peerAppearanceIconPadding);
box->showFinishes(
) | rpl::on_next([animate = std::move(icon.animate)] {
animate(anim::repeat::once);
}, box->lifetime());
box->addRow(std::move(icon.widget));
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
AgeVerifyAbout(session),
st::settingsAgeVerifyText),
st::boxRowPadding + st::settingsAgeVerifyMargin,
style::al_top);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_age_verify_mobile(tr::rich),
st::settingsAgeVerifyText),
st::boxRowPadding + st::settingsAgeVerifyMargin,
style::al_top);
box->addButton(tr::lng_box_ok(), [=] {
box->closeBox();
});
}));
}
void ShowAgeVerificationRequired(
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session,
Fn<void()> reveal) {
struct State {
Fn<void()> check;
rpl::lifetime lifetime;
std::optional<PeerData*> bot;
};
const auto state = std::make_shared<State>();
const auto username = session->appConfig().ageVerifyBotUsername();
const auto bot = session->data().peerByUsername(username);
if (username.isEmpty() || bot) {
state->bot = bot;
} else {
session->api().request(MTPcontacts_ResolveUsername(
MTP_flags(0),
MTP_string(username),
MTPstring()
)).done([=](const MTPcontacts_ResolvedPeer &result) {
const auto &data = result.data();
session->data().processUsers(data.vusers());
session->data().processChats(data.vchats());
const auto botId = peerFromMTP(data.vpeer());
state->bot = session->data().peerLoaded(botId);
state->check();
}).fail([=] {
state->bot = nullptr;
state->check();
}).send();
}
state->check = [=] {
const auto sensitive = &session->api().sensitiveContent();
const auto ready = sensitive->loaded();
if (!ready) {
state->lifetime = sensitive->loadedValue(
) | rpl::filter(
rpl::mappers::_1
) | rpl::take(1) | rpl::on_next(state->check);
return;
} else if (!state->bot.has_value()) {
return;
}
const auto has = Core::App().mediaDevices().recordAvailability();
const auto available = Webview::Availability();
const auto bot = (*state->bot)->asUser();
if (available.error == Webview::Available::Error::None
&& has == Webrtc::RecordAvailability::VideoAndAudio
&& sensitive->canChangeCurrent()
&& bot
&& bot->isBot()
&& bot->botInfo->hasMainApp) {
ShowAgeVerification(show, bot, reveal);
} else {
ShowAgeVerificationMobile(show, session);
}
state->lifetime.destroy();
state->check = nullptr;
};
state->check();
}
void ShowSensitiveConfirm(
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session,
Fn<void()> reveal) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
struct State {
rpl::variable<bool> canChange;
Ui::Checkbox *checkbox = nullptr;
};
const auto state = box->lifetime().make_state<State>();
const auto sensitive = &session->api().sensitiveContent();
state->canChange = sensitive->canChange();
const auto done = [=](Fn<void()> close) {
if (state->canChange.current()
&& state->checkbox->checked()) {
show->showToast({
.text = tr::lng_sensitive_toast(
tr::now,
tr::rich),
.adaptive = true,
.duration = 5 * crl::time(1000),
});
sensitive->update(true);
} else {
reveal();
}
close();
};
Ui::ConfirmBox(box, {
.text = tr::lng_sensitive_text(tr::rich),
.confirmed = done,
.confirmText = tr::lng_sensitive_view(),
.title = tr::lng_sensitive_title(),
});
const auto skip = st::defaultCheckbox.margin.bottom();
const auto wrap = box->addRow(
object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
box,
object_ptr<Ui::Checkbox>(
box,
tr::lng_sensitive_always(tr::now),
false)),
st::boxRowPadding + QMargins(0, 0, 0, skip));
wrap->toggleOn(state->canChange.value());
wrap->finishAnimating();
state->checkbox = wrap->entity();
}));
}
ClickHandlerPtr MakeSensitiveMediaLink(
ClickHandlerPtr reveal,
not_null<HistoryItem*> item) {
const auto session = &item->history()->session();
session->api().sensitiveContent().preload();
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto plain = [reveal, context] {
if (const auto raw = reveal.get()) {
raw->onClick(context);
}
};
const auto my = context.other.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get();
const auto show = controller ? controller->uiShow() : my.show;
if (!show) {
plain();
} else if (session->appConfig().ageVerifyNeeded()) {
ShowAgeVerificationRequired(show, session, plain);
} else {
ShowSensitiveConfirm(show, session, plain);
}
});
}
} // namespace HistoryView

View File

@@ -0,0 +1,97 @@
/*
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
*/
#pragma once
class DocumentData;
class PhotoData;
class Image;
namespace HistoryView {
class Element;
} // namespace HistoryView
namespace Data {
class Media;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Media::Streaming {
struct ExpandDecision;
} // namespace Media::Streaming
namespace Ui {
class Show;
} // namespace Ui
namespace HistoryView {
class Media;
void PaintInterpolatedIcon(
QPainter &p,
const style::icon &a,
const style::icon &b,
float64 b_ratio,
QRect rect);
[[nodiscard]] std::unique_ptr<Media> CreateAttach(
not_null<Element*> parent,
DocumentData *document,
PhotoData *photo);
[[nodiscard]] std::unique_ptr<Media> CreateAttach(
not_null<Element*> parent,
DocumentData *document,
PhotoData *photo,
const std::vector<std::unique_ptr<Data::Media>> &collage,
const QString &webpageUrl);
[[nodiscard]] int UnitedLineHeight();
[[nodiscard]] inline QSize NonEmptySize(QSize size) {
return QSize(std::max(size.width(), 1), std::max(size.height(), 1));
}
[[nodiscard]] inline QSize DownscaledSize(QSize size, QSize box) {
return NonEmptySize(
((size.width() > box.width() || size.height() > box.height())
? size.scaled(box, Qt::KeepAspectRatio)
: size));
}
[[nodiscard]] QImage PrepareWithBlurredBackground(
QSize outer,
::Media::Streaming::ExpandDecision resize,
Image *large,
Image *blurred);
[[nodiscard]] QImage PrepareWithBlurredBackground(
QSize outer,
::Media::Streaming::ExpandDecision resize,
QImage large,
QImage blurred);
[[nodiscard]] QSize CountDesiredMediaSize(QSize original);
[[nodiscard]] QSize CountMediaSize(QSize desired, int newWidth);
[[nodiscard]] QSize CountPhotoMediaSize(
QSize desired,
int newWidth,
int maxWidth);
void ShowAgeVerificationRequired(
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session,
Fn<void()> reveal);
[[nodiscard]] ClickHandlerPtr MakePaidMediaLink(
not_null<HistoryItem*> item);
[[nodiscard]] ClickHandlerPtr MakeSensitiveMediaLink(
ClickHandlerPtr reveal,
not_null<HistoryItem*> item);
} // namespace HistoryView

View File

@@ -0,0 +1,848 @@
/*
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/media/history_view_media_generic.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "ui/chat/chat_style.h"
#include "ui/dynamic_image.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/rect.h"
#include "ui/round_rect.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
constexpr auto kAdditionalPrizesWithLineOpacity = 0.6;
} // namespace
TextState MediaGenericPart::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
return {};
}
void MediaGenericPart::clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) {
}
bool MediaGenericPart::hasHeavyPart() {
return false;
}
void MediaGenericPart::unloadHeavyPart() {
}
auto MediaGenericPart::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements
) -> std::unique_ptr<StickerPlayer> {
return nullptr;
}
MediaGeneric::MediaGeneric(
not_null<Element*> parent,
Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<Part>)>)> generate,
MediaGenericDescriptor &&descriptor)
: Media(parent)
, _paintBgFactory(std::move(descriptor.paintBgFactory))
, _paintBg(_paintBgFactory ? _paintBgFactory() : nullptr)
, _fullAreaLink(descriptor.fullAreaLink)
, _maxWidthCap(descriptor.maxWidth)
, _service(descriptor.service)
, _hideServiceText(descriptor.hideServiceText) {
generate(this, [&](std::unique_ptr<Part> part) {
_entries.push_back({
.object = std::move(part),
});
});
}
MediaGeneric::~MediaGeneric() {
if (hasHeavyPart()) {
unloadHeavyPart();
_parent->checkHeavyPart();
}
}
QSize MediaGeneric::countOptimalSize() {
const auto maxWidth = _maxWidthCap
? _maxWidthCap
: st::chatGiveawayWidth;
auto top = 0;
for (auto &entry : _entries) {
const auto raw = entry.object.get();
raw->initDimensions();
top += raw->resizeGetHeight(maxWidth);
}
return { maxWidth, top };
}
QSize MediaGeneric::countCurrentSize(int newWidth) {
if (newWidth > maxWidth()) {
newWidth = maxWidth();
}
auto top = 0;
for (auto &entry : _entries) {
top += entry.object->resizeGetHeight(newWidth);
}
return { newWidth, top };
}
void MediaGeneric::draw(Painter &p, const PaintContext &context) const {
const auto outer = width();
if (outer < st::msgPadding.left() + st::msgPadding.right() + 1) {
return;
}
if (!_paintBg && _paintBgFactory) {
_paintBg = _paintBgFactory();
}
if (_paintBg) {
_paintBg(p, context, this);
} else if (_service) {
PainterHighQualityEnabler hq(p);
const auto radius = st::msgServiceGiftBoxRadius;
p.setPen(Qt::NoPen);
p.setBrush(context.st->msgServiceBg());
const auto rect = QRect(0, 0, width(), height());
p.drawRoundedRect(rect, radius, radius);
//if (context.selected()) {
// p.setBrush(context.st->serviceTextPalette().selectBg);
// p.drawRoundedRect(rect, radius, radius);
//}
}
auto translated = 0;
for (const auto &entry : _entries) {
const auto raw = entry.object.get();
const auto height = raw->height();
raw->draw(p, this, context, outer);
translated += height;
p.translate(0, height);
}
p.translate(0, -translated);
}
TextState MediaGeneric::textState(
QPoint point,
StateRequest request) const {
auto result = TextState(_parent);
const auto outer = width();
if (outer < st::msgPadding.left() + st::msgPadding.right() + 1) {
return result;
}
if (_fullAreaLink && QRect(0, 0, width(), height()).contains(point)) {
result.link = _fullAreaLink;
return result;
}
for (const auto &entry : _entries) {
const auto raw = entry.object.get();
const auto height = raw->height();
if (point.y() >= 0 && point.y() < height) {
const auto part = raw->textState(point, request, outer);
result.link = part.link;
return result;
}
point.setY(point.y() - height);
}
return result;
}
void MediaGeneric::clickHandlerActiveChanged(
const ClickHandlerPtr &p,
bool active) {
}
void MediaGeneric::clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) {
for (const auto &entry : _entries) {
entry.object->clickHandlerPressedChanged(p, pressed);
}
}
std::unique_ptr<StickerPlayer> MediaGeneric::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
for (const auto &entry : _entries) {
if (auto result = entry.object->stickerTakePlayer(
data,
replacements)) {
return result;
}
}
return nullptr;
}
bool MediaGeneric::hideFromName() const {
return !parent()->data()->Has<HistoryMessageForwarded>();
}
bool MediaGeneric::hideServiceText() const {
return _hideServiceText;
}
bool MediaGeneric::hasHeavyPart() const {
for (const auto &entry : _entries) {
if (entry.object->hasHeavyPart()) {
return true;
}
}
return false;
}
void MediaGeneric::unloadHeavyPart() {
_paintBg = nullptr;
for (const auto &entry : _entries) {
entry.object->unloadHeavyPart();
}
}
QMargins MediaGeneric::inBubblePadding() const {
auto lshift = st::msgPadding.left();
auto rshift = st::msgPadding.right();
auto bshift = isBubbleBottom()
? st::msgPadding.top()
: st::mediaInBubbleSkip;
auto tshift = isBubbleTop()
? st::msgPadding.bottom()
: st::mediaInBubbleSkip;
return QMargins(lshift, tshift, rshift, bshift);
}
MediaGenericTextPart::MediaGenericTextPart(
TextWithEntities text,
QMargins margins,
const style::TextStyle &st,
const base::flat_map<uint16, ClickHandlerPtr> &links,
const Ui::Text::MarkedContext &context,
style::align align)
: _text(st::msgMinWidth)
, _margins(margins)
, _align(align) {
_text.setMarkedText(
st,
text,
kMarkupTextOptions,
context);
for (const auto &[index, link] : links) {
_text.setLink(index, link);
}
}
void MediaGenericTextPart::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
const auto use = (width() - _margins.left() - _margins.right());
setupPen(p, owner, context);
_text.draw(p, {
.position = {
((_align == style::al_top)
? ((outerWidth - use) / 2)
: _margins.left()),
_margins.top(),
},
.outerWidth = outerWidth,
.availableWidth = use,
.align = _align,
.palette = &(owner->service()
? context.st->serviceTextPalette()
: context.messageStyle()->textPalette),
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.elisionLines = elisionLines(),
});
}
void MediaGenericTextPart::setupPen(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context) const {
const auto service = owner->service();
p.setPen(service
? context.st->msgServiceFg()
: context.messageStyle()->historyTextFg);
}
int MediaGenericTextPart::elisionLines() const {
return 0;
}
TextState MediaGenericTextPart::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
const auto use = (width() - _margins.left() - _margins.right());
point -= QPoint{
((_align == style::al_top)
? ((outerWidth - use) / 2)
: _margins.left()),
_margins.top(),
};
auto result = TextState();
auto forText = request.forText();
forText.align = _align;
result.link = _text.getState(point, use, forText).link;
return result;
}
QSize MediaGenericTextPart::countOptimalSize() {
const auto lines = elisionLines();
const auto height = lines
? std::min(_text.minHeight(), lines * _text.style()->font->height)
: _text.minHeight();
return {
_margins.left() + _text.maxWidth() + _margins.right(),
_margins.top() + height + _margins.bottom(),
};
}
QSize MediaGenericTextPart::countCurrentSize(int newWidth) {
auto skip = _margins.left() + _margins.right();
const auto size = (_align == style::al_top)
? Ui::Text::CountOptimalTextSize(
_text,
st::msgMinWidth,
std::max(st::msgMinWidth, newWidth - skip))
: QSize(newWidth - skip, _text.countHeight(newWidth - skip));
const auto lines = elisionLines();
const auto height = lines
? std::min(size.height(), lines * _text.style()->font->height)
: size.height();
return {
size.width() + skip,
_margins.top() + height + _margins.bottom(),
};
}
TextDelimeterPart::TextDelimeterPart(
const QString &text,
QMargins margins)
: _margins(margins) {
_text.setText(st::defaultTextStyle, text);
}
void TextDelimeterPart::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
const auto stm = context.messageStyle();
const auto available = outerWidth - _margins.left() - _margins.right();
p.setPen(stm->msgDateFg);
_text.draw(p, {
.position = { _margins.left(), _margins.top() },
.outerWidth = outerWidth,
.availableWidth = available,
.align = style::al_top,
.palette = &stm->textPalette,
.now = context.now,
.elisionLines = 1,
});
const auto skip = st::chatGiveawayPrizesWithSkip;
const auto inner = available - 2 * skip;
const auto sub = _text.maxWidth();
if (inner > sub + 1) {
const auto fill = (inner - sub) / 2;
const auto stroke = st::lineWidth;
const auto top = _margins.top()
+ st::chatGiveawayPrizesWithLineTop;
p.setOpacity(kAdditionalPrizesWithLineOpacity);
p.fillRect(_margins.left(), top, fill, stroke, stm->msgDateFg);
const auto start = outerWidth - _margins.right() - fill;
p.fillRect(start, top, fill, stroke, stm->msgDateFg);
p.setOpacity(1.);
}
}
QSize TextDelimeterPart::countOptimalSize() {
return {
_margins.left() + _text.maxWidth() + _margins.right(),
_margins.top() + st::normalFont->height + _margins.bottom(),
};
}
QSize TextDelimeterPart::countCurrentSize(int newWidth) {
return { newWidth, minHeight() };
}
StickerInBubblePart::StickerInBubblePart(
not_null<Element*> parent,
Element *replacing,
Fn<Data()> lookup,
QMargins padding)
: _parent(parent)
, _lookup(std::move(lookup))
, _padding(padding) {
ensureCreated(replacing);
}
void StickerInBubblePart::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
ensureCreated();
if (_sticker) {
const auto stickerSize = _sticker->countOptimalSize();
const auto sticker = QRect(
(outerWidth - stickerSize.width()) / 2,
_padding.top() + _skipTop,
stickerSize.width(),
stickerSize.height());
_sticker->draw(p, context, sticker);
}
}
TextState StickerInBubblePart::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
auto result = TextState(_parent);
if (_sticker) {
const auto stickerSize = _sticker->countOptimalSize();
const auto sticker = QRect(
(outerWidth - stickerSize.width()) / 2,
_padding.top() + _skipTop,
stickerSize.width(),
stickerSize.height());
if (sticker.contains(point)) {
result.link = _link;
}
}
return result;
}
bool StickerInBubblePart::hasHeavyPart() {
return _sticker && _sticker->hasHeavyPart();
}
void StickerInBubblePart::unloadHeavyPart() {
if (_sticker) {
_sticker->unloadHeavyPart();
}
}
std::unique_ptr<StickerPlayer> StickerInBubblePart::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return _sticker
? _sticker->stickerTakePlayer(data, replacements)
: nullptr;
}
QSize StickerInBubblePart::countOptimalSize() {
ensureCreated();
const auto size = _sticker ? _sticker->countOptimalSize() : [&] {
const auto fallback = _lookup().size;
return QSize{ fallback, fallback };
}();
return {
_padding.left() + size.width() + _padding.right(),
_padding.top() + size.height() + _padding.bottom(),
};
}
QSize StickerInBubblePart::countCurrentSize(int newWidth) {
return { newWidth, minHeight() };
}
void StickerInBubblePart::ensureCreated(Element *replacing) const {
if (_sticker) {
return;
} else if (const auto data = _lookup()) {
const auto sticker = data.sticker;
if (sticker->sticker()) {
const auto skipPremiumEffect = true;
_link = data.link;
_skipTop = data.skipTop;
_sticker.emplace(_parent, sticker, skipPremiumEffect, replacing);
if (data.stopOnLastFrame) {
_sticker->setStopOnLastFrame(true);
}
_sticker->initSize(data.size);
_sticker->setCustomCachingTag(data.cacheTag);
}
}
}
StickerWithBadgePart::StickerWithBadgePart(
not_null<Element*> parent,
Element *replacing,
Fn<Data()> lookup,
QMargins padding,
QString badge,
QImage customLeftIcon,
std::optional<QColor> colorOverride)
: _customLeftIcon(std::move(customLeftIcon))
, _sticker(parent, replacing, std::move(lookup), padding)
, _badgeText(badge)
, _colorOverride(std::move(colorOverride)) {
}
void StickerWithBadgePart::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
_sticker.draw(p, owner, context, outerWidth);
if (_sticker.resolved()) {
paintBadge(p, context);
}
}
TextState StickerWithBadgePart::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
return _sticker.textState(point, request, outerWidth);
}
bool StickerWithBadgePart::hasHeavyPart() {
return _sticker.hasHeavyPart();
}
void StickerWithBadgePart::unloadHeavyPart() {
_sticker.unloadHeavyPart();
}
std::unique_ptr<StickerPlayer> StickerWithBadgePart::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return _sticker.stickerTakePlayer(data, replacements);
}
QSize StickerWithBadgePart::countOptimalSize() {
_sticker.initDimensions();
return { _sticker.maxWidth(), _sticker.minHeight() };
}
QSize StickerWithBadgePart::countCurrentSize(int newWidth) {
return _sticker.countCurrentSize(newWidth);
}
void StickerWithBadgePart::paintBadge(
Painter &p,
const PaintContext &context) const {
validateBadge(context);
const auto badge = _badge.size() / _badge.devicePixelRatio();
const auto left = (width() - badge.width()) / 2;
const auto top = st::chatGiveawayBadgeTop;
const auto rect = QRect(left, top, badge.width(), badge.height());
const auto paintContent = [&](QPainter &q) {
q.drawImage(rect.topLeft(), _badge);
};
{
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
if (_colorOverride) {
p.setBrush(*_colorOverride);
} else {
p.setBrush(context.messageStyle()->msgFileBg);
}
const auto half = st::chatGiveawayBadgeStroke / 2.;
const auto inner = QRectF(rect) - Margins(half);
const auto radius = inner.height() / 2.;
p.drawRoundedRect(inner, radius, radius);
if (_colorOverride && context.selected()) {
p.setBrush(context.st->msgStickerOverlay());
p.drawRoundedRect(inner, radius, radius);
}
}
if (!_sticker.parent()->usesBubblePattern(context)) {
paintContent(p);
} else {
Ui::PaintPatternBubblePart(
p,
context.viewport,
context.bubblesPattern->pixmap,
rect,
paintContent,
_badgeCache);
}
}
void StickerWithBadgePart::validateBadge(
const PaintContext &context) const {
const auto stm = context.messageStyle();
const auto &badgeFg = st::premiumButtonFg->c;
const auto &badgeBorder = stm->msgBg->c;
if (!_badge.isNull()
&& _badgeFg == badgeFg
&& _badgeBorder == badgeBorder) {
return;
}
const auto &font = st::chatGiveawayBadgeFont;
_badgeFg = badgeFg;
_badgeBorder = badgeBorder;
const auto iconWidth = _customLeftIcon.isNull()
? 0
: (_customLeftIcon.width() / style::DevicePixelRatio());
const auto width = font->width(_badgeText) + iconWidth;
const auto inner = QRect(0, 0, width, font->height);
const auto rect = inner + st::chatGiveawayBadgePadding;
const auto size = rect.size();
const auto ratio = style::DevicePixelRatio();
_badge = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied);
_badge.setDevicePixelRatio(ratio);
_badge.fill(Qt::transparent);
auto p = QPainter(&_badge);
auto hq = PainterHighQualityEnabler(p);
p.setPen(QPen(_badgeBorder, st::chatGiveawayBadgeStroke * 1.));
p.setBrush(Qt::NoBrush);
const auto half = st::chatGiveawayBadgeStroke / 2.;
const auto left = _customLeftIcon.isNull()
? st::chatGiveawayBadgePadding.left()
: (st::chatGiveawayBadgePadding.left() - half * 2);
const auto smaller = QRectF(rect.translated(-rect.topLeft()))
- Margins(half);
const auto radius = smaller.height() / 2.;
p.drawRoundedRect(smaller, radius, radius);
p.setPen(_badgeFg);
p.setFont(font);
p.drawText(
left + iconWidth,
st::chatGiveawayBadgePadding.top() + font->ascent,
_badgeText);
if (!_customLeftIcon.isNull()) {
const auto iconHeight = _customLeftIcon.height()
/ style::DevicePixelRatio();
p.drawImage(
left,
half + (inner.height() - iconHeight) / 2,
context.selected()
? Images::Colored(base::duplicate(_customLeftIcon), _badgeFg)
: _customLeftIcon);
}
}
PeerBubbleListPart::PeerBubbleListPart(
not_null<Element*> parent,
const std::vector<not_null<PeerData*>> &list)
: _parent(parent) {
for (const auto &peer : list) {
_peers.push_back({
.name = Ui::Text::String(
st::semiboldTextStyle,
peer->name(),
kDefaultTextOptions,
st::msgMinWidth),
.thumbnail = Ui::MakeUserpicThumbnail(peer),
.link = peer->openLink(),
.colorIndex = peer->colorIndex(),
});
}
}
PeerBubbleListPart::~PeerBubbleListPart() = default;
void PeerBubbleListPart::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
if (_peers.empty()) {
return;
}
const auto size = _peers[0].geometry.height();
const auto st = context.st;
const auto stm = context.messageStyle();
const auto selected = context.selected();
const auto padding = st::chatGiveawayPeerPadding;
for (const auto &peer : _peers) {
const auto &thumbnail = peer.thumbnail;
const auto &geometry = peer.geometry;
if (!_subscribed) {
thumbnail->subscribeToUpdates([=] { _parent->repaint(); });
}
const auto colorIndex = peer.colorIndex;
const auto cache = context.outbg
? stm->replyCache[st->colorPatternIndex(colorIndex)].get()
: st->coloredReplyCache(selected, colorIndex).get();
if (peer.corners[0].isNull() || peer.bg != cache->bg) {
peer.bg = cache->bg;
peer.corners = Images::CornersMask(size / 2);
for (auto &image : peer.corners) {
style::colorizeImage(image, cache->bg, &image);
}
}
p.setPen(cache->icon);
Ui::DrawRoundedRect(p, geometry, peer.bg, peer.corners);
if (peer.ripple) {
peer.ripple->paint(
p,
geometry.x(),
geometry.y(),
width(),
&cache->bg);
if (peer.ripple->empty()) {
peer.ripple = nullptr;
}
}
p.drawImage(geometry.topLeft(), thumbnail->image(size));
const auto left = size + padding.left();
const auto top = padding.top();
const auto available = geometry.width() - left - padding.right();
peer.name.draw(p, {
.position = { geometry.left() + left, geometry.top() + top },
.outerWidth = width(),
.availableWidth = available,
.align = style::al_left,
.palette = &stm->textPalette,
.now = context.now,
.elisionLines = 1,
.elisionBreakEverywhere = true,
});
}
_subscribed = true;
}
int PeerBubbleListPart::layout(int x, int y, int available) {
const auto size = st::chatGiveawayPeerSize;
const auto skip = st::chatGiveawayPeerSkip;
const auto padding = st::chatGiveawayPeerPadding;
auto left = available;
const auto shiftRow = [&](int i, int top, int shift) {
for (auto j = i; j != 0; --j) {
auto &geometry = _peers[j - 1].geometry;
if (geometry.top() != top) {
break;
}
geometry.moveLeft(geometry.x() + shift);
}
};
const auto count = int(_peers.size());
for (auto i = 0; i != count; ++i) {
const auto desired = size
+ padding.left()
+ _peers[i].name.maxWidth()
+ padding.right();
const auto width = std::min(desired, available);
if (left < width) {
shiftRow(i, y, (left + skip) / 2);
left = available;
y += size + skip;
}
_peers[i].geometry = { x + available - left, y, width, size };
left -= width + skip;
}
shiftRow(count, y, (left + skip) / 2);
return y + size + skip;
}
TextState PeerBubbleListPart::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
auto result = TextState(_parent);
for (const auto &peer : _peers) {
if (peer.geometry.contains(point)) {
result.link = peer.link;
_lastPoint = point;
break;
}
}
return result;
}
void PeerBubbleListPart::clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) {
for (auto &peer : _peers) {
if (peer.link != p) {
continue;
}
if (pressed) {
if (!peer.ripple) {
peer.ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
peer.geometry.size(),
peer.geometry.height() / 2),
[=] { _parent->repaint(); });
}
peer.ripple->add(_lastPoint - peer.geometry.topLeft());
} else if (peer.ripple) {
peer.ripple->lastStop();
}
break;
}
}
bool PeerBubbleListPart::hasHeavyPart() {
return _subscribed;
}
void PeerBubbleListPart::unloadHeavyPart() {
if (_subscribed) {
_subscribed = false;
for (const auto &peer : _peers) {
peer.thumbnail->subscribeToUpdates(nullptr);
}
}
}
QSize PeerBubbleListPart::countOptimalSize() {
if (_peers.empty()) {
return {};
}
const auto size = st::chatGiveawayPeerSize;
const auto skip = st::chatGiveawayPeerSkip;
const auto padding = st::chatGiveawayPeerPadding;
auto left = st::msgPadding.left();
for (const auto &peer : _peers) {
const auto desired = size
+ padding.left()
+ peer.name.maxWidth()
+ padding.right();
left += desired + skip;
}
return { left - skip + st::msgPadding.right(), size };
}
QSize PeerBubbleListPart::countCurrentSize(int newWidth) {
if (_peers.empty()) {
return {};
}
const auto padding = st::msgPadding;
const auto available = newWidth - padding.left() - padding.right();
const auto channelsBottom = layout(
padding.left(),
0,
available);
return { newWidth, channelsBottom };
}
} // namespace HistoryView

View File

@@ -0,0 +1,347 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
#include "history/view/media/history_view_sticker.h"
namespace Ui {
class DynamicImage;
class RippleAnimation;
} // namespace Ui
namespace style {
struct TextStyle;
} // namespace style
namespace st {
extern const style::TextStyle &defaultTextStyle;
} // namespace st
namespace HistoryView {
class MediaGeneric;
class MediaGenericPart : public Object {
public:
using PaintBg = Fn<void(
Painter&,
const PaintContext&,
not_null<const MediaGeneric*>)>;
using PaintBgFactory = Fn<PaintBg()>;
virtual ~MediaGenericPart() = default;
virtual void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const = 0;
[[nodiscard]] virtual TextState textState(
QPoint point,
StateRequest request,
int outerWidth) const;
virtual void clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed);
[[nodiscard]] virtual bool hasHeavyPart();
virtual void unloadHeavyPart();
[[nodiscard]] virtual auto stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements
) -> std::unique_ptr<StickerPlayer>;
};
struct MediaGenericDescriptor {
int maxWidth = 0;
MediaGenericPart::PaintBgFactory paintBgFactory;
ClickHandlerPtr fullAreaLink;
bool service = false;
bool hideServiceText = false;
};
class MediaGeneric final : public Media {
public:
using Part = MediaGenericPart;
MediaGeneric(
not_null<Element*> parent,
Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<Part>)>)> generate,
MediaGenericDescriptor &&descriptor = {});
~MediaGeneric();
[[nodiscard]] bool service() const {
return _service;
}
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
void clickHandlerActiveChanged(
const ClickHandlerPtr &p,
bool active) override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) override;
bool needsBubble() const override {
return !_service;
}
bool customInfoLayout() const override {
return false;
}
std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) override;
bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const override {
return true;
}
bool dragItemByHandler(const ClickHandlerPtr &p) const override {
return true;
}
bool hideFromName() const override;
bool hideServiceText() const override;
void unloadHeavyPart() override;
bool hasHeavyPart() const override;
private:
struct Entry {
std::unique_ptr<Part> object;
};
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
[[nodiscard]] QMargins inBubblePadding() const;
std::vector<Entry> _entries;
Part::PaintBgFactory _paintBgFactory;
mutable Part::PaintBg _paintBg;
ClickHandlerPtr _fullAreaLink;
int _maxWidthCap = 0;
int _marginTop = 0;
int _marginBottom = 0;
bool _service : 1 = false;
bool _hideServiceText : 1 = false;
};
class MediaGenericTextPart : public MediaGenericPart {
public:
MediaGenericTextPart(
TextWithEntities text,
QMargins margins,
const style::TextStyle &st = st::defaultTextStyle,
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
const Ui::Text::MarkedContext &context = {},
style::align align = style::al_top);
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
TextState textState(
QPoint point,
StateRequest request,
int outerWidth) const override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
protected:
virtual void setupPen(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context) const;
virtual int elisionLines() const;
private:
Ui::Text::String _text;
QMargins _margins;
style::align _align = {};
};
class TextDelimeterPart final : public MediaGenericPart {
public:
TextDelimeterPart(const QString &text, QMargins margins);
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
private:
Ui::Text::String _text;
QMargins _margins;
};
class StickerInBubblePart final : public MediaGenericPart {
public:
struct Data {
DocumentData *sticker = nullptr;
int skipTop = 0;
int size = 0;
ChatHelpers::StickerLottieSize cacheTag = {};
bool stopOnLastFrame = false;
ClickHandlerPtr link;
explicit operator bool() const {
return sticker != nullptr;
}
};
StickerInBubblePart(
not_null<Element*> parent,
Element *replacing,
Fn<Data()> lookup,
QMargins padding);
[[nodiscard]] not_null<Element*> parent() const {
return _parent;
}
[[nodiscard]] bool resolved() const {
return _sticker.has_value();
}
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
TextState textState(
QPoint point,
StateRequest request,
int outerWidth) const override;
bool hasHeavyPart() override;
void unloadHeavyPart() override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) override;
private:
void ensureCreated(Element *replacing = nullptr) const;
const not_null<Element*> _parent;
Fn<Data()> _lookup;
mutable int _skipTop = 0;
mutable QMargins _padding;
mutable std::optional<Sticker> _sticker;
mutable ClickHandlerPtr _link;
};
class StickerWithBadgePart final : public MediaGenericPart {
public:
using Data = StickerInBubblePart::Data;
StickerWithBadgePart(
not_null<Element*> parent,
Element *replacing,
Fn<Data()> lookup,
QMargins padding,
QString badge,
QImage customLeftIcon,
std::optional<QColor> colorOverride);
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
TextState textState(
QPoint point,
StateRequest request,
int outerWidth) const override;
bool hasHeavyPart() override;
void unloadHeavyPart() override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) override;
private:
void validateBadge(const PaintContext &context) const;
void paintBadge(Painter &p, const PaintContext &context) const;
const QImage _customLeftIcon;
StickerInBubblePart _sticker;
QString _badgeText;
mutable QColor _badgeFg;
mutable QColor _badgeBorder;
mutable QImage _badge;
mutable QImage _badgeCache;
std::optional<QColor> _colorOverride;
};
class PeerBubbleListPart final : public MediaGenericPart {
public:
PeerBubbleListPart(
not_null<Element*> parent,
const std::vector<not_null<PeerData*>> &list);
~PeerBubbleListPart();
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
TextState textState(
QPoint point,
StateRequest request,
int outerWidth) const override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) override;
bool hasHeavyPart() override;
void unloadHeavyPart() override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
private:
int layout(int x, int y, int available);
struct Peer {
Ui::Text::String name;
std::shared_ptr<Ui::DynamicImage> thumbnail;
QRect geometry;
ClickHandlerPtr link;
mutable std::unique_ptr<Ui::RippleAnimation> ripple;
mutable std::array<QImage, 4> corners;
mutable QColor bg;
uint8 colorIndex = 0;
};
const not_null<Element*> _parent;
std::vector<Peer> _peers;
mutable QPoint _lastPoint;
mutable bool _subscribed = false;
};
} // namespace HistoryView

View File

@@ -0,0 +1,919 @@
/*
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/media/history_view_media_grouped.h"
#include "history/history_item_components.h"
#include "history/history_item.h"
#include "history/history.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "data/data_document.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "storage/storage_shared_media.h"
#include "lang/lang_keys.h"
#include "media/streaming/media_streaming_utility.h"
#include "ui/grouped_layout.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/message_bubble.h"
#include "ui/text/text_options.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "layout/layout_selection.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
std::vector<Ui::GroupMediaLayout> LayoutPlaylist(
const std::vector<QSize> &sizes) {
Expects(!sizes.empty());
auto result = std::vector<Ui::GroupMediaLayout>();
result.reserve(sizes.size());
const auto width = ranges::max_element(
sizes,
std::less<>(),
&QSize::width)->width();
auto top = 0;
for (const auto &size : sizes) {
result.push_back({
.geometry = QRect(0, top, width, size.height()),
.sides = RectPart::Left | RectPart::Right
});
top += size.height();
}
result.front().sides |= RectPart::Top;
result.back().sides |= RectPart::Bottom;
return result;
}
} // namespace
GroupedMedia::Part::Part(
not_null<Element*> parent,
not_null<Data::Media*> media)
: item(media->parent())
, content(media->createView(parent, item)) {
Assert(media->canBeGrouped());
}
GroupedMedia::GroupedMedia(
not_null<Element*> parent,
const std::vector<std::unique_ptr<Data::Media>> &medias)
: Media(parent) {
const auto truncated = ranges::views::all(
medias
) | ranges::views::transform([](const std::unique_ptr<Data::Media> &v) {
return v.get();
}) | ranges::views::take(kMaxSize);
const auto result = applyGroup(truncated);
Ensures(result);
}
GroupedMedia::GroupedMedia(
not_null<Element*> parent,
const std::vector<not_null<HistoryItem*>> &items)
: Media(parent) {
const auto medias = ranges::views::all(
items
) | ranges::views::transform([](not_null<HistoryItem*> item) {
return item->media();
}) | ranges::views::take(kMaxSize);
const auto result = applyGroup(medias);
Ensures(result);
}
GroupedMedia::~GroupedMedia() {
// Destroy all parts while the media object is still not destroyed.
base::take(_parts);
}
HistoryItem *GroupedMedia::itemForText() const {
if (_mode == Mode::Column) {
return Media::itemForText();
} else if (!_captionItem) {
_captionItem = [&]() -> HistoryItem* {
auto result = (HistoryItem*)nullptr;
for (const auto &part : _parts) {
if (!part.item->emptyText()) {
if (result == part.item) {
// All parts are from the same message, that means
// this is an album with a single item, single text.
return result;
} else if (result) {
return nullptr;
} else {
result = part.item;
}
}
}
return result;
}();
}
return *_captionItem;
}
bool GroupedMedia::hideMessageText() const {
return (_mode == Mode::Column);
}
GroupedMedia::Mode GroupedMedia::DetectMode(not_null<Data::Media*> media) {
const auto document = media->document();
return (document && !document->isVideoFile())
? Mode::Column
: Mode::Grid;
}
QSize GroupedMedia::countOptimalSize() {
_purchasedPriceTag = hasPurchasedTag();
std::vector<QSize> sizes;
const auto partsCount = _parts.size();
sizes.reserve(partsCount);
auto maxWidth = 0;
if (_mode == Mode::Column) {
for (const auto &part : _parts) {
const auto &media = part.content;
media->setBubbleRounding(bubbleRounding());
media->initDimensions();
accumulate_max(maxWidth, media->maxWidth());
}
}
auto index = 0;
for (const auto &part : _parts) {
const auto last = (++index == _parts.size());
sizes.push_back(
part.content->sizeForGroupingOptimal(maxWidth, last));
}
const auto layout = (_mode == Mode::Grid)
? Ui::LayoutMediaGroup(
sizes,
st::historyGroupWidthMax,
st::historyGroupWidthMin,
st::historyGroupSkip)
: LayoutPlaylist(sizes);
Assert(layout.size() == _parts.size());
auto minHeight = 0;
for (auto i = 0, count = int(layout.size()); i != count; ++i) {
const auto &item = layout[i];
accumulate_max(maxWidth, item.geometry.x() + item.geometry.width());
accumulate_max(minHeight, item.geometry.y() + item.geometry.height());
_parts[i].initialGeometry = item.geometry;
_parts[i].sides = item.sides;
}
if (_mode == Mode::Column
&& isBubbleBottom()
&& _parts.back().item->emptyText()) {
const auto item = _parent->data();
const auto msgsigned = item->Get<HistoryMessageSigned>();
const auto views = item->Get<HistoryMessageViews>();
if ((msgsigned && !msgsigned->isAnonymousRank)
|| (views
&& (views->views.count >= 0 || views->replies.count > 0))
|| displayedEditBadge()) {
minHeight += st::msgDateFont->height - st::msgDateDelta.y();
}
}
const auto groupPadding = groupedPadding();
minHeight += groupPadding.top() + groupPadding.bottom();
return { maxWidth, minHeight };
}
QSize GroupedMedia::countCurrentSize(int newWidth) {
accumulate_min(newWidth, maxWidth());
auto newHeight = 0;
if (_mode == Mode::Grid && newWidth < st::historyGroupWidthMin) {
return { newWidth, newHeight };
} else if (_mode == Mode::Column) {
auto top = 0;
for (auto &part : _parts) {
const auto size = part.content->sizeForGrouping(newWidth);
part.geometry = QRect(0, top, newWidth, size.height());
top += size.height();
}
newHeight = top;
} else {
const auto initialSpacing = st::historyGroupSkip;
const auto factor = newWidth / float64(maxWidth());
const auto scale = [&](int value) {
return int(base::SafeRound(value * factor));
};
const auto spacing = scale(initialSpacing);
for (auto &part : _parts) {
const auto sides = part.sides;
const auto initialGeometry = part.initialGeometry;
const auto needRightSkip = !(sides & RectPart::Right);
const auto needBottomSkip = !(sides & RectPart::Bottom);
const auto initialLeft = initialGeometry.x();
const auto initialTop = initialGeometry.y();
const auto initialRight = initialLeft
+ initialGeometry.width()
+ (needRightSkip ? initialSpacing : 0);
const auto initialBottom = initialTop
+ initialGeometry.height()
+ (needBottomSkip ? initialSpacing : 0);
const auto left = scale(initialLeft);
const auto top = scale(initialTop);
const auto width = scale(initialRight)
- left
- (needRightSkip ? spacing : 0);
const auto height = scale(initialBottom)
- top
- (needBottomSkip ? spacing : 0);
part.geometry = QRect(left, top, width, height);
accumulate_max(newHeight, top + height);
}
}
if (_mode == Mode::Column
&& isBubbleBottom()
&& _parts.back().item->emptyText()) {
const auto item = _parent->data();
const auto msgsigned = item->Get<HistoryMessageSigned>();
const auto views = item->Get<HistoryMessageViews>();
if ((msgsigned && !msgsigned->isAnonymousRank)
|| (views
&& (views->views.count >= 0 || views->replies.count > 0))
|| displayedEditBadge()) {
newHeight += st::msgDateFont->height - st::msgDateDelta.y();
}
}
const auto groupPadding = groupedPadding();
newHeight += groupPadding.top() + groupPadding.bottom();
return { newWidth, newHeight };
}
void GroupedMedia::refreshParentId(
not_null<HistoryItem*> realParent) {
for (const auto &part : _parts) {
part.content->refreshParentId(part.item);
}
}
Ui::BubbleRounding GroupedMedia::applyRoundingSides(
Ui::BubbleRounding already,
RectParts sides) const {
auto result = Ui::GetCornersFromSides(sides);
if (!(result & RectPart::TopLeft)) {
already.topLeft = Ui::BubbleCornerRounding::None;
}
if (!(result & RectPart::TopRight)) {
already.topRight = Ui::BubbleCornerRounding::None;
}
if (!(result & RectPart::BottomLeft)) {
already.bottomLeft = Ui::BubbleCornerRounding::None;
}
if (!(result & RectPart::BottomRight)) {
already.bottomRight = Ui::BubbleCornerRounding::None;
}
return already;
}
QMargins GroupedMedia::groupedPadding() const {
if (_mode != Mode::Column) {
return QMargins();
}
const auto normal = st::msgFileLayout.padding;
const auto grouped = st::msgFileLayoutGrouped.padding;
const auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;
const auto lastHasCaption = isBubbleBottom()
&& !_parts.back().item->emptyText();
const auto addToBottom = lastHasCaption ? st::msgPadding.bottom() : 0;
return QMargins(
0,
(normal.top() - grouped.top()) - topMinus,
0,
(normal.bottom() - grouped.bottom()) + addToBottom);
}
Media *GroupedMedia::lookupSpoilerTagMedia() const {
if (_parts.empty()) {
return nullptr;
}
const auto media = _parts.front().content.get();
if (media && _parts.front().item->isMediaSensitive()) {
return media;
}
const auto photo = media ? media->getPhoto() : nullptr;
return (photo && photo->extendedMediaPreview()) ? media : nullptr;
}
QImage GroupedMedia::generateSpoilerTagBackground(QRect full) const {
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
full.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
auto p = QPainter(&result);
const auto shift = -full.topLeft();
const auto skip1 = st::historyGroupSkip / 2;
const auto skip2 = st::historyGroupSkip - skip1;
for (const auto &part : _parts) {
auto background = part.content->spoilerTagBackground();
const auto extended = part.geometry.translated(shift).marginsAdded(
{ skip1, skip1, skip2, skip2 });
if (background.isNull()) {
p.fillRect(extended, Qt::black);
} else {
p.drawImage(extended, background);
}
}
p.end();
return ::Media::Streaming::PrepareBlurredBackground(
full.size(),
std::move(result));
}
void GroupedMedia::drawHighlight(
Painter &p,
const PaintContext &context,
int top) const {
if (context.highlight.opacity == 0.) {
return;
}
auto selection = context.highlight.range;
if (_mode != Mode::Column) {
if (!selection.empty() && !IsSubGroupSelection(selection)) {
_parent->paintCustomHighlight(
p,
context,
top,
height(),
_parent->data().get());
}
return;
}
const auto empty = selection.empty();
const auto subpart = IsSubGroupSelection(selection);
const auto skip = top + groupedPadding().top();
for (auto i = 0, count = int(_parts.size()); i != count; ++i) {
const auto &part = _parts[i];
const auto rect = part.geometry.translated(0, skip);
const auto full = (!i && empty)
|| (subpart && IsGroupItemSelection(selection, i))
|| (!subpart
&& !selection.empty()
&& (selection.from < part.content->fullSelectionLength()));
if (!subpart) {
selection = part.content->skipSelection(selection);
}
if (full) {
auto copy = context;
copy.highlight.range = {};
_parent->paintCustomHighlight(
p,
copy,
rect.y(),
rect.height(),
part.item);
}
}
}
void GroupedMedia::draw(Painter &p, const PaintContext &context) const {
auto wasCache = false;
auto nowCache = false;
const auto groupPadding = groupedPadding();
auto selection = context.selection;
const auto fullSelection = (selection == FullSelection);
const auto textSelection = (_mode == Mode::Column)
&& !fullSelection
&& !IsSubGroupSelection(selection);
const auto inWebPage = (_parent->media() != this);
constexpr auto kSmall = Ui::BubbleCornerRounding::Small;
const auto rounding = inWebPage
? Ui::BubbleRounding{ kSmall, kSmall, kSmall, kSmall }
: adjustedBubbleRounding();
auto highlight = context.highlight.range;
const auto tagged = lookupSpoilerTagMedia();
auto fullRect = QRect();
const auto subpartHighlight = IsSubGroupSelection(highlight);
for (auto i = 0, count = int(_parts.size()); i != count; ++i) {
const auto &part = _parts[i];
auto partContext = context.withSelection(fullSelection
? FullSelection
: textSelection
? selection
: IsGroupItemSelection(selection, i)
? FullSelection
: TextSelection());
const auto highlighted = (highlight.empty() && !i)
|| IsGroupItemSelection(highlight, i);
const auto highlightOpacity = highlighted
? context.highlight.opacity
: 0.;
partContext.highlight.range = highlighted
? TextSelection()
: highlight;
if (textSelection) {
selection = part.content->skipSelection(selection);
}
if (!subpartHighlight) {
highlight = part.content->skipSelection(highlight);
}
if (!part.cache.isNull()) {
wasCache = true;
}
part.content->drawGrouped(
p,
partContext,
part.geometry.translated(0, groupPadding.top()),
part.sides,
applyRoundingSides(rounding, part.sides),
highlightOpacity,
&part.cacheKey,
&part.cache);
if (!part.cache.isNull()) {
nowCache = true;
}
if (tagged || _purchasedPriceTag) {
fullRect = fullRect.united(part.geometry);
}
}
if (nowCache && !wasCache) {
history()->owner().registerHeavyViewPart(_parent);
}
if (tagged) {
tagged->drawSpoilerTag(p, fullRect, context, [&] {
return generateSpoilerTagBackground(fullRect);
});
} else if (_purchasedPriceTag) {
drawPurchasedTag(p, fullRect, context);
}
// date
if (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) {
auto fullRight = width();
auto fullBottom = height();
if (needInfoDisplay()) {
_parent->drawInfo(
p,
context,
fullRight,
fullBottom,
width(),
InfoDisplayType::Image);
}
if (const auto size = _parent->hasBubble() ? std::nullopt : _parent->rightActionSize()) {
auto fastShareLeft = _parent->hasRightLayout()
? (-size->width() - st::historyFastShareLeft)
: (fullRight + st::historyFastShareLeft);
auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
_parent->drawRightAction(p, context, fastShareLeft, fastShareTop, width());
}
}
}
TextState GroupedMedia::getPartState(
QPoint point,
StateRequest request) const {
auto shift = 0;
for (const auto &part : _parts) {
if (part.geometry.contains(point)) {
auto result = part.content->getStateGrouped(
part.geometry,
part.sides,
point,
request);
result.symbol += shift;
result.itemId = part.item->fullId();
return result;
}
shift += part.content->fullSelectionLength();
}
return TextState(_parent->data());
}
PointState GroupedMedia::pointState(QPoint point) const {
if (!QRect(0, 0, width(), height()).contains(point)) {
return PointState::Outside;
}
const auto groupPadding = groupedPadding();
point -= QPoint(0, groupPadding.top());
for (const auto &part : _parts) {
if (part.geometry.contains(point)) {
return PointState::GroupPart;
}
}
return PointState::Inside;
}
TextState GroupedMedia::textState(QPoint point, StateRequest request) const {
const auto groupPadding = groupedPadding();
auto result = getPartState(point - QPoint(0, groupPadding.top()), request);
if (const auto tagged = lookupSpoilerTagMedia()) {
if (QRect(0, 0, width(), height()).contains(point)) {
if (auto link = tagged->spoilerTagLink()) {
result.link = std::move(link);
}
}
}
if (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) {
auto fullRight = width();
auto fullBottom = height();
const auto bottomInfoResult = _parent->bottomInfoTextState(
fullRight,
fullBottom,
point,
InfoDisplayType::Image);
if (bottomInfoResult.link
|| bottomInfoResult.cursor != CursorState::None
|| bottomInfoResult.customTooltip) {
return bottomInfoResult;
}
if (const auto size = _parent->hasBubble() ? std::nullopt : _parent->rightActionSize()) {
auto fastShareLeft = _parent->hasRightLayout()
? (-size->width() - st::historyFastShareLeft)
: (fullRight + st::historyFastShareLeft);
auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
if (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) {
result.link = _parent->rightActionLink(point
- QPoint(fastShareLeft, fastShareTop));
}
}
}
return result;
}
bool GroupedMedia::toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const {
for (const auto &part : _parts) {
if (part.content->toggleSelectionByHandlerClick(p)) {
return true;
}
}
return false;
}
bool GroupedMedia::dragItemByHandler(const ClickHandlerPtr &p) const {
for (const auto &part : _parts) {
if (part.content->dragItemByHandler(p)) {
return true;
}
}
return false;
}
TextSelection GroupedMedia::adjustSelection(
TextSelection selection,
TextSelectType type) const {
if (_mode != Mode::Column) {
return {};
}
auto checked = 0;
for (const auto &part : _parts) {
const auto modified = ShiftItemSelection(
part.content->adjustSelection(
UnshiftItemSelection(selection, checked),
type),
checked);
const auto till = checked + part.content->fullSelectionLength();
if (selection.from >= checked && selection.from < till) {
selection.from = modified.from;
}
if (selection.to <= till) {
selection.to = modified.to;
return selection;
}
checked = till;
}
return selection;
}
uint16 GroupedMedia::fullSelectionLength() const {
if (_mode != Mode::Column) {
return {};
}
auto result = 0;
for (const auto &part : _parts) {
result += part.content->fullSelectionLength();
}
return result;
}
bool GroupedMedia::hasTextForCopy() const {
if (_mode != Mode::Column) {
return {};
}
for (const auto &part : _parts) {
if (part.content->hasTextForCopy()) {
return true;
}
}
return false;
}
TextForMimeData GroupedMedia::selectedText(
TextSelection selection) const {
if (_mode != Mode::Column) {
return {};
}
auto result = TextForMimeData();
for (const auto &part : _parts) {
auto text = part.content->selectedText(selection);
if (!text.empty()) {
if (result.empty()) {
result = std::move(text);
} else {
result.append(u"\n\n"_q).append(std::move(text));
}
}
selection = part.content->skipSelection(selection);
}
return result;
}
SelectedQuote GroupedMedia::selectedQuote(TextSelection selection) const {
if (_mode != Mode::Column) {
return {};
}
for (const auto &part : _parts) {
const auto next = part.content->skipSelection(selection);
if (next.to - next.from != selection.to - selection.from) {
if (!next.empty()) {
return SelectedQuote();
}
auto result = part.content->selectedQuote(selection);
result.item = part.item;
return result;
}
selection = next;
}
return {};
}
TextSelection GroupedMedia::selectionFromQuote(
const SelectedQuote &quote) const {
Expects(quote.item != nullptr);
if (_mode != Mode::Column) {
return {};
}
const auto i = ranges::find(_parts, not_null(quote.item), &Part::item);
if (i == end(_parts)) {
return {};
}
const auto index = int(i - begin(_parts));
auto result = i->content->selectionFromQuote(quote);
if (result.empty()) {
return AddGroupItemSelection({}, index);
}
for (auto j = i; j != begin(_parts);) {
result = (--j)->content->unskipSelection(result);
}
return result;
}
auto GroupedMedia::getBubbleSelectionIntervals(
TextSelection selection) const
-> std::vector<Ui::BubbleSelectionInterval> {
if (_mode != Mode::Column) {
return {};
}
auto result = std::vector<Ui::BubbleSelectionInterval>();
for (auto i = 0, count = int(_parts.size()); i != count; ++i) {
const auto &part = _parts[i];
if (!IsGroupItemSelection(selection, i)) {
continue;
}
const auto &geometry = part.geometry;
if (result.empty()
|| (result.back().top + result.back().height
< geometry.top())
|| (result.back().top > geometry.top() + geometry.height())) {
result.push_back({ geometry.top(), geometry.height() });
} else {
auto &last = result.back();
const auto newTop = std::min(last.top, geometry.top());
const auto newHeight = std::max(
last.top + last.height - newTop,
geometry.top() + geometry.height() - newTop);
last = Ui::BubbleSelectionInterval{ newTop, newHeight };
}
}
const auto groupPadding = groupedPadding();
for (auto &part : result) {
part.top += groupPadding.top();
}
if (IsGroupItemSelection(selection, 0)) {
result.front().top -= groupPadding.top();
result.front().height += groupPadding.top();
}
if (IsGroupItemSelection(selection, _parts.size() - 1)) {
result.back().height = height() - result.back().top;
}
return result;
}
void GroupedMedia::clickHandlerActiveChanged(
const ClickHandlerPtr &p,
bool active) {
for (const auto &part : _parts) {
part.content->clickHandlerActiveChanged(p, active);
}
}
void GroupedMedia::clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) {
for (const auto &part : _parts) {
part.content->clickHandlerPressedChanged(p, pressed);
if (pressed && part.content->dragItemByHandler(p)) {
// #TODO drag by item from album
// App::pressedLinkItem(part.view);
}
}
}
template <typename DataMediaRange>
bool GroupedMedia::applyGroup(const DataMediaRange &medias) {
if (validateGroupParts(medias)) {
return true;
}
auto modeChosen = false;
for (const auto media : medias) {
const auto mediaMode = DetectMode(media);
if (!modeChosen) {
_mode = mediaMode;
modeChosen = true;
} else if (mediaMode != _mode) {
continue;
}
_parts.push_back(Part(_parent, media));
}
if (_parts.empty()) {
return false;
}
Ensures(_parts.size() <= kMaxSize);
return true;
}
template <typename DataMediaRange>
bool GroupedMedia::validateGroupParts(
const DataMediaRange &medias) const {
auto i = 0;
const auto count = _parts.size();
for (const auto media : medias) {
if (i >= count || _parts[i].item != media->parent()) {
return false;
}
++i;
}
return (i == count);
}
not_null<Media*> GroupedMedia::main() const {
Expects(!_parts.empty());
return _parts.back().content.get();
}
void GroupedMedia::hideSpoilers() {
for (const auto &part : _parts) {
part.content->hideSpoilers();
}
}
Storage::SharedMediaTypesMask GroupedMedia::sharedMediaTypes() const {
return main()->sharedMediaTypes();
}
PhotoData *GroupedMedia::getPhoto() const {
return main()->getPhoto();
}
DocumentData *GroupedMedia::getDocument() const {
return main()->getDocument();
}
HistoryMessageEdited *GroupedMedia::displayedEditBadge() const {
for (const auto &part : _parts) {
if (!part.item->hideEditedBadge()) {
if (const auto edited = part.item->Get<HistoryMessageEdited>()) {
return edited;
}
}
}
return nullptr;
}
void GroupedMedia::updateNeedBubbleState() {
_needBubble = computeNeedBubble();
}
void GroupedMedia::stopAnimation() {
for (const auto &part : _parts) {
part.content->stopAnimation();
}
}
void GroupedMedia::checkAnimation() {
for (const auto &part : _parts) {
part.content->checkAnimation();
}
}
bool GroupedMedia::hasHeavyPart() const {
for (const auto &part : _parts) {
if (!part.cache.isNull() || part.content->hasHeavyPart()) {
return true;
}
}
return false;
}
void GroupedMedia::unloadHeavyPart() {
for (const auto &part : _parts) {
part.content->unloadHeavyPart();
part.cacheKey = 0;
part.cache = QPixmap();
}
}
void GroupedMedia::parentTextUpdated() {
if (_parent->media() == this) {
if (_mode == Mode::Column) {
for (const auto &part : _parts) {
part.content->parentTextUpdated();
}
} else {
_captionItem = std::nullopt;
}
}
}
bool GroupedMedia::needsBubble() const {
return _needBubble;
}
QPoint GroupedMedia::resolveCustomInfoRightBottom() const {
const auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x());
const auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y());
return QPoint(width() - skipx, height() - skipy);
}
std::optional<PaidInformation> GroupedMedia::paidInformation() const {
auto result = PaidInformation();
for (const auto &part : _parts) {
++result.messages;
result.stars += part.item->starsPaid();
}
return result;
}
bool GroupedMedia::enforceBubbleWidth() const {
return _mode == Mode::Grid;
}
bool GroupedMedia::computeNeedBubble() const {
Expects(_mode == Mode::Column || _captionItem.has_value());
if (_mode == Mode::Column || *_captionItem) {
return true;
}
if (const auto item = _parent->data()) {
if (item->repliesAreComments()
|| item->externalReply()
|| item->viaBot()
|| _parent->displayReply()
|| _parent->displayForwardedFrom()
|| _parent->displayFromName()
|| _parent->displayedTopicButton()
) {
return true;
}
}
return false;
}
bool GroupedMedia::needInfoDisplay() const {
const auto item = _parent->data();
return (_mode != Mode::Column)
&& (item->isSending()
|| item->awaitingVideoProcessing()
|| item->hasFailed()
|| _parent->isUnderCursor()
|| (_parent->delegate()->elementContext() == Context::ChatPreview)
|| _parent->isLastAndSelfMessage());
}
} // namespace HistoryView

View File

@@ -0,0 +1,164 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
#include "data/data_document.h"
#include "data/data_photo.h"
namespace Data {
class Media;
} // namespace Data
namespace HistoryView {
class GroupedMedia : public Media {
public:
static constexpr auto kMaxSize = 10;
GroupedMedia(
not_null<Element*> parent,
const std::vector<std::unique_ptr<Data::Media>> &medias);
GroupedMedia(
not_null<Element*> parent,
const std::vector<not_null<HistoryItem*>> &items);
~GroupedMedia();
void refreshParentId(not_null<HistoryItem*> realParent) override;
HistoryItem *itemForText() const override;
bool hideMessageText() const override;
void drawHighlight(
Painter &p,
const PaintContext &context,
int top) const override;
void draw(Painter &p, const PaintContext &context) const override;
PointState pointState(QPoint point) const override;
TextState textState(
QPoint point,
StateRequest request) const override;
bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const override;
bool dragItemByHandler(const ClickHandlerPtr &p) const override;
[[nodiscard]] TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const override;
uint16 fullSelectionLength() const override;
bool hasTextForCopy() const override;
PhotoData *getPhoto() const override;
DocumentData *getDocument() const override;
TextForMimeData selectedText(TextSelection selection) const override;
SelectedQuote selectedQuote(TextSelection selection) const override;
TextSelection selectionFromQuote(
const SelectedQuote &quote) const override;
std::vector<Ui::BubbleSelectionInterval> getBubbleSelectionIntervals(
TextSelection selection) const override;
void clickHandlerActiveChanged(
const ClickHandlerPtr &p,
bool active) override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) override;
void hideSpoilers() override;
Storage::SharedMediaTypesMask sharedMediaTypes() const override;
bool overrideEditedDate() const override {
return true;
}
HistoryMessageEdited *displayedEditBadge() const override;
bool skipBubbleTail() const override {
return (_mode == Mode::Grid) && isRoundedInBubbleBottom();
}
void updateNeedBubbleState() override;
bool needsBubble() const override;
bool customInfoLayout() const override {
return (_mode != Mode::Column);
}
QPoint resolveCustomInfoRightBottom() const override;
bool allowsFastShare() const override {
return true;
}
std::optional<PaidInformation> paidInformation() const override;
bool customHighlight() const override {
return true;
}
bool enforceBubbleWidth() const override;
void stopAnimation() override;
void checkAnimation() override;
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
void parentTextUpdated() override;
private:
enum class Mode : char {
Grid,
Column,
};
struct Part {
Part(
not_null<Element*> parent,
not_null<Data::Media*> media);
not_null<HistoryItem*> item;
std::unique_ptr<Media> content;
RectParts sides = RectPart::None;
QRect initialGeometry;
QRect geometry;
mutable uint64 cacheKey = 0;
mutable QPixmap cache;
};
[[nodiscard]] static Mode DetectMode(not_null<Data::Media*> media);
template <typename DataMediaRange>
bool applyGroup(const DataMediaRange &medias);
template <typename DataMediaRange>
bool validateGroupParts(const DataMediaRange &medias) const;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
bool needInfoDisplay() const;
bool computeNeedBubble() const;
not_null<Media*> main() const;
TextState getPartState(
QPoint point,
StateRequest request) const;
[[nodiscard]] Ui::BubbleRounding applyRoundingSides(
Ui::BubbleRounding already,
RectParts sides) const;
[[nodiscard]] QMargins groupedPadding() const;
[[nodiscard]] Media *lookupSpoilerTagMedia() const;
[[nodiscard]] QImage generateSpoilerTagBackground(QRect full) const;
mutable std::optional<HistoryItem*> _captionItem;
std::vector<Part> _parts;
Mode _mode = Mode::Grid;
bool _needBubble : 1 = false;
bool _purchasedPriceTag : 1 = false;
};
} // namespace HistoryView

View File

@@ -0,0 +1,9 @@
/*
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/media/history_view_media_spoiler.h"

View File

@@ -0,0 +1,39 @@
/*
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
*/
#pragma once
#include "ui/chat/message_bubble.h"
#include "ui/effects/animations.h"
namespace Ui {
class SpoilerAnimation;
} // namespace Ui
namespace HistoryView {
struct MediaSpoiler {
ClickHandlerPtr link;
std::unique_ptr<Ui::SpoilerAnimation> animation;
QImage cornerCache;
QImage background;
std::optional<Ui::BubbleRounding> backgroundRounding;
Ui::Animations::Simple revealAnimation;
bool revealed = false;
};
struct MediaSpoilerTag {
uint64 price : 63 = 0;
uint64 sensitive : 1 = 0;
QImage cache;
QColor darken;
QColor fg;
QColor star;
ClickHandlerPtr link;
};
} // namespace HistoryView

View File

@@ -0,0 +1,679 @@
/*
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/media/history_view_media_unwrapped.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/view/media/history_view_media_common.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_reply.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "lottie/lottie_single_player.h"
#include "ui/cached_round_corners.h"
#include "ui/chat/chat_style.h"
#include "ui/painter.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
constexpr auto kMaxForwardedBarLines = 4;
} // namespace
std::unique_ptr<StickerPlayer> UnwrappedMedia::Content::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return nullptr;
}
QSize UnwrappedMedia::Content::countCurrentSize(int newWidth) {
return countOptimalSize();
}
UnwrappedMedia::UnwrappedMedia(
not_null<Element*> parent,
std::unique_ptr<Content> content)
: Media(parent)
, _content(std::move(content)) {
}
QSize UnwrappedMedia::countOptimalSize() {
_content->refreshLink();
const auto optimal = _content->countOptimalSize();
auto maxWidth = optimal.width();
const auto minimal = std::max(st::emojiSize, st::msgPhotoSize);
auto minHeight = std::max(optimal.height(), minimal);
if (_parent->media() == this) {
const auto item = _parent->data();
const auto via = item->Get<HistoryMessageVia>();
const auto reply = _parent->Get<Reply>();
const auto topic = _parent->displayedTopicButton();
const auto forwarded = getDisplayedForwardedInfo();
if (forwarded) {
forwarded->create(via, item);
}
maxWidth += additionalWidth(topic, reply, via, forwarded);
accumulate_max(maxWidth, _parent->reactionsOptimalWidth());
if (const auto size = _parent->rightActionSize()) {
minHeight = std::max(
minHeight,
st::historyFastShareBottom + size->height());
}
}
return { maxWidth, minHeight };
}
QSize UnwrappedMedia::countCurrentSize(int newWidth) {
const auto item = _parent->data();
accumulate_min(newWidth, maxWidth());
_contentSize = _content->countCurrentSize(newWidth);
auto newHeight = std::max(minHeight(), _contentSize.height());
_additionalOnTop = false;
if (_parent->media() != this) {
return { newWidth, newHeight };
}
if (_parent->hasRightLayout()) {
// Add some height to isolated emoji for the timestamp info.
const auto infoHeight = st::msgDateImgPadding.y() * 2
+ st::msgDateFont->height;
const auto minimal = std::min(
st::largeEmojiSize + 2 * st::largeEmojiOutline,
_contentSize.height());
accumulate_max(newHeight, minimal + st::msgDateImgDelta + infoHeight);
}
accumulate_max(newWidth, _parent->reactionsOptimalWidth());
_topAdded = 0;
const auto via = item->Get<HistoryMessageVia>();
const auto reply = _parent->Get<Reply>();
const auto topic = _parent->displayedTopicButton();
const auto forwarded = getDisplayedForwardedInfo();
if (topic || via || reply || forwarded) {
const auto additional = additionalWidth(topic, reply, via, forwarded);
const auto optimalw = maxWidth() - additional;
const auto additionalMinWidth = std::min(additional, st::msgReplyPadding.left() + st::msgMinWidth / 2);
_additionalOnTop = (optimalw + additionalMinWidth) > newWidth;
const auto surroundingWidth = _additionalOnTop
? std::min(newWidth - st::msgReplyPadding.left(), additional)
: (newWidth - _contentSize.width() - st::msgReplyPadding.left());
if (reply) {
[[maybe_unused]] auto h = reply->resizeToWidth(surroundingWidth);
}
const auto surrounding = surroundingInfo(topic, reply, via, forwarded, surroundingWidth);
if (_additionalOnTop) {
_topAdded = surrounding.height + st::msgMargin.bottom();
newHeight += _topAdded;
} else {
const auto infoHeight = st::msgDateImgPadding.y() * 2
+ st::msgDateFont->height;
const auto minimal = surrounding.height
+ st::msgDateImgDelta
+ infoHeight;
newHeight = std::max(newHeight, minimal);
}
const auto availw = newWidth
- (_additionalOnTop ? 0 : optimalw + st::msgReplyPadding.left())
- 2 * st::msgReplyPadding.left();
if (via) {
via->resize(availw);
}
}
return { newWidth, newHeight };
}
void UnwrappedMedia::draw(Painter &p, const PaintContext &context) const {
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
return;
}
const auto rightAligned = _parent->hasRightLayout();
const auto inWebPage = (_parent->media() != this);
const auto item = _parent->data();
auto usex = 0;
auto usew = _contentSize.width();
if (!inWebPage && rightAligned) {
usex = width() - usew;
}
if (rtl()) {
usex = width() - usex - usew;
}
const auto usey = rightAligned ? _topAdded : (height() - _contentSize.height());
const auto useh = rightAligned
? std::max(
_contentSize.height(),
(height()
- _topAdded
- st::msgDateImgPadding.y() * 2
- st::msgDateFont->height))
: _contentSize.height();
const auto inner = QRect(usex, usey, usew, useh);
if (context.skipDrawingParts != PaintContext::SkipDrawingParts::Content) {
_content->draw(p, context, inner);
}
if (!inWebPage && (context.skipDrawingParts
!= PaintContext::SkipDrawingParts::Surrounding)) {
const auto via = inWebPage ? nullptr : item->Get<HistoryMessageVia>();
const auto reply = inWebPage ? nullptr : _parent->Get<Reply>();
const auto topic = inWebPage ? nullptr : _parent->displayedTopicButton();
const auto forwarded = inWebPage ? nullptr : getDisplayedForwardedInfo();
drawSurrounding(p, inner, context, topic, reply, via, forwarded);
}
}
UnwrappedMedia::SurroundingInfo UnwrappedMedia::surroundingInfo(
const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via,
const HistoryMessageForwarded *forwarded,
int outerw) const {
if (!topic && !via && !reply && !forwarded) {
return {};
}
const auto innerw = outerw - st::msgReplyPadding.left() - st::msgReplyPadding.right();
auto topicSize = QSize();
if (topic) {
const auto padding = st::topicButtonPadding;
const auto height = padding.top()
+ st::msgNameFont->height
+ padding.bottom();
const auto width = std::max(
std::min(
outerw,
(st::msgReplyPadding.left()
+ topic->name.maxWidth()
+ st::topicButtonArrowSkip
+ st::topicButtonPadding.right())),
height);
topicSize = { width, height };
}
auto panelHeight = 0;
auto forwardedHeightReal = forwarded
? forwarded->text.countHeight(innerw)
: 0;
auto forwardedHeight = std::min(
forwardedHeightReal,
kMaxForwardedBarLines * st::msgServiceNameFont->height);
const auto breakEverywhere = (forwardedHeightReal > forwardedHeight);
if (forwarded) {
panelHeight += forwardedHeight;
} else if (via) {
panelHeight += st::msgServiceNameFont->height
+ (reply ? st::msgReplyPadding.top() : 0);
}
if (panelHeight) {
panelHeight += st::msgReplyPadding.top();
}
if (reply) {
const auto replyMargins = reply->margins();
panelHeight += reply->height()
- ((forwarded || via) ? 0 : replyMargins.top())
- replyMargins.bottom();
} else {
panelHeight += st::msgReplyPadding.bottom();
}
const auto total = (topicSize.isEmpty() ? 0 : topicSize.height())
+ ((panelHeight || !topicSize.height()) ? st::topicButtonSkip : 0)
+ panelHeight;
return {
.topicSize = topicSize,
.height = total,
.panelHeight = panelHeight,
.forwardedHeight = forwardedHeight,
.forwardedBreakEverywhere = breakEverywhere,
};
}
void UnwrappedMedia::drawSurrounding(
Painter &p,
const QRect &inner,
const PaintContext &context,
const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via,
const HistoryMessageForwarded *forwarded) const {
const auto st = context.st;
const auto sti = context.imageStyle();
const auto rightAligned = _parent->hasRightLayout();
const auto rightActionSize = _parent->rightActionSize();
const auto fullRight = calculateFullRight(inner);
auto fullBottom = height();
if (needInfoDisplay()) {
_parent->drawInfo(
p,
context,
fullRight,
fullBottom,
inner.x() * 2 + inner.width(),
InfoDisplayType::Background);
}
auto replyLeft = 0;
auto replyRight = 0;
auto rectw = _additionalOnTop
? std::min(width() - st::msgReplyPadding.left(), additionalWidth(topic, reply, via, forwarded))
: (width() - inner.width() - st::msgReplyPadding.left());
if (const auto surrounding = surroundingInfo(topic, reply, via, forwarded, rectw)) {
auto recth = surrounding.panelHeight;
if (!surrounding.topicSize.isEmpty()) {
auto rectw = surrounding.topicSize.width();
int rectx = _additionalOnTop
? (rightAligned ? (inner.x() + inner.width() - rectw) : 0)
: (rightAligned ? 0 : (inner.width() + st::msgReplyPadding.left()));
int recty = 0;
if (rtl()) rectx = width() - rectx - rectw;
{
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(sti->msgServiceBg);
const auto recth = surrounding.topicSize.height();
p.drawRoundedRect(
QRect{ rectx, recty, rectw, recth },
recth / 2,
recth / 2);
}
p.setPen(st->msgServiceFg());
rectx += st::msgReplyPadding.left();
recty += st::topicButtonPadding.top();
rectw -= st::msgReplyPadding.left() + st::topicButtonPadding.right() + st::topicButtonArrowSkip;
p.setTextPalette(st->serviceTextPalette());
topic->name.drawElided(p, rectx, recty, rectw);
p.restoreTextPalette();
const auto &icon = st::topicButtonArrow;
icon.paint(
p,
rectx + rectw + st::topicButtonArrowPosition.x(),
recty + st::topicButtonArrowPosition.y(),
width(),
st->msgServiceFg()->c);
}
if (recth) {
int rectx = _additionalOnTop
? (rightAligned ? (inner.x() + inner.width() - rectw) : 0)
: (rightAligned ? 0 : (inner.width() + st::msgReplyPadding.left()));
int recty = surrounding.height - recth;
if (rtl()) rectx = width() - rectx - rectw;
Ui::FillRoundRect(p, rectx, recty, rectw, recth, sti->msgServiceBg, sti->msgServiceBgCornersSmall);
p.setPen(st->msgServiceFg());
const auto textx = rectx + st::msgReplyPadding.left();
const auto textw = rectw - st::msgReplyPadding.left() - st::msgReplyPadding.right();
if (forwarded) {
p.setTextPalette(st->serviceTextPalette());
forwarded->text.drawElided(p, textx, recty + st::msgReplyPadding.top(), textw, kMaxForwardedBarLines, style::al_left, 0, -1, 0, surrounding.forwardedBreakEverywhere);
p.restoreTextPalette();
const auto skip = std::min(
forwarded->text.countHeight(textw),
kMaxForwardedBarLines * st::msgServiceNameFont->height);
recty += skip;
} else if (via) {
p.setFont(st::msgDateFont);
p.drawTextLeft(textx, recty + st::msgReplyPadding.top(), 2 * textx + textw, via->text);
const auto skip = st::msgServiceNameFont->height
+ (reply ? st::msgReplyPadding.top() : 0);
recty += skip;
}
if (reply) {
if (forwarded || via) {
recty += st::msgReplyPadding.top();
recth -= st::msgReplyPadding.top();
} else {
recty -= reply->margins().top();
}
reply->paint(p, _parent, context, rectx, recty, rectw, false);
}
replyLeft = rectx;
replyRight = rectx + rectw;
}
}
if (rightActionSize) {
const auto position = calculateFastActionPosition(
inner,
rightAligned,
replyLeft,
replyRight,
reply ? reply->height() : 0,
fullBottom,
fullRight,
*rightActionSize);
const auto outer = 2 * inner.x() + inner.width();
_parent->drawRightAction(p, context, position.x(), position.y(), outer);
}
}
PointState UnwrappedMedia::pointState(QPoint point) const {
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
return PointState::Outside;
}
const auto rightAligned = _parent->hasRightLayout();
const auto inWebPage = (_parent->media() != this);
auto usex = 0;
auto usew = _contentSize.width();
if (!inWebPage && rightAligned) {
usex = width() - usew;
}
if (rtl()) {
usex = width() - usex - usew;
}
const auto datey = height() - st::msgDateImgPadding.y() * 2
- st::msgDateFont->height;
const auto usey = rightAligned ? _topAdded : (height() - _contentSize.height());
const auto useh = rightAligned
? std::max(_contentSize.height(), datey)
: _contentSize.height();
const auto inner = QRect(usex, usey, usew, useh);
// Rectangle of date bubble.
if (point.x() < calculateFullRight(inner) && point.y() > datey) {
return PointState::Inside;
}
return inner.contains(point) ? PointState::Inside : PointState::Outside;
}
TextState UnwrappedMedia::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
return result;
}
const auto rightAligned = _parent->hasRightLayout();
const auto inWebPage = (_parent->media() != this);
const auto item = _parent->data();
auto usex = 0;
auto usew = _contentSize.width();
if (!inWebPage && rightAligned) {
usex = width() - usew;
}
if (rtl()) {
usex = width() - usex - usew;
}
const auto usey = rightAligned ? _topAdded : (height() - _contentSize.height());
const auto useh = rightAligned
? std::max(
_contentSize.height(),
height() - st::msgDateImgPadding.y() * 2 - st::msgDateFont->height)
: _contentSize.height();
const auto inner = QRect(usex, usey, usew, useh);
if (_parent->media() == this) {
const auto via = inWebPage ? nullptr : item->Get<HistoryMessageVia>();
const auto reply = inWebPage ? nullptr : _parent->Get<Reply>();
const auto topic = inWebPage ? nullptr : _parent->displayedTopicButton();
const auto forwarded = inWebPage ? nullptr : getDisplayedForwardedInfo();
auto replyLeft = 0;
auto replyRight = 0;
auto rectw = _additionalOnTop
? std::min(width() - st::msgReplyPadding.left(), additionalWidth(topic, reply, via, forwarded))
: (width() - inner.width() - st::msgReplyPadding.left());
if (const auto surrounding = surroundingInfo(topic, reply, via, forwarded, rectw)) {
auto recth = surrounding.panelHeight;
if (!surrounding.topicSize.isEmpty()) {
auto rectw = surrounding.topicSize.width();
int rectx = _additionalOnTop
? (rightAligned ? (inner.x() + inner.width() - rectw) : 0)
: (rightAligned ? 0 : (inner.width() + st::msgReplyPadding.left()));
int recty = 0;
if (rtl()) rectx = width() - rectx - rectw;
if (QRect(QPoint(rectx, recty), surrounding.topicSize).contains(point)) {
result.link = topic->link;
return result;
}
}
if (recth) {
int rectx = _additionalOnTop
? (rightAligned ? (inner.x() + inner.width() - rectw) : 0)
: (rightAligned ? 0 : (inner.width() + st::msgReplyPadding.left()));
int recty = surrounding.height - recth;
if (rtl()) rectx = width() - rectx - rectw;
if (forwarded) {
if (QRect(rectx, recty, rectw, st::msgReplyPadding.top() + surrounding.forwardedHeight).contains(point)) {
auto textRequest = request.forText();
if (surrounding.forwardedBreakEverywhere) {
textRequest.flags |= Ui::Text::StateRequest::Flag::BreakEverywhere;
}
const auto innerw = rectw - st::msgReplyPadding.left() - st::msgReplyPadding.right();
result = TextState(_parent, forwarded->text.getState(
point - QPoint(rectx + st::msgReplyPadding.left(), recty + st::msgReplyPadding.top()),
innerw,
textRequest));
result.symbol = 0;
result.afterSymbol = false;
if (surrounding.forwardedBreakEverywhere) {
result.cursor = CursorState::Forwarded;
} else {
result.cursor = CursorState::None;
}
return result;
}
recty += surrounding.forwardedHeight;
recth -= surrounding.forwardedHeight;
} else if (via) {
int viah = st::msgReplyPadding.top() + st::msgServiceNameFont->height + (reply ? 0 : st::msgReplyPadding.bottom());
if (QRect(rectx, recty, rectw, viah).contains(point)) {
result.link = via->link;
return result;
}
int skip = st::msgServiceNameFont->height + (reply ? 2 * st::msgReplyPadding.top() : 0);
recty += skip;
recth -= skip;
}
if (reply) {
if (forwarded || via) {
recty += st::msgReplyPadding.top();
recth -= st::msgReplyPadding.top() + reply->margins().top();
} else {
recty -= reply->margins().top();
}
const auto replyRect = QRect(rectx, recty, rectw, recth);
if (replyRect.contains(point)) {
result.link = reply->link();
reply->saveRipplePoint(point - replyRect.topLeft());
reply->createRippleAnimation(_parent, replyRect.size());
}
}
replyLeft = rectx;
replyRight = rectx + rectw;
}
}
const auto fullRight = calculateFullRight(inner);
const auto rightActionSize = _parent->rightActionSize();
auto fullBottom = height();
const auto bottomInfoResult = _parent->bottomInfoTextState(
fullRight,
fullBottom,
point,
InfoDisplayType::Background);
if (bottomInfoResult.link
|| bottomInfoResult.cursor != CursorState::None
|| bottomInfoResult.customTooltip) {
return bottomInfoResult;
}
if (rightActionSize) {
const auto position = calculateFastActionPosition(
inner,
rightAligned,
replyLeft,
replyRight,
reply ? reply->height() : 0,
fullBottom,
fullRight,
*rightActionSize);
if (QRect(position.x(), position.y(), rightActionSize->width(), rightActionSize->height()).contains(point)) {
result.link = _parent->rightActionLink(point - position);
return result;
}
}
}
// Link of content can be nullptr (e.g. sticker without stickerpack).
// So we have to process it to avoid overriding the previous result.
if (_content->link() && inner.contains(point)) {
result.link = _content->link();
return result;
}
return result;
}
bool UnwrappedMedia::hasTextForCopy() const {
return _content->hasTextForCopy();
}
bool UnwrappedMedia::dragItemByHandler(const ClickHandlerPtr &p) const {
const auto reply = _parent->Get<Reply>();
return !reply || (reply->link() != p);
}
QRect UnwrappedMedia::contentRectForReactions() const {
const auto inWebPage = (_parent->media() != this);
if (inWebPage) {
return QRect(0, 0, width(), height());
}
const auto rightAligned = _parent->hasRightLayout();
auto usex = 0;
auto usew = _contentSize.width();
accumulate_max(usew, _parent->reactionsOptimalWidth());
if (rightAligned) {
usex = width() - usew;
}
if (rtl()) {
usex = width() - usex - usew;
}
const auto usey = rightAligned ? _topAdded : (height() - _contentSize.height());
const auto useh = rightAligned
? std::max(
_contentSize.height(),
height() - st::msgDateImgPadding.y() * 2 - st::msgDateFont->height)
: _contentSize.height();
return QRect(usex, usey, usew, useh);
}
std::optional<int> UnwrappedMedia::reactionButtonCenterOverride() const {
const auto fullRight = calculateFullRight(contentRectForReactions());
const auto right = fullRight
- _parent->infoWidth()
- st::msgDateImgPadding.x() * 2
- st::msgReplyPadding.left();
return right - st::reactionCornerSize.width() / 2;
}
QPoint UnwrappedMedia::resolveCustomInfoRightBottom() const {
const auto inner = contentRectForReactions();
const auto fullBottom = inner.y() + inner.height();
const auto fullRight = calculateFullRight(inner);
const auto skipx = st::msgDateImgPadding.x();
const auto skipy = st::msgDateImgPadding.y();
return QPoint(fullRight - skipx, fullBottom - skipy);
}
std::unique_ptr<StickerPlayer> UnwrappedMedia::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return _content->stickerTakePlayer(data, replacements);
}
int UnwrappedMedia::calculateFullRight(const QRect &inner) const {
const auto rightAligned = _parent->hasRightLayout();
const auto infoWidth = _parent->infoWidth()
+ st::msgDateImgPadding.x() * 2
+ st::msgReplyPadding.left();
const auto rightActionSize = _parent->rightActionSize();
const auto rightSkip = st::msgPadding.left()
+ (_parent->hasFromPhoto()
? st::msgMargin.right()
: st::msgPadding.right());
const auto rightActionWidth = rightActionSize
? (st::historyFastShareLeft * 2
+ rightActionSize->width())
: 0;
auto fullRight = inner.x()
+ inner.width()
+ (rightAligned ? 0 : infoWidth);
const auto rightActionSkip = rightAligned ? 0 : rightActionWidth;
if (fullRight + rightActionSkip + rightSkip > _parent->width()) {
fullRight = _parent->width()
- (rightAligned ? 0 : rightActionSkip)
- rightSkip;
}
return fullRight;
}
QPoint UnwrappedMedia::calculateFastActionPosition(
QRect inner,
bool rightAligned,
int replyLeft,
int replyRight,
int replyHeight,
int fullBottom,
int fullRight,
QSize size) const {
const auto fastShareTop = (fullBottom
- st::historyFastShareBottom
- size.height());
const auto doesRightActionHitReply = replyRight
&& (fastShareTop < replyHeight);
const auto fastShareLeft = rightAligned
? ((doesRightActionHitReply ? replyLeft : inner.x())
- size.width()
- st::historyFastShareLeft)
: ((doesRightActionHitReply ? replyRight : fullRight)
+ st::historyFastShareLeft);
return QPoint(fastShareLeft, fastShareTop);
}
bool UnwrappedMedia::needInfoDisplay() const {
return _parent->data()->isSending()
|| _parent->data()->hasFailed()
|| _parent->isUnderCursor()
|| _parent->rightActionSize()
|| _parent->isLastAndSelfMessage()
|| (_parent->delegate()->elementContext() == Context::ChatPreview)
|| (_parent->hasRightLayout()
&& _content->alwaysShowOutTimestamp());
}
int UnwrappedMedia::additionalWidth(
const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via,
const HistoryMessageForwarded *forwarded) const {
auto result = st::msgReplyPadding.left() + _parent->infoWidth() + 2 * st::msgDateImgPadding.x();
if (topic) {
accumulate_max(result, 2 * st::msgReplyPadding.left() + topic->name.maxWidth() + st::topicButtonArrowSkip + st::topicButtonPadding.right());
}
if (forwarded) {
accumulate_max(result, 2 * st::msgReplyPadding.left() + forwarded->text.maxWidth() + st::msgReplyPadding.right());
} else if (via) {
accumulate_max(result, 2 * st::msgReplyPadding.left() + via->maxWidth + st::msgReplyPadding.right());
}
if (reply) {
accumulate_max(result, st::msgReplyPadding.left() + reply->maxWidth());
}
return result;
}
auto UnwrappedMedia::getDisplayedForwardedInfo() const
-> const HistoryMessageForwarded * {
return _parent->displayForwardedFrom()
? _parent->data()->Get<HistoryMessageForwarded>()
: nullptr;
}
} // namespace HistoryView

View File

@@ -0,0 +1,167 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
#include "base/weak_ptr.h"
#include "base/timer.h"
struct HistoryMessageVia;
struct HistoryMessageReply;
struct HistoryMessageForwarded;
namespace HistoryView {
class Reply;
struct TopicButton;
class UnwrappedMedia final : public Media {
public:
class Content {
public:
[[nodiscard]] virtual QSize countOptimalSize() = 0;
[[nodiscard]] virtual QSize countCurrentSize(int newWidth);
virtual void draw(
Painter &p,
const PaintContext &context,
const QRect &r) = 0;
[[nodiscard]] virtual ClickHandlerPtr link() {
return nullptr;
}
[[nodiscard]] virtual DocumentData *document() {
return nullptr;
}
virtual void stickerClearLoopPlayed() {
}
virtual std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements);
virtual bool hasHeavyPart() const {
return false;
}
virtual void unloadHeavyPart() {
}
virtual void refreshLink() {
}
[[nodiscard]] virtual bool alwaysShowOutTimestamp() {
return false;
}
virtual bool hasTextForCopy() const {
return false;
}
virtual ~Content() = default;
};
UnwrappedMedia(
not_null<Element*> parent,
std::unique_ptr<Content> content);
void draw(Painter &p, const PaintContext &context) const override;
PointState pointState(QPoint point) const override;
TextState textState(QPoint point, StateRequest request) const override;
bool hasTextForCopy() const override;
bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
return true;
}
bool dragItemByHandler(const ClickHandlerPtr &p) const override;
DocumentData *getDocument() const override {
return _content->document();
}
bool needsBubble() const override {
return false;
}
bool unwrapped() const override {
return true;
}
bool customInfoLayout() const override {
return true;
}
QRect contentRectForReactions() const override;
std::optional<int> reactionButtonCenterOverride() const override;
QPoint resolveCustomInfoRightBottom() const override;
void stickerClearLoopPlayed() override {
_content->stickerClearLoopPlayed();
}
std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) override;
bool hasHeavyPart() const override {
return _content->hasHeavyPart();
}
void unloadHeavyPart() override {
_content->unloadHeavyPart();
}
private:
struct SurroundingInfo {
QSize topicSize;
int height = 0;
int panelHeight = 0;
int forwardedHeight = 0;
bool forwardedBreakEverywhere = false;
explicit operator bool() const {
return (height > 0);
}
};
[[nodiscard]] SurroundingInfo surroundingInfo(
const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via,
const HistoryMessageForwarded *forwarded,
int outerw) const;
void drawSurrounding(
Painter &p,
const QRect &inner,
const PaintContext &context,
const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via,
const HistoryMessageForwarded *forwarded) const;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
bool needInfoDisplay() const;
int additionalWidth(
const TopicButton *topic,
const Reply *reply,
const HistoryMessageVia *via,
const HistoryMessageForwarded *forwarded) const;
int calculateFullRight(const QRect &inner) const;
QPoint calculateFastActionPosition(
QRect inner,
bool rightAligned,
int replyLeft,
int replyRight,
int replyHeight,
int fullBottom,
int fullRight,
QSize size) const;
const HistoryMessageForwarded *getDisplayedForwardedInfo() const;
std::unique_ptr<Content> _content;
QSize _contentSize;
int _topAdded = 0;
bool _additionalOnTop = false;
};
} // namespace HistoryView

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,178 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_file.h"
class Image;
enum class ImageRoundRadius;
namespace Data {
class PhotoMedia;
} // namespace Data
namespace Media {
namespace Streaming {
class Instance;
struct Update;
enum class Error;
struct Information;
} // namespace Streaming
} // namespace Media
namespace HistoryView {
class Photo final : public File {
public:
Photo(
not_null<Element*> parent,
not_null<HistoryItem*> realParent,
not_null<PhotoData*> photo,
bool spoiler);
Photo(
not_null<Element*> parent,
not_null<PeerData*> chat,
not_null<PhotoData*> photo,
int width);
~Photo();
bool hideMessageText() const override {
return false;
}
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
PhotoData *getPhoto() const override {
return _data;
}
void showPhoto(FullMsgId id);
void paintUserpicFrame(
Painter &p,
QPoint photoPosition,
bool markFrameShown) const;
QSize sizeForGroupingOptimal(int maxWidth, bool last) const override;
QSize sizeForGrouping(int width) const override;
void drawGrouped(
Painter &p,
const PaintContext &context,
const QRect &geometry,
RectParts sides,
Ui::BubbleRounding rounding,
float64 highlightOpacity,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const override;
TextState getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const override;
void drawSpoilerTag(
Painter &p,
QRect rthumb,
const PaintContext &context,
Fn<QImage()> generateBackground) const override;
ClickHandlerPtr spoilerTagLink() const override;
QImage spoilerTagBackground() const override;
void hideSpoilers() override;
bool needsBubble() const override;
bool customInfoLayout() const override {
return true;
}
QPoint resolveCustomInfoRightBottom() const override;
bool skipBubbleTail() const override {
return isRoundedInBubbleBottom();
}
bool isReadyForOpen() const override;
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
bool enforceBubbleWidth() const override;
protected:
float64 dataProgress() const override;
bool dataFinished() const override;
bool dataLoaded() const override;
private:
struct Streamed;
void create(FullMsgId contextId, PeerData *chat = nullptr);
void playAnimation(bool autoplay) override;
void stopAnimation() override;
void checkAnimation() override;
void ensureDataMediaCreated() const;
void dataMediaCreated() const;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
[[nodiscard]] int adjustHeightForLessCrop(
QSize dimensions,
QSize current) const;
bool needInfoDisplay() const;
void validateGroupedCache(
const QRect &geometry,
Ui::BubbleRounding rounding,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const;
void validateImageCache(
QSize outer,
std::optional<Ui::BubbleRounding> rounding) const;
void validateUserpicImageCache(QSize size, bool forum) const;
[[nodiscard]] QImage prepareImageCache(QSize outer) const;
void validateSpoilerImageCache(
QSize outer,
std::optional<Ui::BubbleRounding> rounding) const;
[[nodiscard]] QImage prepareImageCacheWithLarge(
QSize outer,
Image *large) const;
bool videoAutoplayEnabled() const;
void setStreamed(std::unique_ptr<Streamed> value);
void repaintStreamedContent();
void checkStreamedIsStarted() const;
bool createStreamingObjects();
void handleStreamingUpdate(::Media::Streaming::Update &&update);
void handleStreamingError(::Media::Streaming::Error &&error);
void streamingReady(::Media::Streaming::Information &&info);
void paintUserpicFrame(
Painter &p,
const PaintContext &context,
QPoint photoPosition) const;
[[nodiscard]] QSize photoSize() const;
[[nodiscard]] QRect enlargeRect() const;
void togglePollingStory(bool enabled) const;
const not_null<PhotoData*> _data;
const FullStoryId _storyId;
mutable std::shared_ptr<Data::PhotoMedia> _dataMedia;
mutable std::unique_ptr<Streamed> _streamed;
const std::unique_ptr<MediaSpoiler> _spoiler;
mutable std::unique_ptr<MediaSpoilerTag> _spoilerTag;
mutable QImage _imageCache;
mutable std::optional<Ui::BubbleRounding> _imageCacheRounding;
uint32 _serviceWidth : 26 = 0;
uint32 _purchasedPriceTag : 1 = 0;
const uint32 _sensitiveSpoiler : 1 = 0;
mutable uint32 _imageCacheForum : 1 = 0;
mutable uint32 _imageCacheBlurred : 1 = 0;
mutable uint32 _pollingStory : 1 = 0;
mutable uint32 _showEnlarge : 1 = 0;
};
} // namespace HistoryView

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,232 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
#include "ui/effects/animations.h"
#include "data/data_poll.h"
#include "base/weak_ptr.h"
namespace Ui {
class RippleAnimation;
class FireworksAnimation;
} // namespace Ui
namespace HistoryView {
class Message;
class Poll final : public Media {
public:
Poll(
not_null<Element*> parent,
not_null<PollData*> poll);
~Poll();
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
return true;
}
bool dragItemByHandler(const ClickHandlerPtr &p) const override {
return true;
}
bool needsBubble() const override {
return true;
}
bool customInfoLayout() const override {
return false;
}
[[nodiscard]] TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const override;
uint16 fullSelectionLength() const override;
TextForMimeData selectedText(TextSelection selection) const override;
BubbleRoll bubbleRoll() const override;
QMargins bubbleRollRepaintMargins() const override;
void paintBubbleFireworks(
Painter &p,
const QRect &bubble,
crl::time ms) const override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) override;
void unloadHeavyPart() override;
bool hasHeavyPart() const override;
private:
struct AnswerAnimation;
struct AnswersAnimation;
struct SendingAnimation;
struct Answer;
struct CloseInformation;
struct RecentVoter;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
[[nodiscard]] bool showVotes() const;
[[nodiscard]] bool canVote() const;
[[nodiscard]] bool canSendVotes() const;
[[nodiscard]] int countAnswerTop(
const Answer &answer,
int innerWidth) const;
[[nodiscard]] int countAnswerHeight(
const Answer &answer,
int innerWidth) const;
[[nodiscard]] ClickHandlerPtr createAnswerClickHandler(
const Answer &answer);
void updateTexts();
void updateRecentVoters();
void updateAnswers();
void updateVotes();
void updateTotalVotes();
bool showVotersCount() const;
bool inlineFooter() const;
void updateAnswerVotes();
void updateAnswerVotesFromOriginal(
Answer &answer,
const PollAnswer &original,
int percent,
int maxVotes);
void checkSendingAnimation() const;
void paintRecentVoters(
Painter &p,
int left,
int top,
const PaintContext &context) const;
void paintCloseByTimer(
Painter &p,
int right,
int top,
const PaintContext &context) const;
void paintShowSolution(
Painter &p,
int right,
int top,
const PaintContext &context) const;
int paintAnswer(
Painter &p,
const Answer &answer,
const AnswerAnimation *animation,
int left,
int top,
int width,
int outerWidth,
const PaintContext &context) const;
void paintRadio(
Painter &p,
const Answer &answer,
int left,
int top,
const PaintContext &context) const;
void paintPercent(
Painter &p,
const QString &percent,
int percentWidth,
int left,
int top,
int outerWidth,
const PaintContext &context) const;
void paintFilling(
Painter &p,
bool chosen,
bool correct,
float64 filling,
int left,
int top,
int width,
int height,
const PaintContext &context) const;
void paintInlineFooter(
Painter &p,
int left,
int top,
int paintw,
const PaintContext &context) const;
void paintBottom(
Painter &p,
int left,
int top,
int paintw,
const PaintContext &context) const;
bool checkAnimationStart() const;
bool answerVotesChanged() const;
void saveStateInAnimation() const;
void startAnswersAnimation() const;
void resetAnswersAnimation() const;
void radialAnimationCallback() const;
void toggleRipple(Answer &answer, bool pressed);
void toggleLinkRipple(bool pressed);
void toggleMultiOption(const QByteArray &option);
void sendMultiOptions();
void showResults();
void checkQuizAnswered();
void showSolution() const;
void solutionToggled(
bool solutionShown,
anim::type animated = anim::type::normal) const;
[[nodiscard]] bool canShowSolution() const;
[[nodiscard]] bool inShowSolution(
QPoint point,
int right,
int top) const;
[[nodiscard]] int bottomButtonHeight() const;
const not_null<PollData*> _poll;
int _pollVersion = 0;
int _totalVotes = 0;
bool _voted = false;
PollData::Flags _flags = PollData::Flags();
Ui::Text::String _question;
Ui::Text::String _subtitle;
std::vector<RecentVoter> _recentVoters;
QImage _recentVotersImage;
std::vector<Answer> _answers;
Ui::Text::String _totalVotesLabel;
ClickHandlerPtr _showResultsLink;
ClickHandlerPtr _sendVotesLink;
mutable ClickHandlerPtr _showSolutionLink;
mutable std::unique_ptr<Ui::RippleAnimation> _linkRipple;
mutable int _linkRippleShift = 0;
mutable std::unique_ptr<AnswersAnimation> _answersAnimation;
mutable std::unique_ptr<SendingAnimation> _sendingAnimation;
mutable std::unique_ptr<Ui::FireworksAnimation> _fireworksAnimation;
Ui::Animations::Simple _wrongAnswerAnimation;
mutable QPoint _lastLinkPoint;
mutable QImage _userpicCircleCache;
mutable QImage _fillingIconCache;
mutable std::unique_ptr<CloseInformation> _close;
mutable Ui::Animations::Simple _solutionButtonAnimation;
mutable bool _solutionShown = false;
mutable bool _solutionButtonVisible = false;
bool _hasSelected = false;
bool _votedFromHere = false;
mutable bool _wrongAnswerAnimated = false;
};
} // namespace HistoryView

View File

@@ -0,0 +1,609 @@
/*
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/media/history_view_premium_gift.h"
#include "apiwrap.h"
#include "api/api_credits.h" // InputSavedStarGiftId
#include "api/api_premium.h"
#include "base/unixtime.h"
#include "boxes/gift_premium_box.h" // ResolveGiftCode
#include "boxes/star_gift_box.h" // GiftReleasedByHandler
#include "chat_helpers/stickers_gift_box_pack.h"
#include "core/click_handler_types.h" // ClickHandlerContext
#include "data/stickers/data_custom_emoji.h"
#include "data/data_channel.h"
#include "data/data_credits.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_element.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_credits.h" // Settings::CreditsId
#include "settings/settings_credits_graphics.h" // GiftedCreditsBox
#include "settings/settings_premium.h" // Settings::ShowGiftPremium
#include "ui/chat/chat_style.h"
#include "ui/controls/ton_common.h" // kNanosInOne
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
namespace HistoryView {
PremiumGift::PremiumGift(
not_null<Element*> parent,
not_null<Data::MediaGiftBox*> gift)
: _parent(parent)
, _gift(gift)
, _data(*gift->gift()) {
}
PremiumGift::~PremiumGift() = default;
int PremiumGift::top() {
return (starGift() || tonGift())
? st::msgServiceStarGiftStickerTop
: st::msgServiceGiftBoxStickerTop;
}
int PremiumGift::width() {
return st::msgServiceStarGiftBoxWidth;
}
QSize PremiumGift::size() {
return (starGift() || tonGift())
? QSize(
st::msgServiceStarGiftStickerSize,
st::msgServiceStarGiftStickerSize)
: QSize(
st::msgServiceGiftBoxStickerSize,
st::msgServiceGiftBoxStickerSize);
}
TextWithEntities PremiumGift::title() {
if (tonGift()) {
return tr::lng_gift_ton_amount(
tr::now,
lt_count_decimal,
CreditsAmount(0, _data.count, CreditsType::Ton).value(),
tr::marked);
} else if (starGift()) {
const auto peer = _parent->history()->peer;
const auto to = _data.auctionTo ? _data.auctionTo : peer.get();
return peer->isSelf()
? tr::lng_action_gift_self_subtitle(tr::now, tr::marked)
: (peer->isServiceUser() && _data.channelFrom)
? tr::lng_action_gift_got_subtitle(
tr::now,
lt_user,
tr::marked()
.append(Ui::Text::SingleCustomEmoji(
peer->owner().customEmojiManager(
).peerUserpicEmojiData(_data.channelFrom)))
.append(' ')
.append(_data.channelFrom->shortName()),
tr::marked)
: (!_data.auctionTo && peer->isServiceUser())
? tr::lng_gift_link_label_gift(tr::now, tr::marked)
: (outgoingGift()
? tr::lng_action_gift_sent_subtitle
: tr::lng_action_gift_got_subtitle)(
tr::now,
lt_user,
tr::marked()
.append(Ui::Text::SingleCustomEmoji(
to->owner().customEmojiManager(
).peerUserpicEmojiData(to)))
.append(' ')
.append(to->shortName()),
tr::marked);
} else if (creditsPrize()) {
return tr::lng_prize_title(tr::now, tr::marked);
} else if (const auto stars = credits()) {
return tr::lng_gift_stars_title(tr::now, lt_count_decimal, stars, tr::marked);
}
return gift()
? tr::lng_action_gift_premium_months(
tr::now,
lt_count,
premiumMonths(),
tr::marked)
: _data.unclaimed
? tr::lng_prize_unclaimed_title(tr::now, tr::marked)
: tr::lng_prize_title(tr::now, tr::marked);
}
TextWithEntities PremiumGift::author() {
if (!_data.stargiftReleasedBy) {
return {};
}
return tr::lng_gift_released_by(
tr::now,
lt_name,
tr::link('@' + _data.stargiftReleasedBy->username()),
tr::marked);
}
TextWithEntities PremiumGift::subtitle() {
if (tonGift()) {
return tr::lng_action_gift_got_ton(tr::now, tr::marked);
} else if (starGift()) {
const auto toChannel = _data.channel
&& _parent->history()->peer->isServiceUser();
return !_data.message.empty()
? _data.message
: _data.refunded
? tr::lng_action_gift_refunded(tr::now, tr::rich)
: outgoingGift()
? (_data.auctionTo
? tr::lng_action_gift_self_auction(
tr::now,
lt_cost,
tr::lng_action_gift_for_stars(
tr::now,
lt_count_decimal,
_data.starsBid,
tr::marked),
tr::rich)
: _data.starsUpgradedBySender
? tr::lng_action_gift_sent_upgradable(
tr::now,
lt_user,
tr::bold(_parent->history()->peer->shortName()),
tr::rich)
: tr::lng_action_gift_sent_text(
tr::now,
lt_count_decimal,
_data.starsConverted,
lt_user,
tr::bold(_parent->history()->peer->shortName()),
tr::rich))
: _data.starsUpgradedBySender
? tr::lng_action_gift_got_upgradable_text(tr::now, tr::rich)
: (_data.starsToUpgrade
&& !_data.converted
&& _parent->history()->peer->isSelf())
? tr::lng_action_gift_self_about_unique(tr::now, tr::rich)
: (_data.starsToUpgrade
&& !_data.converted
&& _parent->history()->peer->isServiceUser()
&& _data.channel)
? tr::lng_action_gift_channel_about_unique(tr::now, tr::rich)
: (!_data.converted && !_data.starsConverted)
? (_data.saved
? (toChannel
? tr::lng_action_gift_can_remove_channel
: tr::lng_action_gift_can_remove_text)
: (toChannel
? tr::lng_action_gift_got_gift_channel
: tr::lng_action_gift_got_gift_text))(tr::now, tr::rich)
: (_data.converted
? (toChannel
? tr::lng_gift_channel_got
: tr::lng_gift_got_stars)
: _parent->history()->peer->isSelf()
? tr::lng_action_gift_self_about
: toChannel
? tr::lng_action_gift_channel_about
: tr::lng_action_gift_got_stars_text)(
tr::now,
lt_count,
_data.starsConverted,
tr::rich);
}
const auto isCreditsPrize = creditsPrize();
if (const auto count = credits(); count && !isCreditsPrize) {
return outgoingGift()
? tr::lng_gift_stars_outgoing(
tr::now,
lt_user,
tr::bold(_parent->history()->peer->shortName()),
tr::rich)
: tr::lng_gift_stars_incoming(tr::now, tr::marked);
} else if (gift()) {
return !_data.message.empty()
? _data.message
: tr::lng_action_gift_premium_about(tr::now, tr::rich);
}
const auto name = _data.channel ? _data.channel->name() : "channel";
auto result = (_data.unclaimed
? tr::lng_prize_unclaimed_about
: _data.viaGiveaway
? tr::lng_prize_about
: tr::lng_prize_gift_about)(
tr::now,
lt_channel,
tr::bold(name),
tr::rich);
result.append("\n\n");
result.append(isCreditsPrize
? tr::lng_prize_credits(
tr::now,
lt_amount,
tr::lng_prize_credits_amount(
tr::now,
lt_count_decimal,
credits(),
tr::marked),
tr::rich)
: (_data.unclaimed
? tr::lng_prize_unclaimed_duration
: _data.viaGiveaway
? tr::lng_prize_duration
: tr::lng_prize_gift_duration)(
tr::now,
lt_duration,
tr::bold(GiftDuration(premiumDays())),
tr::rich));
return result;
}
rpl::producer<QString> PremiumGift::button() {
return (starGift() && outgoingGift())
? tr::lng_sticker_premium_view()
: creditsPrize()
? tr::lng_view_button_giftcode()
: (starGift() && _data.starsUpgradedBySender && !_data.upgraded)
? tr::lng_gift_view_unpack()
: (gift() && (outgoingGift() || !_data.unclaimed))
? tr::lng_sticker_premium_view()
: tr::lng_prize_open();
}
std::optional<Ui::Premium::MiniStarsType> PremiumGift::buttonMinistars() {
return tonGift()
? Ui::Premium::MiniStarsType::SlowDiamondStars
: Ui::Premium::MiniStarsType::SlowStars;
}
ClickHandlerPtr PremiumGift::createViewLink() {
if (tonGift()) {
const auto lifetime = std::make_shared<rpl::lifetime>();
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
const auto weak = my.sessionWindow;
if (const auto window = weak.get()) {
window->session().credits().tonLoad();
*lifetime = window->session().credits().tonLoadedValue(
) | rpl::filter([=] {
if (const auto window = weak.get()) {
return window->session().credits().tonLoaded();
}
return false;
}) | rpl::take(1) | rpl::on_next([=] {
if (const auto window = weak.get()) {
window->showSettings(Settings::CurrencyId());
}
});
}
});
}
if (auto link = OpenStarGiftLink(_parent->data())) {
return link;
}
const auto from = _gift->from();
const auto peer = _parent->history()->peer;
const auto date = _parent->data()->date();
const auto data = *_gift->gift();
const auto showForWeakWindow = [=](
base::weak_ptr<Window::SessionController> weak) {
const auto controller = weak.get();
if (!controller) {
return;
}
const auto selfId = controller->session().userPeerId();
const auto sent = (from->id == selfId);
if (creditsPrize()) {
controller->show(Box(
Settings::CreditsPrizeBox,
controller,
data,
date));
} else if (data.type == Data::GiftType::Credits) {
const auto to = sent ? peer : peer->session().user();
controller->show(Box(
Settings::GiftedCreditsBox,
controller,
from,
to,
data.count,
date));
} else if (data.slug.isEmpty()) {
const auto days = data.count;
Settings::ShowGiftPremium(controller, peer, days, sent);
} else {
const auto fromId = from->id;
const auto toId = sent ? peer->id : selfId;
ResolveGiftCode(controller, data.slug, fromId, toId);
}
};
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
showForWeakWindow(
context.other.value<ClickHandlerContext>().sessionWindow);
});
}
ClickHandlerPtr PremiumGift::authorLink() {
if (const auto by = _data.stargiftReleasedBy) {
if (!_authorLink) {
_authorLink = std::make_shared<LambdaClickHandler>([=] {
Ui::GiftReleasedByHandler(by);
});
}
return _authorLink;
}
return nullptr;
}
int PremiumGift::buttonSkip() {
return st::msgServiceGiftBoxButtonMargins.top();
}
void PremiumGift::draw(
Painter &p,
const PaintContext &context,
const QRect &geometry) {
if (_sticker) {
_sticker->draw(p, context, geometry);
} else {
ensureStickerCreated();
}
}
QImage PremiumGift::cornerTag(const PaintContext &context) {
auto badge = Info::PeerGifts::GiftBadge();
if (_data.unique) {
badge = {
.text = tr::lng_gift_collectible_tag(tr::now),
.bg1 = _data.unique->backdrop.edgeColor,
.bg2 = _data.unique->backdrop.patternColor,
.fg = QColor(255, 255, 255),
};
} else if (const auto count = _data.limitedCount) {
badge = {
.text = ((count == 1)
? tr::lng_gift_limited_of_one(tr::now)
: tr::lng_gift_limited_of_count(
tr::now,
lt_amount,
(((count % 1000) && (count < 10'000))
? Lang::FormatCountDecimal(count)
: Lang::FormatCountToShort(count).string))),
.bg1 = context.st->msgServiceBg()->c,
.fg = context.st->msgServiceFg()->c,
};
} else {
return {};
}
if (_badgeCache.isNull() || _badgeKey != badge) {
_badgeKey = badge;
_badgeCache = ValidateRotatedBadge(
badge,
st::msgServiceGiftBoxBadgePadding);
}
return _badgeCache;
}
bool PremiumGift::hideServiceText() {
return !gift();
}
void PremiumGift::stickerClearLoopPlayed() {
if (_sticker) {
_sticker->stickerClearLoopPlayed();
}
}
std::unique_ptr<StickerPlayer> PremiumGift::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return _sticker
? _sticker->stickerTakePlayer(data, replacements)
: nullptr;
}
bool PremiumGift::hasHeavyPart() {
return (_sticker ? _sticker->hasHeavyPart() : false);
}
void PremiumGift::unloadHeavyPart() {
if (_sticker) {
_sticker->unloadHeavyPart();
}
}
bool PremiumGift::incomingGift() const {
const auto out = _parent->data()->out();
return gift() && !_data.auctionTo && (starGiftUpgrade() ? out : !out);
}
bool PremiumGift::outgoingGift() const {
const auto out = _parent->data()->out();
return gift() && (_data.auctionTo || (starGiftUpgrade() ? !out : out));
}
bool PremiumGift::gift() const {
return _data.slug.isEmpty() || !_data.channel;
}
bool PremiumGift::tonGift() const {
return (_data.type == Data::GiftType::Ton);
}
bool PremiumGift::starGift() const {
return (_data.type == Data::GiftType::StarGift);
}
bool PremiumGift::starGiftUpgrade() const {
return (_data.type == Data::GiftType::StarGift) && _data.upgrade;
}
bool PremiumGift::creditsPrize() const {
return _data.viaGiveaway
&& (_data.type == Data::GiftType::Credits)
&& !_data.slug.isEmpty();
}
int PremiumGift::credits() const {
return (_data.type == Data::GiftType::Credits) ? _data.count : 0;
}
int PremiumGift::premiumDays() const {
return (_data.type == Data::GiftType::Premium) ? _data.count : 0;
}
int PremiumGift::premiumMonths() const {
return premiumDays() / 30;
}
void PremiumGift::ensureStickerCreated() const {
if (_sticker) {
return;
} else if (tonGift()) {
const auto &session = _parent->history()->session();
auto &packs = session.giftBoxStickersPacks();
const auto count = _data.count / Ui::kNanosInOne;
if (const auto document = packs.tonLookup(count)) {
if (document->sticker()) {
const auto skipPremiumEffect = false;
_sticker.emplace(_parent, document, skipPremiumEffect, _parent);
_sticker->setStopOnLastFrame(true);
_sticker->initSize(st::msgServiceGiftBoxStickerSize);
}
}
return;
} else if (const auto document = _data.document) {
const auto sticker = document->sticker();
Assert(sticker != nullptr);
_sticker.emplace(_parent, document, false, _parent);
_sticker->setPlayingOnce(true);
_sticker->initSize(st::msgServiceStarGiftStickerSize);
_parent->repaint();
return;
}
const auto &session = _parent->history()->session();
auto &packs = session.giftBoxStickersPacks();
const auto count = credits();
const auto months = count
? packs.monthsForStars(count)
: premiumMonths();
if (const auto document = packs.lookup(months)) {
if (document->sticker()) {
const auto skipPremiumEffect = false;
_sticker.emplace(_parent, document, skipPremiumEffect, _parent);
_sticker->setStopOnLastFrame(true);
_sticker->initSize(st::msgServiceGiftBoxStickerSize);
}
}
}
ClickHandlerPtr OpenStarGiftLink(not_null<HistoryItem*> item) {
const auto media = item->media();
const auto gift = media ? media->gift() : nullptr;
if (!gift || gift->type != Data::GiftType::StarGift) {
return nullptr;
}
const auto data = *gift;
const auto itemId = item->fullId();
const auto upgradedMsgId = data.upgraded
? data.realGiftMsgId
: MsgId(0);
const auto openInsteadId = data.realGiftMsgId
? Data::SavedStarGiftId::User(data.realGiftMsgId)
: (data.channel && data.channelSavedId)
? Data::SavedStarGiftId::Chat(data.channel, data.channelSavedId)
: Data::SavedStarGiftId();
const auto requesting = std::make_shared<bool>();
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
const auto weak = my.sessionWindow;
const auto controller = weak.get();
if (!controller) {
return;
}
const auto quick = [=](not_null<Window::SessionController*> window) {
Settings::ShowStarGiftViewBox(window, data, itemId);
};
if (!openInsteadId) {
quick(controller);
return;
} else if (*requesting) {
return;
}
*requesting = true;
const auto requestSavedGift = [=] {
controller->session().api().request(MTPpayments_GetSavedStarGift(
MTP_vector<MTPInputSavedStarGift>(
1,
Api::InputSavedStarGiftId(openInsteadId))
)).done([=](const MTPpayments_SavedStarGifts &result) {
*requesting = false;
if (const auto window = weak.get()) {
const auto &data = result.data();
window->session().data().processUsers(data.vusers());
window->session().data().processChats(data.vchats());
const auto owner = openInsteadId.chat()
? openInsteadId.chat()
: window->session().user();
const auto &list = data.vgifts().v;
if (list.empty()) {
quick(window);
} else if (auto g = Api::FromTL(owner, list[0])) {
Settings::ShowSavedStarGiftBox(window, owner, *g);
}
}
}).fail([=](const MTP::Error &error) {
*requesting = false;
if (const auto window = weak.get()) {
window->showToast(error.type());
quick(window);
}
}).send();
};
if (const auto msgId = upgradedMsgId) {
const auto session = &controller->session();
const auto owner = &controller->session().data();
const auto processItem = [=](not_null<HistoryItem*> item) {
const auto media = item->media();
if (!media || !media->gift() || !media->gift()->unique) {
*requesting = false;
if (const auto window = weak.get()) {
quick(window);
}
return;
}
// It is not possible to request a saved star gift
// when it is transferred and does not belong to you.
if (!media->gift()->transferred) {
return requestSavedGift();
}
*requesting = false;
const auto local = u"nft/"_q + media->gift()->unique->slug;
UrlClickHandler::Open(session->createInternalLinkFull(local));
};
if (const auto item = owner->nonChannelMessage(msgId)) {
processItem(item);
} else {
session->api().requestMessageData(nullptr, msgId, [=] {
if (const auto item = owner->nonChannelMessage(msgId)) {
processItem(item);
} else {
*requesting = false;
}
});
}
} else {
requestSavedGift();
}
});
}
} // namespace HistoryView

View File

@@ -0,0 +1,79 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_service_box.h"
#include "info/peer_gifts/info_peer_gifts_common.h"
namespace Data {
class MediaGiftBox;
struct GiftCode;
} // namespace Data
namespace HistoryView {
class PremiumGift final : public ServiceBoxContent {
public:
PremiumGift(
not_null<Element*> parent,
not_null<Data::MediaGiftBox*> gift);
~PremiumGift();
int top() override;
int width() override;
QSize size() override;
TextWithEntities title() override;
TextWithEntities author() override;
TextWithEntities subtitle() override;
rpl::producer<QString> button() override;
std::optional<Ui::Premium::MiniStarsType> buttonMinistars() override;
QImage cornerTag(const PaintContext &context) override;
int buttonSkip() override;
void draw(
Painter &p,
const PaintContext &context,
const QRect &geometry) override;
ClickHandlerPtr createViewLink() override;
ClickHandlerPtr authorLink() override;
bool hideServiceText() override;
void stickerClearLoopPlayed() override;
std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) override;
bool hasHeavyPart() override;
void unloadHeavyPart() override;
private:
[[nodiscard]] bool incomingGift() const;
[[nodiscard]] bool outgoingGift() const;
[[nodiscard]] bool tonGift() const;
[[nodiscard]] bool starGift() const;
[[nodiscard]] bool starGiftUpgrade() const;
[[nodiscard]] bool gift() const;
[[nodiscard]] bool creditsPrize() const;
[[nodiscard]] int credits() const;
[[nodiscard]] int premiumDays() const;
[[nodiscard]] int premiumMonths() const;
void ensureStickerCreated() const;
const not_null<Element*> _parent;
const not_null<Data::MediaGiftBox*> _gift;
const Data::GiftCode &_data;
ClickHandlerPtr _authorLink;
QImage _badgeCache;
Info::PeerGifts::GiftBadge _badgeKey;
mutable std::optional<Sticker> _sticker;
};
[[nodiscard]] ClickHandlerPtr OpenStarGiftLink(not_null<HistoryItem*> item);
} // namespace HistoryView

View File

@@ -0,0 +1,126 @@
/*
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/media/history_view_save_document_action.h"
#include "base/call_delayed.h"
#include "data/data_document.h"
#include "data/data_file_click_handler.h"
#include "data/data_file_origin.h"
#include "data/data_peer.h"
#include "data/data_saved_music.h"
#include "data/data_session.h"
#include "history/view/history_view_context_menu.h"
#include "history/view/history_view_list_widget.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/menu/menu_multiline_action.h"
#include "ui/widgets/popup_menu.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_menu_icons.h"
#include "styles/style_widgets.h"
namespace HistoryView {
void AddSaveDocumentAction(
const Ui::Menu::MenuCallback &addAction,
not_null<HistoryItem*> item,
not_null<DocumentData*> document,
not_null<Window::SessionController*> controller) {
const auto contextId = item->fullId();
const auto fromSaved = item->history()->peer->isSelf();
const auto savedMusic = &document->owner().savedMusic();
const auto show = controller->uiShow();
const auto inProfile = savedMusic->has(document);
const auto &ripple = st::defaultDropdownMenu.menu.ripple;
const auto duration = ripple.hideDuration;
const auto saveAs = base::fn_delayed(duration, controller, [=] {
DocumentSaveClickHandler::SaveAndTrack(
contextId,
document,
DocumentSaveClickHandler::Mode::ToNewFile);
});
if (!document->isMusicForProfile() || (fromSaved && inProfile)) {
const auto text = document->isVideoFile()
? tr::lng_context_save_video(tr::now)
: document->isVoiceMessage()
? tr::lng_context_save_audio(tr::now)
: document->isAudioFile()
? tr::lng_context_save_audio_file(tr::now)
: document->sticker()
? tr::lng_context_save_image(tr::now)
: tr::lng_context_save_file(tr::now);
addAction(text, saveAs, &st::menuIconDownload);
return;
}
const auto fill = [&](not_null<Ui::PopupMenu*> menu) {
if (!inProfile) {
const auto saved = [=] {
savedMusic->save(document, contextId);
show->showToast(tr::lng_saved_music_added(tr::now));
};
menu->addAction(
tr::lng_context_save_music_profile(tr::now),
saved,
&st::menuIconProfile);
}
if (!fromSaved) {
menu->addAction(
tr::lng_context_save_music_saved(tr::now),
[=] { Window::ForwardToSelf(show, { { contextId } }); },
&st::menuIconSavedMessages);
}
menu->addAction(
tr::lng_context_save_music_folder(tr::now),
saveAs,
&st::menuIconDownload);
menu->addSeparator(&st::expandedMenuSeparator);
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
menu,
st::saveMusicInfoMenu,
st::historyHasCustomEmoji,
QPoint(
st::saveMusicInfoMenu.itemPadding.left(),
st::saveMusicInfoMenu.itemPadding.top()),
TextWithEntities{ tr::lng_context_save_music_about(tr::now) });
item->setAttribute(Qt::WA_TransparentForMouseEvents);
item->setPointerCursor(false);
menu->addAction(std::move(item));
};
addAction(Ui::Menu::MenuCallback::Args{
.text = tr::lng_context_save_music_to(tr::now),
.handler = nullptr,
.icon = &st::menuIconSoundAdd,
.fillSubmenu = fill,
.submenuSt = &st::popupMenuWithIcons,
});
}
void AddSaveDocumentAction(
not_null<Ui::PopupMenu*> menu,
HistoryItem *item,
not_null<DocumentData*> document,
not_null<ListWidget*> list) {
if (!item || list->hasCopyMediaRestriction(item) || ItemHasTtl(item)) {
return;
}
AddSaveDocumentAction(
Ui::Menu::CreateAddActionCallback(menu),
item,
document,
list->controller());
}
} // namespace HistoryView

View File

@@ -0,0 +1,41 @@
/*
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
*/
#pragma once
class DocumentData;
class HistoryItem;
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Ui::Menu {
struct MenuCallback;
} // namespace Ui::Menu
namespace Window {
class SessionController;
} // namespace Window
namespace HistoryView {
class ListWidget;
void AddSaveDocumentAction(
const Ui::Menu::MenuCallback &addAction,
not_null<HistoryItem*> item,
not_null<DocumentData*> document,
not_null<Window::SessionController*> controller);
void AddSaveDocumentAction(
not_null<Ui::PopupMenu*> menu,
HistoryItem *item,
not_null<DocumentData*> document,
not_null<ListWidget*> list);
} // namespace HistoryView

View File

@@ -0,0 +1,487 @@
/*
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/media/history_view_service_box.h"
#include "core/ui_integration.h"
#include "data/data_session.h"
#include "history/view/media/history_view_sticker_player_abstract.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_text_helper.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/animation_value.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/power_saving.h"
#include "styles/style_chat.h"
#include "styles/style_credits.h"
#include "styles/style_premium.h"
#include "styles/style_layers.h"
namespace HistoryView {
int ServiceBoxContent::width() {
return st::msgServiceGiftBoxSize.width();
}
ServiceBox::ServiceBox(
not_null<Element*> parent,
std::unique_ptr<ServiceBoxContent> content)
: Media(parent)
, _parent(parent)
, _content(std::move(content))
, _button({ .link = _content->createViewLink() })
, _maxWidth(_content->width()
- st::msgPadding.left()
- st::msgPadding.right())
, _title(
st::defaultSubsectionTitle.style,
_content->title(),
kMarkupTextOptions,
_maxWidth,
Core::TextContext({
.session = &parent->history()->session(),
.repaint = [parent] { parent->customEmojiRepaint(); },
}))
, _author(
st::uniqueGiftReleasedBy.style,
_content->author(),
kMarkupTextOptions,
_maxWidth)
, _subtitle(
st::premiumPreviewAbout.style,
Ui::Text::Filtered(
_content->subtitle(),
{
EntityType::Bold,
EntityType::StrikeOut,
EntityType::Underline,
EntityType::Italic,
EntityType::Spoiler,
EntityType::CustomEmoji,
}),
kMarkupTextOptions,
_maxWidth,
Core::TextContext({
.session = &parent->history()->session(),
.repaint = [parent] { parent->customEmojiRepaint(); },
}))
, _size(
_content->width(),
(st::msgServiceGiftBoxTopSkip
+ _content->top()
+ _content->size().height()
+ st::msgServiceGiftBoxTitlePadding.top()
+ (_title.isEmpty()
? 0
: (_title.countHeight(_maxWidth)
+ st::msgServiceGiftBoxTitlePadding.bottom()))
+ (_author.isEmpty()
? 0
: (st::giftBoxReleasedByMargin.top()
+ st::uniqueGiftReleasedBy.style.font->height
+ st::giftBoxReleasedByMargin.bottom()
+ st::msgServiceGiftBoxTitlePadding.bottom()))
+ _subtitle.countHeight(_maxWidth)
+ (!_content->button()
? 0
: (_content->buttonSkip() + st::msgServiceGiftBoxButtonHeight))
+ st::msgServiceGiftBoxButtonMargins.bottom()))
, _innerSize(_size - QSize(0, st::msgServiceGiftBoxTopSkip)) {
InitElementTextPart(_parent, _subtitle);
if (auto text = _content->button()) {
_button.repaint = [=] { repaint(); };
std::move(text) | rpl::on_next([=](QString value) {
_button.text.setText(st::semiboldTextStyle, value);
const auto height = st::msgServiceGiftBoxButtonHeight;
const auto &padding = st::msgServiceGiftBoxButtonPadding;
const auto empty = _button.size.isEmpty();
_button.size = QSize(
(_button.text.maxWidth()
+ height
+ padding.left()
+ padding.right()),
height);
if (!empty) {
repaint();
}
}, _lifetime);
}
if (const auto type = _content->buttonMinistars()) {
_button.stars = std::make_unique<Ui::Premium::ColoredMiniStars>(
[=](const QRect &) { repaint(); },
*type);
_button.lastFg = std::make_unique<QColor>();
}
if (auto changes = _content->changes()) {
std::move(changes) | rpl::on_next([=] {
applyContentChanges();
}, _lifetime);
}
}
ServiceBox::~ServiceBox() = default;
void ServiceBox::applyContentChanges() {
const auto subtitleWas = _subtitle.countHeight(_maxWidth);
const auto parent = _parent;
_subtitle = Ui::Text::String(
st::premiumPreviewAbout.style,
Ui::Text::Filtered(
_content->subtitle(),
{
EntityType::Bold,
EntityType::StrikeOut,
EntityType::Underline,
EntityType::Italic,
EntityType::Spoiler,
EntityType::CustomEmoji,
}),
kMarkupTextOptions,
_maxWidth,
Core::TextContext({
.session = &parent->history()->session(),
.repaint = [parent] { parent->customEmojiRepaint(); },
}));
InitElementTextPart(parent, _subtitle);
const auto subtitleNow = _subtitle.countHeight(_maxWidth);
if (subtitleNow != subtitleWas) {
_size.setHeight(_size.height() - subtitleWas + subtitleNow);
_innerSize = _size - QSize(0, st::msgServiceGiftBoxTopSkip);
const auto item = parent->data();
item->history()->owner().requestItemResize(item);
} else {
parent->repaint();
}
}
QSize ServiceBox::countOptimalSize() {
return _size;
}
QSize ServiceBox::countCurrentSize(int newWidth) {
return _size;
}
void ServiceBox::draw(Painter &p, const PaintContext &context) const {
p.translate(0, st::msgServiceGiftBoxTopSkip);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(context.st->msgServiceBg());
const auto radius = st::msgServiceGiftBoxRadius;
if (_parent->data()->inlineReplyKeyboard()) {
const auto r = Rect(_innerSize);
const auto half = r.height() / 2;
p.setClipRect(r - QMargins(0, 0, 0, half));
p.drawRoundedRect(r, radius, radius);
p.setClipRect(r - QMargins(0, r.height() - half, 0, 0));
const auto small = Ui::BubbleRadiusSmall();
p.drawRoundedRect(r, small, small);
p.setClipping(false);
} else {
p.drawRoundedRect(Rect(_innerSize), radius, radius);
}
if (_button.stars) {
const auto &c = context.st->msgServiceFg()->c;
if ((*_button.lastFg) != c) {
_button.lastFg->setRgb(c.red(), c.green(), c.blue());
const auto padding = _button.size.height() / 2;
_button.stars->setColorOverride(QGradientStops{
{ 0., anim::with_alpha(c, .3) },
{ 1., c },
});
_button.stars->setCenter(
Rect(_button.size) - QMargins(padding, 0, padding, 0));
}
}
const auto content = contentRect();
auto top = content.top() + content.height();
{
p.setPen(context.st->msgServiceFg());
const auto &padding = st::msgServiceGiftBoxTitlePadding;
top += padding.top();
if (!_title.isEmpty()) {
_title.draw(p, {
.position = QPoint(st::msgPadding.left(), top),
.availableWidth = _maxWidth,
.align = style::al_top,
.palette = &context.st->serviceTextPalette(),
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
});
top += _title.countHeight(_maxWidth) + padding.bottom();
}
if (!_author.isEmpty()) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(context.st->msgServiceBg());
const auto use = std::min(_maxWidth, _author.maxWidth())
+ st::giftBoxReleasedByMargin.left()
+ st::giftBoxReleasedByMargin.right();
const auto left = st::msgPadding.left() + (_maxWidth - use) / 2;
const auto height = st::giftBoxReleasedByMargin.top()
+ st::uniqueGiftReleasedBy.style.font->height
+ st::giftBoxReleasedByMargin.bottom();
const auto radius = height / 2.;
p.drawRoundedRect(left, top, use, height, radius, radius);
auto fg = context.st->msgServiceFg()->c;
fg.setAlphaF(0.65 * fg.alphaF());
p.setPen(fg);
_author.draw(p, {
.position = QPoint(
left + st::giftBoxReleasedByMargin.left(),
top + st::giftBoxReleasedByMargin.top()),
.availableWidth = (use
- st::giftBoxReleasedByMargin.left()
- st::giftBoxReleasedByMargin.right()),
.palette = &context.st->serviceTextPalette(),
.elisionLines = 1,
});
p.setPen(context.st->msgServiceFg());
top += height + st::msgServiceGiftBoxTitlePadding.bottom();
}
_parent->prepareCustomEmojiPaint(p, context, _subtitle);
_subtitle.draw(p, {
.position = QPoint(st::msgPadding.left(), top),
.availableWidth = _maxWidth,
.align = style::al_top,
.palette = &context.st->serviceTextPalette(),
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
});
top += _subtitle.countHeight(_maxWidth) + padding.bottom();
}
if (!_button.empty()) {
const auto position = buttonRect().topLeft();
p.translate(position);
p.setPen(Qt::NoPen);
p.setBrush(context.st->msgServiceBg()); // ?
if (const auto stars = _button.stars.get()) {
stars->setPaused(context.paused);
}
_button.drawBg(p);
p.setPen(context.st->msgServiceFg());
if (_button.ripple) {
const auto opacity = p.opacity();
p.setOpacity(st::historyPollRippleOpacity);
_button.ripple->paint(
p,
0,
0,
width(),
&context.messageStyle()->msgWaveformInactive->c);
p.setOpacity(opacity);
}
_button.text.draw(
p,
0,
(_button.size.height() - _button.text.minHeight()) / 2,
_button.size.width(),
style::al_top);
p.translate(-position);
}
_content->draw(p, context, content);
if (const auto tag = _content->cornerTag(context); !tag.isNull()) {
const auto width = tag.width() / tag.devicePixelRatio();
p.drawImage(_innerSize.width() - width, 0, tag);
}
p.translate(0, -st::msgServiceGiftBoxTopSkip);
}
TextState ServiceBox::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
point.setY(point.y() - st::msgServiceGiftBoxTopSkip);
const auto content = contentRect();
const auto lookupSubtitleLink = [&] {
auto top = content.top() + content.height();
const auto &padding = st::msgServiceGiftBoxTitlePadding;
top += padding.top();
if (!_title.isEmpty()) {
top += _title.countHeight(_maxWidth) + padding.bottom();
}
if (!_author.isEmpty()) {
const auto use = std::min(_maxWidth, _author.maxWidth())
+ st::giftBoxReleasedByMargin.left()
+ st::giftBoxReleasedByMargin.right();
const auto left = st::msgPadding.left() + (_maxWidth - use) / 2;
const auto height = st::giftBoxReleasedByMargin.top()
+ st::defaultTextStyle.font->height
+ st::giftBoxReleasedByMargin.bottom();
if (point.x() >= left
&& point.y() >= top
&& point.x() < left + use
&& point.y() < top + height) {
result.link = _content->authorLink();
}
top += height + st::msgServiceGiftBoxTitlePadding.bottom();
}
auto subtitleRequest = request.forText();
subtitleRequest.align = style::al_top;
const auto state = _subtitle.getState(
point - QPoint(st::msgPadding.left(), top),
_maxWidth,
subtitleRequest);
if (state.link) {
result.link = state.link;
}
};
if (_button.empty()) {
if (!_button.link) {
lookupSubtitleLink();
} else if (QRect(QPoint(), _innerSize).contains(point)) {
result.link = _button.link;
}
} else {
const auto rect = buttonRect();
if (rect.contains(point)) {
result.link = _button.link;
_button.lastPoint = point - rect.topLeft();
} else if (content.contains(point)) {
if (!_contentLink) {
_contentLink = _content->createViewLink();
}
result.link = _contentLink;
} else {
lookupSubtitleLink();
}
}
return result;
}
bool ServiceBox::toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const {
return false;
}
bool ServiceBox::dragItemByHandler(const ClickHandlerPtr &p) const {
return false;
}
void ServiceBox::clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) {
if (!handler) {
return;
}
if (handler == _button.link) {
_button.toggleRipple(pressed);
}
}
void ServiceBox::stickerClearLoopPlayed() {
_content->stickerClearLoopPlayed();
}
std::unique_ptr<StickerPlayer> ServiceBox::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return _content->stickerTakePlayer(data, replacements);
}
bool ServiceBox::needsBubble() const {
return false;
}
bool ServiceBox::customInfoLayout() const {
return false;
}
void ServiceBox::hideSpoilers() {
_subtitle.setSpoilerRevealed(false, anim::type::instant);
}
bool ServiceBox::hasHeavyPart() const {
return _content->hasHeavyPart();
}
void ServiceBox::unloadHeavyPart() {
_content->unloadHeavyPart();
}
QRect ServiceBox::buttonRect() const {
const auto &padding = st::msgServiceGiftBoxButtonMargins;
const auto position = QPoint(
(width() - _button.size.width()) / 2,
height() - padding.bottom() - _button.size.height());
return QRect(position, _button.size);
}
QRect ServiceBox::contentRect() const {
const auto size = _content->size();
const auto top = _content->top();
return QRect(QPoint((width() - size.width()) / 2, top), size);
}
void ServiceBox::Button::toggleRipple(bool pressed) {
if (empty()) {
return;
} else if (pressed) {
const auto linkWidth = size.width();
const auto linkHeight = size.height();
if (!ripple) {
const auto drawMask = [&](QPainter &p) { drawBg(p); };
auto mask = Ui::RippleAnimation::MaskByDrawer(
QSize(linkWidth, linkHeight),
false,
drawMask);
ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
repaint);
}
ripple->add(lastPoint);
} else if (ripple) {
ripple->lastStop();
}
}
bool ServiceBox::Button::empty() const {
return text.isEmpty();
}
void ServiceBox::Button::drawBg(QPainter &p) const {
const auto radius = size.height() / 2.;
const auto r = Rect(size);
p.drawRoundedRect(r, radius, radius);
if (stars) {
auto clipPath = QPainterPath();
clipPath.addRoundedRect(r, radius, radius);
p.setClipPath(clipPath);
stars->paint(p);
p.setClipping(false);
}
}
} // namespace HistoryView

View File

@@ -0,0 +1,148 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
namespace Ui {
class RippleAnimation;
} // namespace Ui
namespace Ui::Premium {
class ColoredMiniStars;
enum class MiniStarsType;
} // namespace Ui::Premium
namespace HistoryView {
class ServiceBoxContent {
public:
virtual ~ServiceBoxContent() = default;
[[nodiscard]] virtual int width();
[[nodiscard]] virtual int top() = 0;
[[nodiscard]] virtual QSize size() = 0;
[[nodiscard]] virtual TextWithEntities title() = 0;
[[nodiscard]] virtual TextWithEntities author() {
return {};
}
[[nodiscard]] virtual TextWithEntities subtitle() = 0;
[[nodiscard]] virtual int buttonSkip() {
return top();
}
[[nodiscard]] virtual rpl::producer<QString> button() = 0;
// For now only subtitle() changes are observed.
[[nodiscard]] virtual rpl::producer<> changes() {
return nullptr;
}
[[nodiscard]] virtual auto buttonMinistars()
-> std::optional<Ui::Premium::MiniStarsType> {
return std::nullopt;
}
[[nodiscard]] virtual QImage cornerTag(const PaintContext &context) {
return {};
}
virtual void draw(
Painter &p,
const PaintContext &context,
const QRect &geometry) = 0;
[[nodiscard]] virtual ClickHandlerPtr createViewLink() = 0;
[[nodiscard]] virtual ClickHandlerPtr authorLink() {
return nullptr;
}
[[nodiscard]] virtual bool hideServiceText() = 0;
virtual void stickerClearLoopPlayed() = 0;
[[nodiscard]] virtual std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) = 0;
[[nodiscard]] virtual bool hasHeavyPart() = 0;
virtual void unloadHeavyPart() = 0;
};
class ServiceBox final : public Media {
public:
ServiceBox(
not_null<Element*> parent,
std::unique_ptr<ServiceBoxContent> content);
~ServiceBox();
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
[[nodiscard]] bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const override;
[[nodiscard]] bool dragItemByHandler(
const ClickHandlerPtr &p) const override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) override;
void stickerClearLoopPlayed() override;
std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) override;
[[nodiscard]] bool needsBubble() const override;
[[nodiscard]] bool customInfoLayout() const override;
[[nodiscard]] bool hideServiceText() const override {
return _content->hideServiceText();
}
void hideSpoilers() override;
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
private:
[[nodiscard]] QRect buttonRect() const;
[[nodiscard]] QRect contentRect() const;
void applyContentChanges();
const not_null<Element*> _parent;
const std::unique_ptr<ServiceBoxContent> _content;
mutable ClickHandlerPtr _contentLink;
struct Button {
void drawBg(QPainter &p) const;
void toggleRipple(bool pressed);
[[nodiscard]] bool empty() const;
Fn<void()> repaint;
Ui::Text::String text;
QSize size;
ClickHandlerPtr link;
std::unique_ptr<Ui::RippleAnimation> ripple;
std::unique_ptr<Ui::Premium::ColoredMiniStars> stars;
std::unique_ptr<QColor> lastFg;
mutable QPoint lastPoint;
} _button;
const int _maxWidth = 0;
Ui::Text::String _title;
Ui::Text::String _author;
Ui::Text::String _subtitle;
QSize _size;
QSize _innerSize;
rpl::lifetime _lifetime;
};
} // namespace HistoryView

View File

@@ -0,0 +1,649 @@
/*
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/media/history_view_similar_channels.h"
#include "api/api_chat_participants.h"
#include "apiwrap.h"
#include "boxes/peer_lists_box.h"
#include "core/click_handler_types.h"
#include "data/data_channel.h"
#include "data/data_premium_limits.h"
#include "data/data_session.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "history/history.h"
#include "history/history_item.h"
#include "info/similar_peers/info_similar_peers_widget.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_premium.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/dynamic_image.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/painter.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
using Channels = Api::ChatParticipants::Peers;
//void SimilarChannelsController::prepare() {
// for (const auto &channel : _channels.list) {
// auto row = std::make_unique<PeerListRow>(channel);
// if (const auto count = channel->membersCount(); count > 1) {
// row->setCustomStatus(tr::lng_chat_status_subscribers(
// tr::now,
// lt_count,
// count));
// }
// delegate()->peerListAppendRow(std::move(row));
// }
// delegate()->peerListRefreshRows();
//}
[[nodiscard]] ClickHandlerPtr MakeViewAllLink(
not_null<ChannelData*> channel,
bool promoForNonPremium) {
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto strong = my.sessionWindow.get()) {
Assert(channel != nullptr);
if (promoForNonPremium && !channel->session().premium()) {
const auto upto = Data::PremiumLimits(
&channel->session()).similarChannelsPremium();
Settings::ShowPremiumPromoToast(
strong->uiShow(),
tr::lng_similar_channels_premium_all(
tr::now,
lt_count,
upto,
lt_link,
tr::link(
tr::bold(
tr::lng_similar_channels_premium_all_link(
tr::now))),
tr::rich),
u"similar_channels"_q);
return;
}
const auto api = &channel->session().api();
const auto &list = api->chatParticipants().similar(channel);
if (list.list.empty()) {
return;
}
strong->showSection(
std::make_shared<Info::Memento>(
channel,
Info::Section::Type::SimilarPeers));
}
});
}
} // namespace
SimilarChannels::SimilarChannels(not_null<Element*> parent)
: Media(parent) {
}
SimilarChannels::~SimilarChannels() {
if (hasHeavyPart()) {
unloadHeavyPart();
parent()->checkHeavyPart();
}
}
void SimilarChannels::clickHandlerActiveChanged(
const ClickHandlerPtr &p,
bool active) {
}
void SimilarChannels::clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) {
for (auto &channel : _channels) {
if (channel.link != p) {
continue;
}
if (pressed) {
if (!channel.ripple) {
channel.ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
channel.geometry.size(),
st::roundRadiusLarge),
[=] { repaint(); });
}
channel.ripple->add(_lastPoint);
} else if (channel.ripple) {
channel.ripple->lastStop();
}
break;
}
}
void SimilarChannels::draw(Painter &p, const PaintContext &context) const {
if (!_toggled) {
return;
}
const auto large = Ui::BubbleCornerRounding::Large;
const auto geometry = QRect(0, 0, width(), height());
Ui::PaintBubble(
p,
Ui::SimpleBubble{
.st = context.st,
.geometry = geometry,
.pattern = context.bubblesPattern,
.patternViewport = context.viewport,
.outerWidth = width(),
.rounding = { large, large, large, large },
});
const auto stm = context.messageStyle();
{
auto hq = PainterHighQualityEnabler(p);
auto path = QPainterPath();
const auto x = geometry.center().x();
const auto y = geometry.y();
const auto size = st::chatSimilarArrowSize;
path.moveTo(x, y - size);
path.lineTo(x + size, y);
path.lineTo(x - size, y);
path.lineTo(x, y - size);
p.fillPath(path, stm->msgBg);
}
const auto padding = st::chatSimilarChannelPadding;
p.setClipRect(geometry);
_hasHeavyPart = 1;
validateLastPremiumLock();
const auto drawOne = [&](const Channel &channel) {
const auto geometry = channel.geometry.translated(-int(_scrollLeft), 0);
const auto right = geometry.x() + geometry.width();
if (right <= 0) {
return;
}
const auto subscribing = !channel.subscribed;
if (subscribing) {
channel.subscribed = 1;
const auto raw = channel.thumbnail.get();
channel.thumbnail->subscribeToUpdates([=] {
for (const auto &channel : _channels) {
if (channel.thumbnail.get() == raw) {
channel.counterBgValid = 0;
repaint();
}
}
});
}
auto cachedp = std::optional<Painter>();
const auto cached = (geometry.x() < padding.left())
|| (right > width() - padding.right());
if (cached) {
ensureCacheReady(geometry.size());
_roundedCache.fill(Qt::transparent);
cachedp.emplace(&_roundedCache);
cachedp->translate(-geometry.topLeft());
}
const auto q = cachedp ? &*cachedp : &p;
if (channel.more) {
channel.ripple.reset();
} else if (channel.ripple) {
q->setOpacity(st::historyPollRippleOpacity);
channel.ripple->paint(
*q,
geometry.x(),
geometry.y(),
width(),
&stm->msgWaveformInactive->c);
if (channel.ripple->empty()) {
channel.ripple.reset();
}
q->setOpacity(1.);
}
auto pen = stm->msgBg->p;
auto left = geometry.x() + 2 * padding.left();
const auto stroke = st::lineWidth * 2.;
const auto add = stroke / 2.;
const auto top = geometry.y() + padding.top();
const auto size = st::chatSimilarChannelPhoto;
const auto paintCircle = [&] {
auto hq = PainterHighQualityEnabler(*q);
q->drawEllipse(QRectF(left, top, size, size).marginsAdded(
{ add, add, add, add }));
};
if (channel.more) {
pen.setWidthF(stroke);
p.setPen(pen);
for (auto i = 2; i != 0;) {
--i;
if (const auto &thumbnail = _moreThumbnails[i]) {
if (subscribing) {
thumbnail->subscribeToUpdates([=] {
repaint();
});
}
q->drawImage(left, top, thumbnail->image(size));
q->setBrush(Qt::NoBrush);
} else {
q->setBrush(st::windowBgRipple->c);
}
if (!i || !_moreThumbnails[i]) {
paintCircle();
}
left -= padding.left();
}
} else {
left -= padding.left();
}
q->drawImage(
left,
top,
channel.thumbnail->image(size));
if (channel.more) {
q->setBrush(Qt::NoBrush);
paintCircle();
}
if (!channel.counter.isEmpty()) {
validateCounterBg(channel);
const auto participants = channel.counterRect.translated(
geometry.topLeft());
q->drawImage(participants.topLeft(), channel.counterBg);
const auto badge = participants.marginsRemoved(
st::chatSimilarBadgePadding);
auto textLeft = badge.x();
const auto &font = st::chatSimilarBadgeFont;
const auto textTop = badge.y() + font->ascent;
const auto icon = !channel.more
? &st::chatSimilarBadgeIcon
: channel.moreLocked
? &st::chatSimilarLockedIcon
: nullptr;
const auto position = !channel.more
? st::chatSimilarBadgeIconPosition
: st::chatSimilarLockedIconPosition;
if (icon) {
const auto skip = channel.more
? (badge.width() - icon->width())
: 0;
icon->paint(
*q,
badge.x() + position.x() + skip,
badge.y() + position.y(),
width());
if (!channel.more) {
textLeft += position.x() + icon->width();
}
}
q->setFont(font);
q->setPen(st::premiumButtonFg);
q->drawText(textLeft, textTop, channel.counter);
}
q->setPen(channel.more ? st::windowSubTextFg : stm->historyTextFg);
channel.name.drawLeftElided(
*q,
geometry.x() + st::normalFont->spacew,
geometry.y() + st::chatSimilarNameTop,
(geometry.width() - 2 * st::normalFont->spacew),
width(),
2,
style::al_top);
if (cachedp) {
q->setCompositionMode(QPainter::CompositionMode_DestinationIn);
const auto corners = _roundedCorners.data();
const auto side = st::bubbleRadiusLarge;
q->drawImage(0, 0, corners[Images::kTopLeft]);
q->drawImage(width() - side, 0, corners[Images::kTopRight]);
q->drawImage(0, height() - side, corners[Images::kBottomLeft]);
q->drawImage(
QPoint(width() - side, height() - side),
corners[Images::kBottomRight]);
cachedp.reset();
p.drawImage(geometry.topLeft(), _roundedCache);
}
};
for (const auto &channel : _channels) {
if (channel.geometry.x() >= _scrollLeft + width()) {
break;
}
drawOne(channel);
}
p.setPen(stm->historyTextFg);
p.setFont(st::chatSimilarTitle);
p.drawTextLeft(
st::chatSimilarTitlePosition.x(),
st::chatSimilarTitlePosition.y(),
width(),
_title);
if (!_hasViewAll) {
return;
}
p.setFont(ClickHandler::showAsActive(_viewAllLink)
? st::normalFont->underline()
: st::normalFont);
p.setPen(stm->textPalette.linkFg);
const auto add = st::normalFont->ascent - st::chatSimilarTitle->ascent;
p.drawTextRight(
st::chatSimilarTitlePosition.x(),
st::chatSimilarTitlePosition.y() + add,
width(),
_viewAll);
p.setClipping(false);
}
void SimilarChannels::validateLastPremiumLock() const {
if (_channels.empty()) {
return;
}
if (!_moreThumbnailsValid) {
_moreThumbnailsValid = 1;
fillMoreThumbnails();
}
const auto &last = _channels.back();
if (!last.more) {
return;
}
const auto premium = history()->session().premium();
const auto locked = !premium && history()->session().premiumPossible();
if (last.moreLocked == locked) {
return;
}
last.moreLocked = locked ? 1 : 0;
last.counterBgValid = 0;
}
void SimilarChannels::fillMoreThumbnails() const {
const auto channel = parent()->history()->peer->asChannel();
Assert(channel != nullptr);
_moreThumbnails = {};
const auto api = &channel->session().api();
const auto &similar = api->chatParticipants().similar(channel);
for (auto i = 0, count = int(_moreThumbnails.size()); i != count; ++i) {
if (similar.list.size() <= _channels.size() + i) {
break;
}
_moreThumbnails[i] = Ui::MakeUserpicThumbnail(
similar.list[_channels.size() + i]);
}
}
void SimilarChannels::validateCounterBg(const Channel &channel) const {
if (channel.counterBgValid) {
return;
}
channel.counterBgValid = 1;
const auto photo = st::chatSimilarChannelPhoto;
const auto inner = QRect(0, 0, photo, photo);
const auto outer = inner.marginsAdded(st::chatSimilarChannelPadding);
const auto length = st::chatSimilarBadgeFont->width(channel.counter);
const auto contents = length
+ (!channel.more
? st::chatSimilarBadgeIcon.width()
: channel.moreLocked
? st::chatSimilarLockedIcon.width()
: 0);
const auto delta = (outer.width() - contents) / 2;
const auto badge = QRect(
delta,
st::chatSimilarBadgeTop,
outer.width() - 2 * delta,
st::chatSimilarBadgeFont->height);
channel.counterRect = badge.marginsAdded(
st::chatSimilarBadgePadding);
constexpr auto kMinSaturation = 0;
constexpr auto kMaxSaturation = 96;
constexpr auto kMinLightness = 160;
constexpr auto kMaxLightness = 208;
const auto width = channel.counterRect.width();
const auto height = channel.counterRect.height();
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
channel.counterRect.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
auto color = channel.more
? QColor(kMinLightness, kMinLightness, kMinLightness)
: Ui::CountAverageColor(
channel.thumbnail->image(photo).copy(
QRect(photo / 3, photo / 3, photo / 3, photo / 3)));
const auto hsl = color.toHsl();
if (!base::in_range(hsl.saturation(), kMinSaturation, kMaxSaturation)
|| !base::in_range(hsl.lightness(), kMinLightness, kMaxLightness)) {
color = QColor::fromHsl(
hsl.hue(),
std::clamp(hsl.saturation(), kMinSaturation, kMaxSaturation),
std::clamp(hsl.lightness(), kMinLightness, kMaxLightness)
).toRgb();
}
result.fill(color);
result.setDevicePixelRatio(ratio);
const auto radius = height / 2;
auto corners = Images::CornersMask(radius);
auto p = QPainter(&result);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p.drawImage(0, 0, corners[Images::kTopLeft]);
p.drawImage(width - radius, 0, corners[Images::kTopRight]);
p.drawImage(0, height - radius, corners[Images::kBottomLeft]);
p.drawImage(
width - radius,
height - radius,
corners[Images::kBottomRight]);
p.end();
channel.counterBg = std::move(result);
}
ClickHandlerPtr SimilarChannels::ensureToggleLink() const {
if (_toggleLink) {
return _toggleLink;
}
_toggleLink = std::make_shared<LambdaClickHandler>(crl::guard(this, [=](
ClickContext context) {
const auto channel = history()->peer->asChannel();
Assert(channel != nullptr);
using Flag = ChannelDataFlag;
const auto flags = channel->flags();
channel->setFlags((flags & Flag::SimilarExpanded)
? (flags & ~Flag::SimilarExpanded)
: (flags | Flag::SimilarExpanded));
}));
return _toggleLink;
}
void SimilarChannels::ensureCacheReady(QSize size) const {
const auto ratio = style::DevicePixelRatio();
if (_roundedCache.size() != size * ratio) {
_roundedCache = QImage(
size * ratio,
QImage::Format_ARGB32_Premultiplied);
_roundedCache.setDevicePixelRatio(ratio);
}
const auto radius = st::bubbleRadiusLarge;
if (_roundedCorners.front().size() != QSize(radius, radius) * ratio) {
_roundedCorners = Images::CornersMask(radius);
}
}
TextState SimilarChannels::textState(
QPoint point,
StateRequest request) const {
auto result = TextState();
if (point.y() < 0 && !_empty) {
result.link = ensureToggleLink();
return result;
}
result.horizontalScroll = (_scrollMax > 0);
const auto skip = st::chatSimilarTitlePosition;
const auto viewWidth = _hasViewAll ? (_viewAllWidth + 2 * skip.x()) : 0;
const auto viewHeight = st::normalFont->height + 2 * skip.y();
const auto viewLeft = width() - viewWidth;
if (QRect(viewLeft, 0, viewWidth, viewHeight).contains(point)) {
if (!_viewAllLink) {
const auto channel = parent()->history()->peer->asChannel();
Assert(channel != nullptr);
_viewAllLink = MakeViewAllLink(channel, false);
}
result.link = _viewAllLink;
return result;
}
for (const auto &channel : _channels) {
if (channel.geometry.translated(-int(_scrollLeft), 0).contains(point)) {
result.link = channel.link;
_lastPoint = point
+ QPoint(_scrollLeft, 0)
- channel.geometry.topLeft();
break;
}
}
return result;
}
QSize SimilarChannels::countOptimalSize() {
const auto channel = parent()->history()->peer->asChannel();
Assert(channel != nullptr);
_channels.clear();
_moreThumbnails = {};
const auto api = &channel->session().api();
api->chatParticipants().loadSimilarPeers(channel);
const auto premium = channel->session().premium();
const auto &similar = api->chatParticipants().similar(channel);
_empty = similar.list.empty() ? 1 : 0;
_moreThumbnailsValid = 0;
using Flag = ChannelDataFlag;
_toggled = (channel->flags() & Flag::SimilarExpanded) ? 1 : 0;
if (_empty || !_toggled) {
return {};
}
_channels.reserve(similar.list.size());
auto x = st::chatSimilarPadding.left();
auto y = st::chatSimilarPadding.top();
const auto skip = st::chatSimilarSkip;
const auto photo = st::chatSimilarChannelPhoto;
const auto inner = QRect(0, 0, photo, photo);
const auto outer = inner.marginsAdded(st::chatSimilarChannelPadding);
const auto limit = Data::PremiumLimits(
&channel->session()).similarChannelsDefault();
const auto take = (similar.more > 0 || similar.list.size() > 2 * limit)
? limit
: int(similar.list.size());
const auto more = similar.more + int(similar.list.size() - take);
auto &&peers = ranges::views::all(similar.list)
| ranges::views::take(limit);
for (const auto &peer : peers) {
const auto channel = peer->asBroadcast();
if (!channel) {
continue;
}
const auto moreCounter = (_channels.size() + 1 == take) ? more : 0;
_channels.push_back({
.geometry = QRect(QPoint(x, y), outer.size()),
.name = Ui::Text::String(
st::chatSimilarName,
(moreCounter
? tr::lng_similar_channels_more(tr::now)
: channel->name()),
kDefaultTextOptions,
st::chatSimilarChannelPhoto),
.thumbnail = Ui::MakeUserpicThumbnail(channel),
.more = uint32(moreCounter),
.moreLocked = uint32((moreCounter && !premium) ? 1 : 0),
});
auto &last = _channels.back();
last.link = moreCounter
? MakeViewAllLink(parent()->history()->peer->asChannel(), true)
: channel->openLink();
const auto counter = moreCounter
? moreCounter
: channel->membersCount();
if (moreCounter || counter > 1) {
last.counter = (moreCounter ? u"+"_q : QString())
+ Lang::FormatCountToShort(counter).string;
}
x += outer.width() + skip;
}
_title = tr::lng_similar_channels_title(tr::now);
_titleWidth = st::chatSimilarTitle->width(_title);
_viewAll = tr::lng_similar_channels_view_all(tr::now);
_viewAllWidth = std::max(st::normalFont->width(_viewAll), 0);
const auto count = int(_channels.size());
const auto desired = (count ? (x - skip) : x)
- st::chatSimilarPadding.left();
const auto full = QRect(0, 0, desired, outer.height());
const auto bubble = full.marginsAdded(st::chatSimilarPadding);
_fullWidth = bubble.width();
const auto titleSkip = st::chatSimilarTitlePosition.x();
const auto min = int(_titleWidth) + 2 * titleSkip;
const auto limited = std::max(
std::min(int(_fullWidth), st::chatSimilarWidthMax),
min);
if (limited > _fullWidth) {
const auto shift = (limited - _fullWidth) / 2;
for (auto &channel : _channels) {
channel.geometry.translate(shift, 0);
}
}
return { limited, bubble.height() };
}
QSize SimilarChannels::countCurrentSize(int newWidth) {
if (!_toggled) {
return {};
}
_scrollMax = std::max(int(_fullWidth) - newWidth, 0);
_scrollLeft = std::clamp(_scrollLeft, uint32(), _scrollMax);
_hasViewAll = (_scrollMax != 0) ? 1 : 0;
return { newWidth, minHeight() };
}
bool SimilarChannels::hasHeavyPart() const {
return _hasHeavyPart != 0;
}
void SimilarChannels::unloadHeavyPart() {
_hasHeavyPart = 0;
for (const auto &channel : _channels) {
channel.subscribed = 0;
channel.thumbnail->subscribeToUpdates(nullptr);
}
for (const auto &thumbnail : _moreThumbnails) {
if (thumbnail) {
thumbnail->subscribeToUpdates(nullptr);
}
}
}
bool SimilarChannels::consumeHorizontalScroll(QPoint position, int delta) {
if (_scrollMax == 0) {
return false;
}
const auto left = _scrollLeft;
_scrollLeft = std::clamp(
int(_scrollLeft) - delta,
0,
int(_scrollMax));
if (_scrollLeft == left) {
return false;
}
repaint();
return true;
}
} // namespace HistoryView

View File

@@ -0,0 +1,104 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
namespace Ui {
class DynamicImage;
class RippleAnimation;
} // namespace Ui
namespace HistoryView {
class SimilarChannels final : public Media {
public:
explicit SimilarChannels(not_null<Element*> parent);
~SimilarChannels();
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
void clickHandlerActiveChanged(
const ClickHandlerPtr &p,
bool active) override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) override;
bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const override {
return false;
}
bool dragItemByHandler(const ClickHandlerPtr &p) const override {
return false;
}
bool needsBubble() const override {
return false;
}
bool customInfoLayout() const override {
return true;
}
bool isDisplayed() const override {
return !_empty && _toggled;
}
void unloadHeavyPart() override;
bool hasHeavyPart() const override;
bool consumeHorizontalScroll(QPoint position, int delta) override;
private:
struct Channel {
QRect geometry;
Ui::Text::String name;
std::shared_ptr<Ui::DynamicImage> thumbnail;
ClickHandlerPtr link;
QString counter;
mutable QRect counterRect;
mutable QImage counterBg;
mutable std::unique_ptr<Ui::RippleAnimation> ripple;
uint32 more : 29 = 0;
mutable uint32 moreLocked : 1 = 0;
mutable uint32 subscribed : 1 = 0;
mutable uint32 counterBgValid : 1 = 0;
};
void ensureCacheReady(QSize size) const;
void validateLastPremiumLock() const;
void fillMoreThumbnails() const;
void validateCounterBg(const Channel &channel) const;
[[nodiscard]] ClickHandlerPtr ensureToggleLink() const;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
QString _title, _viewAll;
mutable QImage _roundedCache;
mutable std::array<QImage, 4> _roundedCorners;
mutable QPoint _lastPoint;
uint32 _titleWidth : 15 = 0;
mutable uint32 _moreThumbnailsValid : 1 = 0;
uint32 _viewAllWidth : 15 = 0;
uint32 _fullWidth : 15 = 0;
uint32 _empty : 1 = 0;
mutable uint32 _toggled : 1 = 0;
uint32 _scrollLeft : 15 = 0;
uint32 _scrollMax : 15 = 0;
uint32 _hasViewAll : 1 = 0;
mutable uint32 _hasHeavyPart : 1 = 0;
std::vector<Channel> _channels;
mutable std::array<std::shared_ptr<Ui::DynamicImage>, 2> _moreThumbnails;
mutable ClickHandlerPtr _viewAllLink;
mutable ClickHandlerPtr _toggleLink;
};
} // namespace HistoryView

View File

@@ -0,0 +1,214 @@
/*
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/media/history_view_slot_machine.h"
#include "data/data_session.h"
#include "chat_helpers/stickers_dice_pack.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/view/history_view_element.h"
#include "main/main_session.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
constexpr auto kStartBackIndex = 0;
constexpr auto kWinBackIndex = 1;
constexpr auto kPullIndex = 2;
constexpr auto kShifts = std::array<int, 3>{ 3, 9, 15 };
constexpr auto kSevenWinIndex = 0;
constexpr auto kSevenIndex = 1;
constexpr auto kBarIndex = 2;
constexpr auto kBerriesIndex = 3;
constexpr auto kLemonIndex = 4;
constexpr auto kStartIndex = 5;
constexpr auto kWinValue = 64;
constexpr auto kSkipFramesBeforeWinEnding = 90;
const auto &kEmoji = ::Stickers::DicePacks::kSlotString;
[[nodiscard]] DocumentData *Lookup(
not_null<Element*> view,
int value) {
const auto &session = view->history()->session();
return session.diceStickersPacks().lookup(kEmoji, value);
}
[[nodiscard]] int ComplexIndex(int partIndex, int inPartIndex) {
Expects(partIndex >= 0 && partIndex < 3);
return kShifts[partIndex] + inPartIndex;
}
[[nodiscard]] int ComputePartValue(int value, int partIndex) {
return ((value - 1) >> (partIndex * 2)) & 0x03; // 0..3
}
[[nodiscard]] int ComputeComplexIndex(int value, int partIndex) {
Expects(value > 0 && value <= 64);
if (value == kWinValue) {
return ComplexIndex(partIndex, kSevenWinIndex);
}
return ComplexIndex(partIndex, [&] {
switch (ComputePartValue(value, partIndex)) {
case 0: return kBarIndex;
case 1: return kBerriesIndex;
case 2: return kLemonIndex;
case 3: return kSevenIndex;
}
Unexpected("Part value value in ComputeComplexIndex.");
}());
}
} // namespace
SlotMachine::SlotMachine(
not_null<Element*> parent,
not_null<Data::MediaDice*> dice)
: _parent(parent)
, _dice(dice)
, _link(dice->makeHandler()) {
resolveStarts();
_showLastFrame = _parent->data()->Has<HistoryMessageForwarded>();
if (_showLastFrame) {
for (auto &drawingEnd : _drawingEnd) {
drawingEnd = true;
}
}
}
SlotMachine::~SlotMachine() = default;
void SlotMachine::resolve(
std::optional<Sticker> &sticker,
int singleTimeIndex,
int index,
bool initSize) const {
if (sticker) {
return;
}
const auto document = Lookup(_parent, index);
if (!document) {
return;
}
const auto skipPremiumEffect = false;
sticker.emplace(_parent, document, skipPremiumEffect);
sticker->setDiceIndex(kEmoji, singleTimeIndex);
if (initSize) {
sticker->initSize();
}
}
void SlotMachine::resolveStarts(bool initSize) {
resolve(_pull, kPullIndex, kPullIndex, initSize);
resolve(_start[0], 0, kStartBackIndex, initSize);
for (auto i = 0; i != 3; ++i) {
resolve(_start[i + 1], 0, ComplexIndex(i, kStartIndex), initSize);
}
}
void SlotMachine::resolveEnds(int value) {
if (value <= 0 || value > 64) {
return;
}
const auto firstPartValue = ComputePartValue(value, 0);
if (ComputePartValue(value, 1) == firstPartValue
&& ComputePartValue(value, 2) == firstPartValue) { // Three in a row.
resolve(_end[0], kWinBackIndex, kWinBackIndex, true);
}
for (auto i = 0; i != 3; ++i) {
const auto index = ComputeComplexIndex(value, i);
resolve(_end[i + 1], index, index, true);
}
}
bool SlotMachine::isEndResolved() const {
for (auto i = 0; i != 3; ++i) {
if (!_end[i + 1]) {
return false;
}
}
return _end[0].has_value() || (_dice->value() != kWinValue);
}
QSize SlotMachine::countOptimalSize() {
return _pull ? _pull->countOptimalSize() : Sticker::EmojiSize();
}
ClickHandlerPtr SlotMachine::link() {
return _link;
}
void SlotMachine::draw(
Painter &p,
const PaintContext &context,
const QRect &r) {
resolveStarts(true);
resolveEnds(_dice->value());
//const auto endResolved = isEndResolved();
//if (!endResolved) {
// for (auto &drawingEnd : _drawingEnd) {
// drawingEnd = false;
// }
//}
auto switchedToEnd = _drawingEnd;
const auto pullReady = _pull && _pull->readyToDrawAnimationFrame();
const auto paintReady = [&] {
auto result = pullReady;
auto allPlayedEnough = true;
for (auto i = 1; i != 4; ++i) {
if (!_end[i] || !_end[i]->readyToDrawAnimationFrame()) {
switchedToEnd[i] = false;
}
if (!switchedToEnd[i]
&& (!_start[i] || !_start[i]->readyToDrawAnimationFrame())) {
result = false;
}
const auto playedTillFrame = !switchedToEnd[i]
? 0
: _end[i]->frameIndex().value_or(0);
if (playedTillFrame < kSkipFramesBeforeWinEnding) {
allPlayedEnough = false;
}
}
if (!_end[0]
|| !_end[0]->readyToDrawAnimationFrame()
|| !allPlayedEnough) {
switchedToEnd[0] = false;
}
if (ranges::contains(switchedToEnd, false)
&& (!_start[0] || !_start[0]->readyToDrawAnimationFrame())) {
result = false;
}
return result;
}();
if (!paintReady) {
return;
}
for (auto i = 0; i != 4; ++i) {
if (switchedToEnd[i]) {
_end[i]->draw(p, context, r);
} else {
_start[i]->draw(p, context, r);
if (_end[i]
&& _end[i]->readyToDrawAnimationFrame()
&& _start[i]->atTheEnd()) {
_drawingEnd[i] = true;
}
}
}
_pull->draw(p, context, r);
}
} // namespace HistoryView

View File

@@ -0,0 +1,79 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media_unwrapped.h"
#include "history/view/media/history_view_sticker.h"
namespace Data {
class MediaDice;
} // namespace Data
namespace HistoryView {
class SlotMachine final : public UnwrappedMedia::Content {
public:
SlotMachine(not_null<Element*> parent, not_null<Data::MediaDice*> dice);
~SlotMachine();
QSize countOptimalSize() override;
void draw(
Painter &p,
const PaintContext &context,
const QRect &r) override;
ClickHandlerPtr link() override;
bool hasHeavyPart() const override {
if (_pull && _pull->hasHeavyPart()) {
return true;
}
for (auto i = 0; i != 4; ++i) {
if ((_start[i] && _start[i]->hasHeavyPart())
|| (_end[i] && _end[i]->hasHeavyPart())) {
return true;
}
}
return false;
}
void unloadHeavyPart() override {
if (_pull) {
_pull->unloadHeavyPart();
}
for (auto i = 0; i != 4; ++i) {
if (_start[i]) {
_start[i]->unloadHeavyPart();
}
if (_end[i]) {
_end[i]->unloadHeavyPart();
}
}
}
private:
void resolveStarts(bool initSize = false);
void resolveEnds(int value);
[[nodiscard]] bool isEndResolved() const;
void resolve(
std::optional<Sticker> &sticker,
int singleTimeIndex,
int index,
bool initSize) const;
const not_null<Element*> _parent;
const not_null<Data::MediaDice*> _dice;
ClickHandlerPtr _link;
std::optional<Sticker> _pull;
std::array<std::optional<Sticker>, 4> _start;
std::array<std::optional<Sticker>, 4> _end;
mutable bool _showLastFrame = false;
mutable std::array<bool, 4> _drawingEnd = { { false } };
};
} // namespace HistoryView

View File

@@ -0,0 +1,675 @@
/*
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/media/history_view_sticker.h"
#include "boxes/sticker_set_box.h"
#include "history/history.h"
#include "history/history_item_components.h"
#include "history/history_item.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/media/history_view_media_common.h"
#include "history/view/media/history_view_sticker_player.h"
#include "lang/lang_keys.h"
#include "ui/image/image.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/text/custom_emoji_instance.h"
#include "ui/emoji_config.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/click_handler_types.h"
#include "window/window_session_controller.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_click_handler.h"
#include "data/data_file_origin.h"
#include "chat_helpers/stickers_lottie.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"
namespace HistoryView {
namespace {
constexpr auto kMaxSizeFixed = 512;
constexpr auto kMaxEmojiSizeFixed = 256;
constexpr auto kPremiumMultiplier = (1 + 0.245 * 2);
constexpr auto kEmojiMultiplier = 3;
constexpr auto kMessageEffectMultiplier = 2;
[[nodiscard]] QImage CacheDiceImage(
const QString &emoji,
int index,
const QImage &image) {
static auto Cache = base::flat_map<std::pair<QString, int>, QImage>();
const auto key = std::make_pair(emoji, index);
const auto i = Cache.find(key);
if (i != end(Cache) && i->second.size() == image.size()) {
return i->second;
}
Cache[key] = image;
return image;
}
[[nodiscard]] QColor ComputeEmojiTextColor(const PaintContext &context) {
const auto st = context.st;
const auto result = st->messageStyle(false, false).historyTextFg->c;
if (!context.selected()) {
return result;
}
const auto &add = st->msgStickerOverlay()->c;
const auto ca = add.alpha();
const auto ra = 0x100 - ca;
const auto aa = ca + 1;
const auto red = (result.red() * ra + add.red() * aa) >> 8;
const auto green = (result.green() * ra + add.green() * aa) >> 8;
const auto blue = (result.blue() * ra + add.blue() * aa) >> 8;
return QColor(red, green, blue, result.alpha());
}
} // namespace
Sticker::Sticker(
not_null<Element*> parent,
not_null<DocumentData*> data,
bool skipPremiumEffect,
Element *replacing,
const Lottie::ColorReplacements *replacements)
: _parent(parent)
, _data(data)
, _replacements(replacements)
, _cachingTag(ChatHelpers::StickerLottieSize::MessageHistory)
, _skipPremiumEffect(skipPremiumEffect)
, _sensitiveBlurred(parent->data()->isMediaSensitive()) {
if ((_dataMedia = _data->activeMediaView())) {
dataMediaCreated();
} else {
_data->loadThumbnail(parent->data()->fullId());
if (hasPremiumEffect()) {
_data->loadVideoThumbnail(parent->data()->fullId());
}
}
if (const auto media = replacing ? replacing->media() : nullptr) {
_player = media->stickerTakePlayer(_data, _replacements);
if (_player) {
if (hasPremiumEffect() && !_premiumEffectPlayed) {
_premiumEffectPlayed = true;
if (On(PowerSaving::kStickersChat)
&& !_premiumEffectSkipped) {
_premiumEffectSkipped = true;
} else {
_parent->delegate()->elementStartPremium(
_parent,
replacing);
}
}
playerCreated();
}
}
}
Sticker::~Sticker() {
if (_player || _dataMedia) {
if (_player) {
unloadPlayer();
}
if (_dataMedia) {
_data->owner().keepAlive(base::take(_dataMedia));
_parent->checkHeavyPart();
}
}
}
bool Sticker::hasPremiumEffect() const {
return !_skipPremiumEffect && _data->isPremiumSticker();
}
bool Sticker::customEmojiPart() const {
return _customEmojiPart;
}
bool Sticker::emojiSticker() const {
return _emojiSticker;
}
bool Sticker::webpagePart() const {
return _webpagePart;
}
void Sticker::initSize(int customSize) {
if (customSize > 0) {
const auto original = Size(_data);
const auto proposed = QSize{ customSize, customSize };
_size = original.isEmpty()
? proposed
: DownscaledSize(original, proposed);
} else if (emojiSticker() || _diceIndex >= 0) {
_size = EmojiSize();
if (_diceIndex > 0) {
[[maybe_unused]] bool result = readyToDrawAnimationFrame();
}
} else {
_size = Size(_data);
}
_size = DownscaledSize(_size, Size());
}
QSize Sticker::countOptimalSize() {
if (_size.isEmpty()) {
initSize();
}
return _size;
}
bool Sticker::readyToDrawAnimationFrame() {
if (!_lastFrameCached.isNull()) {
return true;
}
const auto sticker = _data->sticker();
if (!sticker || _sensitiveBlurred) {
return false;
}
ensureDataMediaCreated();
_dataMedia->checkStickerLarge();
const auto loaded = _dataMedia->loaded();
const auto waitingForPremium = hasPremiumEffect()
&& _dataMedia->videoThumbnailContent().isEmpty();
if (!_player && loaded && !waitingForPremium && sticker->isAnimated()) {
setupPlayer();
}
return ready();
}
QSize Sticker::Size() {
const auto side = std::min(st::maxStickerSize, kMaxSizeFixed);
return { side, side };
}
QSize Sticker::Size(not_null<DocumentData*> document) {
return DownscaledSize(document->dimensions, Size());
}
QSize Sticker::PremiumEffectSize(not_null<DocumentData*> document) {
return Size(document) * kPremiumMultiplier;
}
QSize Sticker::UsualPremiumEffectSize() {
return DownscaledSize({ kMaxSizeFixed, kMaxSizeFixed }, Size())
* kPremiumMultiplier;
}
QSize Sticker::EmojiEffectSize() {
return EmojiSize() * kEmojiMultiplier;
}
QSize Sticker::MessageEffectSize() {
return EmojiSize() * kMessageEffectMultiplier;
}
QSize Sticker::EmojiSize() {
const auto side = std::min(st::maxAnimatedEmojiSize, kMaxEmojiSizeFixed);
return { side, side };
}
void Sticker::draw(
Painter &p,
const PaintContext &context,
const QRect &r) {
if (!customEmojiPart()) {
_parent->clearCustomEmojiRepaint();
}
ensureDataMediaCreated();
if (readyToDrawAnimationFrame()) {
paintAnimationFrame(p, context, r);
} else if (!_data->sticker()
|| (_data->sticker()->isLottie() && _replacements)
|| !paintPixmap(p, context, r)) {
paintPath(p, context, r);
}
if (_sensitiveBlurred) {
paintSensitiveTag(p, context, r);
}
}
void Sticker::paintSensitiveTag(
Painter &p,
const PaintContext &context,
const QRect &r) {
auto text = Ui::Text::String();
auto iconSkip = 0;
text.setText(
st::semiboldTextStyle,
tr::lng_sensitive_tag(tr::now));
iconSkip = st::mediaMenuIconStealth.width() * 1.4;
const auto width = iconSkip + text.maxWidth();
const auto inner = QRect(0, 0, width, text.minHeight());
const auto outer = style::centerrect(
r,
inner.marginsAdded(st::paidTagPadding));
const auto size = outer.size();
const auto real = outer.marginsRemoved(st::paidTagPadding);
const auto radius = std::min(size.width(), size.height()) / 2;
p.setPen(Qt::NoPen);
p.setBrush(context.st->msgServiceBg());
p.drawRoundedRect(outer, radius, radius);
p.setPen(context.st->msgServiceFg());
if (iconSkip) {
st::mediaMenuIconStealth.paint(
p,
real.x(),
(outer.y()
+ (size.height() - st::mediaMenuIconStealth.height()) / 2),
outer.width(),
context.st->msgServiceFg()->c);
}
text.draw(p, real.x() + iconSkip, real.y(), width);
}
ClickHandlerPtr Sticker::link() {
return _link;
}
bool Sticker::ready() const {
return _player && _player->ready();
}
DocumentData *Sticker::document() {
return _data;
}
void Sticker::stickerClearLoopPlayed() {
if (!_playingOnce) {
_oncePlayed = false;
}
_premiumEffectSkipped = false;
}
void Sticker::paintAnimationFrame(
Painter &p,
const PaintContext &context,
const QRect &r) {
const auto colored = (customEmojiPart() && _data->emojiUsesTextColor())
? ComputeEmojiTextColor(context)
: (context.selected() && !_nextLastFrame)
? context.st->msgStickerOverlay()->c
: QColor(0, 0, 0, 0);
const auto powerSavingFlag = emojiSticker()
? PowerSaving::kEmojiChat
: PowerSaving::kStickersChat;
const auto paused = context.paused
|| (_diceIndex < 0 && On(powerSavingFlag));
const auto frame = _player
? _player->frame(
_size,
colored,
mirrorHorizontal(),
context.now,
paused)
: StickerPlayer::FrameInfo();
if (_nextLastFrame) {
_nextLastFrame = false;
_lastFrameCached = (_diceIndex > 0)
? CacheDiceImage(_diceEmoji, _diceIndex, frame.image)
: frame.image;
}
const auto &image = _lastFrameCached.isNull()
? frame.image
: _lastFrameCached;
const auto prepared = (!_lastFrameCached.isNull() && context.selected())
? Images::Colored(
base::duplicate(image),
context.st->msgStickerOverlay()->c)
: image;
const auto size = prepared.size() / style::DevicePixelRatio();
p.drawImage(
QRect(
QPoint(
r.x() + (r.width() - size.width()) / 2,
r.y() + (r.height() - size.height()) / 2),
size),
prepared);
if (!_lastFrameCached.isNull()) {
return;
}
const auto count = _player->framesCount();
_frameIndex = frame.index;
_framesCount = count;
_nextLastFrame = !paused
&& _stopOnLastFrame
&& (_frameIndex + 2 == count);
const auto playOnce = _playingOnce
? true
: (_diceIndex == 0)
? false
: ((!customEmojiPart() && emojiSticker())
|| !Core::App().settings().loopAnimatedStickers());
const auto lastFrame = _stopOnLastFrame && atTheEnd();
const auto switchToNext = !playOnce
|| (!lastFrame && (_frameIndex != 0 || !_oncePlayed));
if (!paused
&& switchToNext
&& _player->markFrameShown()
&& playOnce
&& !_oncePlayed) {
_oncePlayed = true;
_parent->delegate()->elementStartStickerLoop(_parent);
}
checkPremiumEffectStart();
}
bool Sticker::paintPixmap(
Painter &p,
const PaintContext &context,
const QRect &r) {
const auto pixmap = paintedPixmap(context);
if (pixmap.isNull()) {
return false;
}
const auto size = pixmap.size() / pixmap.devicePixelRatio();
const auto position = QPoint(
r.x() + (r.width() - size.width()) / 2,
r.y() + (r.height() - size.height()) / 2);
const auto mirror = mirrorHorizontal();
if (mirror) {
p.save();
const auto middle = QPointF(
position.x() + size.width() / 2.,
position.y() + size.height() / 2.);
p.translate(middle);
p.scale(-1., 1.);
p.translate(-middle);
}
p.drawPixmap(position, pixmap);
if (mirror) {
p.restore();
}
return true;
}
void Sticker::paintPath(
Painter &p,
const PaintContext &context,
const QRect &r) {
const auto pathGradient = _parent->delegate()->elementPathShiftGradient();
auto helper = std::optional<style::owned_color>();
if (customEmojiPart() && _data->emojiUsesTextColor()) {
helper.emplace(Ui::CustomEmoji::PreviewColorFromTextColor(
ComputeEmojiTextColor(context)));
pathGradient->overrideColors(helper->color(), helper->color());
} else if (webpagePart()) {
pathGradient->overrideColors(st::shadowFg, st::shadowFg);
} else if (context.selected()) {
pathGradient->overrideColors(
context.st->msgServiceBgSelected(),
context.st->msgServiceBg());
} else {
pathGradient->clearOverridenColors();
}
p.setBrush(context.imageStyle()->msgServiceBg);
ChatHelpers::PaintStickerThumbnailPath(
p,
_dataMedia.get(),
r,
pathGradient,
mirrorHorizontal());
if (helper) {
pathGradient->clearOverridenColors();
}
}
QPixmap Sticker::paintedPixmap(const PaintContext &context) const {
auto helper = std::optional<style::owned_color>();
const auto sticker = _data->sticker();
const auto ratio = style::DevicePixelRatio();
const auto adjust = [&](int side) {
return (((side * ratio) / 8) * 8) / ratio;
};
const auto useSize = (sticker && sticker->type == StickerType::Tgs)
? QSize(adjust(_size.width()), adjust(_size.height()))
: _size;
const auto colored = (customEmojiPart() && _data->emojiUsesTextColor())
? &helper.emplace(ComputeEmojiTextColor(context)).color()
: context.selected()
? &context.st->msgStickerOverlay()
: nullptr;
const auto good = _sensitiveBlurred
? nullptr
: _dataMedia->goodThumbnail();
const auto image = _sensitiveBlurred
? nullptr
: _dataMedia->getStickerLarge();
if (image) {
return image->pix(useSize, { .colored = colored });
//
// Inline thumbnails can't have alpha channel.
//
//} else if (const auto blurred = _data->thumbnailInline()) {
// return blurred->pix(
// useSize,
// { .colored = colored, .options = Images::Option::Blur });
} else if (good) {
return good->pix(useSize, { .colored = colored });
} else if (const auto thumbnail = _dataMedia->thumbnail()) {
return thumbnail->pix(
useSize,
{ .colored = colored, .options = Images::Option::Blur });
}
return QPixmap();
}
bool Sticker::mirrorHorizontal() const {
if (!hasPremiumEffect()) {
return false;
}
const auto rightAligned = _parent->hasRightLayout();
return !rightAligned;
}
ClickHandlerPtr Sticker::ShowSetHandler(not_null<DocumentData*> document) {
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto window = my.sessionWindow.get()) {
StickerSetBox::Show(window->uiShow(), document);
}
});
}
void Sticker::refreshLink() {
if (_link) {
return;
}
const auto sticker = _data->sticker();
if (_sensitiveBlurred) {
_link = MakeSensitiveMediaLink(nullptr, _parent->data());
} else if (emojiSticker()) {
const auto weak = base::make_weak(this);
_link = std::make_shared<LambdaClickHandler>([weak] {
if (const auto that = weak.get()) {
that->emojiStickerClicked();
}
});
} else if (sticker && sticker->set) {
if (hasPremiumEffect()) {
const auto weak = base::make_weak(this);
_link = std::make_shared<LambdaClickHandler>([weak] {
if (const auto that = weak.get()) {
that->premiumStickerClicked();
}
});
} else {
_link = ShowSetHandler(_data);
}
} else if (sticker
&& (_data->dimensions.width() > kStickerSideSize
|| _data->dimensions.height() > kStickerSideSize)
&& !_parent->data()->isSending()
&& !_parent->data()->hasFailed()) {
// In case we have a .webp file that is displayed as a sticker, but
// that doesn't fit in 512x512, we assume it may be a regular large
// .webp image and we allow to open it in media viewer.
_link = std::make_shared<DocumentOpenClickHandler>(
_data,
crl::guard(this, [=](FullMsgId id) {
_parent->delegate()->elementOpenDocument(_data, id);
}),
_parent->data()->fullId());
}
}
void Sticker::emojiStickerClicked() {
if (_player) {
_parent->delegate()->elementStartInteraction(_parent);
}
_oncePlayed = false;
_parent->history()->owner().requestViewRepaint(_parent);
}
void Sticker::premiumStickerClicked() {
_premiumEffectPlayed = false;
// Remove when we start playing sticker itself on click.
_premiumEffectSkipped = false;
_parent->history()->owner().requestViewRepaint(_parent);
}
void Sticker::ensureDataMediaCreated() const {
if (_dataMedia) {
return;
}
_dataMedia = _data->createMediaView();
dataMediaCreated();
}
void Sticker::dataMediaCreated() const {
Expects(_dataMedia != nullptr);
_dataMedia->goodThumbnailWanted();
if (_dataMedia->thumbnailPath().isEmpty()) {
_dataMedia->thumbnailWanted(_parent->data()->fullId());
}
if (hasPremiumEffect()) {
_data->loadVideoThumbnail(_parent->data()->fullId());
}
_parent->history()->owner().registerHeavyViewPart(_parent);
}
void Sticker::setDiceIndex(const QString &emoji, int index) {
_diceEmoji = emoji;
_diceIndex = index;
_playingOnce = (index > 0);
_stopOnLastFrame = (index > 0);
}
void Sticker::setPlayingOnce(bool once) {
_playingOnce = once;
}
void Sticker::setStopOnLastFrame(bool stop) {
_stopOnLastFrame = stop;
_playingOnce = true;
}
void Sticker::setCustomCachingTag(ChatHelpers::StickerLottieSize tag) {
_cachingTag = tag;
}
void Sticker::setCustomEmojiPart() {
_customEmojiPart = true;
}
void Sticker::setEmojiSticker() {
_emojiSticker = true;
}
void Sticker::setWebpagePart() {
_webpagePart = true;
}
void Sticker::setupPlayer() {
Expects(_dataMedia != nullptr);
if (_data->sticker()->isLottie()) {
_player = std::make_unique<LottiePlayer>(
ChatHelpers::LottiePlayerFromDocument(
_dataMedia.get(),
_replacements,
_cachingTag,
countOptimalSize() * style::DevicePixelRatio(),
Lottie::Quality::High));
} else if (_data->sticker()->isWebm()) {
_player = std::make_unique<WebmPlayer>(
_dataMedia->owner()->location(),
_dataMedia->bytes(),
countOptimalSize());
}
checkPremiumEffectStart();
playerCreated();
}
void Sticker::checkPremiumEffectStart() {
if (!_premiumEffectPlayed && hasPremiumEffect()) {
_premiumEffectPlayed = true;
if (On(PowerSaving::kStickersChat)
&& !_premiumEffectSkipped) {
_premiumEffectSkipped = true;
} else {
_parent->delegate()->elementStartPremium(_parent, nullptr);
}
}
}
void Sticker::playerCreated() {
Expects(_player != nullptr);
_parent->history()->owner().registerHeavyViewPart(_parent);
_player->setRepaintCallback([=] { _parent->customEmojiRepaint(); });
}
bool Sticker::hasHeavyPart() const {
return _player || _dataMedia;
}
void Sticker::unloadHeavyPart() {
unloadPlayer();
_dataMedia = nullptr;
}
void Sticker::unloadPlayer() {
if (!_player) {
return;
}
if (_stopOnLastFrame && _lastFrameCached.isNull()) {
_nextLastFrame = false;
_oncePlayed = false;
}
_player = nullptr;
if (hasPremiumEffect()) {
_parent->delegate()->elementCancelPremium(_parent);
}
_parent->checkHeavyPart();
}
std::unique_ptr<StickerPlayer> Sticker::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return (data == _data && replacements == _replacements)
? std::move(_player)
: nullptr;
}
} // namespace HistoryView

View File

@@ -0,0 +1,156 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media_unwrapped.h"
#include "history/view/media/history_view_sticker_player_abstract.h"
#include "base/weak_ptr.h"
namespace Main {
class Session;
} // namespace Main
namespace Data {
struct FileOrigin;
class DocumentMedia;
} // namespace Data
namespace Lottie {
struct ColorReplacements;
} // namespace Lottie
namespace ChatHelpers {
enum class StickerLottieSize : uint8;
} // namespace ChatHelpers
namespace HistoryView {
class Sticker final
: public UnwrappedMedia::Content
, public base::has_weak_ptr {
public:
Sticker(
not_null<Element*> parent,
not_null<DocumentData*> data,
bool skipPremiumEffect,
Element *replacing = nullptr,
const Lottie::ColorReplacements *replacements = nullptr);
~Sticker();
void initSize(int customSize = 0);
QSize countOptimalSize() override;
void draw(
Painter &p,
const PaintContext &context,
const QRect &r) override;
ClickHandlerPtr link() override;
[[nodiscard]] bool ready() const;
DocumentData *document() override;
void stickerClearLoopPlayed() override;
std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) override;
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
void refreshLink() override;
bool hasTextForCopy() const override {
return emojiSticker();
}
void setDiceIndex(const QString &emoji, int index);
void setPlayingOnce(bool once);
void setStopOnLastFrame(bool stop);
void setCustomCachingTag(ChatHelpers::StickerLottieSize tag);
void setCustomEmojiPart();
void setEmojiSticker();
void setWebpagePart();
[[nodiscard]] bool atTheEnd() const {
return (_frameIndex >= 0) && (_frameIndex + 1 == _framesCount);
}
[[nodiscard]] std::optional<int> frameIndex() const {
return (_frameIndex >= 0)
? std::make_optional(_frameIndex)
: std::nullopt;
}
[[nodiscard]] std::optional<int> framesCount() const {
return (_framesCount > 0)
? std::make_optional(_framesCount)
: std::nullopt;
}
[[nodiscard]] bool readyToDrawAnimationFrame();
[[nodiscard]] static QSize Size();
[[nodiscard]] static QSize Size(not_null<DocumentData*> document);
[[nodiscard]] static QSize PremiumEffectSize(
not_null<DocumentData*> document);
[[nodiscard]] static QSize UsualPremiumEffectSize();
[[nodiscard]] static QSize EmojiEffectSize();
[[nodiscard]] static QSize MessageEffectSize();
[[nodiscard]] static QSize EmojiSize();
[[nodiscard]] static ClickHandlerPtr ShowSetHandler(
not_null<DocumentData*> document);
private:
[[nodiscard]] bool hasPremiumEffect() const;
[[nodiscard]] bool customEmojiPart() const;
[[nodiscard]] bool emojiSticker() const;
[[nodiscard]] bool webpagePart() const;
void paintAnimationFrame(
Painter &p,
const PaintContext &context,
const QRect &r);
bool paintPixmap(Painter &p, const PaintContext &context, const QRect &r);
void paintPath(Painter &p, const PaintContext &context, const QRect &r);
[[nodiscard]] QPixmap paintedPixmap(const PaintContext &context) const;
[[nodiscard]] bool mirrorHorizontal() const;
void paintSensitiveTag(
Painter &p,
const PaintContext &context,
const QRect &r);
void ensureDataMediaCreated() const;
void dataMediaCreated() const;
void setupPlayer();
void playerCreated();
void unloadPlayer();
void emojiStickerClicked();
void premiumStickerClicked();
void checkPremiumEffectStart();
const not_null<Element*> _parent;
const not_null<DocumentData*> _data;
const Lottie::ColorReplacements *_replacements = nullptr;
std::unique_ptr<StickerPlayer> _player;
mutable std::shared_ptr<Data::DocumentMedia> _dataMedia;
ClickHandlerPtr _link;
QSize _size;
QImage _lastFrameCached;
QString _diceEmoji;
int _diceIndex = -1;
mutable int _frameIndex = -1;
mutable int _framesCount = -1;
ChatHelpers::StickerLottieSize _cachingTag = {};
mutable bool _oncePlayed : 1 = false;
mutable bool _premiumEffectPlayed : 1 = false;
mutable bool _premiumEffectSkipped : 1 = false;
mutable bool _nextLastFrame : 1 = false;
bool _skipPremiumEffect : 1 = false;
bool _customEmojiPart : 1 = false;
bool _emojiSticker : 1 = false;
bool _webpagePart : 1 = false;
bool _playingOnce : 1 = false;
bool _stopOnLastFrame : 1 = false;
bool _sensitiveBlurred : 1 = false;
};
} // namespace HistoryView

View File

@@ -0,0 +1,168 @@
/*
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/media/history_view_sticker_player.h"
#include "core/file_location.h"
namespace HistoryView {
namespace {
using ClipNotification = ::Media::Clip::Notification;
} // namespace
LottiePlayer::LottiePlayer(std::unique_ptr<Lottie::SinglePlayer> lottie)
: _lottie(std::move(lottie)) {
}
void LottiePlayer::setRepaintCallback(Fn<void()> callback) {
if (!callback) {
_repaintLifetime.destroy();
return;
}
_repaintLifetime = _lottie->updates(
) | rpl::on_next([=](Lottie::Update update) {
v::match(update.data, [&](const Lottie::Information &) {
callback();
//markFramesTillExternal();
}, [&](const Lottie::DisplayFrameRequest &) {
callback();
});
});
}
bool LottiePlayer::ready() {
return _lottie->ready();
}
int LottiePlayer::framesCount() {
return _lottie->information().framesCount;
}
LottiePlayer::FrameInfo LottiePlayer::frame(
QSize size,
QColor colored,
bool mirrorHorizontal,
crl::time now,
bool paused) {
auto request = Lottie::FrameRequest();
request.box = size * style::DevicePixelRatio();
request.colored = colored;
request.mirrorHorizontal = mirrorHorizontal;
const auto info = _lottie->frameInfo(request);
return { .image = info.image, .index = info.index };
}
bool LottiePlayer::markFrameShown() {
return _lottie->markFrameShown();
}
WebmPlayer::WebmPlayer(
const Core::FileLocation &location,
const QByteArray &data,
QSize size)
: _reader(
::Media::Clip::MakeReader(location, data, [=](ClipNotification update) {
clipCallback(update);
}))
, _size(size) {
}
void WebmPlayer::clipCallback(ClipNotification notification) {
switch (notification) {
case ClipNotification::Reinit: {
if (_reader->state() == ::Media::Clip::State::Error) {
_reader.setBad();
} else if (_reader->ready() && !_reader->started()) {
_reader->start({ .frame = _size, .keepAlpha = true });
}
} break;
case ClipNotification::Repaint: break;
}
if (const auto onstack = _repaintCallback) {
onstack();
}
}
void WebmPlayer::setRepaintCallback(Fn<void()> callback) {
_repaintCallback = std::move(callback);
}
bool WebmPlayer::ready() {
return _reader && _reader->started();
}
int WebmPlayer::framesCount() {
return -1;
}
WebmPlayer::FrameInfo WebmPlayer::frame(
QSize size,
QColor colored,
bool mirrorHorizontal,
crl::time now,
bool paused) {
auto request = ::Media::Clip::FrameRequest();
request.frame = size;
request.factor = style::DevicePixelRatio();
request.keepAlpha = true;
request.colored = colored;
const auto info = _reader->frameInfo(request, paused ? 0 : now);
return { .image = info.image, .index = info.index };
}
bool WebmPlayer::markFrameShown() {
return _reader->moveToNextFrame();
}
StaticStickerPlayer::StaticStickerPlayer(
const Core::FileLocation &location,
const QByteArray &data,
QSize size)
: _frame(Images::Read({
.path = location.name(),
.content = data,
}).image) {
if (!_frame.isNull()) {
size = _frame.size().scaled(size, Qt::KeepAspectRatio);
const auto ratio = style::DevicePixelRatio();
_frame = Images::Prepare(std::move(_frame), size * ratio, {});
_frame.setDevicePixelRatio(ratio);
}
}
void StaticStickerPlayer::setRepaintCallback(Fn<void()> callback) {
if (callback) {
callback();
}
}
bool StaticStickerPlayer::ready() {
return true;
}
int StaticStickerPlayer::framesCount() {
return 1;
}
StaticStickerPlayer::FrameInfo StaticStickerPlayer::frame(
QSize size,
QColor colored,
bool mirrorHorizontal,
crl::time now,
bool paused) {
return { _frame };
}
bool StaticStickerPlayer::markFrameShown() {
return false;
}
} // namespace HistoryView

View File

@@ -0,0 +1,92 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_sticker_player_abstract.h"
#include "lottie/lottie_single_player.h"
#include "media/clip/media_clip_reader.h"
namespace Core {
class FileLocation;
} // namespace Core
namespace HistoryView {
class LottiePlayer final : public StickerPlayer {
public:
explicit LottiePlayer(std::unique_ptr<Lottie::SinglePlayer> lottie);
void setRepaintCallback(Fn<void()> callback) override;
bool ready() override;
int framesCount() override;
FrameInfo frame(
QSize size,
QColor colored,
bool mirrorHorizontal,
crl::time now,
bool paused) override;
bool markFrameShown() override;
private:
std::unique_ptr<Lottie::SinglePlayer> _lottie;
rpl::lifetime _repaintLifetime;
};
class WebmPlayer final : public StickerPlayer {
public:
WebmPlayer(
const Core::FileLocation &location,
const QByteArray &data,
QSize size);
void setRepaintCallback(Fn<void()> callback) override;
bool ready() override;
int framesCount() override;
FrameInfo frame(
QSize size,
QColor colored,
bool mirrorHorizontal,
crl::time now,
bool paused) override;
bool markFrameShown() override;
private:
void clipCallback(::Media::Clip::Notification notification);
::Media::Clip::ReaderPointer _reader;
Fn<void()> _repaintCallback;
QSize _size;
};
class StaticStickerPlayer final : public StickerPlayer {
public:
StaticStickerPlayer(
const Core::FileLocation &location,
const QByteArray &data,
QSize size);
void setRepaintCallback(Fn<void()> callback) override;
bool ready() override;
int framesCount() override;
FrameInfo frame(
QSize size,
QColor colored,
bool mirrorHorizontal,
crl::time now,
bool paused) override;
bool markFrameShown() override;
private:
QImage _frame;
};
} // namespace HistoryView

View File

@@ -0,0 +1,33 @@
/*
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
*/
#pragma once
namespace HistoryView {
class StickerPlayer {
public:
virtual ~StickerPlayer() = default;
struct FrameInfo {
QImage image;
int index = 0;
};
virtual void setRepaintCallback(Fn<void()> callback) = 0;
[[nodiscard]] virtual bool ready() = 0;
[[nodiscard]] virtual int framesCount() = 0;
[[nodiscard]] virtual FrameInfo frame(
QSize size,
QColor colored,
bool mirrorHorizontal,
crl::time now,
bool paused) = 0;
virtual bool markFrameShown() = 0;
};
} // namespace HistoryView

View File

@@ -0,0 +1,186 @@
/*
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/media/history_view_story_mention.h"
#include "core/click_handler_types.h" // ClickHandlerContext
#include "data/data_document.h"
#include "data/data_photo.h"
#include "data/data_user.h"
#include "data/data_photo_media.h"
#include "data/data_file_click_handler.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "editor/photo_editor_common.h"
#include "editor/photo_editor_layer_widget.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/media/history_view_sticker_player_abstract.h"
#include "history/view/history_view_element.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/outline_segments.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/dynamic_image.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/painter.h"
#include "mainwidget.h"
#include "apiwrap.h"
#include "api/api_peer_photo.h"
#include "settings/settings_information.h" // UpdatePhotoLocally
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
constexpr auto kReadOutlineAlpha = 0.5;
} // namespace
StoryMention::StoryMention(
not_null<Element*> parent,
not_null<Data::Story*> story)
: _parent(parent)
, _story(story)
, _unread(story->owner().stories().isUnread(story) ? 1 : 0) {
}
StoryMention::~StoryMention() {
if (_subscribed) {
changeSubscribedTo(0);
_parent->checkHeavyPart();
}
}
int StoryMention::top() {
return st::msgServiceGiftBoxButtonMargins.top();
}
QSize StoryMention::size() {
return { st::msgServicePhotoWidth, st::msgServicePhotoWidth };
}
TextWithEntities StoryMention::title() {
return {};
}
int StoryMention::buttonSkip() {
return st::storyMentionButtonSkip;
}
rpl::producer<QString> StoryMention::button() {
return tr::lng_action_story_mention_button();
}
TextWithEntities StoryMention::subtitle() {
return _parent->data()->notificationText();
}
ClickHandlerPtr StoryMention::createViewLink() {
const auto itemId = _parent->data()->fullId();
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=](
ClickContext) {
if (const auto photo = _story->photo()) {
_parent->delegate()->elementOpenPhoto(photo, itemId);
} else if (const auto video = _story->document()) {
_parent->delegate()->elementOpenDocument(video, itemId);
}
}));
}
void StoryMention::draw(
Painter &p,
const PaintContext &context,
const QRect &geometry) {
const auto showStory = _story->forbidsForward() ? 0 : 1;
if (!_thumbnail || _thumbnailFromStory != showStory) {
const auto item = _parent->data();
const auto history = item->history();
_thumbnail = showStory
? Ui::MakeStoryThumbnail(_story)
: Ui::MakeUserpicThumbnail(item->out()
? history->session().user()
: history->peer);
_thumbnailFromStory = showStory;
changeSubscribedTo(0);
}
if (changeSubscribedTo(1)) {
_thumbnail->subscribeToUpdates([=] {
_parent->data()->history()->owner().requestViewRepaint(_parent);
});
}
const auto padding = (geometry.width() - st::storyMentionSize) / 2;
const auto size = geometry.width() - 2 * padding;
p.drawImage(
geometry.topLeft() + QPoint(padding, padding),
_thumbnail->image(size));
const auto thumbnail = QRectF(geometry.marginsRemoved(
QMargins(padding, padding, padding, padding)));
const auto added = 0.5 * (_unread
? st::storyMentionUnreadSkipTwice
: st::storyMentionReadSkipTwice);
const auto outline = thumbnail.marginsAdded(
QMarginsF(added, added, added, added));
if (_unread && _paletteVersion != style::PaletteVersion()) {
_paletteVersion = style::PaletteVersion();
_unreadBrush = QBrush(Ui::UnreadStoryOutlineGradient(outline));
}
auto readColor = context.st->msgServiceFg()->c;
readColor.setAlphaF(std::min(1. * readColor.alphaF(), kReadOutlineAlpha));
p.setPen(QPen(
_unread ? _unreadBrush : QBrush(readColor),
0.5 * (_unread
? st::storyMentionUnreadStrokeTwice
: st::storyMentionReadStrokeTwice)));
p.setBrush(Qt::NoBrush);
auto hq = PainterHighQualityEnabler(p);
p.drawEllipse(outline);
}
void StoryMention::stickerClearLoopPlayed() {
}
std::unique_ptr<StickerPlayer> StoryMention::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return nullptr;
}
bool StoryMention::hasHeavyPart() {
return _subscribed != 0;
}
void StoryMention::unloadHeavyPart() {
if (changeSubscribedTo(0)) {
_thumbnail->subscribeToUpdates(nullptr);
}
}
bool StoryMention::changeSubscribedTo(uint32 value) {
Expects(value == 0 || value == 1);
if (_subscribed == value) {
return false;
}
_subscribed = value;
const auto stories = &_parent->history()->owner().stories();
if (value) {
_parent->history()->owner().registerHeavyViewPart(_parent);
stories->registerPolling(_story, Data::Stories::Polling::Chat);
} else {
stories->unregisterPolling(_story, Data::Stories::Polling::Chat);
}
return true;
}
} // namespace HistoryView

View File

@@ -0,0 +1,69 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
#include "history/view/media/history_view_media_unwrapped.h"
#include "history/view/media/history_view_service_box.h"
namespace Data {
class Story;
} // namespace Data
namespace Ui {
class DynamicImage;
} // namespace Ui
namespace HistoryView {
class StoryMention final
: public ServiceBoxContent
, public base::has_weak_ptr {
public:
StoryMention(not_null<Element*> parent, not_null<Data::Story*> story);
~StoryMention();
int top() override;
QSize size() override;
TextWithEntities title() override;
TextWithEntities subtitle() override;
int buttonSkip() override;
rpl::producer<QString> button() override;
void draw(
Painter &p,
const PaintContext &context,
const QRect &geometry) override;
ClickHandlerPtr createViewLink() override;
bool hideServiceText() override {
return true;
}
void stickerClearLoopPlayed() override;
std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) override;
bool hasHeavyPart() override;
void unloadHeavyPart() override;
private:
bool changeSubscribedTo(uint32 value);
const not_null<Element*> _parent;
const not_null<Data::Story*> _story;
std::shared_ptr<Ui::DynamicImage> _thumbnail;
QBrush _unreadBrush;
uint32 _paletteVersion : 29 = 0;
uint32 _thumbnailFromStory : 1 = 0;
uint32 _subscribed : 1 = 0;
uint32 _unread : 1 = 0;
};
} // namespace HistoryView

View File

@@ -0,0 +1,370 @@
/*
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/media/history_view_suggest_decision.h"
#include "base/unixtime.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "history/view/media/history_view_media_generic.h"
#include "history/view/media/history_view_unique_gift.h"
#include "history/view/history_view_element.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "lang/lang_keys.h"
#include "ui/chat/chat_style.h"
#include "ui/text/text_utilities.h"
#include "ui/text/format_values.h"
#include "styles/style_chat.h"
#include "styles/style_credits.h"
namespace HistoryView {
namespace {
constexpr auto kFadedOpacity = 0.85;
enum EmojiType {
kAgreement,
kCalendar,
kMoney,
kHourglass,
kReload,
kDecline,
kDiscard,
kWarning,
};
[[nodiscard]] const char *Raw(EmojiType type) {
switch (type) {
case EmojiType::kAgreement: return "\xf0\x9f\xa4\x9d";
case EmojiType::kCalendar: return "\xf0\x9f\x93\x86";
case EmojiType::kMoney: return "\xf0\x9f\x92\xb0";
case EmojiType::kHourglass: return "\xe2\x8c\x9b\xef\xb8\x8f";
case EmojiType::kReload: return "\xf0\x9f\x94\x84";
case EmojiType::kDecline: return "\xe2\x9d\x8c";
case EmojiType::kDiscard: return "\xf0\x9f\x9a\xab";
case EmojiType::kWarning: return "\xe2\x9a\xa0\xef\xb8\x8f";
}
Unexpected("EmojiType in Raw.");
}
[[nodiscard]] QString Emoji(EmojiType type) {
return QString::fromUtf8(Raw(type));
}
struct Changes {
bool date = false;
bool price = false;
bool message = true;
};
[[nodiscard]] std::optional<Changes> ResolveChanges(
not_null<HistoryItem*> changed,
HistoryItem *original) {
const auto wasSuggest = original
? original->Get<HistoryMessageSuggestion>()
: nullptr;
const auto nowSuggest = changed->Get<HistoryMessageSuggestion>();
if (!wasSuggest || !nowSuggest) {
return {};
}
auto result = Changes();
if (wasSuggest->date != nowSuggest->date) {
result.date = true;
}
if (wasSuggest->price != nowSuggest->price) {
result.price = true;
}
const auto wasText = original->originalText();
const auto nowText = changed->originalText();
const auto mediaSame = [&] {
const auto wasMedia = original->media();
const auto nowMedia = changed->media();
if (!wasMedia && !nowMedia) {
return true;
} else if (!wasMedia
|| !nowMedia
|| !wasMedia->allowsEditCaption()
|| !nowMedia->allowsEditCaption()) {
return false;
}
// We treat as "same" only same photo or same file.
return (wasMedia->photo() == nowMedia->photo())
&& (wasMedia->document() == nowMedia->document());
};
if (!result.price && !result.date) {
result.message = true;
} else if (wasText == nowText && mediaSame()) {
result.message = false;
}
return result;
}
} // namespace
auto GenerateSuggestDecisionMedia(
not_null<Element*> parent,
not_null<const HistoryServiceSuggestDecision*> decision)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
return [=](
not_null<MediaGeneric*> media,
Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto peer = parent->history()->peer;
const auto broadcast = peer->monoforumBroadcast();
if (!broadcast) {
return;
}
const auto sublistPeerId = parent->data()->sublistPeerId();
const auto sublistPeer = peer->owner().peer(sublistPeerId);
auto pushText = [&](
TextWithEntities text,
QMargins margins = {},
style::align align = style::al_left,
const base::flat_map<uint16, ClickHandlerPtr> &links = {}) {
push(std::make_unique<MediaGenericTextPart>(
std::move(text),
margins,
st::defaultTextStyle,
links,
Ui::Text::MarkedContext(),
align));
};
if (decision->balanceTooLow) {
pushText(
TextWithEntities(
).append(Emoji(kWarning)).append(' ').append(
(sublistPeer->isSelf()
? (decision->price.ton()
? tr::lng_suggest_action_your_not_enough_ton
: tr::lng_suggest_action_your_not_enough_stars)
: (decision->price.ton()
? tr::lng_suggest_action_his_not_enough_ton
: tr::lng_suggest_action_his_not_enough_stars))(
tr::now,
tr::rich)),
st::chatSuggestInfoFullMargin,
style::al_top);
} else if (decision->rejected) {
const auto withComment = !decision->rejectComment.isEmpty();
pushText(
TextWithEntities(
).append(Emoji(kDecline)).append(' ').append(
(withComment
? tr::lng_suggest_action_declined_reason
: tr::lng_suggest_action_declined)(
tr::now,
lt_from,
tr::bold(broadcast->name()),
tr::marked)),
(withComment
? st::chatSuggestInfoTitleMargin
: st::chatSuggestInfoFullMargin));
if (withComment) {
const auto fadedFg = [](const PaintContext &context) {
auto result = context.st->msgServiceFg()->c;
result.setAlphaF(result.alphaF() * kFadedOpacity);
return result;
};
push(std::make_unique<TextPartColored>(
TextWithEntities().append('"').append(
decision->rejectComment
).append('"'),
st::chatSuggestInfoLastMargin,
fadedFg));
}
} else {
const auto price = decision->price;
pushText(
TextWithEntities(
).append(Emoji(kAgreement)).append(' ').append(
tr::bold(tr::lng_suggest_action_agreement(tr::now))
),
st::chatSuggestInfoTitleMargin,
style::al_top);
const auto date = base::unixtime::parse(decision->date);
pushText(
TextWithEntities(
).append(Emoji(kCalendar)).append(' ').append(
tr::lng_suggest_action_agree_date(
tr::now,
lt_channel,
tr::bold(broadcast->name()),
lt_date,
tr::bold(tr::lng_mediaview_date_time(
tr::now,
lt_date,
QLocale().toString(
date.date(),
QLocale::ShortFormat),
lt_time,
QLocale().toString(
date.time(),
QLocale::ShortFormat))),
tr::marked)),
(price
? st::chatSuggestInfoMiddleMargin
: st::chatSuggestInfoLastMargin));
if (price) {
pushText(
TextWithEntities(
).append(Emoji(kMoney)).append(' ').append(
(sublistPeer->isSelf()
? (price.stars()
? tr::lng_suggest_action_your_charged_stars
: tr::lng_suggest_action_your_charged_ton)(
tr::now,
lt_count_decimal,
price.value(),
tr::rich)
: (price.stars()
? tr::lng_suggest_action_his_charged_stars
: tr::lng_suggest_action_his_charged_ton)(
tr::now,
lt_count_decimal,
price.value(),
lt_from,
tr::bold(sublistPeer->shortName()),
tr::rich))),
st::chatSuggestInfoMiddleMargin);
pushText(
TextWithEntities(
).append(Emoji(kHourglass)).append(' ').append(
(price.ton()
? tr::lng_suggest_action_agree_receive_ton
: tr::lng_suggest_action_agree_receive_stars)(
tr::now,
lt_channel,
tr::bold(broadcast->name()),
tr::marked)),
st::chatSuggestInfoMiddleMargin);
pushText(
TextWithEntities(
).append(Emoji(kReload)).append(' ').append(
(price.ton()
? tr::lng_suggest_action_agree_removed_ton
: tr::lng_suggest_action_agree_removed_stars)(
tr::now,
lt_channel,
tr::bold(broadcast->name()),
tr::marked)),
st::chatSuggestInfoLastMargin);
}
}
};
}
auto GenerateSuggestRequestMedia(
not_null<Element*> parent,
not_null<const HistoryMessageSuggestion*> suggest)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
return [=](
not_null<MediaGeneric*> media,
Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto normalFg = [](const PaintContext &context) {
return context.st->msgServiceFg()->c;
};
const auto fadedFg = [](const PaintContext &context) {
auto result = context.st->msgServiceFg()->c;
result.setAlphaF(result.alphaF() * kFadedOpacity);
return result;
};
const auto item = parent->data();
const auto replyData = item->Get<HistoryMessageReply>();
const auto original = replyData
? replyData->resolvedMessage.get()
: nullptr;
const auto changes = ResolveChanges(item, original);
const auto from = item->from();
auto pushText = [&](
TextWithEntities text,
QMargins margins = {},
style::align align = style::al_left,
const base::flat_map<uint16, ClickHandlerPtr> &links = {}) {
push(std::make_unique<MediaGenericTextPart>(
std::move(text),
margins,
st::defaultTextStyle,
links,
Ui::Text::MarkedContext(),
align));
};
pushText(
((!changes && from->isSelf())
? tr::lng_suggest_action_your(
tr::now,
tr::marked)
: (!changes
? tr::lng_suggest_action_his
: changes->message
? tr::lng_suggest_change_content
: (changes->date && changes->price)
? tr::lng_suggest_change_price_time
: changes->price
? tr::lng_suggest_change_price
: tr::lng_suggest_change_time)(
tr::now,
lt_from,
tr::bold(from->shortName()),
tr::marked)),
st::chatSuggestInfoTitleMargin,
style::al_top);
auto entries = std::vector<AttributeTable::Entry>();
entries.push_back({
((changes && changes->price)
? tr::lng_suggest_change_price_label
: tr::lng_suggest_action_price_label)(tr::now),
tr::bold(!suggest->price
? tr::lng_suggest_action_price_free(tr::now)
: suggest->price.stars()
? tr::lng_suggest_stars_amount(
tr::now,
lt_count_decimal,
suggest->price.value())
: tr::lng_suggest_ton_amount(
tr::now,
lt_count_decimal,
suggest->price.value())),
});
entries.push_back({
((changes && changes->date)
? tr::lng_suggest_change_time_label
: tr::lng_suggest_action_time_label)(tr::now),
tr::bold(suggest->date
? Ui::FormatDateTime(base::unixtime::parse(suggest->date))
: tr::lng_suggest_action_time_any(tr::now)),
});
push(std::make_unique<AttributeTable>(
std::move(entries),
((changes && changes->message)
? st::chatSuggestTableMiddleMargin
: st::chatSuggestTableLastMargin),
fadedFg,
normalFg));
if (changes && changes->message) {
push(std::make_unique<TextPartColored>(
tr::lng_suggest_change_text_label(
tr::now,
tr::marked),
st::chatSuggestInfoLastMargin,
fadedFg));
}
};
}
} // namespace HistoryView

View File

@@ -0,0 +1,33 @@
/*
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
*/
#pragma once
struct HistoryMessageSuggestion;
struct HistoryServiceSuggestDecision;
namespace HistoryView {
class Element;
class MediaGeneric;
class MediaGenericPart;
auto GenerateSuggestDecisionMedia(
not_null<Element*> parent,
not_null<const HistoryServiceSuggestDecision*> decision
) -> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
auto GenerateSuggestRequestMedia(
not_null<Element*> parent,
not_null<const HistoryMessageSuggestion*> suggest
) -> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
} // namespace HistoryView

View File

@@ -0,0 +1,897 @@
/*
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/media/history_view_theme_document.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "boxes/background_preview_box.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/media/history_view_sticker_player_abstract.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_document_media.h"
#include "data/data_file_click_handler.h"
#include "data/data_file_origin.h"
#include "data/data_wall_paper.h"
#include "base/qthelp_url.h"
#include "core/click_handler_types.h"
#include "core/local_url_handlers.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/cached_round_corners.h"
#include "ui/painter.h"
#include "ui/top_background_gradient.h"
#include "ui/ui_utility.h"
#include "window/section_widget.h"
#include "window/window_session_controller.h"
#include "window/themes/window_theme.h"
#include "styles/style_chat.h"
#include "styles/style_credits.h"
namespace HistoryView {
namespace {
[[nodiscard]] bool WallPaperRevertable(
not_null<PeerData*> peer,
const Data::WallPaper &paper) {
if (!peer->wallPaperOverriden()) {
return false;
}
const auto now = peer->wallPaper();
return now && now->equals(paper);
}
[[nodiscard]] bool WallPaperRevertable(not_null<HistoryItem*> item) {
const auto media = item->media();
const auto paper = media ? media->paper() : nullptr;
return paper
&& media->paperForBoth()
&& WallPaperRevertable(item->history()->peer, *paper);
}
[[nodiscard]] rpl::producer<bool> WallPaperRevertableValue(
not_null<HistoryItem*> item) {
const auto media = item->media();
const auto paper = media ? media->paper() : nullptr;
if (!paper || !media->paperForBoth()) {
return rpl::single(false);
}
const auto peer = item->history()->peer;
return peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::ChatWallPaper
) | rpl::map([peer, paper = *paper] {
return WallPaperRevertable(peer, paper);
});
}
} // namespace
ThemeDocument::ThemeDocument(
not_null<Element*> parent,
DocumentData *document)
: ThemeDocument(parent, document, std::nullopt, 0) {
}
ThemeDocument::ThemeDocument(
not_null<Element*> parent,
DocumentData *document,
const std::optional<Data::WallPaper> &params,
int serviceWidth)
: File(parent, parent->data())
, _data(document)
, _serviceWidth(serviceWidth) {
Expects(params.has_value()
|| (_data && (_data->hasThumbnail() || _data->isTheme())));
if (params) {
_background = params->backgroundColors();
_patternOpacity = params->patternOpacity();
_gradientRotation = params->gradientRotation();
_blurredWallPaper = params->isBlurred();
_dimmingIntensity = (!params->document()
|| params->isPattern()
|| !_serviceWidth)
? 0
: std::max(params->patternIntensity(), 0);
}
const auto fullId = _parent->data()->fullId();
if (_data) {
_data->loadThumbnail(fullId);
setDocumentLinks(_data, parent->data());
setStatusSize(Ui::FileStatusSizeReady, _data->size, -1, 0);
} else {
class EmptyFileClickHandler final : public FileClickHandler {
public:
using FileClickHandler::FileClickHandler;
private:
void onClickImpl() const override {
}
};
// We could open BackgroundPreviewBox here, but right now
// WebPage that created ThemeDocument as its attachment does it.
//
// So just provide a non-null click handler for this hack to work.
setLinks(
std::make_shared<EmptyFileClickHandler>(fullId),
nullptr,
nullptr);
}
}
ThemeDocument::~ThemeDocument() {
if (_dataMedia) {
_data->owner().keepAlive(base::take(_dataMedia));
_parent->checkHeavyPart();
}
}
std::optional<Data::WallPaper> ThemeDocument::ParamsFromUrl(
const QString &url) {
const auto local = Core::TryConvertUrlToLocal(url);
const auto paramsPosition = local.indexOf('?');
if (paramsPosition < 0) {
return std::nullopt;
}
const auto paramsString = local.mid(paramsPosition + 1);
const auto params = qthelp::url_parse_params(
paramsString,
qthelp::UrlParamNameTransform::ToLower);
auto paper = Data::DefaultWallPaper().withUrlParams(params);
return paper.backgroundColors().empty()
? std::nullopt
: std::make_optional(std::move(paper));
}
QSize ThemeDocument::countOptimalSize() {
if (_serviceWidth > 0) {
return { _serviceWidth, _serviceWidth };
}
if (!_data) {
return { st::maxWallPaperWidth, st::maxWallPaperHeight };
} else if (_data->isTheme()) {
return st::historyThemeSize;
}
const auto &location = _data->thumbnailLocation();
auto tw = style::ConvertScale(location.width());
auto th = style::ConvertScale(location.height());
if (!tw || !th) {
tw = th = 1;
}
th = (st::maxWallPaperWidth * th) / tw;
tw = st::maxWallPaperWidth;
const auto maxWidth = tw;
const auto minHeight = std::clamp(
th,
st::minPhotoSize,
st::maxWallPaperHeight);
return { maxWidth, minHeight };
}
QSize ThemeDocument::countCurrentSize(int newWidth) {
if (_serviceWidth) {
_pixw = _pixh = _serviceWidth;
return { _serviceWidth, _serviceWidth };
}
if (!_data) {
_pixw = st::maxWallPaperWidth;
_pixh = st::maxWallPaperHeight;
return { _pixw, _pixh };
} else if (_data->isTheme()) {
_pixw = st::historyThemeSize.width();
_pixh = st::historyThemeSize.height();
return st::historyThemeSize;
}
const auto &location = _data->thumbnailLocation();
auto tw = style::ConvertScale(location.width());
auto th = style::ConvertScale(location.height());
if (!tw || !th) {
tw = th = 1;
}
// We use pix() for image copies, because we rely that backgrounds
// are always displayed with the same dimensions (not pixSingle()).
_pixw = maxWidth();// std::min(newWidth, maxWidth());
_pixh = minHeight();// (_pixw * th / tw);
newWidth = _pixw;
const auto newHeight = _pixh; /*std::clamp(
_pixh,
st::minPhotoSize,
st::maxWallPaperHeight);*/
return { newWidth, newHeight };
}
void ThemeDocument::draw(Painter &p, const PaintContext &context) const {
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;
ensureDataMediaCreated();
if (_data) {
_dataMedia->automaticLoad(_realParent->fullId(), _parent->data());
}
const auto st = context.st;
const auto sti = context.imageStyle();
auto loaded = dataLoaded();
auto displayLoading = _data && _data->displayLoading();
auto paintx = 0, painty = 0, paintw = width(), painth = height();
if (displayLoading) {
ensureAnimation();
if (!_animation->radial.animating()) {
_animation->radial.start(dataProgress());
}
}
const auto radial = isRadialAnimation();
auto rthumb = style::rtlrect(paintx, painty, paintw, painth, width());
validateThumbnail();
p.drawPixmap(rthumb.topLeft(), _thumbnail);
if (context.selected()) {
Ui::FillComplexOverlayRect(
p,
rthumb,
st->msgSelectOverlay(),
st->msgSelectOverlayCorners(Ui::CachedCornerRadius::Small));
}
if (_data) {
if (!_serviceWidth) {
auto statusX = paintx + st::msgDateImgDelta + st::msgDateImgPadding.x();
auto statusY = painty + st::msgDateImgDelta + st::msgDateImgPadding.y();
auto statusW = st::normalFont->width(_statusText) + 2 * st::msgDateImgPadding.x();
auto statusH = st::normalFont->height + 2 * st::msgDateImgPadding.y();
Ui::FillRoundRect(p, style::rtlrect(statusX - st::msgDateImgPadding.x(), statusY - st::msgDateImgPadding.y(), statusW, statusH, width()), sti->msgDateImgBg, sti->msgDateImgBgCorners);
p.setFont(st::normalFont);
p.setPen(st->msgDateImgFg());
p.drawTextLeft(statusX, statusY, width(), _statusText, statusW - 2 * st::msgDateImgPadding.x());
}
if (radial || (!loaded && !_data->loading())) {
const auto radialOpacity = (radial && loaded && !_data->uploading())
? _animation->radial.opacity() :
1.;
const auto innerSize = st::msgFileLayout.thumbSize;
QRect inner(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);
p.setPen(Qt::NoPen);
if (context.selected()) {
p.setBrush(st->msgDateImgBgSelected());
} else if (isThumbAnimation()) {
auto over = _animation->a_thumbOver.value(1.);
p.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over));
} else {
auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _openl);
p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());
}
p.setOpacity(radialOpacity * p.opacity());
{
PainterHighQualityEnabler hq(p);
p.drawEllipse(inner);
}
p.setOpacity(radialOpacity);
const auto &icon = (radial || _data->loading())
? sti->historyFileThumbCancel
: sti->historyFileThumbDownload;
icon.paintInCenter(p, inner);
p.setOpacity(1);
if (radial) {
QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));
_animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg);
}
}
}
}
void ThemeDocument::ensureDataMediaCreated() const {
if (_dataMedia || !_data) {
return;
}
_dataMedia = _data->createMediaView();
if (checkGoodThumbnail()) {
_dataMedia->goodThumbnailWanted();
}
_dataMedia->thumbnailWanted(_realParent->fullId());
_parent->history()->owner().registerHeavyViewPart(_parent);
}
bool ThemeDocument::checkGoodThumbnail() const {
return _data && (!_data->hasThumbnail() || !_data->isPatternWallPaper());
}
void ThemeDocument::validateThumbnail() const {
const auto isDark = Window::Theme::IsNightMode();
if (_isDark != isDark) {
_isDark = isDark;
_thumbnailGood = -1;
}
if (checkGoodThumbnail()) {
if (_thumbnailGood > 0) {
return;
}
ensureDataMediaCreated();
if (const auto good = _dataMedia->goodThumbnail()) {
prepareThumbnailFrom(good, 1);
return;
}
}
if (_thumbnailGood >= 0) {
return;
}
if (!_data) {
generateThumbnail();
return;
}
ensureDataMediaCreated();
if (const auto normal = _dataMedia->thumbnail()) {
prepareThumbnailFrom(normal, 0);
} else if (_thumbnail.isNull()) {
if (const auto blurred = _dataMedia->thumbnailInline()) {
prepareThumbnailFrom(blurred, -1);
}
}
}
QImage ThemeDocument::finishServiceThumbnail(QImage image) const {
if (!_serviceWidth) {
return image;
} else if (_isDark && _dimmingIntensity > 0) {
image.setDevicePixelRatio(style::DevicePixelRatio());
auto p = QPainter(&image);
const auto alpha = 255 * _dimmingIntensity / 100;
p.fillRect(0, 0, _pixw, _pixh, QColor(0, 0, 0, alpha));
}
if (_blurredWallPaper) {
constexpr auto kRadius = 16;
image = Images::BlurLargeImage(std::move(image), kRadius);
}
return Images::Circle(std::move(image));
}
void ThemeDocument::generateThumbnail() const {
auto image = Ui::GenerateBackgroundImage(
QSize(_pixw, _pixh) * style::DevicePixelRatio(),
_background,
_gradientRotation,
_patternOpacity);
_thumbnail = Ui::PixmapFromImage(
finishServiceThumbnail(std::move(image)));
_thumbnail.setDevicePixelRatio(style::DevicePixelRatio());
_thumbnailGood = 1;
}
void ThemeDocument::prepareThumbnailFrom(
not_null<Image*> image,
int good) const {
Expects(_data != nullptr);
Expects(_thumbnailGood <= good);
const auto isTheme = _data->isTheme();
const auto isPattern = _data->isPatternWallPaper();
auto options = (good >= 0 ? Images::Option(0) : Images::Option::Blur)
| (isPattern
? Images::Option::TransparentBackground
: Images::Option(0));
auto original = image->original();
const auto &location = _data->thumbnailLocation();
auto tw = isTheme ? _pixw : style::ConvertScale(location.width());
auto th = isTheme ? _pixh : style::ConvertScale(location.height());
if (!tw || !th) {
tw = th = 1;
}
const auto ratio = style::DevicePixelRatio();
const auto resizeTo = _serviceWidth
? QSize(tw, th).scaled(_pixw, _pixh, Qt::KeepAspectRatioByExpanding)
: QSize(_pixw, (_pixw * th) / tw);
original = Images::Prepare(
std::move(original),
resizeTo * ratio,
{ .options = options, .outer = { _pixw, _pixh } });
if (isPattern) {
original = Ui::PreparePatternImage(
std::move(original),
_background,
_gradientRotation,
_patternOpacity);
original.setDevicePixelRatio(ratio);
}
_thumbnail = Ui::PixmapFromImage(
finishServiceThumbnail(std::move(original)));
_thumbnailGood = good;
}
TextState ThemeDocument::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
return result;
}
auto paintx = 0, painty = 0, paintw = width(), painth = height();
if (QRect(paintx, painty, paintw, painth).contains(point)) {
if (!_data) {
result.link = _openl;
} else if (_data->uploading()) {
result.link = _cancell;
} else if (dataLoaded()) {
result.link = _openl;
} else if (_data->loading()) {
result.link = _cancell;
} else {
result.link = _openl;
}
}
return result;
}
float64 ThemeDocument::dataProgress() const {
ensureDataMediaCreated();
return _data ? _dataMedia->progress() : 1.;
}
bool ThemeDocument::dataFinished() const {
return !_data
|| (!_data->loading()
&& (!_data->uploading() || _data->waitingForAlbum()));
}
bool ThemeDocument::dataLoaded() const {
ensureDataMediaCreated();
return !_data || _dataMedia->loaded();
}
bool ThemeDocument::isReadyForOpen() const {
ensureDataMediaCreated();
return !_data || _dataMedia->loaded();
}
bool ThemeDocument::hasHeavyPart() const {
return (_dataMedia != nullptr);
}
void ThemeDocument::unloadHeavyPart() {
_dataMedia = nullptr;
}
ThemeDocumentBox::ThemeDocumentBox(
not_null<Element*> parent,
const Data::WallPaper &paper)
: _parent(parent)
, _emojiId(paper.emojiId()) {
Window::WallPaperResolved(
&_parent->history()->owner(),
&paper
) | rpl::on_next([=](const Data::WallPaper *paper) {
_parent->repaint();
if (!paper) {
_preview.reset();
} else {
createPreview(*paper);
}
}, _lifetime);
}
void ThemeDocumentBox::createPreview(const Data::WallPaper &paper) {
_preview.emplace(
_parent,
paper.document(),
paper,
st::msgServicePhotoWidth);
_preview->initDimensions();
_preview->resizeGetHeight(_preview->maxWidth());
}
ThemeDocumentBox::~ThemeDocumentBox() = default;
int ThemeDocumentBox::top() {
return st::msgServiceGiftBoxButtonMargins.top();
}
QSize ThemeDocumentBox::size() {
return _preview
? QSize(_preview->maxWidth(), _preview->minHeight())
: QSize(st::msgServicePhotoWidth, st::msgServicePhotoWidth);
}
TextWithEntities ThemeDocumentBox::title() {
return {};
}
TextWithEntities ThemeDocumentBox::subtitle() {
return _parent->data()->notificationText();
}
rpl::producer<QString> ThemeDocumentBox::button() {
if (_parent->data()->out() || _parent->history()->peer->isChannel()) {
return {};
}
return rpl::conditional(
WallPaperRevertableValue(_parent->data()),
tr::lng_action_set_wallpaper_remove(),
tr::lng_action_set_wallpaper_button());
}
ClickHandlerPtr ThemeDocumentBox::createViewLink() {
const auto to = _parent->history()->peer;
if (to->isChannel()) {
return nullptr;
}
const auto out = _parent->data()->out();
const auto media = _parent->data()->media();
const auto weak = base::make_weak(_parent);
const auto paper = media ? media->paper() : nullptr;
const auto maybe = paper ? *paper : std::optional<Data::WallPaper>();
const auto itemId = _parent->data()->fullId();
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
const auto view = weak.get();
if (view
&& !view->data()->out()
&& WallPaperRevertable(view->data())) {
const auto reset = crl::guard(weak, [=](Fn<void()> close) {
const auto api = &controller->session().api();
api->request(MTPmessages_SetChatWallPaper(
MTP_flags(MTPmessages_SetChatWallPaper::Flag::f_revert),
view->data()->history()->peer->input(),
MTPInputWallPaper(),
MTPWallPaperSettings(),
MTPint()
)).done([=](const MTPUpdates &result) {
api->applyUpdates(result);
}).send();
close();
});
controller->show(Ui::MakeConfirmBox({
.text = tr::lng_background_sure_reset_default(),
.confirmed = reset,
.confirmText = tr::lng_background_reset_default(),
}));
} else if (out) {
controller->toggleChooseChatTheme(to);
} else if (maybe) {
controller->show(Box<BackgroundPreviewBox>(
controller,
*maybe,
BackgroundPreviewArgs{ to, itemId }));
}
}
});
}
void ThemeDocumentBox::draw(
Painter &p,
const PaintContext &context,
const QRect &geometry) {
if (_preview) {
p.translate(geometry.topLeft());
_preview->draw(p, context);
p.translate(-geometry.topLeft());
}
}
void ThemeDocumentBox::stickerClearLoopPlayed() {
}
std::unique_ptr<StickerPlayer> ThemeDocumentBox::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return nullptr;
}
bool ThemeDocumentBox::hasHeavyPart() {
return _preview && _preview->hasHeavyPart();
}
void ThemeDocumentBox::unloadHeavyPart() {
if (_preview) {
_preview->unloadHeavyPart();
}
}
GiftServiceBox::GiftServiceBox(
not_null<Element*> parent,
not_null<Data::MediaGiftBox*> gift)
: _parent(parent)
, _data(*gift->gift()) {
}
GiftServiceBox::~GiftServiceBox() = default;
int GiftServiceBox::top() {
return st::msgServiceStarGiftStickerTop;
}
int GiftServiceBox::width() {
return st::msgServiceStarGiftBoxWidth;
}
QSize GiftServiceBox::size() {
return QSize(
st::msgServiceGiftThemeStickerSize,
st::msgServiceGiftThemeStickerSize).grownBy(
st::msgServiceGiftThemeStickerPadding);
}
TextWithEntities GiftServiceBox::title() {
return {};
}
TextWithEntities GiftServiceBox::subtitle() {
const auto giftName = tr::bold(Data::UniqueGiftName(*_data.unique));
if (_data.type == Data::GiftType::GiftOffer) {
const auto item = _parent->data();
if (const auto suggestion = item->Get<HistoryMessageSuggestion>()) {
const auto amount = suggestion->price;
const auto cost = tr::bold(PrepareCreditsAmountText(amount));
auto text = tr::marked();
if (_parent->data()->out()) {
text.append(tr::lng_action_gift_offer_you(
tr::now,
lt_cost,
cost,
lt_name,
giftName,
tr::marked));
} else {
text.append(tr::lng_action_gift_offer(
tr::now,
lt_user,
tr::bold(_parent->data()->from()->shortName()),
lt_cost,
cost,
lt_name,
giftName,
tr::marked));
}
text.append(u"\n\n"_q);
const auto ends = suggestion->date;
const auto now = base::unixtime::now();
const auto expired = (now >= ends);
checkKeyboardRemoval(suggestion, expired);
if (suggestion->accepted) {
text.append(
tr::lng_action_gift_offer_state_accepted(tr::now));
} else if (suggestion->rejected) {
text.append(
tr::lng_action_gift_offer_state_rejected(tr::now));
} else {
if (expired) {
text.append(
tr::lng_action_gift_offer_state_expired(tr::now));
} else {
auto time = QString();
const auto left = (ends - now) + 59;
if (left >= 3600) {
const auto hours = left / 3600;
const auto minutes = (left % 3600) / 60;
time = minutes
? tr::lng_action_gift_offer_time_medium(
tr::now,
lt_hours,
QString::number(hours),
lt_minutes,
QString::number(minutes))
: tr::lng_action_gift_offer_time_large(
tr::now,
lt_hours,
QString::number(hours));
} else {
const auto minutes = left / 60;
time = tr::lng_action_gift_offer_time_small(
tr::now,
lt_minutes,
QString::number(minutes));
}
text.append(tr::lng_action_gift_offer_state_expires(
tr::now,
lt_time,
tr::bold(time),
tr::marked));
const auto tillNext = left % 60;
_changeTimer.setCallback([=] { _changes.fire({}); });
_changeTimer.callOnce((tillNext ? tillNext : 60)
* crl::time(1000));
}
}
return text;
}
} else if (_parent->data()->out()) {
return tr::lng_action_you_gift_theme_changed(
tr::now,
lt_name,
giftName,
tr::marked);
} else {
return tr::lng_action_gift_theme_changed(
tr::now,
lt_from,
tr::bold(_parent->data()->from()->shortName()),
lt_name,
giftName,
tr::marked);
}
return _parent->data()->originalText();
}
void GiftServiceBox::checkKeyboardRemoval(
not_null<const HistoryMessageSuggestion*> suggestion,
bool expired) {
Expects(_data.type == Data::GiftType::GiftOffer);
const auto item = _parent->data();
if (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {
if (!markup->data.isTrivial()) {
if (suggestion->accepted || suggestion->rejected || expired) {
crl::on_main(this, [=] {
clearKeyboard();
});
}
}
}
}
void GiftServiceBox::clearKeyboard() {
Expects(_data.type == Data::GiftType::GiftOffer);
const auto item = _parent->data();
if (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {
markup->updateSuggestControls(SuggestionActions::None);
item->history()->owner().requestItemResize(item);
}
}
rpl::producer<> GiftServiceBox::changes() {
if (_data.type != Data::GiftType::GiftOffer) {
return nullptr;
}
return _changes.events();
}
rpl::producer<QString> GiftServiceBox::button() {
if (_data.type == Data::GiftType::GiftOffer) {
return nullptr;
}
return tr::lng_sticker_premium_view();
}
ClickHandlerPtr GiftServiceBox::createViewLink() {
return std::make_shared<UrlClickHandler>(
u"tg://nft?slug="_q + _data.unique->slug);
}
int GiftServiceBox::buttonSkip() {
return st::msgServiceGiftBoxButtonMargins.top();
}
void GiftServiceBox::cacheUniqueBackground(int width, int height) {
if (!_patternEmoji) {
const auto session = &_parent->data()->history()->session();
_patternEmoji = session->data().customEmojiManager().create(
_data.unique->pattern.document,
[=] { _parent->repaint(); },
Data::CustomEmojiSizeTag::Large);
[[maybe_unused]] const auto preload = _patternEmoji->ready();
}
const auto inner = QRect(0, 0, width, height);
const auto ratio = style::DevicePixelRatio();
if (_backgroundCache.size() != inner.size() * ratio) {
_backgroundCache = QImage(
inner.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
_backgroundCache.fill(Qt::transparent);
_backgroundCache.setDevicePixelRatio(ratio);
const auto radius = st::giftBoxGiftRadius;
auto p = QPainter(&_backgroundCache);
auto hq = PainterHighQualityEnabler(p);
auto gradient = QRadialGradient(inner.center(), inner.width() / 2);
gradient.setStops({
{ 0., _data.unique->backdrop.centerColor },
{ 1., _data.unique->backdrop.edgeColor },
});
p.setBrush(gradient);
p.setPen(Qt::NoPen);
p.drawRoundedRect(inner, radius, radius);
_backroundPatterned = false;
}
if (!_backroundPatterned && _patternEmoji->ready()) {
_backroundPatterned = true;
auto p = QPainter(&_backgroundCache);
p.setClipRect(inner);
const auto skip = inner.width() / 3;
Ui::PaintBgPoints(
p,
Ui::PatternBgPointsSmall(),
_patternCache,
_patternEmoji.get(),
*_data.unique,
QRect(-skip, 0, inner.width() + 2 * skip, inner.height()));
}
}
void GiftServiceBox::draw(
Painter &p,
const PaintContext &context,
const QRect &geometry) {
cacheUniqueBackground(geometry.width(), geometry.height());
p.drawImage(geometry.topLeft(), _backgroundCache);
if (_sticker) {
_sticker->draw(
p,
context,
geometry.marginsRemoved(st::msgServiceGiftThemeStickerPadding));
} else {
ensureStickerCreated();
}
}
bool GiftServiceBox::hideServiceText() {
return true;
}
void GiftServiceBox::stickerClearLoopPlayed() {
if (_sticker) {
_sticker->stickerClearLoopPlayed();
}
}
std::unique_ptr<StickerPlayer> GiftServiceBox::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return _sticker
? _sticker->stickerTakePlayer(data, replacements)
: nullptr;
}
bool GiftServiceBox::hasHeavyPart() {
return (_sticker ? _sticker->hasHeavyPart() : false);
}
void GiftServiceBox::unloadHeavyPart() {
if (_sticker) {
_sticker->unloadHeavyPart();
}
}
void GiftServiceBox::ensureStickerCreated() const {
if (_sticker) {
return;
}
const auto document = _data.unique->model.document;
const auto sticker = document->sticker();
Assert(sticker != nullptr);
_sticker.emplace(_parent, document, false, _parent);
_sticker->setPlayingOnce(true);
_sticker->initSize(st::msgServiceGiftThemeStickerSize);
_parent->repaint();
}
} // namespace HistoryView

View File

@@ -0,0 +1,188 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_file.h"
#include "history/view/media/history_view_service_box.h"
#include "history/view/media/history_view_sticker.h"
class Image;
struct HistoryMessageSuggestion;
namespace Data {
class DocumentMedia;
class WallPaper;
class MediaGiftBox;
struct GiftCode;
} // namespace Data
namespace HistoryView {
class ThemeDocument final : public File {
public:
ThemeDocument(not_null<Element*> parent, DocumentData *document);
ThemeDocument(
not_null<Element*> parent,
DocumentData *document,
const std::optional<Data::WallPaper> &params,
int serviceWidth = 0);
~ThemeDocument();
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
DocumentData *getDocument() const override {
return _data;
}
bool needsBubble() const override {
return false;
}
bool customInfoLayout() const override {
return false;
}
bool skipBubbleTail() const override {
return true;
}
bool isReadyForOpen() const override;
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
[[nodiscard]] static std::optional<Data::WallPaper> ParamsFromUrl(
const QString &url);
protected:
float64 dataProgress() const override;
bool dataFinished() const override;
bool dataLoaded() const override;
private:
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
[[nodiscard]] bool checkGoodThumbnail() const;
void validateThumbnail() const;
void prepareThumbnailFrom(not_null<Image*> image, int good) const;
void generateThumbnail() const;
void ensureDataMediaCreated() const;
[[nodiscard]] QImage finishServiceThumbnail(QImage image) const;
DocumentData *_data = nullptr;
int _pixw = 1;
int _pixh = 1;
const int _serviceWidth = 0;
mutable QPixmap _thumbnail;
mutable int _thumbnailGood = -1; // -1 inline, 0 thumbnail, 1 good
mutable std::shared_ptr<Data::DocumentMedia> _dataMedia;
// For wallpaper documents.
std::vector<QColor> _background;
float64 _patternOpacity = 0.;
int _gradientRotation = 0;
mutable bool _isDark = false;
int _dimmingIntensity = 0;
bool _blurredWallPaper = false;
};
class ThemeDocumentBox final : public ServiceBoxContent {
public:
ThemeDocumentBox(
not_null<Element*> parent,
const Data::WallPaper &paper);
~ThemeDocumentBox();
int top() override;
QSize size() override;
TextWithEntities title() override;
TextWithEntities subtitle() override;
rpl::producer<QString> button() override;
void draw(
Painter &p,
const PaintContext &context,
const QRect &geometry) override;
ClickHandlerPtr createViewLink() override;
bool hideServiceText() override {
return true;
}
void stickerClearLoopPlayed() override;
std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) override;
bool hasHeavyPart() override;
void unloadHeavyPart() override;
private:
void createPreview(const Data::WallPaper &paper);
const not_null<Element*> _parent;
QString _emojiId;
std::optional<ThemeDocument> _preview;
rpl::lifetime _lifetime;
};
class GiftServiceBox final
: public ServiceBoxContent
, public base::has_weak_ptr {
public:
GiftServiceBox(
not_null<Element*> parent,
not_null<Data::MediaGiftBox*> gift);
~GiftServiceBox();
int top() override;
int width() override;
QSize size() override;
TextWithEntities title() override;
TextWithEntities subtitle() override;
rpl::producer<QString> button() override;
int buttonSkip() override;
void draw(
Painter &p,
const PaintContext &context,
const QRect &geometry) override;
ClickHandlerPtr createViewLink() override;
rpl::producer<> changes() override;
bool hideServiceText() override;
void stickerClearLoopPlayed() override;
std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) override;
bool hasHeavyPart() override;
void unloadHeavyPart() override;
private:
void ensureStickerCreated() const;
void cacheUniqueBackground(int width, int height);
void checkKeyboardRemoval(
not_null<const HistoryMessageSuggestion*> suggestion,
bool expired);
void clearKeyboard();
const not_null<Element*> _parent;
const Data::GiftCode &_data;
std::unique_ptr<Ui::Text::CustomEmoji> _patternEmoji;
QImage _backgroundCache;
base::flat_map<float64, QImage> _patternCache;
bool _backroundPatterned = false;
mutable std::optional<Sticker> _sticker;
rpl::event_stream<> _changes;
base::Timer _changeTimer;
};
} // namespace HistoryView

View File

@@ -0,0 +1,922 @@
/*
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/media/history_view_todo_list.h"
#include "base/unixtime.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "core/ui_integration.h" // TextContext
#include "lang/lang_keys.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_text_helper.h"
#include "calls/calls_instance.h"
#include "ui/chat/message_bubble.h"
#include "ui/chat/chat_style.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/text/format_values.h"
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
#include "ui/effects/ripple_animation.h"
#include "ui/effects/fireworks_animation.h"
#include "ui/toast/toast.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "data/data_media_types.h"
#include "data/data_poll.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "base/unixtime.h"
#include "base/timer.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "api/api_todo_lists.h"
#include "window/window_peer_menu.h"
#include "styles/style_chat.h"
#include "styles/style_widgets.h"
#include "styles/style_window.h"
namespace HistoryView {
namespace {
constexpr auto kShowRecentVotersCount = 3;
constexpr auto kRotateSegments = 8;
constexpr auto kRotateAmplitude = 3.;
constexpr auto kScaleSegments = 2;
constexpr auto kScaleAmplitude = 0.03;
constexpr auto kLargestRadialDuration = 30 * crl::time(1000);
constexpr auto kCriticalCloseDuration = 5 * crl::time(1000);
} // namespace
struct TodoList::Task {
Task();
void fillData(
not_null<Element*> view,
not_null<TodoListData*> todolist,
const TodoListItem &original,
Ui::Text::MarkedContext context);
void setCompletedBy(PeerData *by);
Ui::Text::String text;
Ui::Text::String name;
PeerData *completedBy = nullptr;
mutable Ui::PeerUserpicView userpic;
TimeId completionDate = 0;
int id = 0;
ClickHandlerPtr handler;
Ui::Animations::Simple selectedAnimation;
mutable std::unique_ptr<Ui::RippleAnimation> ripple;
};
TodoList::Task::Task()
: text(st::msgMinWidth / 2)
, name(st::msgMinWidth / 2) {
}
void TodoList::Task::fillData(
not_null<Element*> view,
not_null<TodoListData*> todolist,
const TodoListItem &original,
Ui::Text::MarkedContext context) {
id = original.id;
setCompletedBy(original.completedBy);
completionDate = original.completionDate;
if (!text.isEmpty() && text.toTextWithEntities() == original.text) {
return;
}
text.setMarkedText(
st::historyPollAnswerStyle,
original.text,
Ui::WebpageTextTitleOptions(),
context);
InitElementTextPart(view, text);
}
void TodoList::Task::setCompletedBy(PeerData *by) {
if (!by || completedBy == by) {
return;
}
completedBy = by;
name.setText(st::historyPollAnswerStyle, completedBy->name());
}
TodoList::TodoList(
not_null<Element*> parent,
not_null<TodoListData*> todolist,
Element *replacing)
: Media(parent)
, _todolist(todolist)
, _title(st::msgMinWidth / 2) {
history()->owner().registerTodoListView(_todolist, _parent);
if (const auto media = replacing ? replacing->media() : nullptr) {
const auto info = media->takeTasksInfo();
if (!info.empty()) {
setupPreviousState(info);
}
}
}
void TodoList::setupPreviousState(const std::vector<TodoTaskInfo> &info) {
// If we restore state from the view we're replacing we'll be able to
// animate the changes properly.
updateTasks(true);
for (auto &task : _tasks) {
const auto i = ranges::find(info, task.id, &TodoTaskInfo::id);
if (i != end(info)) {
task.setCompletedBy(i->completedBy);
task.completionDate = i->completionDate;
}
}
}
QSize TodoList::countOptimalSize() {
updateTexts();
const auto paddings = st::msgPadding.left() + st::msgPadding.right();
auto maxWidth = st::msgFileMinWidth;
accumulate_max(maxWidth, paddings + _title.maxWidth());
for (const auto &task : _tasks) {
accumulate_max(
maxWidth,
paddings
+ st::historyChecklistTaskPadding.left()
+ task.text.maxWidth()
+ st::historyChecklistTaskPadding.right());
}
const auto tasksHeight = ranges::accumulate(ranges::views::all(
_tasks
) | ranges::views::transform([](const Task &task) {
return st::historyChecklistTaskPadding.top()
+ task.text.minHeight()
+ st::historyChecklistTaskPadding.bottom();
}), 0);
const auto bottomButtonHeight = st::historyPollBottomButtonSkip;
auto minHeight = st::historyPollQuestionTop
+ _title.minHeight()
+ st::historyPollSubtitleSkip
+ st::msgDateFont->height
+ st::historyPollAnswersSkip
+ tasksHeight
+ st::historyPollTotalVotesSkip
+ bottomButtonHeight
+ st::msgDateFont->height
+ st::msgPadding.bottom();
if (!isBubbleTop()) {
minHeight -= st::msgFileTopMinus;
}
return { maxWidth, minHeight };
}
bool TodoList::canComplete() const {
return (_parent->data()->out()
|| _parent->history()->peer->isSelf()
|| _todolist->othersCanComplete())
&& _parent->data()->isRegular()
&& !_parent->data()->Has<HistoryMessageForwarded>();
}
int TodoList::countTaskTop(
const Task &task,
int innerWidth) const {
auto tshift = st::historyPollQuestionTop;
if (!isBubbleTop()) {
tshift -= st::msgFileTopMinus;
}
tshift += _title.countHeight(innerWidth) + st::historyPollSubtitleSkip;
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
const auto i = ranges::find(
_tasks,
&task,
[](const Task &task) { return &task; });
const auto countHeight = [&](const Task &task) {
return countTaskHeight(task, innerWidth);
};
tshift += ranges::accumulate(
begin(_tasks),
i,
0,
ranges::plus(),
countHeight);
return tshift;
}
int TodoList::countTaskHeight(
const Task &task,
int innerWidth) const {
const auto answerWidth = innerWidth
- st::historyChecklistTaskPadding.left()
- st::historyChecklistTaskPadding.right();
return st::historyChecklistTaskPadding.top()
+ task.text.countHeight(answerWidth)
+ st::historyChecklistTaskPadding.bottom();
}
QSize TodoList::countCurrentSize(int newWidth) {
accumulate_min(newWidth, maxWidth());
const auto innerWidth = newWidth
- st::msgPadding.left()
- st::msgPadding.right();
const auto tasksHeight = ranges::accumulate(ranges::views::all(
_tasks
) | ranges::views::transform([&](const Task &task) {
return countTaskHeight(task, innerWidth);
}), 0);
const auto bottomButtonHeight = st::historyPollBottomButtonSkip;
auto newHeight = st::historyPollQuestionTop
+ _title.countHeight(innerWidth)
+ st::historyPollSubtitleSkip
+ st::msgDateFont->height
+ st::historyPollAnswersSkip
+ tasksHeight
+ st::historyPollTotalVotesSkip
+ bottomButtonHeight
+ st::msgDateFont->height
+ st::msgPadding.bottom();
if (!isBubbleTop()) {
newHeight -= st::msgFileTopMinus;
}
return { newWidth, newHeight };
}
void TodoList::updateTexts() {
if (_todoListVersion == _todolist->version) {
return;
}
const auto skipAnimations = _tasks.empty();
_todoListVersion = _todolist->version;
if (_title.toTextWithEntities() != _todolist->title) {
auto options = Ui::WebpageTextTitleOptions();
options.maxw = options.maxh = 0;
_title.setMarkedText(
st::historyPollQuestionStyle,
_todolist->title,
options,
Core::TextContext({
.session = &_todolist->session(),
.repaint = [=] { repaint(); },
.customEmojiLoopLimit = 2,
}));
InitElementTextPart(_parent, _title);
}
if (_flags != _todolist->flags() || _subtitle.isEmpty()) {
_flags = _todolist->flags();
_subtitle.setText(
st::msgDateTextStyle,
(!_todolist->othersCanComplete()
? tr::lng_todo_title(tr::now)
: _parent->data()->history()->peer->isUser()
? tr::lng_todo_title_user(tr::now)
: tr::lng_todo_title_group(tr::now)));
}
updateTasks(skipAnimations);
}
void TodoList::updateTasks(bool skipAnimations) {
const auto context = Core::TextContext({
.session = &_todolist->session(),
.repaint = [=] { repaint(); },
.customEmojiLoopLimit = 2,
});
const auto changed = !ranges::equal(
_tasks,
_todolist->items,
ranges::equal_to(),
&Task::id,
&TodoListItem::id);
if (!changed) {
auto animated = false;
auto &&tasks = ranges::views::zip(_tasks, _todolist->items);
for (auto &&[task, original] : tasks) {
const auto wasDate = task.completionDate;
task.fillData(_parent, _todolist, original, context);
if (!skipAnimations && (!wasDate != !task.completionDate)) {
startToggleAnimation(task);
animated = true;
}
}
updateCompletionStatus();
if (animated) {
maybeStartFireworks();
}
return;
}
const auto has = hasHeavyPart();
_tasks = ranges::views::all(
_todolist->items
) | ranges::views::transform([&](const TodoListItem &item) {
auto result = Task();
result.id = item.id;
result.fillData(_parent, _todolist, item, context);
return result;
}) | ranges::to_vector;
for (auto &task : _tasks) {
task.handler = createTaskClickHandler(task);
}
updateCompletionStatus();
if (has && !hasHeavyPart()) {
_parent->checkHeavyPart();
}
}
ClickHandlerPtr TodoList::createTaskClickHandler(
const Task &task) {
const auto id = task.id;
auto result = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
toggleCompletion(id);
}));
result->setProperty(kTodoListItemIdProperty, id);
return result;
}
void TodoList::startToggleAnimation(Task &task) {
const auto selected = (task.completionDate != 0);
task.selectedAnimation.start(
[=] { repaint(); },
selected ? 0. : 1.,
selected ? 1. : 0.,
st::defaultCheck.duration);
}
void TodoList::toggleCompletion(int id) {
if (_parent->data()->isBusinessShortcut()) {
return;
} else if (_parent->data()->Has<HistoryMessageForwarded>()) {
_parent->delegate()->elementShowTooltip(
tr::lng_todo_mark_forwarded(tr::now, tr::rich),
[] {});
return;
} else if (!canComplete()) {
_parent->delegate()->elementShowTooltip(
tr::lng_todo_mark_restricted(
tr::now,
lt_user,
tr::bold(_parent->data()->from()->shortName()),
tr::rich), [] {});
return;
} else if (!_parent->history()->session().premium()) {
Window::PeerMenuTodoWantsPremium(Window::TodoWantsPremium::Mark);
return;
}
const auto i = ranges::find(
_tasks,
id,
&Task::id);
if (i == end(_tasks)) {
return;
}
const auto selected = (i->completionDate != 0);
i->completionDate = selected ? TimeId() : base::unixtime::now();
if (!selected) {
i->setCompletedBy(_parent->history()->session().user());
}
const auto parentMedia = _parent->data()->media();
const auto baseList = parentMedia ? parentMedia->todolist() : nullptr;
if (baseList) {
const auto j = ranges::find(baseList->items, id, &TodoListItem::id);
if (j != end(baseList->items)) {
j->completionDate = i->completionDate;
j->completedBy = i->completedBy;
}
history()->owner().updateDependentMessages(_parent->data());
}
startToggleAnimation(*i);
repaint();
history()->session().api().todoLists().toggleCompletion(
_parent->data()->fullId(),
id,
!selected);
maybeStartFireworks();
}
void TodoList::maybeStartFireworks() {
if (!ranges::contains(_tasks, TimeId(), &Task::completionDate)
&& !_fireworksAnimation) {
_fireworksAnimation = std::make_unique<Ui::FireworksAnimation>(
[=] { repaint(); });
}
}
void TodoList::updateCompletionStatus() {
const auto incompleted = int(ranges::count(
_todolist->items,
nullptr,
&TodoListItem::completedBy));
const auto total = int(_todolist->items.size());
if (_total == total
&& _incompleted == incompleted
&& !_completionStatusLabel.isEmpty()) {
return;
}
_total = total;
_incompleted = incompleted;
const auto totalText = QString::number(total);
const auto string = (incompleted == total)
? tr::lng_todo_completed_none(tr::now, lt_total, totalText)
: tr::lng_todo_completed(
tr::now,
lt_count,
total - incompleted,
lt_total,
totalText);
_completionStatusLabel.setText(st::msgDateTextStyle, string);
}
void TodoList::draw(Painter &p, const PaintContext &context) const {
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;
auto paintw = width();
const auto stm = context.messageStyle();
const auto padding = st::msgPadding;
auto tshift = st::historyPollQuestionTop;
if (!isBubbleTop()) {
tshift -= st::msgFileTopMinus;
}
paintw -= padding.left() + padding.right();
p.setPen(stm->historyTextFg);
_title.draw(p, {
.position = { padding.left(), tshift },
.availableWidth = paintw,
.palette = &stm->textPalette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.selection = context.selection,
});
tshift += _title.countHeight(paintw) + st::historyPollSubtitleSkip;
p.setPen(stm->msgDateFg);
_subtitle.drawLeftElided(p, padding.left(), tshift, paintw, width());
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
auto heavy = false;
auto created = false;
auto &&tasks = ranges::views::zip(
_tasks,
ranges::views::ints(0, int(_tasks.size())));
for (const auto &[task, index] : tasks) {
const auto was = !task.userpic.null();
const auto height = paintTask(
p,
task,
padding.left(),
tshift,
paintw,
width(),
context);
appendTaskHighlight(task.id, tshift, height, context);
if (was) {
heavy = true;
} else if (!task.userpic.null()) {
created = true;
}
tshift += height;
}
if (!heavy && created) {
history()->owner().registerHeavyViewPart(_parent);
}
paintBottom(p, padding.left(), tshift, paintw, context);
}
void TodoList::paintBottom(
Painter &p,
int left,
int top,
int paintw,
const PaintContext &context) const {
const auto stringtop = top
+ st::msgPadding.bottom()
+ st::historyChecklistBottomTop;
const auto stm = context.messageStyle();
p.setPen(stm->msgDateFg);
_completionStatusLabel.draw(p, left, stringtop, paintw, style::al_top);
}
void TodoList::radialAnimationCallback() const {
if (!anim::Disabled()) {
repaint();
}
}
int TodoList::paintTask(
Painter &p,
const Task &task,
int left,
int top,
int width,
int outerWidth,
const PaintContext &context) const {
const auto height = countTaskHeight(task, width);
const auto stm = context.messageStyle();
const auto aleft = left + st::historyChecklistTaskPadding.left();
const auto awidth = width
- st::historyChecklistTaskPadding.left()
- st::historyChecklistTaskPadding.right();
if (task.ripple) {
p.setOpacity(st::historyPollRippleOpacity);
task.ripple->paint(
p,
left - st::msgPadding.left(),
top,
outerWidth,
&stm->msgWaveformInactive->c);
if (task.ripple->empty()) {
task.ripple.reset();
}
p.setOpacity(1.);
}
if (canComplete()) {
paintRadio(p, task, left, top, context);
} else {
paintStatus(p, task, left, top, context);
}
top += task.completionDate
? st::historyChecklistCheckedTop
: st::historyChecklistTaskPadding.top();
p.setPen(stm->historyTextFg);
task.text.draw(p, {
.position = { aleft, top },
.availableWidth = awidth,
.palette = &stm->textPalette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
});
if (task.completionDate) {
const auto nameTop = top
+ height
- st::historyChecklistTaskPadding.bottom()
+ st::historyChecklistCheckedTop
- st::normalFont->height;
p.setPen(stm->msgDateFg);
task.name.drawLeft(p, aleft, nameTop, awidth, outerWidth);
}
return height;
}
void TodoList::appendTaskHighlight(
int id,
int top,
int height,
const PaintContext &context) const {
if (context.highlight.todoItemId != id
|| context.highlight.collapsion <= 0.) {
return;
}
const auto to = context.highlightInterpolateTo;
const auto toProgress = (1. - context.highlight.collapsion);
if (toProgress >= 1.) {
context.highlightPathCache->addRect(to);
} else if (toProgress <= 0.) {
context.highlightPathCache->addRect(0, top, width(), height);
} else {
const auto lerp = [=](int from, int to) {
return from + (to - from) * toProgress;
};
context.highlightPathCache->addRect(
lerp(0, to.x()),
lerp(top, to.y()),
lerp(width(), to.width()),
lerp(height, to.height()));
}
}
void TodoList::paintRadio(
Painter &p,
const Task &task,
int left,
int top,
const PaintContext &context) const {
top += st::historyChecklistTaskPadding.top();
const auto stm = context.messageStyle();
PainterHighQualityEnabler hq(p);
const auto &radio = st::historyPollRadio;
const auto over = ClickHandler::showAsActive(task.handler);
const auto &regular = stm->msgDateFg;
const auto checkmark = task.selectedAnimation.value(
task.completionDate ? 1. : 0.);
const auto o = p.opacity();
if (checkmark < 1.) {
p.setBrush(Qt::NoBrush);
p.setOpacity(o * (over ? st::historyPollRadioOpacityOver : st::historyPollRadioOpacity));
}
const auto rect = QRectF(left, top, radio.diameter, radio.diameter).marginsRemoved(QMarginsF(radio.thickness / 2., radio.thickness / 2., radio.thickness / 2., radio.thickness / 2.));
if (checkmark > 0. && task.completedBy) {
const auto skip = st::lineWidth;
const auto userpic = QRect(
left + (radio.diameter / 2) + skip,
top + skip,
radio.diameter - 2 * skip,
radio.diameter - 2 * skip);
if (checkmark < 1.) {
p.save();
p.setOpacity(checkmark);
p.translate(QRectF(userpic).center());
const auto ratio = 0.4 + 0.6 * checkmark;
p.scale(ratio, ratio);
p.translate(-QRectF(userpic).center());
}
task.completedBy->paintUserpic(
p,
task.userpic,
userpic.left(),
userpic.top(),
userpic.width());
if (checkmark < 1.) {
p.restore();
}
}
if (checkmark < 1.) {
auto pen = regular->p;
pen.setWidth(radio.thickness);
p.setPen(pen);
p.drawEllipse(rect);
}
if (checkmark > 0.) {
const auto removeFull = (radio.diameter / 2 - radio.thickness);
const auto removeNow = removeFull * (1. - checkmark);
const auto color = stm->msgFileThumbLinkFg;
auto pen = color->p;
pen.setWidth(radio.thickness);
p.setPen(pen);
p.setBrush(color);
p.drawEllipse(rect.marginsRemoved({ removeNow, removeNow, removeNow, removeNow }));
const auto &icon = stm->historyPollChosen;
icon.paint(p, left + (radio.diameter - icon.width()) / 2, top + (radio.diameter - icon.height()) / 2, width());
const auto stm = context.messageStyle();
auto bgpen = stm->msgBg->p;
bgpen.setWidth(st::lineWidth);
const auto outline = QRect(left, top, radio.diameter, radio.diameter);
const auto paintContent = [&](QPainter &p) {
p.setPen(bgpen);
p.setBrush(Qt::NoBrush);
PainterHighQualityEnabler hq(p);
p.drawEllipse(outline);
};
if (usesBubblePattern(context)) {
const auto add = st::lineWidth * 3;
const auto target = outline.marginsAdded(
{ add, add, add, add });
Ui::PaintPatternBubblePart(
p,
context.viewport,
context.bubblesPattern->pixmap,
target,
paintContent,
_userpicCircleCache);
} else {
paintContent(p);
}
}
p.setOpacity(o);
}
void TodoList::paintStatus(
Painter &p,
const Task &task,
int left,
int top,
const PaintContext &context) const {
top += st::historyChecklistTaskPadding.top();
const auto stm = context.messageStyle();
const auto &radio = st::historyPollRadio;
const auto completed = (task.completionDate != 0);
const auto rect = QRect(left, top, radio.diameter, radio.diameter);
if (completed) {
const auto &icon = stm->historyPollChosen;
icon.paint(
p,
left + (radio.diameter - icon.width()) / 2,
top + (radio.diameter - icon.height()) / 2,
width(),
stm->msgFileBg->c);
} else {
p.setPen(Qt::NoPen);
p.setBrush(stm->msgFileBg);
PainterHighQualityEnabler hq(p);
p.drawEllipse(style::centerrect(
rect,
QRect(0, 0, st::mediaUnreadSize, st::mediaUnreadSize)));
}
}
TextSelection TodoList::adjustSelection(
TextSelection selection,
TextSelectType type) const {
return _title.adjustSelection(selection, type);
}
uint16 TodoList::fullSelectionLength() const {
return _title.length();
}
TextForMimeData TodoList::selectedText(TextSelection selection) const {
return _title.toTextForMimeData(selection);
}
TextState TodoList::textState(QPoint point, StateRequest request) const {
auto result = TextState(_parent);
const auto padding = st::msgPadding;
auto paintw = width();
auto tshift = st::historyPollQuestionTop;
if (!isBubbleTop()) {
tshift -= st::msgFileTopMinus;
}
paintw -= padding.left() + padding.right();
const auto questionH = _title.countHeight(paintw);
if (QRect(padding.left(), tshift, paintw, questionH).contains(point)) {
result = TextState(_parent, _title.getState(
point - QPoint(padding.left(), tshift),
paintw,
request.forText()));
return result;
}
const auto aleft = padding.left()
+ st::historyChecklistTaskPadding.left();
const auto awidth = paintw
- st::historyChecklistTaskPadding.left()
- st::historyChecklistTaskPadding.right();
tshift += questionH + st::historyPollSubtitleSkip;
tshift += st::msgDateFont->height + st::historyPollAnswersSkip;
for (const auto &task : _tasks) {
const auto height = countTaskHeight(task, paintw);
if (point.y() >= tshift && point.y() < tshift + height) {
const auto atop = tshift
+ (task.completionDate
? st::historyChecklistCheckedTop
: st::historyChecklistTaskPadding.top());
auto taskTextResult = task.text.getState(
point - QPoint(aleft, atop),
awidth,
request.forText());
if (taskTextResult.link) {
result.link = taskTextResult.link;
} else {
_lastLinkPoint = point;
result.link = task.handler;
}
if (task.completionDate) {
result.customTooltip = true;
using Flag = Ui::Text::StateRequest::Flag;
if (request.flags & Flag::LookupCustomTooltip) {
result.customTooltipText = langDateTimeFull(
base::unixtime::parse(task.completionDate));
}
}
return result;
}
tshift += height;
}
return result;
}
void TodoList::paintBubbleFireworks(
Painter &p,
const QRect &bubble,
crl::time ms) const {
if (!_fireworksAnimation || _fireworksAnimation->paint(p, bubble)) {
return;
}
_fireworksAnimation = nullptr;
}
void TodoList::clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) {
if (!handler) return;
const auto i = ranges::find(
_tasks,
handler,
&Task::handler);
if (i != end(_tasks)) {
toggleRipple(*i, pressed);
}
}
void TodoList::unloadHeavyPart() {
for (auto &task : _tasks) {
task.userpic = {};
}
}
bool TodoList::hasHeavyPart() const {
for (auto &task : _tasks) {
if (!task.userpic.null()) {
return true;
}
}
return false;
}
void TodoList::hideSpoilers() {
if (_title.hasSpoilers()) {
_title.setSpoilerRevealed(false, anim::type::instant);
}
for (auto &task : _tasks) {
if (task.text.hasSpoilers()) {
task.text.setSpoilerRevealed(false, anim::type::instant);
}
}
}
std::vector<Media::TodoTaskInfo> TodoList::takeTasksInfo() {
if (_tasks.empty()) {
return {};
}
return _tasks | ranges::views::transform([](const Task &task) {
return TodoTaskInfo{
.id = task.id,
.completedBy = task.completedBy,
.completionDate = task.completionDate,
};
}) | ranges::to_vector;
}
void TodoList::toggleRipple(Task &task, bool pressed) {
if (pressed) {
const auto outerWidth = width();
const auto innerWidth = outerWidth
- st::msgPadding.left()
- st::msgPadding.right();
if (!task.ripple) {
auto mask = Ui::RippleAnimation::RectMask(QSize(
outerWidth,
countTaskHeight(task, innerWidth)));
task.ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
[=] { repaint(); });
}
const auto top = countTaskTop(task, innerWidth);
task.ripple->add(_lastLinkPoint - QPoint(0, top));
} else if (task.ripple) {
task.ripple->lastStop();
}
}
int TodoList::bottomButtonHeight() const {
const auto skip = st::historyPollChoiceRight.height()
- st::historyPollFillingBottom
- st::historyPollFillingHeight
- (st::historyPollChoiceRight.height() - st::historyPollFillingHeight) / 2;
return st::historyPollTotalVotesSkip
- skip
+ st::historyPollBottomButtonSkip
+ st::msgDateFont->height
+ st::msgPadding.bottom();
}
TodoList::~TodoList() {
history()->owner().unregisterTodoListView(_todolist, _parent);
if (hasHeavyPart()) {
unloadHeavyPart();
_parent->checkHeavyPart();
}
}
} // namespace HistoryView

View File

@@ -0,0 +1,152 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
#include "ui/effects/animations.h"
#include "data/data_todo_list.h"
#include "base/weak_ptr.h"
namespace Ui {
class RippleAnimation;
class FireworksAnimation;
} // namespace Ui
namespace HistoryView {
class Message;
class TodoList final : public Media {
public:
TodoList(
not_null<Element*> parent,
not_null<TodoListData*> todolist,
Element *replacing);
~TodoList();
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
bool toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const override {
return true;
}
bool dragItemByHandler(const ClickHandlerPtr &p) const override {
return true;
}
bool needsBubble() const override {
return true;
}
bool customInfoLayout() const override {
return false;
}
[[nodiscard]] TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const override;
uint16 fullSelectionLength() const override;
TextForMimeData selectedText(TextSelection selection) const override;
void paintBubbleFireworks(
Painter &p,
const QRect &bubble,
crl::time ms) const override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) override;
void unloadHeavyPart() override;
bool hasHeavyPart() const override;
void hideSpoilers() override;
std::vector<TodoTaskInfo> takeTasksInfo() override;
private:
struct Task;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
[[nodiscard]] bool canComplete() const;
[[nodiscard]] int countTaskTop(
const Task &task,
int innerWidth) const;
[[nodiscard]] int countTaskHeight(
const Task &task,
int innerWidth) const;
[[nodiscard]] ClickHandlerPtr createTaskClickHandler(
const Task &task);
void updateTexts();
void updateTasks(bool skipAnimations);
void startToggleAnimation(Task &task);
void updateCompletionStatus();
void maybeStartFireworks();
void setupPreviousState(const std::vector<TodoTaskInfo> &info);
int paintTask(
Painter &p,
const Task &task,
int left,
int top,
int width,
int outerWidth,
const PaintContext &context) const;
void paintRadio(
Painter &p,
const Task &task,
int left,
int top,
const PaintContext &context) const;
void paintStatus(
Painter &p,
const Task &task,
int left,
int top,
const PaintContext &context) const;
void paintBottom(
Painter &p,
int left,
int top,
int paintw,
const PaintContext &context) const;
void appendTaskHighlight(
int id,
int top,
int height,
const PaintContext &context) const;
void radialAnimationCallback() const;
void toggleRipple(Task &task, bool pressed);
void toggleCompletion(int id);
[[nodiscard]] int bottomButtonHeight() const;
const not_null<TodoListData*> _todolist;
int _todoListVersion = 0;
int _total = 0;
int _incompleted = 0;
TodoListData::Flags _flags = TodoListData::Flags();
Ui::Text::String _title;
Ui::Text::String _subtitle;
std::vector<Task> _tasks;
Ui::Text::String _completionStatusLabel;
mutable std::unique_ptr<Ui::FireworksAnimation> _fireworksAnimation;
mutable QPoint _lastLinkPoint;
mutable QImage _userpicCircleCache;
mutable QImage _fillingIconCache;
};
} // namespace HistoryView

View File

@@ -0,0 +1,874 @@
/*
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/media/history_view_unique_gift.h"
#include "base/unixtime.h"
#include "boxes/star_gift_box.h"
#include "chat_helpers/stickers_lottie.h"
#include "core/click_handler_types.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_birthday.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "data/data_star_gift.h"
#include "data/data_web_page.h"
#include "history/view/media/history_view_media_generic.h"
#include "history/view/media/history_view_premium_gift.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_element.h"
#include "history/history.h"
#include "history/history_item.h"
#include "info/peer_gifts/info_peer_gifts_common.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_credits_graphics.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/ministar_particles.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/rect.h"
#include "ui/top_background_gradient.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_credits.h"
namespace HistoryView {
namespace {
class ButtonPart final : public MediaGenericPart {
public:
ButtonPart(
const QString &text,
QMargins margins,
Fn<void()> repaint,
ClickHandlerPtr link,
QColor bg = QColor(0, 0, 0, 0));
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
TextState textState(
QPoint point,
StateRequest request,
int outerWidth) const override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
private:
Ui::Text::String _text;
QMargins _margins;
QColor _bg;
QSize _size;
ClickHandlerPtr _link;
std::unique_ptr<Ui::RippleAnimation> _ripple;
mutable Ui::Premium::ColoredMiniStars _stars;
mutable std::optional<QColor> _starsLastColor;
Fn<void()> _repaint;
mutable QPoint _lastPoint;
};
class TextBubblePart final : public MediaGenericTextPart {
public:
TextBubblePart(
TextWithEntities text,
QMargins margins,
Data::UniqueGiftBackdrop backdrop,
ClickHandlerPtr link);
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
TextState textState(
QPoint point,
StateRequest request,
int outerWidth) const override;
private:
void setupPen(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context) const override;
int elisionLines() const override;
Data::UniqueGiftBackdrop _backdrop;
ClickHandlerPtr _link;
};
TextBubblePart::TextBubblePart(
TextWithEntities text,
QMargins margins,
Data::UniqueGiftBackdrop backdrop,
ClickHandlerPtr link)
: MediaGenericTextPart(
std::move(text),
margins,
st::uniqueGiftReleasedBy.style,
{},
{},
style::al_top)
, _backdrop(backdrop)
, _link(std::move(link)) {
}
void TextBubblePart::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setOpacity(0.5);
p.setBrush(_backdrop.patternColor);
const auto radius = height() / 2.;
const auto left = (outerWidth - width()) / 2;
const auto r = QRect(left, 0, width(), height());
p.drawRoundedRect(r, radius, radius);
p.setOpacity(1.);
MediaGenericTextPart::draw(p, owner, context, outerWidth);
}
TextState TextBubblePart::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
auto result = TextState();
const auto left = (outerWidth - width()) / 2;
if (point.x() >= left
&& point.y() >= 0
&& point.x() < left + width()
&& point.y() < height()) {
result.link = _link;
}
return result;
}
void TextBubblePart::setupPen(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context) const {
p.setPen(_backdrop.textColor);
}
int TextBubblePart::elisionLines() const {
return 1;
}
ButtonPart::ButtonPart(
const QString &text,
QMargins margins,
Fn<void()> repaint,
ClickHandlerPtr link,
QColor bg)
: _text(st::semiboldTextStyle, text)
, _margins(margins)
, _bg(bg)
, _size(
(_text.maxWidth()
+ st::msgServiceGiftBoxButtonHeight
+ st::msgServiceGiftBoxButtonPadding.left()
+ st::msgServiceGiftBoxButtonPadding.right()),
st::msgServiceGiftBoxButtonHeight)
, _link(std::move(link))
, _stars([=](const QRect &) {
repaint();
}, Ui::Premium::MiniStarsType::SlowStars)
, _repaint(std::move(repaint)) {
}
void ButtonPart::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
PainterHighQualityEnabler hq(p);
const auto customColors = (_bg.alpha() > 0);
const auto position = QPoint(
(outerWidth - width()) / 2 + _margins.left(),
_margins.top());
p.translate(position);
p.setPen(Qt::NoPen);
p.setBrush(customColors ? QBrush(_bg) : context.st->msgServiceBg());
const auto radius = _size.height() / 2.;
const auto r = Rect(_size);
p.drawRoundedRect(r, radius, radius);
auto white = QColor(255, 255, 255);
const auto fg = customColors ? white : context.st->msgServiceFg()->c;
if (!_starsLastColor || *_starsLastColor != fg) {
_starsLastColor = fg;
_stars.setColorOverride(QGradientStops{
{ 0., anim::with_alpha(fg, .3) },
{ 1., fg },
});
const auto padding = _size.height() / 2;
_stars.setCenter(
Rect(_size) - QMargins(padding, 0, padding, 0));
}
auto clipPath = QPainterPath();
clipPath.addRoundedRect(r, radius, radius);
p.setClipPath(clipPath);
_stars.setPaused(context.paused);
_stars.paint(p);
p.setClipping(false);
if (_ripple) {
const auto opacity = p.opacity();
const auto ripple = customColors
? anim::with_alpha(fg, .3)
: context.messageStyle()->msgWaveformInactive->c;
p.setOpacity(st::historyPollRippleOpacity);
_ripple->paint(
p,
0,
0,
width(),
&ripple);
p.setOpacity(opacity);
}
p.setPen(fg);
_text.draw(
p,
0,
(_size.height() - _text.minHeight()) / 2,
_size.width(),
style::al_top);
p.translate(-position);
}
TextState ButtonPart::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
point -= QPoint{
(outerWidth - width()) / 2 + _margins.left(),
_margins.top()
};
if (QRect(QPoint(), _size).contains(point)) {
auto result = TextState();
result.link = _link;
_lastPoint = point;
return result;
}
return {};
}
void ButtonPart::clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) {
if (p != _link) {
return;
} else if (pressed) {
if (!_ripple) {
const auto radius = _size.height() / 2;
_ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(_size, radius),
_repaint);
}
_ripple->add(_lastPoint);
} else if (_ripple) {
_ripple->lastStop();
}
}
QSize ButtonPart::countOptimalSize() {
return {
_margins.left() + _size.width() + _margins.right(),
_margins.top() + _size.height() + _margins.bottom(),
};
}
QSize ButtonPart::countCurrentSize(int newWidth) {
return optimalSize();
}
} // namespace
auto GenerateUniqueGiftMedia(
not_null<Element*> parent,
Element *replacing,
std::shared_ptr<Data::UniqueGift> gift)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
return [=](
not_null<MediaGeneric*> media,
Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
auto pushText = [&](
TextWithEntities text,
const style::TextStyle &st,
QColor color,
QMargins margins) {
if (text.empty()) {
return;
}
push(std::make_unique<TextPartColored>(
std::move(text),
margins,
[color](const auto&) { return color; },
st));
};
const auto item = parent->data();
const auto itemMedia = item->media();
const auto fields = itemMedia ? itemMedia->gift() : nullptr;
const auto upgrade = fields && fields->upgrade;
const auto outgoing = upgrade ? !item->out() : item->out();
const auto white = QColor(255, 255, 255);
const auto sticker = [=] {
using Tag = ChatHelpers::StickerLottieSize;
return StickerInBubblePart::Data{
.sticker = gift->model.document,
.size = st::chatIntroStickerSize,
.cacheTag = Tag::ChatIntroHelloSticker,
};
};
push(std::make_unique<StickerInBubblePart>(
parent,
replacing,
sticker,
st::chatUniqueStickerPadding));
const auto peer = parent->history()->peer;
pushText(
tr::bold(peer->isSelf()
? tr::lng_action_gift_self_subtitle(tr::now)
: peer->isServiceUser()
? tr::lng_gift_link_label_gift(tr::now)
: (outgoing
? tr::lng_action_gift_sent_subtitle
: tr::lng_action_gift_got_subtitle)(
tr::now,
lt_user,
peer->shortName())),
st::chatUniqueTitle,
white,
st::chatUniqueTitlePadding);
pushText(
tr::bold(Data::UniqueGiftName(*gift)),
st::chatUniqueTextStyle,
gift->backdrop.textColor,
st::chatUniqueTextPadding);
if (const auto by = gift->releasedBy) {
const auto handler = std::make_shared<LambdaClickHandler>([=] {
Ui::GiftReleasedByHandler(by);
});
push(std::make_unique<TextBubblePart>(
tr::lng_gift_released_by(
tr::now,
lt_name,
tr::link('@' + by->username()),
tr::marked),
st::giftBoxReleasedByMargin,
gift->backdrop,
handler));
}
const auto name = [](const Data::UniqueGiftAttribute &value) {
return tr::bold(value.name);
};
auto attributes = std::vector<AttributeTable::Entry>{
{ tr::lng_gift_unique_model(tr::now), name(gift->model) },
{ tr::lng_gift_unique_symbol(tr::now), name(gift->pattern) },
{ tr::lng_gift_unique_backdrop(tr::now), name(gift->backdrop) },
};
const auto tableAddedMargins = gift->releasedBy
? QMargins(0, st::chatUniqueAuthorSkip, 0, 0)
: QMargins();
push(std::make_unique<AttributeTable>(
std::move(attributes),
st::chatUniqueTextPadding + tableAddedMargins,
[c = gift->backdrop.textColor](const auto&) { return c; },
[](const auto&) { return QColor(255, 255, 255); }));
auto link = OpenStarGiftLink(parent->data());
push(std::make_unique<ButtonPart>(
tr::lng_sticker_premium_view(tr::now),
st::chatUniqueButtonPadding,
[=] { parent->repaint(); },
std::move(link),
anim::with_alpha(gift->backdrop.patternColor, 0.75)));
};
}
auto UniqueGiftBg(
not_null<Element*> view,
std::shared_ptr<Data::UniqueGift> gift)
-> Fn<void(
Painter&,
const Ui::ChatPaintContext&,
not_null<const MediaGeneric*>)> {
struct State {
QImage bg;
base::flat_map<float64, QImage> cache;
std::unique_ptr<Ui::Text::CustomEmoji> pattern;
QImage badgeCache;
Info::PeerGifts::GiftBadge badgeKey;
};
const auto state = std::make_shared<State>();
state->pattern = view->history()->owner().customEmojiManager().create(
gift->pattern.document,
[=] { view->repaint(); },
Data::CustomEmojiSizeTag::Large);
[[maybe_unused]] const auto preload = state->pattern->ready();
return [=](
Painter &p,
const Ui::ChatPaintContext &context,
not_null<const MediaGeneric*> media) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
const auto webpreview = (media.get() != view->media());
const auto sub = webpreview ? 0 : (st::chatUniqueGiftBorder / 2);
const auto thickness = webpreview ? 0 : st::chatUniqueGiftBorder * 2;
const auto removed = thickness + sub;
const auto radius = webpreview
? st::roundRadiusLarge
: (st::msgServiceGiftBoxRadius - thickness + sub);
const auto full = QRect(0, 0, media->width(), media->height());
const auto inner = full.marginsRemoved(
{ removed, removed, removed, removed });
if (!webpreview) {
auto pen = context.st->msgServiceBg()->p;
pen.setWidthF(thickness);
p.setPen(pen);
p.setBrush(Qt::transparent);
p.drawRoundedRect(inner, radius, radius);
}
auto gradient = QRadialGradient(inner.center(), inner.height() / 2);
gradient.setStops({
{ 0., gift->backdrop.centerColor },
{ 1., gift->backdrop.edgeColor },
});
p.setBrush(gradient);
p.setPen(Qt::NoPen);
p.drawRoundedRect(inner, radius, radius);
const auto width = media->width();
const auto shift = width / 12;
const auto doubled = width + 2 * shift;
const auto top = (webpreview ? 2 : 1) * (-shift);
const auto outer = QRect(-shift, top, doubled, doubled);
p.setClipRect(inner);
Ui::PaintBgPoints(
p,
Ui::PatternBgPoints(),
state->cache,
state->pattern.get(),
*gift,
outer);
p.setClipping(false);
const auto padding = webpreview
? QMargins()
: st::chatUniqueGiftBadgePadding;
p.setClipRect(inner.marginsAdded(padding));
auto badge = Info::PeerGifts::GiftBadge{
.text = tr::lng_gift_collectible_tag(tr::now),
.bg1 = gift->backdrop.edgeColor,
.bg2 = gift->backdrop.patternColor,
.fg = gift->backdrop.textColor,
};
if (state->badgeCache.isNull() || state->badgeKey != badge) {
state->badgeKey = badge;
state->badgeCache = ValidateRotatedBadge(badge, padding);
}
const auto badgeRatio = state->badgeCache.devicePixelRatio();
const auto badgeWidth = state->badgeCache.width() / badgeRatio;
p.drawImage(
inner.x() + inner.width() - badgeWidth,
inner.y(),
state->badgeCache);
p.setClipping(false);
};
}
auto GenerateUniqueGiftPreview(
not_null<Element*> parent,
Element *replacing,
std::shared_ptr<Data::UniqueGift> gift)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
return [=](
not_null<MediaGeneric*> media,
Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto sticker = [=] {
using Tag = ChatHelpers::StickerLottieSize;
return StickerInBubblePart::Data{
.sticker = gift->model.document,
.size = st::chatIntroStickerSize,
.cacheTag = Tag::ChatIntroHelloSticker,
};
};
push(std::make_unique<StickerInBubblePart>(
parent,
replacing,
sticker,
st::chatUniquePreviewPadding));
};
}
auto GenerateAuctionPreview(
not_null<Element*> parent,
Element *replacing,
std::shared_ptr<Data::StarGift> gift,
Data::UniqueGiftBackdrop backdrop)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
return [=](
not_null<MediaGeneric*> media,
Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto sticker = [=] {
using Tag = ChatHelpers::StickerLottieSize;
return StickerInBubblePart::Data{
.sticker = gift->document,
.size = st::chatIntroStickerSize,
.cacheTag = Tag::ChatIntroHelloSticker,
};
};
push(std::make_unique<StickerInBubblePart>(
parent,
replacing,
sticker,
st::webPageAuctionPreviewPadding));
const auto name = gift->unique
? Data::UniqueGiftName(*gift->unique)
: gift->resellTitle;
if (!name.isEmpty()) {
push(std::make_unique<TextPartColored>(
tr::bold(name),
QMargins(0, 0, 0, st::defaultVerticalListSkip),
[c = backdrop.textColor](const auto&) { return c; },
st::chatUniqueTitle));
}
if (const auto all = gift->limitedCount) {
push(std::make_unique<TextPartColored>(
tr::lng_boosts_list_tab_gifts(
tr::now,
lt_count_decimal,
all,
tr::marked),
QMargins(0, 0, 0, st::webPageAuctionPreviewPadding.top()),
[c = backdrop.textColor](const auto&) { return c; },
st::chatUniqueTextStyle));
}
};
}
auto AuctionBg(
not_null<Element*> view,
Data::UniqueGiftBackdrop backdrop,
std::shared_ptr<Data::StarGift> gift,
TimeId startDate,
TimeId endDate)
-> Fn<void(
Painter&,
const Ui::ChatPaintContext&,
not_null<const MediaGeneric*>)> {
struct State {
std::unique_ptr<Ui::Text::CustomEmoji> pattern;
base::flat_map<float64, QImage> cache;
std::optional<Ui::StarParticles> particles;
std::unique_ptr<base::Timer> timer;
crl::time pausedAt = 0;
crl::time pauseOffset = 0;
};
const auto state = std::make_shared<State>();
if (gift->unique && gift->unique->pattern.document) {
state->pattern = view->history()->owner().customEmojiManager().create(
gift->unique->pattern.document,
[=] { view->repaint(); },
Data::CustomEmojiSizeTag::Large);
}
state->particles.emplace(
Ui::StarParticles::Type::RadialInside,
25,
st::lineWidth * 8);
state->particles->setSpeed(0.05);
state->particles->setColor(backdrop.textColor);
return [=](
Painter &p,
const Ui::ChatPaintContext &context,
not_null<const MediaGeneric*> media) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
const auto webpreview = (media.get() != view->media());
const auto radius = webpreview
? st::roundRadiusLarge
: st::msgServiceGiftBoxRadius;
const auto full = QRect(0, 0, media->width(), media->height());
auto gradient = QRadialGradient(full.center(), full.height() / 2);
gradient.setStops({
{ 0., backdrop.centerColor },
{ 1., backdrop.edgeColor },
});
p.setBrush(gradient);
p.drawRoundedRect(full, radius, radius);
/*if (state->pattern) {
const auto width = media->width();
const auto shift = width / 12;
const auto doubled = width + 2 * shift;
const auto top = (webpreview ? 2 : 1) * (-shift);
const auto outer = QRect(-shift, top, doubled, doubled);
p.setClipRect(full);
if (gift->unique) {
Ui::PaintBgPoints(
p,
Ui::PatternBgPoints(),
state->cache,
state->pattern.get(),
*gift->unique,
outer);
}
p.setClipping(false);
}*/
if (state->particles) {
p.setClipRect(full);
if (context.paused) {
if (!state->pausedAt) {
state->pausedAt = crl::now();
}
const auto diff = state->pausedAt - state->pauseOffset;
state->particles->paint(p, full, diff);
} else {
if (state->pausedAt) {
state->pauseOffset += crl::now() - state->pausedAt;
state->pausedAt = 0;
}
const auto diff = context.now - state->pauseOffset;
state->particles->paint(p, full, diff);
}
p.setClipping(false);
}
const auto now = base::unixtime::now();
const auto startsIn = std::max(startDate - now, 0);
const auto left = std::max(endDate - now, 0);
if (startsIn > 0 || left > 0) {
if (!state->timer) {
state->timer = std::make_unique<base::Timer>([=] {
view->repaint();
});
}
state->timer->callOnce(1000);
} else if (state->timer) {
state->timer = nullptr;
}
const auto still = (startsIn > 0) ? startsIn : left;
const auto time = (still >= 3600)
? u"%1:%2:%3"_q
.arg(still / 3600)
.arg((still % 3600) / 60, 2, 10, QChar('0'))
.arg(still % 60, 2, 10, QChar('0'))
: u"%1:%2"_q
.arg(still / 60)
.arg(still % 60, 2, 10, QChar('0'));
const auto text = (startsIn > 0)
? tr::lng_auction_join_starts_in(tr::now, lt_time, time)
: (left > 0)
? time
: tr::lng_auctino_preview_finished(tr::now);
const auto &font = st::webPageAuctionTimeFont;
const auto textWidth = font->width(text);
const auto padding = st::webPageAuctionTimerPadding;
const auto timerWidth = textWidth + rect::m::sum::h(padding);
const auto timerHeight = font->height + rect::m::sum::v(padding);
const auto timerRadius = timerHeight / 2.;
const auto timerRect = QRectF(
padding.top(),
padding.top(),
timerWidth,
timerHeight);
p.setPen(Qt::NoPen);
p.setBrush(st::slideFadeOutBg);
p.drawRoundedRect(timerRect, timerRadius, timerRadius);
p.setPen(backdrop.textColor);
p.setFont(font);
p.drawText(
timerRect.x() + padding.left(),
timerRect.y() + padding.top() + font->ascent,
text);
};
}
std::unique_ptr<MediaGenericPart> MakeGenericButtonPart(
const QString &text,
QMargins margins,
Fn<void()> repaint,
ClickHandlerPtr link,
QColor bg) {
return std::make_unique<ButtonPart>(text, margins, repaint, link, bg);
}
TextPartColored::TextPartColored(
TextWithEntities text,
QMargins margins,
Fn<QColor(const PaintContext &)> color,
const style::TextStyle &st,
const base::flat_map<uint16, ClickHandlerPtr> &links,
const Ui::Text::MarkedContext &context)
: MediaGenericTextPart(text, margins, st, links, context)
, _color(std::move(color)) {
}
void TextPartColored::setupPen(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context) const {
p.setPen(_color(context));
}
AttributeTable::AttributeTable(
std::vector<Entry> entries,
QMargins margins,
Fn<QColor(const PaintContext &)> labelColor,
Fn<QColor(const PaintContext &)> valueColor,
const Ui::Text::MarkedContext &context)
: _margins(margins)
, _labelColor(std::move(labelColor))
, _valueColor(std::move(valueColor)) {
for (const auto &entry : entries) {
_parts.emplace_back();
auto &part = _parts.back();
part.label.setText(st::chatUniqueTextStyle, entry.label);
part.value.setMarkedText(
st::chatUniqueTextStyle,
entry.value,
kMarkupTextOptions,
context);
}
}
void AttributeTable::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
const auto labelRight = _valueLeft - st::chatUniqueTableSkip;
const auto palette = &context.st->serviceTextPalette();
auto top = _margins.top();
const auto paint = [&](
const Ui::Text::String &text,
int left,
int availableWidth,
style::align align) {
text.draw(p, {
.position = { left, top },
.outerWidth = outerWidth,
.availableWidth = availableWidth,
.align = align,
.palette = palette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.elisionLines = 1,
});
};
const auto forLabel = labelRight - _margins.left();
const auto forValue = width() - _valueLeft - _margins.right();
for (const auto &part : _parts) {
p.setPen(_labelColor(context));
paint(part.label, _margins.left(), forLabel, style::al_topright);
p.setPen(_valueColor(context));
paint(part.value, _valueLeft, forValue, style::al_topleft);
top += st::normalFont->height + st::chatUniqueRowSkip;
}
}
TextState AttributeTable::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
auto top = _margins.top();
for (const auto &part : _parts) {
const auto height = st::normalFont->height + st::chatUniqueRowSkip;
if (point.y() >= top && point.y() < top + height) {
point -= QPoint((outerWidth - width()) / 2 + _valueLeft, top);
auto result = TextState();
auto forText = request.forText();
forText.align = style::al_topleft;
result.link = part.value.getState(point, width(), forText).link;
return result;
}
top += height;
}
return {};
}
QSize AttributeTable::countOptimalSize() {
auto maxLabel = 0;
auto maxValue = 0;
for (const auto &part : _parts) {
maxLabel = std::max(maxLabel, part.label.maxWidth());
maxValue = std::max(maxValue, part.value.maxWidth());
}
const auto skip = st::chatUniqueTableSkip;
const auto row = st::normalFont->height + st::chatUniqueRowSkip;
const auto height = int(_parts.size()) * row - st::chatUniqueRowSkip;
return {
_margins.left() + maxLabel + skip + maxValue + _margins.right(),
_margins.top() + height + _margins.bottom(),
};
}
QSize AttributeTable::countCurrentSize(int newWidth) {
const auto skip = st::chatUniqueTableSkip;
const auto width = newWidth - _margins.left() - _margins.right() - skip;
auto maxLabel = 0;
auto maxValue = 0;
for (const auto &part : _parts) {
maxLabel = std::max(maxLabel, part.label.maxWidth());
maxValue = std::max(maxValue, part.value.maxWidth());
}
if (width <= 0 || !maxLabel) {
_valueLeft = _margins.left();
} else if (!maxValue) {
_valueLeft = newWidth - _margins.right();
} else {
_valueLeft = _margins.left()
+ int((int64(maxLabel) * width) / (maxLabel + maxValue))
+ skip;
}
return { newWidth, minHeight() };
}
} // namespace HistoryView

View File

@@ -0,0 +1,144 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media_generic.h"
class Painter;
namespace Data {
class MediaGiftBox;
struct UniqueGift;
struct UniqueGiftBackdrop;
struct StarGift;
class Birthday;
} // namespace Data
namespace Ui {
struct ChatPaintContext;
} // namespace Ui
namespace HistoryView {
class Element;
class MediaGeneric;
class MediaGenericPart;
[[nodiscard]] auto GenerateUniqueGiftMedia(
not_null<Element*> parent,
Element *replacing,
std::shared_ptr<Data::UniqueGift> gift)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
[[nodiscard]] auto UniqueGiftBg(
not_null<Element*> view,
std::shared_ptr<Data::UniqueGift> gift)
-> Fn<void(
Painter&,
const Ui::ChatPaintContext&,
not_null<const MediaGeneric*>)>;
[[nodiscard]] auto GenerateUniqueGiftPreview(
not_null<Element*> parent,
Element *replacing,
std::shared_ptr<Data::UniqueGift> gift)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
[[nodiscard]] auto GenerateAuctionPreview(
not_null<Element*> parent,
Element *replacing,
std::shared_ptr<Data::StarGift> gift,
Data::UniqueGiftBackdrop backdrop)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
[[nodiscard]] auto AuctionBg(
not_null<Element*> view,
Data::UniqueGiftBackdrop backdrop,
std::shared_ptr<Data::StarGift> gift,
TimeId startDate,
TimeId endDate)
-> Fn<void(
Painter&,
const Ui::ChatPaintContext&,
not_null<const MediaGeneric*>)>;
[[nodiscard]] std::unique_ptr<MediaGenericPart> MakeGenericButtonPart(
const QString &text,
QMargins margins,
Fn<void()> repaint,
ClickHandlerPtr link,
QColor bg = QColor(0, 0, 0, 0));
class TextPartColored : public MediaGenericTextPart {
public:
TextPartColored(
TextWithEntities text,
QMargins margins,
Fn<QColor(const PaintContext &)> color,
const style::TextStyle &st = st::defaultTextStyle,
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
const Ui::Text::MarkedContext &context = {});
private:
void setupPen(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context) const override;
Fn<QColor(const PaintContext &)> _color;
};
class AttributeTable final : public MediaGenericPart {
public:
struct Entry {
QString label;
TextWithEntities value;
};
AttributeTable(
std::vector<Entry> entries,
QMargins margins,
Fn<QColor(const PaintContext &)> labelColor,
Fn<QColor(const PaintContext &)> valueColor,
const Ui::Text::MarkedContext &context = {});
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
TextState textState(
QPoint point,
StateRequest request,
int outerWidth) const override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
private:
struct Part {
Ui::Text::String label;
Ui::Text::String value;
};
std::vector<Part> _parts;
QMargins _margins;
Fn<QColor(const PaintContext &)> _labelColor;
Fn<QColor(const PaintContext &)> _valueColor;
int _valueLeft = 0;
};
} // namespace HistoryView

View File

@@ -0,0 +1,278 @@
/*
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/media/history_view_userpic_suggestion.h"
#include "core/click_handler_types.h" // ClickHandlerContext
#include "data/data_document.h"
#include "data/data_photo.h"
#include "data/data_user.h"
#include "data/data_photo_media.h"
#include "data/data_file_click_handler.h"
#include "data/data_session.h"
#include "editor/photo_editor_common.h"
#include "editor/photo_editor_layer_widget.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/media/history_view_sticker_player_abstract.h"
#include "history/view/history_view_element.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "ui/boxes/confirm_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/painter.h"
#include "mainwidget.h"
#include "apiwrap.h"
#include "api/api_peer_photo.h"
#include "settings/settings_information.h" // UpdatePhotoLocally
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
constexpr auto kToastDuration = 5 * crl::time(1000);
void ShowUserpicSuggestion(
not_null<Window::SessionController*> controller,
const std::shared_ptr<Data::PhotoMedia> &media,
const FullMsgId itemId,
not_null<PeerData*> peer,
Fn<void()> setDone) {
const auto photo = media->owner();
const auto from = peer->asUser();
const auto name = (from && !from->firstName.isEmpty())
? from->firstName
: peer->name();
if (photo->hasVideo()) {
const auto done = [=](Fn<void()> close) {
using namespace Settings;
const auto session = &photo->session();
auto &peerPhotos = session->api().peerPhoto();
peerPhotos.updateSelf(photo, itemId, setDone);
close();
};
controller->show(Ui::MakeConfirmBox({
.text = tr::lng_profile_accept_video_sure(
tr::now,
lt_user,
name),
.confirmed = done,
.confirmText = tr::lng_profile_set_video_button(
tr::now),
}));
} else {
const auto original = std::make_shared<QImage>(
media->image(Data::PhotoSize::Large)->original());
const auto callback = [=](QImage &&image) {
using namespace Settings;
const auto session = &photo->session();
const auto user = session->user();
UpdatePhotoLocally(user, image);
auto &peerPhotos = session->api().peerPhoto();
if (original->size() == image.size()
&& original->constBits() == image.constBits()) {
peerPhotos.updateSelf(photo, itemId, setDone);
} else {
peerPhotos.upload(user, { std::move(image) }, setDone);
}
};
using namespace Editor;
PrepareProfilePhoto(
controller->content(),
&controller->window(),
{
.about = { tr::lng_profile_accept_photo_sure(
tr::now,
lt_user,
name) },
.confirm = tr::lng_profile_set_photo_button(tr::now),
.cropType = EditorData::CropType::Ellipse,
.keepAspectRatio = true,
},
callback,
base::duplicate(*original));
}
}
[[nodiscard]] QImage GrabUserpicFrame(base::weak_ptr<Photo> photo) {
const auto strong = photo.get();
if (!strong || !strong->width() || !strong->height()) {
return {};
}
const auto ratio = style::DevicePixelRatio();
auto frame = QImage(
QSize(strong->width(), strong->height()) * ratio,
QImage::Format_ARGB32_Premultiplied);
frame.fill(Qt::transparent);
frame.setDevicePixelRatio(ratio);
auto p = Painter(&frame);
strong->paintUserpicFrame(p, QPoint(0, 0), false);
p.end();
return frame;
}
void ShowSetToast(
not_null<Window::SessionController*> controller,
const QImage &frame) {
const auto text = tr::bold(
tr::lng_profile_changed_photo_title(tr::now)
).append('\n').append(
tr::lng_profile_changed_photo_about(
tr::now,
lt_link,
tr::link(
tr::lng_profile_changed_photo_link(tr::now),
u"tg://settings/edit_profile"_q),
tr::marked)
);
auto st = std::make_shared<style::Toast>(st::historyPremiumToast);
const auto skip = st->padding.top();
const auto size = st->style.font->height * 2;
const auto ratio = style::DevicePixelRatio();
auto copy = frame.scaled(
QSize(size, size) * ratio,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
copy.setDevicePixelRatio(ratio);
st->padding.setLeft(skip + size + skip);
st->palette.linkFg = st->palette.selectLinkFg = st::mediaviewTextLinkFg;
const auto weak = controller->showToast({
.text = text,
.st = st.get(),
.attach = RectPart::Bottom,
.duration = kToastDuration,
});
if (const auto strong = weak.get()) {
const auto widget = strong->widget();
widget->lifetime().add([st = std::move(st)] {});
const auto preview = Ui::CreateChild<Ui::RpWidget>(widget.get());
preview->moveToLeft(skip, skip);
preview->resize(size, size);
preview->show();
preview->setAttribute(Qt::WA_TransparentForMouseEvents);
preview->paintRequest(
) | rpl::on_next([=] {
QPainter(preview).drawImage(0, 0, copy);
}, preview->lifetime());
}
}
[[nodiscard]] Fn<void()> ShowSetToastCallback(
base::weak_ptr<Window::SessionController> weak,
QImage frame) {
return [weak = std::move(weak), frame = std::move(frame)] {
if (const auto strong = weak.get()) {
ShowSetToast(strong, frame);
}
};
}
} // namespace
UserpicSuggestion::UserpicSuggestion(
not_null<Element*> parent,
not_null<PeerData*> chat,
not_null<PhotoData*> photo,
int width)
: _photo(parent, chat, photo, width) {
_photo.initDimensions();
_photo.resizeGetHeight(_photo.maxWidth());
}
UserpicSuggestion::~UserpicSuggestion() = default;
int UserpicSuggestion::top() {
return st::msgServiceGiftBoxButtonMargins.top();
}
QSize UserpicSuggestion::size() {
return { _photo.maxWidth(), _photo.minHeight() };
}
TextWithEntities UserpicSuggestion::title() {
return {};
}
rpl::producer<QString> UserpicSuggestion::button() {
return _photo.getPhoto()->hasVideo()
? (_photo.parent()->data()->out()
? tr::lng_action_suggested_video_button()
: tr::lng_profile_set_video_button())
: tr::lng_action_suggested_photo_button();
}
TextWithEntities UserpicSuggestion::subtitle() {
return _photo.parent()->data()->notificationText();
}
ClickHandlerPtr UserpicSuggestion::createViewLink() {
const auto out = _photo.parent()->data()->out();
const auto photo = _photo.getPhoto();
const auto itemId = _photo.parent()->data()->fullId();
const auto peer = _photo.parent()->data()->history()->peer;
const auto weak = base::make_weak(&_photo);
const auto show = crl::guard(weak, [=](FullMsgId id) {
_photo.showPhoto(id);
});
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
auto frame = GrabUserpicFrame(weak);
if (frame.isNull()) {
return;
}
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
const auto media = photo->activeMediaView();
if (media->loaded()) {
if (out) {
PhotoOpenClickHandler(photo, show, itemId).onClick(
context);
} else {
ShowUserpicSuggestion(
controller,
media,
itemId,
peer,
ShowSetToastCallback(controller, std::move(frame)));
}
} else if (!photo->loading()) {
PhotoSaveClickHandler(photo, itemId).onClick(context);
}
}
});
}
void UserpicSuggestion::draw(
Painter &p,
const PaintContext &context,
const QRect &geometry) {
p.translate(geometry.topLeft());
_photo.draw(p, context);
p.translate(-geometry.topLeft());
}
void UserpicSuggestion::stickerClearLoopPlayed() {
}
std::unique_ptr<StickerPlayer> UserpicSuggestion::stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) {
return nullptr;
}
bool UserpicSuggestion::hasHeavyPart() {
return _photo.hasHeavyPart();
}
void UserpicSuggestion::unloadHeavyPart() {
_photo.unloadHeavyPart();
}
} // namespace HistoryView

View File

@@ -0,0 +1,58 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
#include "history/view/media/history_view_media_unwrapped.h"
#include "history/view/media/history_view_photo.h"
#include "history/view/media/history_view_service_box.h"
namespace Data {
class MediaGiftBox;
} // namespace Data
namespace HistoryView {
class UserpicSuggestion final : public ServiceBoxContent {
public:
UserpicSuggestion(
not_null<Element*> parent,
not_null<PeerData*> chat,
not_null<PhotoData*> photo,
int width);
~UserpicSuggestion();
int top() override;
QSize size() override;
TextWithEntities title() override;
TextWithEntities subtitle() override;
rpl::producer<QString> button() override;
void draw(
Painter &p,
const PaintContext &context,
const QRect &geometry) override;
ClickHandlerPtr createViewLink() override;
bool hideServiceText() override {
return true;
}
void stickerClearLoopPlayed() override;
std::unique_ptr<StickerPlayer> stickerTakePlayer(
not_null<DocumentData*> data,
const Lottie::ColorReplacements *replacements) override;
bool hasHeavyPart() override;
void unloadHeavyPart() override;
private:
Photo _photo;
};
} // namespace HistoryView

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,214 @@
/*
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
*/
#pragma once
#include "history/view/media/history_view_media.h"
#include "ui/userpic_view.h"
namespace Data {
class DocumentMedia;
class Media;
class PhotoMedia;
} // namespace Data
namespace Ui {
class RippleAnimation;
} // namespace Ui
namespace HistoryView {
class Sticker;
class WebPage : public Media {
public:
WebPage(
not_null<Element*> parent,
not_null<WebPageData*> data,
MediaWebPageFlags flags);
void refreshParentId(not_null<HistoryItem*> realParent) override;
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
bool aboveTextByDefault() const override {
return false;
}
bool hideMessageText() const override {
return false;
}
[[nodiscard]] TextSelection adjustSelection(
TextSelection selection,
TextSelectType type) const override;
uint16 fullSelectionLength() const override;
bool hasTextForCopy() const override {
// We do not add _title and _description in FullSelection text copy.
return false;
}
QString additionalInfoString() const override;
bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const override;
bool allowTextSelectionByHandler(
const ClickHandlerPtr &p) const override;
bool dragItemByHandler(const ClickHandlerPtr &p) const override;
TextForMimeData selectedText(TextSelection selection) const override;
void clickHandlerActiveChanged(
const ClickHandlerPtr &p, bool active) override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &p, bool pressed) override;
bool isDisplayed() const override;
PhotoData *getPhoto() const override {
return _attach ? _attach->getPhoto() : nullptr;
}
DocumentData *getDocument() const override {
return _attach ? _attach->getDocument() : nullptr;
}
void stopAnimation() override {
if (_attach) _attach->stopAnimation();
}
void checkAnimation() override {
if (_attach) _attach->checkAnimation();
}
not_null<WebPageData*> webpage() {
return _data;
}
bool needsBubble() const override {
return true;
}
bool customInfoLayout() const override {
return false;
}
bool allowsFastShare() const override {
return true;
}
bool enforceBubbleWidth() const override;
Media *attach() const {
return _attach.get();
}
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
~WebPage();
private:
struct FactcheckMetrics {
int lines = 0;
bool expandable = false;
bool expanded = false;
};
struct HintData {
QSize size;
QPointF lastPosition;
QString text;
int widthBefore = 0;
std::unique_ptr<Ui::RippleAnimation> ripple;
ClickHandlerPtr link;
};
struct StickerSetData {
std::vector<std::unique_ptr<Sticker>> views;
};
struct SponsoredData {
ClickHandlerPtr link;
ClickHandlerPtr mediaLink;
QString buttonText;
uint64 backgroundEmojiId = 0;
uint8 colorIndex : 6 = 0;
uint8 isLinkInternal : 1 = 0;
uint8 canReport : 1 = 0;
uint8 hasMedia : 1 = 0;
HintData hint;
};
struct FactcheckData {
HintData hint;
Ui::Text::String footer;
uint32 footerHeight : 30 = 0;
uint32 expandable : 1 = 0;
uint32 expanded : 1 = 0;
};
using AdditionalData = std::variant<
StickerSetData,
SponsoredData,
FactcheckData>;
void playAnimation(bool autoplay) override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
void ensurePhotoMediaCreated() const;
[[nodiscard]] TextSelection toTitleSelection(
TextSelection selection) const;
[[nodiscard]] TextSelection fromTitleSelection(
TextSelection selection) const;
[[nodiscard]] TextSelection toDescriptionSelection(
TextSelection selection) const;
[[nodiscard]] TextSelection fromDescriptionSelection(
TextSelection selection) const;
[[nodiscard]] QMargins inBubblePadding() const;
[[nodiscard]] QMargins innerMargin() const;
[[nodiscard]] int bottomInfoPadding() const;
[[nodiscard]] bool isLogEntryOriginal() const;
[[nodiscard]] ClickHandlerPtr replaceAttachLink(
const ClickHandlerPtr &link) const;
[[nodiscard]] bool asArticle() const;
[[nodiscard]] StickerSetData *stickerSetData() const;
[[nodiscard]] SponsoredData *sponsoredData() const;
[[nodiscard]] FactcheckData *factcheckData() const;
[[nodiscard]] HintData *hintData() const;
[[nodiscard]] FactcheckMetrics computeFactcheckMetrics(
int fullHeight) const;
void setupAdditionalData();
const style::QuoteStyle &_st;
const not_null<WebPageData*> _data;
const MediaWebPageFlags _flags;
std::vector<std::unique_ptr<Data::Media>> _collage;
ClickHandlerPtr _openl;
std::unique_ptr<Media> _attach;
mutable std::shared_ptr<Data::PhotoMedia> _photoMedia;
mutable std::unique_ptr<Ui::RippleAnimation> _ripple;
int _dataVersion = -1;
int _siteNameLines = 0;
int _descriptionLines = 0;
uint32 _titleLines : 31 = 0;
uint32 _asArticle : 1 = 0;
Ui::Text::String _siteName;
Ui::Text::String _title;
Ui::Text::String _description;
Ui::Text::String _openButton;
QString _duration;
int _durationWidth = 0;
mutable QPoint _lastPoint;
int _pixw = 0;
int _pixh = 0;
std::unique_ptr<AdditionalData> _additionalData;
};
} // namespace HistoryView