Fly + effects when choosing an emoji status.

This commit is contained in:
John Preston 2022-09-06 20:53:42 +04:00
parent d4810713cb
commit 5cc6275fc3
38 changed files with 941 additions and 1055 deletions

View File

@ -807,8 +807,12 @@ PRIVATE
info/polls/info_polls_results_widget.h
info/profile/info_profile_actions.cpp
info/profile/info_profile_actions.h
info/profile/info_profile_badge.cpp
info/profile/info_profile_badge.h
info/profile/info_profile_cover.cpp
info/profile/info_profile_cover.h
info/profile/info_profile_emoji_status_panel.cpp
info/profile/info_profile_emoji_status_panel.h
info/profile/info_profile_icon.cpp
info/profile/info_profile_icon.h
info/profile/info_profile_inner_widget.cpp

View File

@ -1708,6 +1708,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_unlock_reactions" = "Unlock Premium Reactions";
"lng_premium_unlock_stickers" = "Unlock Premium Stickers";
"lng_premium_unlock_emoji" = "Unlock Animated Emoji";
"lng_premium_unlock_status" = "Unlock Emoji Status";
"lng_premium_subscribe_months_12" = "Annual";
"lng_premium_subscribe_months_6" = "Semiannual";
@ -1741,8 +1742,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_premium_summary_about_voice_to_text" = "Ability to read the transcript of any incoming voice message.";
"lng_premium_summary_subtitle_no_ads" = "No Ads";
"lng_premium_summary_about_no_ads" = "No more ads in public channels where Telegram sometimes shows ads.";
"lng_premium_summary_subtitle_unique_reactions" = "Unique Reactions";
"lng_premium_summary_about_unique_reactions" = "Additional animated reactions on messages available only to the Premium subscribers.";
"lng_premium_summary_subtitle_emoji_status" = "Emoji Status";
"lng_premium_summary_about_emoji_status" = "Add any of thousands emoji next to your name to display current activity.";
"lng_premium_summary_subtitle_infinite_reactions" = "Infinite Reactions";
"lng_premium_summary_about_infinite_reactions" = "React with thousands of emoji — with multiple reactions per message.";
"lng_premium_summary_subtitle_premium_stickers" = "Premium Stickers";
"lng_premium_summary_about_premium_stickers" = "Exclusive enlarged stickers featuring additional effects, updated monthly.";
"lng_premium_summary_subtitle_animated_emoji" = "Animated Emoji";

View File

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/edit_peer_common.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/input_fields.h"
#include "ui/text/format_values.h" // Ui::FormatPhone
#include "ui/text/text_utilities.h"

View File

@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_file_origin.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_message_reactions.h"
#include "data/data_document_media.h"
#include "data/data_streaming.h"
#include "data/data_peer_values.h"
@ -63,7 +62,6 @@ using Data::ReactionId;
struct Descriptor {
PremiumPreview section = PremiumPreview::Stickers;
DocumentData *requestedSticker = nullptr;
base::flat_map<ReactionId, ReactionDisableType> disabled;
bool fromSettings = false;
Fn<void()> hiddenCallback;
Fn<void(not_null<Ui::BoxContent*>)> shownCallback;
@ -72,7 +70,6 @@ struct Descriptor {
bool operator==(const Descriptor &a, const Descriptor &b) {
return (a.section == b.section)
&& (a.requestedSticker == b.requestedSticker)
&& (a.disabled == b.disabled)
&& (a.fromSettings == b.fromSettings);
}
@ -120,8 +117,10 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_premium_summary_subtitle_voice_to_text();
case PremiumPreview::NoAds:
return tr::lng_premium_summary_subtitle_no_ads();
case PremiumPreview::Reactions:
return tr::lng_premium_summary_subtitle_unique_reactions();
case PremiumPreview::EmojiStatus:
return tr::lng_premium_summary_subtitle_emoji_status();
case PremiumPreview::InfiniteReactions:
return tr::lng_premium_summary_subtitle_infinite_reactions();
case PremiumPreview::Stickers:
return tr::lng_premium_summary_subtitle_premium_stickers();
case PremiumPreview::AnimatedEmoji:
@ -146,8 +145,10 @@ void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
return tr::lng_premium_summary_about_voice_to_text();
case PremiumPreview::NoAds:
return tr::lng_premium_summary_about_no_ads();
case PremiumPreview::Reactions:
return tr::lng_premium_summary_about_unique_reactions();
case PremiumPreview::EmojiStatus:
return tr::lng_premium_summary_about_emoji_status();
case PremiumPreview::InfiniteReactions:
return tr::lng_premium_summary_about_infinite_reactions();
case PremiumPreview::Stickers:
return tr::lng_premium_summary_about_premium_stickers();
case PremiumPreview::AnimatedEmoji:
@ -465,6 +466,8 @@ struct VideoPreviewDocument {
case PremiumPreview::AnimatedEmoji: return "animated_emoji";
case PremiumPreview::AdvancedChatManagement:
return "advanced_chat_management";
case PremiumPreview::EmojiStatus: return "emoji_status";
case PremiumPreview::InfiniteReactions: return "infinite_reactions";
case PremiumPreview::ProfileBadge: return "profile_badge";
case PremiumPreview::AnimatedUserpics: return "animated_userpics";
}
@ -726,423 +729,12 @@ struct VideoPreviewDocument {
return result;
}
class ReactionPreview final {
public:
ReactionPreview(
not_null<Window::SessionController*> controller,
const Data::Reaction &reaction,
ReactionDisableType type,
Fn<void()> update,
QPoint position);
[[nodiscard]] bool playsEffect() const;
void paint(Painter &p);
void paintEffect(QPainter &p);
void setOver(bool over);
void startAnimations();
void cancelAnimations();
[[nodiscard]] bool ready() const;
[[nodiscard]] bool disabled() const;
[[nodiscard]] QRect geometry() const;
private:
void checkReady();
const not_null<Window::SessionController*> _controller;
const Fn<void()> _update;
const QPoint _position;
Ui::Animations::Simple _scale;
std::shared_ptr<Data::DocumentMedia> _centerMedia;
std::shared_ptr<Data::DocumentMedia> _aroundMedia;
std::unique_ptr<Lottie::SinglePlayer> _center;
std::unique_ptr<Lottie::SinglePlayer> _around;
std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
QImage _cache1;
QImage _cache2;
bool _over = false;
bool _disabled = false;
bool _playRequested = false;
bool _aroundPlaying = false;
bool _centerPlaying = false;
rpl::lifetime _lifetime;
};
ReactionPreview::ReactionPreview(
not_null<Window::SessionController*> controller,
const Data::Reaction &reaction,
ReactionDisableType type,
Fn<void()> update,
QPoint position)
: _controller(controller)
, _update(std::move(update))
, _position(position)
, _centerMedia(reaction.centerIcon->createMediaView())
, _aroundMedia(reaction.aroundAnimation->createMediaView())
, _pathGradient(
HistoryView::MakePathShiftGradient(
controller->chatStyle(),
_update))
, _disabled(type != ReactionDisableType::None) {
_centerMedia->checkStickerLarge();
_aroundMedia->checkStickerLarge();
checkReady();
if (!_center || !_around) {
_controller->session().downloaderTaskFinished(
) | rpl::take_while([=] {
checkReady();
return !_center || !_around;
}) | rpl::start(_lifetime);
}
}
QRect ReactionPreview::geometry() const {
const auto xsize = st::premiumReactionWidthSkip;
const auto ysize = st::premiumReactionHeightSkip;
return { _position - QPoint(xsize / 2, ysize / 2), QSize(xsize, ysize) };
}
void ReactionPreview::checkReady() {
const auto make = [&](
const std::shared_ptr<Data::DocumentMedia> &media,
int size) {
const auto bytes = media->bytes();
const auto filepath = media->owner()->filepath();
auto result = ChatHelpers::LottiePlayerFromDocument(
media.get(),
nullptr,
ChatHelpers::StickerLottieSize::PremiumReactionPreview,
QSize(size, size) * style::DevicePixelRatio(),
Lottie::Quality::Default);
result->updates() | rpl::start_with_next(_update, _lifetime);
return result;
};
if (!_center && _centerMedia->loaded()) {
_center = make(_centerMedia, st::premiumReactionSize);
}
if (!_around && _aroundMedia->loaded()) {
_around = make(_aroundMedia, st::premiumReactionAround);
}
}
void ReactionPreview::setOver(bool over) {
if (_over == over || _disabled) {
return;
}
_over = over;
const auto from = st::premiumReactionScale;
_scale.start(
_update,
over ? from : 1.,
over ? 1. : from,
st::slideWrapDuration);
}
void ReactionPreview::startAnimations() {
if (_disabled) {
return;
}
_playRequested = true;
if (!_center || !_center->ready() || !_around || !_around->ready()) {
return;
}
_update();
}
void ReactionPreview::cancelAnimations() {
_playRequested = false;
}
bool ReactionPreview::ready() const {
return _center && _center->ready();
}
bool ReactionPreview::disabled() const {
return _disabled;
}
void ReactionPreview::paint(Painter &p) {
const auto center = st::premiumReactionSize;
const auto scale = _scale.value(_over ? 1. : st::premiumReactionScale);
const auto inner = QRect(
-center / 2,
-center / 2,
center,
center
).translated(_position);
auto hq = PainterHighQualityEnabler(p);
const auto centerReady = _center && _center->ready();
const auto staticCenter = centerReady && !_centerPlaying;
const auto use1 = staticCenter && scale == 1.;
const auto use2 = staticCenter && scale == st::premiumReactionScale;
const auto useScale = (!use1 && !use2 && scale != 1.);
if (useScale) {
p.save();
p.translate(inner.center());
p.scale(scale, scale);
p.translate(-inner.center());
}
if (_disabled) {
p.setOpacity(kDisabledOpacity);
}
checkReady();
if (centerReady) {
if (use1 || use2) {
auto &cache = use1 ? _cache1 : _cache2;
const auto use = int(std::round(center * scale));
const auto rect = QRect(-use / 2, -use / 2, use, use).translated(
_position);
if (cache.isNull()) {
cache = _center->frame().scaledToWidth(
use * style::DevicePixelRatio(),
Qt::SmoothTransformation);
}
p.drawImage(rect, cache);
} else {
p.drawImage(inner, _center->frame());
}
if (_centerPlaying) {
const auto almost = (_center->frameIndex() + 1)
== _center->framesCount();
const auto marked = _center->markFrameShown();
if (almost && marked) {
_centerPlaying = false;
}
}
if (_around
&& _around->ready()
&& !_aroundPlaying
&& !_centerPlaying
&& _playRequested) {
_aroundPlaying = _centerPlaying = true;
_playRequested = false;
}
} else {
p.setBrush(_controller->chatStyle()->msgServiceBg());
ChatHelpers::PaintStickerThumbnailPath(
p,
_centerMedia.get(),
inner,
_pathGradient.get());
}
if (useScale) {
p.restore();
} else if (_disabled) {
p.setOpacity(1.);
}
}
bool ReactionPreview::playsEffect() const {
return _aroundPlaying;
}
void ReactionPreview::paintEffect(QPainter &p) {
if (!_aroundPlaying) {
return;
}
const auto size = st::premiumReactionAround;
const auto outer = QRect(-size/2, -size/2, size, size).translated(
_position);
const auto scale = _scale.value(_over ? 1. : st::premiumReactionScale);
auto hq = PainterHighQualityEnabler(p);
if (scale != 1.) {
p.save();
p.translate(outer.center());
p.scale(scale, scale);
p.translate(-outer.center());
}
p.drawImage(outer, _around->frame());
if (scale != 1.) {
p.restore();
}
if (_aroundPlaying) {
const auto almost = (_around->frameIndex() + 1)
== _around->framesCount();
const auto marked = _around->markFrameShown();
if (almost && marked) {
_aroundPlaying = false;
}
}
}
[[nodiscard]] not_null<Ui::RpWidget*> ReactionsPreview(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> controller,
const base::flat_map<ReactionId, ReactionDisableType> &disabled,
Fn<void()> readyCallback) {
struct State {
std::vector<std::unique_ptr<ReactionPreview>> entries;
Ui::Text::String bottom;
int selected = -1;
bool readyInvoked = false;
};
const auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());
result->show();
auto &lifetime = result->lifetime();
const auto state = lifetime.make_state<State>();
result->setMouseTracking(true);
parent->sizeValue(
) | rpl::start_with_next([=] {
result->setGeometry(parent->rect());
}, result->lifetime());
using namespace HistoryView;
const auto list = controller->session().data().reactions().list(
Data::Reactions::Type::Active);
const auto count = ranges::count(list, true, &Data::Reaction::premium);
const auto rows = (count + kReactionsPerRow - 1) / kReactionsPerRow;
const auto inrowmax = rows ? ((count + rows - 1) / rows) : 1;
const auto inrowless = (inrowmax * rows - count);
const auto inrowmore = rows - inrowless;
const auto inmaxrows = inrowmore * inrowmax;
auto index = 0;
auto disableType = ReactionDisableType::None;
for (const auto &reaction : list) {
if (!reaction.premium) {
continue;
}
const auto inrow = (index < inmaxrows) ? inrowmax : (inrowmax - 1);
const auto row = (index < inmaxrows)
? (index / inrow)
: (inrowmore + ((index - inmaxrows) / inrow));
const auto column = (index < inmaxrows)
? (index % inrow)
: ((index - inmaxrows) % inrow);
++index;
if (!reaction.centerIcon || !reaction.aroundAnimation) {
continue;
}
const auto i = disabled.find(reaction.id);
const auto disable = (i != end(disabled))
? i->second
: ReactionDisableType::None;
if (disable != ReactionDisableType::None) {
disableType = disable;
}
state->entries.push_back(std::make_unique<ReactionPreview>(
controller,
reaction,
disable,
[=] { result->update(); },
QPoint(ComputeX(column, inrow), ComputeY(row, rows))));
}
const auto bottom1 = tr::lng_reaction_premium_info(tr::now);
const auto bottom2 = (disableType == ReactionDisableType::None)
? QString()
: (disableType == ReactionDisableType::Group)
? tr::lng_reaction_premium_no_group(tr::now)
: tr::lng_reaction_premium_no_channel(tr::now);
state->bottom.setText(
st::defaultTextStyle,
(bottom1 + '\n' + bottom2).trimmed());
result->paintRequest(
) | rpl::start_with_next([=] {
auto p = Painter(result);
auto effects = std::vector<Fn<void()>>();
auto ready = 0;
for (const auto &entry : state->entries) {
entry->paint(p);
if (entry->ready()) {
++ready;
}
if (entry->playsEffect()) {
effects.push_back([&] {
entry->paintEffect(p);
});
}
}
if (!state->readyInvoked
&& readyCallback
&& ready > 0
&& ready == state->entries.size()) {
state->readyInvoked = true;
readyCallback();
}
const auto padding = st::boxRowPadding;
const auto available = parent->width()
- padding.left()
- padding.right();
const auto top = st::premiumReactionInfoTop
+ ((state->bottom.maxWidth() > available)
? st::normalFont->height
: 0);
p.setPen(st::premiumButtonFg);
state->bottom.draw(
p,
padding.left(),
top,
available,
style::al_top);
for (const auto &paint : effects) {
paint();
}
}, lifetime);
const auto lookup = [=](QPoint point) {
auto index = 0;
for (const auto &entry : state->entries) {
if (entry->geometry().contains(point) && !entry->disabled()) {
return index;
}
++index;
}
return -1;
};
const auto select = [=](int index) {
const auto wasInside = (state->selected >= 0);
const auto nowInside = (index >= 0);
if (state->selected != index) {
if (wasInside) {
state->entries[state->selected]->setOver(false);
}
if (nowInside) {
state->entries[index]->setOver(true);
}
state->selected = index;
}
if (wasInside != nowInside) {
result->setCursor(nowInside
? style::cur_pointer
: style::cur_default);
}
};
result->events(
) | rpl::start_with_next([=](not_null<QEvent*> event) {
if (event->type() == QEvent::MouseButtonPress) {
const auto point = static_cast<QMouseEvent*>(event.get())->pos();
if (state->selected >= 0) {
state->entries[state->selected]->cancelAnimations();
}
if (const auto index = lookup(point); index >= 0) {
state->entries[index]->startAnimations();
}
} else if (event->type() == QEvent::MouseMove) {
const auto point = static_cast<QMouseEvent*>(event.get())->pos();
select(lookup(point));
} else if (event->type() == QEvent::Leave) {
select(-1);
}
}, lifetime);
return result;
}
[[nodiscard]] not_null<Ui::RpWidget*> GenerateDefaultPreview(
not_null<Ui::RpWidget*> parent,
not_null<Window::SessionController*> controller,
PremiumPreview section,
Fn<void()> readyCallback) {
switch (section) {
case PremiumPreview::Reactions:
return ReactionsPreview(parent, controller, {}, readyCallback);
case PremiumPreview::Stickers:
return StickersPreview(parent, controller, readyCallback);
default:
@ -1228,8 +820,6 @@ void PreviewBox(
Ui::RpWidget *content = nullptr;
Ui::RpWidget *stickersPreload = nullptr;
bool stickersPreloadReady = false;
Ui::RpWidget *reactionsPreload = nullptr;
bool reactionsPreloadReady = false;
bool preloadScheduled = false;
bool showFinished = false;
Ui::Animations::Simple animation;
@ -1292,21 +882,6 @@ void PreviewBox(
ready);
state->stickersPreload->hide();
}
if (now != PremiumPreview::Reactions && !state->reactionsPreload) {
const auto ready = [=] {
if (state->reactionsPreload) {
state->reactionsPreloadReady = true;
} else {
state->preload();
}
};
state->reactionsPreload = GenerateDefaultPreview(
outer,
controller,
PremiumPreview::Reactions,
ready);
state->reactionsPreload->hide();
}
};
switch (descriptor.section) {
@ -1315,13 +890,6 @@ void PreviewBox(
? StickerPreview(outer, controller, media, state->preload)
: StickersPreview(outer, controller, state->preload);
break;
case PremiumPreview::Reactions:
state->content = ReactionsPreview(
outer,
controller,
descriptor.disabled,
state->preload);
break;
default:
state->content = GenericPreview(
outer,
@ -1381,13 +949,6 @@ void PreviewBox(
if (base::take(state->stickersPreloadReady)) {
state->preload();
}
} else if (now == PremiumPreview::Reactions
&& state->reactionsPreload) {
state->content = base::take(state->reactionsPreload);
state->content->show();
if (base::take(state->reactionsPreloadReady)) {
state->preload();
}
} else {
state->content = GenerateDefaultPreview(
outer,
@ -1457,12 +1018,14 @@ void PreviewBox(
};
auto unlock = state->selected.value(
) | rpl::map([=](PremiumPreview section) {
return (section == PremiumPreview::Reactions)
return (section == PremiumPreview::InfiniteReactions)
? tr::lng_premium_unlock_reactions()
: (section == PremiumPreview::Stickers)
? tr::lng_premium_unlock_stickers()
: (section == PremiumPreview::AnimatedEmoji)
? tr::lng_premium_unlock_emoji()
: (section == PremiumPreview::EmojiStatus)
? tr::lng_premium_unlock_status()
: tr::lng_premium_more_about();
}) | rpl::flatten_latest();
auto button = descriptor.fromSettings
@ -1626,17 +1189,8 @@ void ShowPremiumPreviewBox(
not_null<Window::SessionController*> controller,
PremiumPreview section,
Fn<void(not_null<Ui::BoxContent*>)> shown) {
ShowPremiumPreviewBox(controller, section, {}, std::move(shown));
}
void ShowPremiumPreviewBox(
not_null<Window::SessionController*> controller,
PremiumPreview section,
const base::flat_map<ReactionId, ReactionDisableType> &disabled,
Fn<void(not_null<Ui::BoxContent*>)> shown) {
Show(controller, Descriptor{
.section = section,
.disabled = disabled,
.shownCallback = std::move(shown),
});
}

View File

@ -42,7 +42,8 @@ enum class PremiumPreview {
FasterDownload,
VoiceToText,
NoAds,
Reactions,
EmojiStatus,
InfiniteReactions,
Stickers,
AnimatedEmoji,
AdvancedChatManagement,
@ -51,23 +52,12 @@ enum class PremiumPreview {
kCount,
};
enum class ReactionDisableType {
None,
Group,
Channel,
};
void ShowPremiumPreviewBox(
not_null<Window::SessionController*> controller,
PremiumPreview section,
Fn<void(not_null<Ui::BoxContent*>)> shown = nullptr);
void ShowPremiumPreviewBox(
not_null<Window::SessionController*> controller,
PremiumPreview section,
const base::flat_map<Data::ReactionId, ReactionDisableType> &disabled,
Fn<void(not_null<Ui::BoxContent*>)> shown = nullptr);
void ShowPremiumPreviewToBuy(
not_null<Window::SessionController*> controller,
PremiumPreview section,

View File

@ -521,7 +521,9 @@ void ReactionsSettingsBox(
button->setClickedCallback([=, id = r.id] {
if (premium && !controller->session().premium()) {
ShowPremiumPreviewBox(controller, PremiumPreview::Reactions);
ShowPremiumPreviewBox(
controller,
PremiumPreview::InfiniteReactions);
return;
}
checkButton(button);

View File

@ -687,8 +687,7 @@ void EmojiListWidget::setSingleSize(QSize size) {
_innerPosition = QPoint(
(area.width() - esize) / 2,
(area.height() - esize) / 2);
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
const auto customSkip = (esize - customSize) / 2;
const auto customSkip = (esize - _customSingleSize) / 2;
_customPosition = QPoint(customSkip, customSkip);
_picker->setSingleSize(_singleSize);
}
@ -964,7 +963,9 @@ void EmojiListWidget::paint(
drawCollapsedBadge(p, w - _areaPosition, info.count);
continue;
}
if (selected && st().overBg->c.alpha() > 0) {
if (!_grabbingChosen
&& selected
&& st().overBg->c.alpha() > 0) {
auto tl = w;
if (rtl()) {
tl.setX(width() - tl.x() - st::emojiPanArea.width());
@ -1128,12 +1129,17 @@ EmojiPtr EmojiListWidget::lookupOverEmoji(const OverEmoji *over) const {
EmojiChosen EmojiListWidget::lookupChosen(
EmojiPtr emoji,
not_null<const OverEmoji*> over) {
const auto rect = emojiRect(over->section, over->index);
const auto icon = QRect(
rect.x() + (_singleSize.width() - st::stickersPremium.width()) / 2,
rect.y() + (_singleSize.height() - st::stickersPremium.height()) / 2,
rect.width(),
rect.height());
return {
.emoji = emoji,
.messageSendingFrom = {
.type = Ui::MessageSendingAnimationFrom::Type::Emoji,
.globalStartGeometry = mapToGlobal(
emojiRect(over->section, over->index)),
.globalStartGeometry = mapToGlobal(icon),
},
};
}
@ -1144,14 +1150,19 @@ FileChosen EmojiListWidget::lookupChosen(
Api::SendOptions options) {
_grabbingChosen = true;
const auto guard = gsl::finally([&] { _grabbingChosen = false; });
const auto rect = emojiRect(over->section, over->index);
const auto rect = over ? emojiRect(over->section, over->index) : QRect();
const auto emoji = over ? QRect(
rect.topLeft() + _areaPosition + _innerPosition + _customPosition,
QSize(_customSingleSize, _customSingleSize)
) : QRect();
return {
.document = custom,
.options = options,
.messageSendingFrom = {
.type = Ui::MessageSendingAnimationFrom::Type::Emoji,
.globalStartGeometry = over ? mapToGlobal(rect) : QRect(),
.frame = over ? Ui::GrabWidgetToImage(this, rect) : QImage(),
.globalStartGeometry = over ? mapToGlobal(emoji) : QRect(),
.frame = over ? Ui::GrabWidgetToImage(this, emoji) : QImage(),
},
};
}
@ -1246,7 +1257,7 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) {
break;
case Mode::FullReactions:
case Mode::RecentReactions:
Settings::ShowPremium(_controller, u"unique_reactions"_q);
Settings::ShowPremium(_controller, u"infinite_reactions"_q);
break;
case Mode::EmojiStatus:
Settings::ShowPremium(_controller, u"emoji_status"_q);

View File

@ -25,15 +25,17 @@ struct CustomEmojiId {
DocumentId id = 0;
};
enum class CustomEmojiSizeTag : uchar {
Normal,
Large,
Isolated,
kCount,
};
class CustomEmojiManager final : public base::has_weak_ptr {
public:
enum class SizeTag : uchar {
Normal,
Large,
Isolated,
kCount,
};
using SizeTag = CustomEmojiSizeTag;
CustomEmojiManager(not_null<Session*> owner);
~CustomEmojiManager();

View File

@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_media.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_web_page.h"
#include "history/view/reactions/history_view_reactions_animation.h"
#include "history/view/reactions/history_view_reactions_button.h"
#include "history/view/reactions/history_view_reactions_selector.h"
#include "history/view/history_view_message.h"
@ -49,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/delete_messages_box.h"
#include "boxes/report_messages_box.h"
#include "boxes/sticker_set_box.h"
#include "boxes/premium_preview_box.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/emoji_interactions.h"
#include "history/history_widget.h"
@ -401,7 +403,9 @@ HistoryInner::HistoryInner(
_reactionsManager->premiumPromoChosen(
) | rpl::start_with_next([=](FullMsgId context) {
_reactionsManager->updateButton({});
premiumPromoChosen(context);
ShowPremiumPreviewBox(
_controller,
PremiumPreview::InfiniteReactions);
}, lifetime());
session().data().itemRemoved(
@ -492,12 +496,6 @@ void HistoryInner::reactionChosen(const ChosenReaction &reaction) {
}
}
void HistoryInner::premiumPromoChosen(FullMsgId context) {
if (const auto item = session().data().message(context)) {
ShowPremiumPromoBox(_controller, item);
}
}
Main::Session &HistoryInner::session() const {
return _controller->session();
}
@ -2456,7 +2454,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
desiredPosition,
reactItem,
[=](ChosenReaction reaction) { reactionChosen(reaction); },
[=](FullMsgId context) { premiumPromoChosen(context); },
[=](FullMsgId context) { ShowPremiumPreviewBox(
controller,
PremiumPreview::InfiniteReactions); },
_controller->cachedReactionIconFactory().createMethod())
: AttachSelectorResult::Skipped;
if (attached == AttachSelectorResult::Failed) {

View File

@ -399,7 +399,6 @@ private:
-> HistoryView::Reactions::ButtonParameters;
void toggleFavoriteReaction(not_null<Element*> view) const;
void reactionChosen(const ChosenReaction &reaction);
void premiumPromoChosen(FullMsgId context);
void setupSharingDisallowed();
[[nodiscard]] bool hasCopyRestriction(HistoryItem *item = nullptr) const;

View File

@ -29,14 +29,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace HistoryView {
ReactionAnimationArgs ReactionAnimationArgs::translated(
QPoint point) const {
return {
.id = id,
.flyIcon = flyIcon,
.flyFrom = flyFrom.translated(point),
};
}
struct BottomInfo::Reaction {
mutable std::unique_ptr<Reactions::Animation> animation;
mutable QImage image;
ReactionId id;
QString countText;
int count = 0;
int countTextWidth = 0;
bool chosen = false;
};
BottomInfo::BottomInfo(
not_null<::Data::Reactions*> reactionsOwner,
@ -389,13 +390,19 @@ void BottomInfo::paintReactions(
}
if (!animations.empty()) {
const auto now = context.now;
context.reactionInfo->effectPaint = [=](QPainter &p) {
context.reactionInfo->effectPaint = [
now,
origin,
list = std::move(animations)
](QPainter &p) {
auto result = QRect();
for (const auto &single : animations) {
for (const auto &single : list) {
const auto area = single.animation->paintGetArea(
p,
origin,
single.target,
QColor(255, 255, 255, 0), // Colored, for emoji status.
QRect(), // Clip, for emoji status.
now);
result = result.isEmpty() ? area : result.united(area);
}
@ -553,7 +560,7 @@ void BottomInfo::setReactionCount(Reaction &reaction, int count) {
}
void BottomInfo::animateReaction(
ReactionAnimationArgs &&args,
Reactions::AnimationArgs &&args,
Fn<void()> repaint) {
const auto i = ranges::find(_reactions, args.id, &Reaction::id);
if (i == end(_reactions)) {

View File

@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "history/view/history_view_object.h"
#include "data/data_message_reaction_id.h"
#include "ui/text/text.h"
#include "base/flags.h"
@ -19,21 +18,16 @@ class AnimatedIcon;
namespace Data {
class Reactions;
struct ReactionId;
struct MessageReaction;
} // namespace Data
namespace HistoryView {
namespace Reactions {
class Animation;
struct AnimationArgs;
} // namespace Reactions
struct ReactionAnimationArgs {
::Data::ReactionId id;
QImage flyIcon;
QRect flyFrom;
[[nodiscard]] ReactionAnimationArgs translated(QPoint point) const;
};
using PaintContext = Ui::ChatPaintContext;
class Message;
@ -86,7 +80,7 @@ public:
const PaintContext &context) const;
void animateReaction(
ReactionAnimationArgs &&args,
Reactions::AnimationArgs &&args,
Fn<void()> repaint);
[[nodiscard]] auto takeReactionAnimations()
-> base::flat_map<ReactionId, std::unique_ptr<Reactions::Animation>>;
@ -95,15 +89,7 @@ public:
std::unique_ptr<Reactions::Animation>> animations);
private:
struct Reaction {
mutable std::unique_ptr<Reactions::Animation> animation;
mutable QImage image;
ReactionId id;
QString countText;
int count = 0;
int countTextWidth = 0;
bool chosen = false;
};
struct Reaction;
void layout();
void layoutDateText();

View File

@ -1116,7 +1116,7 @@ void Element::clickHandlerPressedChanged(
}
}
void Element::animateReaction(ReactionAnimationArgs &&args) {
void Element::animateReaction(Reactions::AnimationArgs &&args) {
}
void Element::animateUnreadReactions() {

View File

@ -34,24 +34,23 @@ struct ChatPaintContext;
class ChatStyle;
} // namespace Ui
namespace HistoryView::Reactions {
struct ButtonParameters;
struct AnimationArgs;
class Animation;
class InlineList;
} // namespace HistoryView::Reactions
namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
enum class PointState : char;
enum class InfoDisplayType : char;
struct ReactionAnimationArgs;
struct StateRequest;
struct TextState;
class Media;
//struct ExternalLottieInfo;
using PaintContext = Ui::ChatPaintContext;
namespace Reactions {
struct ButtonParameters;
class Animation;
class InlineList;
} // namespace Reactions
enum class Context : char {
History,
Replies,
@ -433,7 +432,7 @@ public:
[[nodiscard]] bool markSponsoredViewed(int shownFromTop) const;
virtual void animateReaction(ReactionAnimationArgs &&args);
virtual void animateReaction(Reactions::AnimationArgs &&args);
void animateUnreadReactions();
[[nodiscard]] virtual auto takeReactionAnimations()
-> base::flat_map<

View File

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item_text.h"
#include "history/view/media/history_view_media.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/reactions/history_view_reactions_animation.h"
#include "history/view/reactions/history_view_reactions_button.h"
#include "history/view/history_view_context_menu.h"
#include "history/view/history_view_element.h"
@ -44,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/chat_style.h"
#include "lang/lang_keys.h"
#include "boxes/delete_messages_box.h"
#include "boxes/premium_preview_box.h"
#include "boxes/peers/edit_participant_box.h"
#include "data/data_session.h"
#include "data/data_folder.h"
@ -390,9 +392,9 @@ ListWidget::ListWidget(
_reactionsManager->premiumPromoChosen(
) | rpl::start_with_next([=] {
_reactionsManager->updateButton({});
if (const auto item = _reactionsItem.current()) {
ShowPremiumPromoBox(_controller, item);
}
ShowPremiumPreviewBox(
_controller,
PremiumPreview::InfiniteReactions);
}, lifetime());
Reactions::SetupManagerList(

View File

@ -345,7 +345,7 @@ void Message::applyGroupAdminChanges(
}
}
void Message::animateReaction(ReactionAnimationArgs &&args) {
void Message::animateReaction(Reactions::AnimationArgs &&args) {
const auto item = message();
const auto media = this->media();

View File

@ -138,7 +138,7 @@ public:
void applyGroupAdminChanges(
const base::flat_set<UserId> &changes) override;
void animateReaction(ReactionAnimationArgs &&args) override;
void animateReaction(Reactions::AnimationArgs &&args) override;
auto takeReactionAnimations()
-> base::flat_map<
Data::ReactionId,

View File

@ -453,13 +453,18 @@ void InlineList::paint(
}
if (!animations.empty()) {
const auto now = context.now;
context.reactionInfo->effectPaint = [=](QPainter &p) {
context.reactionInfo->effectPaint = [
now,
list = std::move(animations)
](QPainter &p) {
auto result = QRect();
for (const auto &single : animations) {
for (const auto &single : list) {
const auto area = single.animation->paintGetArea(
p,
QPoint(),
single.target,
QColor(255, 255, 255, 0), // Colored, for emoji status.
QRect(), // Clip, for emoji status.
now);
result = result.isEmpty() ? area : result.united(area);
}
@ -494,7 +499,7 @@ bool InlineList::getState(
}
void InlineList::animate(
ReactionAnimationArgs &&args,
AnimationArgs &&args,
Fn<void()> repaint) {
const auto i = ranges::find(_buttons, args.id, &Button::id);
if (i == end(_buttons)) {

View File

@ -24,13 +24,13 @@ using PaintContext = Ui::ChatPaintContext;
class Message;
struct TextState;
struct UserpicInRow;
struct ReactionAnimationArgs;
} // namespace HistoryView
namespace HistoryView::Reactions {
using ::Data::ReactionId;
using ::Data::MessageReaction;
struct AnimationArgs;
class Animation;
struct InlineListData {
@ -79,7 +79,7 @@ public:
not_null<TextState*> outResult) const;
void animate(
ReactionAnimationArgs &&args,
AnimationArgs &&args,
Fn<void()> repaint);
[[nodiscard]] auto takeAnimations()
-> base::flat_map<ReactionId, std::unique_ptr<Reactions::Animation>>;

View File

@ -32,6 +32,14 @@ constexpr auto kMiniCopiesMaxScaleMax = 0.9;
} // namespace
AnimationArgs AnimationArgs::translated(QPoint point) const {
return {
.id = id,
.flyIcon = flyIcon,
.flyFrom = flyFrom.translated(point),
};
}
auto Animation::flyCallback() {
return [=] {
if (!_fly.animating()) {
@ -54,22 +62,28 @@ auto Animation::callback() {
Animation::Animation(
not_null<::Data::Reactions*> owner,
ReactionAnimationArgs &&args,
AnimationArgs &&args,
Fn<void()> repaint,
int size)
int size,
Data::CustomEmojiSizeTag customSizeTag)
: _owner(owner)
, _repaint(std::move(repaint))
, _flyFrom(args.flyFrom) {
const auto &list = owner->list(::Data::Reactions::Type::All);
auto centerIcon = (DocumentData*)nullptr;
auto centerIconSize = size;
auto aroundAnimation = (DocumentData*)nullptr;
if (const auto customId = args.id.custom()) {
centerIconSize = Ui::Text::AdjustCustomEmojiSize(st::emojiSize);
const auto esize = Data::FrameSizeFromTag(customSizeTag)
/ style::DevicePixelRatio();
const auto data = &owner->owner();
const auto document = data->document(customId);
_custom = data->customEmojiManager().create(document, callback());
_customSize = centerIconSize;
_custom = data->customEmojiManager().create(
document,
callback(),
customSizeTag);
_colored = std::make_unique<Ui::Text::CustomEmojiColored>();
_customSize = esize;
_centerSizeMultiplier = _customSize / float64(size);
aroundAnimation = owner->chooseGenericAnimation(document);
} else {
const auto i = ranges::find(list, args.id, &::Data::Reaction::id);
@ -78,6 +92,7 @@ Animation::Animation(
}
centerIcon = i->centerIcon;
aroundAnimation = i->aroundAnimation;
_centerSizeMultiplier = 1.;
}
const auto resolve = [&](
std::unique_ptr<Ui::AnimatedIcon> &icon,
@ -96,7 +111,7 @@ Animation::Animation(
});
return true;
};
if (!_custom && !resolve(_center, centerIcon, centerIconSize)) {
if (!_custom && !resolve(_center, centerIcon, size)) {
return;
}
resolve(_effect, aroundAnimation, size * 2);
@ -109,7 +124,6 @@ Animation::Animation(
} else {
startAnimations();
}
_centerSizeMultiplier = centerIconSize / float64(size);
_valid = true;
}
@ -119,21 +133,26 @@ QRect Animation::paintGetArea(
QPainter &p,
QPoint origin,
QRect target,
const QColor &colored,
QRect clip,
crl::time now) const {
if (_flyIcon.isNull()) {
paintCenterFrame(p, target, now);
const auto wide = QRect(
target.topLeft() - QPoint(target.width(), target.height()) / 2,
target.size() * 2);
if (const auto effect = _effect.get()) {
p.drawImage(wide, effect->frame());
}
paintMiniCopies(p, target.center(), now);
return _miniCopies.empty()
const auto area = _miniCopies.empty()
? wide
: QRect(
target.topLeft() - QPoint(target.width(), target.height()),
target.size() * 3);
if (clip.isEmpty() || area.intersects(clip)) {
paintCenterFrame(p, target, colored, now);
if (const auto effect = _effect.get()) {
p.drawImage(wide, effect->frame());
}
paintMiniCopies(p, target.center(), colored, now);
}
return area;
}
const auto from = _flyFrom.translated(origin);
const auto lshift = target.width() / 4;
@ -152,21 +171,24 @@ QRect Animation::paintGetArea(
anim::interpolate(from.width(), target.width(), progress),
anim::interpolate(from.height(), target.height(), progress));
const auto wide = rect.marginsAdded(margins);
if (progress < 1.) {
p.setOpacity(1. - progress);
p.drawImage(rect, _flyIcon);
if (clip.isEmpty() || wide.intersects(clip)) {
if (progress < 1.) {
p.setOpacity(1. - progress);
p.drawImage(rect, _flyIcon);
}
if (progress > 0.) {
p.setOpacity(progress);
paintCenterFrame(p, wide, colored, now);
}
p.setOpacity(1.);
}
if (progress > 0.) {
p.setOpacity(progress);
paintCenterFrame(p, wide, now);
}
p.setOpacity(1.);
return wide;
}
void Animation::paintCenterFrame(
QPainter &p,
QRect target,
const QColor &colored,
crl::time now) const {
Expects(_center || _custom);
@ -182,8 +204,10 @@ void Animation::paintCenterFrame(
p.drawImage(rect, _center->frame());
} else {
const auto scaled = (size.width() != _customSize);
_colored->color = colored;
_custom->paint(p, {
.preview = QColor(0, 0, 0, 0),
.colored = _colored.get(),
.size = { _customSize, _customSize },
.now = now,
.scale = (scaled ? (size.width() / float64(_customSize)) : 1.),
@ -198,6 +222,7 @@ void Animation::paintCenterFrame(
void Animation::paintMiniCopies(
QPainter &p,
QPoint center,
const QColor &colored,
crl::time now) const {
Expects(_miniCopies.empty() || _custom != nullptr);
@ -213,8 +238,10 @@ void Animation::paintMiniCopies(
/ float64(kMiniCopiesDurationMax);
const auto scaleOut = kMiniCopiesScaleOutDuration
/ float64(kMiniCopiesDurationMax);
_colored->color = colored;
auto context = Ui::Text::CustomEmoji::Context{
.preview = preview,
.colored = _colored.get(),
.size = size,
.now = now,
.scaled = true,

View File

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "ui/effects/animations.h"
#include "data/data_message_reaction_id.h"
namespace Ui {
class AnimatedIcon;
@ -19,21 +20,27 @@ class CustomEmoji;
namespace Data {
class Reactions;
enum class CustomEmojiSizeTag : uchar;
} // namespace Data
namespace HistoryView {
struct ReactionAnimationArgs;
} // namespace HistoryView
namespace HistoryView::Reactions {
struct AnimationArgs {
::Data::ReactionId id;
QImage flyIcon;
QRect flyFrom;
[[nodiscard]] AnimationArgs translated(QPoint point) const;
};
class Animation final {
public:
Animation(
not_null<::Data::Reactions*> owner,
ReactionAnimationArgs &&args,
AnimationArgs &&args,
Fn<void()> repaint,
int size);
int size,
Data::CustomEmojiSizeTag customSizeTag = {});
~Animation();
void setRepaintCallback(Fn<void()> repaint);
@ -41,6 +48,8 @@ public:
QPainter &p,
QPoint origin,
QRect target,
const QColor &colored,
QRect clip,
crl::time now) const;
[[nodiscard]] bool flying() const;
@ -71,14 +80,23 @@ private:
int to,
int top,
float64 progress) const;
void paintCenterFrame(QPainter &p, QRect target, crl::time now) const;
void paintMiniCopies(QPainter &p, QPoint center, crl::time now) const;
void paintCenterFrame(
QPainter &p,
QRect target,
const QColor &colored,
crl::time now) const;
void paintMiniCopies(
QPainter &p,
QPoint center,
const QColor &colored,
crl::time now) const;
void generateMiniCopies(int size);
const not_null<::Data::Reactions*> _owner;
Fn<void()> _repaint;
QImage _flyIcon;
std::unique_ptr<Ui::Text::CustomEmoji> _custom;
std::unique_ptr<Ui::Text::CustomEmojiColored> _colored;
std::unique_ptr<Ui::AnimatedIcon> _center;
std::unique_ptr<Ui::AnimatedIcon> _effect;
std::vector<MiniCopy> _miniCopies;

View File

@ -0,0 +1,195 @@
/*
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 "info/profile/info_profile_badge.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/stickers/data_custom_emoji.h"
#include "info/profile/info_profile_values.h"
#include "info/profile/info_profile_emoji_status_panel.h"
#include "lang/lang_keys.h"
#include "ui/widgets/buttons.h"
#include "main/main_session.h"
#include "styles/style_info.h"
namespace Info::Profile {
namespace {
} // namespace
Badge::Badge(
not_null<QWidget*> parent,
const style::InfoPeerBadge &st,
not_null<PeerData*> peer,
EmojiStatusPanel *emojiStatusPanel,
Fn<bool()> animationPaused,
int customStatusLoopsLimit,
base::flags<BadgeType> allowed)
: _parent(parent)
, _st(st)
, _peer(peer)
, _emojiStatusPanel(emojiStatusPanel)
, _customStatusLoopsLimit(customStatusLoopsLimit)
, _allowed(allowed)
, _animationPaused(std::move(animationPaused)) {
rpl::combine(
BadgeValue(peer),
EmojiStatusIdValue(peer)
) | rpl::start_with_next([=](BadgeType badge, DocumentId emojiStatusId) {
setBadge(badge, emojiStatusId);
}, _lifetime);
}
Ui::RpWidget *Badge::widget() const {
return _view.data();
}
void Badge::setBadge(BadgeType badge, DocumentId emojiStatusId) {
if (!(_allowed & badge)
|| (!_peer->session().premiumBadgesShown()
&& badge == BadgeType::Premium)) {
badge = BadgeType::None;
}
if (!(_allowed & badge)) {
badge = BadgeType::None;
}
if (badge != BadgeType::Premium) {
emojiStatusId = 0;
}
if (_badge == badge && _emojiStatusId == emojiStatusId) {
return;
}
_badge = badge;
_emojiStatusId = emojiStatusId;
_emojiStatus = nullptr;
_emojiStatusColored = nullptr;
_view.destroy();
if (_badge == BadgeType::None) {
_updated.fire({});
return;
}
_view.create(_parent);
_view->show();
switch (_badge) {
case BadgeType::Verified:
case BadgeType::Premium: {
if (_emojiStatusId) {
_emojiStatus = _peer->owner().customEmojiManager().create(
_emojiStatusId,
[raw = _view.data()] { raw->update(); },
sizeTag());
if (_customStatusLoopsLimit > 0) {
_emojiStatus = std::make_unique<Ui::Text::LimitedLoopsEmoji>(
std::move(_emojiStatus),
_customStatusLoopsLimit);
}
_emojiStatusColored = std::make_unique<
Ui::Text::CustomEmojiColored
>();
const auto emoji = Data::FrameSizeFromTag(sizeTag())
/ style::DevicePixelRatio();
_view->resize(emoji, emoji);
_view->paintRequest(
) | rpl::start_with_next([=, check = _view.data()]{
_emojiStatusColored->color = _st.premiumFg->c;
auto args = Ui::Text::CustomEmoji::Context{
.preview = st::windowBgOver->c,
.colored = _emojiStatusColored.get(),
.now = crl::now(),
.paused = _animationPaused && _animationPaused(),
};
if (!_emojiStatusPanel
|| !_emojiStatusPanel->paintBadgeFrame(check)) {
Painter p(check);
_emojiStatus->paint(p, args);
}
}, _view->lifetime());
} else {
const auto icon = (_badge == BadgeType::Verified)
? &_st.verified
: &_st.premium;
_view->resize(icon->size());
_view->paintRequest(
) | rpl::start_with_next([=, check = _view.data()]{
Painter p(check);
icon->paint(p, 0, 0, check->width());
}, _view->lifetime());
}
} break;
case BadgeType::Scam:
case BadgeType::Fake: {
const auto fake = (_badge == BadgeType::Fake);
const auto size = Ui::ScamBadgeSize(fake);
const auto skip = st::infoVerifiedCheckPosition.x();
_view->resize(
size.width() + 2 * skip,
size.height() + 2 * skip);
_view->paintRequest(
) | rpl::start_with_next([=, badge = _view.data()]{
Painter p(badge);
Ui::DrawScamBadge(
fake,
p,
badge->rect().marginsRemoved({ skip, skip, skip, skip }),
badge->width(),
st::attentionButtonFg);
}, _view->lifetime());
} break;
}
if (_badge != BadgeType::Premium || !_premiumClickCallback) {
_view->setAttribute(Qt::WA_TransparentForMouseEvents);
} else {
_view->setClickedCallback(_premiumClickCallback);
}
_updated.fire({});
}
void Badge::setPremiumClickCallback(Fn<void()> callback) {
_premiumClickCallback = std::move(callback);
if (_view && _badge == BadgeType::Premium) {
if (!_premiumClickCallback) {
_view->setAttribute(Qt::WA_TransparentForMouseEvents);
} else {
_view->setAttribute(Qt::WA_TransparentForMouseEvents, false);
_view->setClickedCallback(_premiumClickCallback);
}
}
}
rpl::producer<> Badge::updated() const {
return _updated.events();
}
void Badge::move(int left, int top, int bottom) {
if (!_view) {
return;
}
const auto star = !_emojiStatus
&& (_badge == BadgeType::Premium || _badge == BadgeType::Verified);
const auto fake = !_emojiStatus && !star;
const auto skip = fake ? 0 : _st.position.x();
const auto badgeLeft = left + skip;
const auto badgeTop = top
+ (star
? _st.position.y()
: (bottom - top - _view->height()) / 2);
_view->moveToLeft(badgeLeft, badgeTop);
}
Data::CustomEmojiSizeTag Badge::sizeTag() const {
using SizeTag = Data::CustomEmojiSizeTag;
return (_st.sizeTag == 2)
? SizeTag::Isolated
: (_st.sizeTag == 1)
? SizeTag::Large
: SizeTag::Normal;
}
} // namespace Info::Profile

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 "base/flags.h"
#include "base/object_ptr.h"
namespace style {
struct InfoPeerBadge;
} // namespace style
namespace Data {
enum class CustomEmojiSizeTag : uchar;
} // namespace Data
namespace Ui {
class RpWidget;
class AbstractButton;
} // namespace Ui
namespace Info::Profile {
class EmojiStatusPanel;
enum class BadgeType {
None = 0x00,
Verified = 0x01,
Premium = 0x02,
Scam = 0x04,
Fake = 0x08,
};
inline constexpr bool is_flag_type(BadgeType) { return true; }
class Badge final {
public:
Badge(
not_null<QWidget*> parent,
const style::InfoPeerBadge &st,
not_null<PeerData*> peer,
EmojiStatusPanel *emojiStatusPanel,
Fn<bool()> animationPaused,
int customStatusLoopsLimit = 0,
base::flags<BadgeType> allowed = base::flags<BadgeType>::from_raw(-1));
[[nodiscard]] Ui::RpWidget *widget() const;
void setPremiumClickCallback(Fn<void()> callback);
[[nodiscard]] rpl::producer<> updated() const;
void move(int left, int top, int bottom);
[[nodiscard]] Data::CustomEmojiSizeTag sizeTag() const;
private:
void setBadge(BadgeType badge, DocumentId emojiStatusId);
const not_null<QWidget*> _parent;
const style::InfoPeerBadge &_st;
const not_null<PeerData*> _peer;
EmojiStatusPanel *_emojiStatusPanel = nullptr;
const int _customStatusLoopsLimit = 0;
DocumentId _emojiStatusId = 0;
std::unique_ptr<Ui::Text::CustomEmoji> _emojiStatus;
std::unique_ptr<Ui::Text::CustomEmojiColored> _emojiStatusColored;
base::flags<BadgeType> _allowed;
BadgeType _badge = BadgeType();
Fn<void()> _premiumClickCallback;
Fn<bool()> _animationPaused;
object_ptr<Ui::AbstractButton> _view = { nullptr };
rpl::event_stream<> _updated;
rpl::lifetime _lifetime;
};
} // namespace Info::Profile

View File

@ -7,48 +7,29 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/profile/info_profile_cover.h"
#include "data/data_photo.h"
#include "data/data_peer_values.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "data/data_peer.h"
#include "data/data_changes.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_emoji_statuses.h"
#include "data/stickers/data_custom_emoji.h"
#include "editor/photo_editor_layer_widget.h"
#include "info/profile/info_profile_values.h"
#include "info/profile/info_profile_badge.h"
#include "info/profile/info_profile_emoji_status_panel.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "lang/lang_keys.h"
#include "menu/menu_send.h" // SendMenu::Type.
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/time_picker_box.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/special_buttons.h"
#include "ui/unread_badge.h"
#include "base/unixtime.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
#include "core/application.h"
#include "main/main_session.h"
#include "settings/settings_premium.h"
#include "apiwrap.h"
#include "mainwindow.h"
#include "api/api_peer_photo.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
#include "styles/style_chat_helpers.h"
namespace Info {
namespace Profile {
namespace Info::Profile {
namespace {
auto MembersStatusText(int count) {
@ -83,293 +64,8 @@ auto ChatStatusText(int fullCount, int onlineCount, bool isGroup) {
: tr::lng_channel_status(tr::now);
};
void PickUntilBox(not_null<Ui::GenericBox*> box, Fn<void(TimeId)> callback) {
box->setTitle(tr::lng_emoji_status_for_title());
const auto seconds = Ui::DefaultTimePickerValues();
const auto phrases = ranges::views::all(
seconds
) | ranges::views::transform(Ui::FormatMuteFor) | ranges::to_vector;
const auto pickerCallback = Ui::TimePickerBox(box, seconds, phrases, 0);
Ui::ConfirmBox(box, {
.confirmed = [=] {
callback(pickerCallback());
box->closeBox();
},
.confirmText = tr::lng_emoji_status_for_submit(),
.cancelText = tr::lng_cancel(),
});
}
} // namespace
BadgeView::BadgeView(
not_null<QWidget*> parent,
const style::InfoPeerBadge &st,
not_null<PeerData*> peer,
Fn<bool()> animationPaused,
int customStatusLoopsLimit,
base::flags<Badge> allowed)
: _parent(parent)
, _st(st)
, _peer(peer)
, _customStatusLoopsLimit(customStatusLoopsLimit)
, _allowed(allowed)
, _animationPaused(std::move(animationPaused)) {
rpl::combine(
BadgeValue(peer),
EmojiStatusIdValue(peer)
) | rpl::start_with_next([=](Badge badge, DocumentId emojiStatusId) {
setBadge(badge, emojiStatusId);
}, _lifetime);
}
Ui::RpWidget *BadgeView::widget() const {
return _view.data();
}
void BadgeView::setBadge(Badge badge, DocumentId emojiStatusId) {
if ((!_peer->session().premiumBadgesShown() && badge == Badge::Premium)
|| !(_allowed & badge)) {
badge = Badge::None;
}
if (!(_allowed & badge)) {
badge = Badge::None;
}
if (badge != Badge::Premium) {
emojiStatusId = 0;
}
if (_badge == badge && _emojiStatusId == emojiStatusId) {
return;
}
_badge = badge;
_emojiStatusId = emojiStatusId;
_emojiStatus = nullptr;
_emojiStatusColored = nullptr;
_view.destroy();
if (_badge == Badge::None) {
_updated.fire({});
return;
}
_view.create(_parent);
_view->show();
switch (_badge) {
case Badge::Verified:
case Badge::Premium: {
if (_emojiStatusId) {
using SizeTag = Data::CustomEmojiManager::SizeTag;
const auto tag = (_st.sizeTag == 2)
? SizeTag::Isolated
: (_st.sizeTag == 1)
? SizeTag::Large
: SizeTag::Normal;
_emojiStatus = _peer->owner().customEmojiManager().create(
_emojiStatusId,
[raw = _view.data()] { raw->update(); },
tag);
if (_customStatusLoopsLimit > 0) {
_emojiStatus = std::make_unique<Ui::Text::LimitedLoopsEmoji>(
std::move(_emojiStatus),
_customStatusLoopsLimit);
}
_emojiStatusColored = std::make_unique<
Ui::Text::CustomEmojiColored
>();
const auto emoji = Data::FrameSizeFromTag(tag)
/ style::DevicePixelRatio();
_view->resize(emoji, emoji);
_view->paintRequest(
) | rpl::start_with_next([=, check = _view.data()]{
Painter p(check);
_emojiStatusColored->color = _st.premiumFg->c;
_emojiStatus->paint(p, {
.preview = st::windowBgOver->c,
.colored = _emojiStatusColored.get(),
.now = crl::now(),
.paused = _animationPaused && _animationPaused(),
});
}, _view->lifetime());
} else {
const auto icon = (_badge == Badge::Verified)
? &_st.verified
: &_st.premium;
_view->resize(icon->size());
_view->paintRequest(
) | rpl::start_with_next([=, check = _view.data()]{
Painter p(check);
icon->paint(p, 0, 0, check->width());
}, _view->lifetime());
}
} break;
case Badge::Scam:
case Badge::Fake: {
const auto fake = (_badge == Badge::Fake);
const auto size = Ui::ScamBadgeSize(fake);
const auto skip = st::infoVerifiedCheckPosition.x();
_view->resize(
size.width() + 2 * skip,
size.height() + 2 * skip);
_view->paintRequest(
) | rpl::start_with_next([=, badge = _view.data()]{
Painter p(badge);
Ui::DrawScamBadge(
fake,
p,
badge->rect().marginsRemoved({ skip, skip, skip, skip }),
badge->width(),
st::attentionButtonFg);
}, _view->lifetime());
} break;
}
if (_badge != Badge::Premium || !_premiumClickCallback) {
_view->setAttribute(Qt::WA_TransparentForMouseEvents);
} else {
_view->setClickedCallback(_premiumClickCallback);
}
_updated.fire({});
}
void BadgeView::setPremiumClickCallback(Fn<void()> callback) {
_premiumClickCallback = std::move(callback);
if (_view && _badge == Badge::Premium) {
if (!_premiumClickCallback) {
_view->setAttribute(Qt::WA_TransparentForMouseEvents);
} else {
_view->setAttribute(Qt::WA_TransparentForMouseEvents, false);
_view->setClickedCallback(_premiumClickCallback);
}
}
}
rpl::producer<> BadgeView::updated() const {
return _updated.events();
}
void BadgeView::move(int left, int top, int bottom) {
if (!_view) {
return;
}
const auto star = !_emojiStatus
&& (_badge == Badge::Premium || _badge == Badge::Verified);
const auto fake = !_emojiStatus && !star;
const auto skip = fake ? 0 : _st.position.x();
const auto badgeLeft = left + skip;
const auto badgeTop = top
+ (star
? _st.position.y()
: (bottom - top - _view->height()) / 2);
_view->moveToLeft(badgeLeft, badgeTop);
}
void EmojiStatusPanel::show(
not_null<Window::SessionController*> controller,
not_null<QWidget*> button) {
const auto self = controller->session().user();
const auto &statuses = controller->session().data().emojiStatuses();
const auto &recent = statuses.list(Data::EmojiStatuses::Type::Recent);
const auto &other = statuses.list(Data::EmojiStatuses::Type::Default);
auto list = statuses.list(Data::EmojiStatuses::Type::Colored);
list.insert(begin(list), 0);
list.reserve(list.size() + recent.size() + other.size() + 1);
for (const auto &id : ranges::views::concat(recent, other)) {
if (!ranges::contains(list, id)) {
list.push_back(id);
}
}
if (!ranges::contains(list, self->emojiStatusId())) {
list.push_back(self->emojiStatusId());
}
if (!_panel) {
create(controller);
const auto weak = Ui::MakeWeak(button.get());
_panel->shownValue(
) | rpl::filter([=](bool shown) {
return !shown && weak;
}) | rpl::start_with_next([=] {
button->removeEventFilter(_panel.get());
}, _panel->lifetime());
}
_panel->selector()->provideRecentEmoji(list);
const auto parent = _panel->parentWidget();
const auto global = button->mapToGlobal(QPoint());
const auto local = parent->mapFromGlobal(global);
_panel->moveTopRight(
local.y() + button->height(),
local.x() + button->width() * 3);
_panel->toggleAnimated();
button->installEventFilter(_panel.get());
}
void EmojiStatusPanel::create(
not_null<Window::SessionController*> controller) {
using Selector = ChatHelpers::TabbedSelector;
_panel = base::make_unique_q<ChatHelpers::TabbedPanel>(
controller->window().widget()->bodyWidget(),
controller,
object_ptr<Selector>(
nullptr,
controller,
Window::GifPauseReason::Layer,
ChatHelpers::TabbedSelector::Mode::EmojiStatus));
_panel->setDropDown(true);
_panel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
_panel->hide();
_panel->selector()->setAllowEmojiWithoutPremium(false);
struct Chosen {
DocumentId id = 0;
TimeId until = 0;
Ui::MessageSendingAnimationFrom animation;
};
_panel->selector()->contextMenuRequested(
) | rpl::start_with_next([=] {
_panel->selector()->showMenuWithType(SendMenu::Type::Scheduled);
}, _panel->lifetime());
auto statusChosen = _panel->selector()->customEmojiChosen(
) | rpl::map([=](Selector::FileChosen data) {
return Chosen{
.id = data.document->id,
.until = data.options.scheduled,
.animation = data.messageSendingFrom,
};
});
auto emojiChosen = _panel->selector()->emojiChosen(
) | rpl::map([=](Selector::EmojiChosen data) {
return Chosen{ .animation = data.messageSendingFrom };
});
rpl::merge(
std::move(statusChosen),
std::move(emojiChosen)
) | rpl::start_with_next([=](const Chosen chosen) {
if (chosen.until == ChatHelpers::TabbedSelector::kPickCustomTimeId) {
controller->show(Box(PickUntilBox, [=](TimeId seconds) {
controller->session().data().emojiStatuses().set(
chosen.id,
base::unixtime::now() + seconds);
}));
} else {
controller->session().data().emojiStatuses().set(
chosen.id,
chosen.until);
_panel->hideAnimated();
}
}, _panel->lifetime());
_panel->selector()->showPromoForPremiumEmoji();
}
Cover::Cover(
QWidget *parent,
not_null<PeerData*> peer,
@ -393,14 +89,19 @@ Cover::Cover(
+ st::infoProfilePhotoBottom)
, _controller(controller)
, _peer(peer)
, _emojiStatusPanel(peer->isSelf()
? std::make_unique<EmojiStatusPanel>()
: nullptr)
, _badge(
this,
st::infoPeerBadge,
peer,
[=] {
return controller->isGifPausedAtLeastFor(
Window::GifPauseReason::Layer);
})
std::make_unique<Badge>(
this,
st::infoPeerBadge,
peer,
_emojiStatusPanel.get(),
[=] {
return controller->isGifPausedAtLeastFor(
Window::GifPauseReason::Layer);
}))
, _userpic(
this,
controller,
@ -423,14 +124,14 @@ Cover::Cover(
_status->setAttribute(Qt::WA_TransparentForMouseEvents);
}
_badge.setPremiumClickCallback([=] {
if (_peer->isSelf()) {
_emojiStatusPanel.show(_controller, _badge.widget());
_badge->setPremiumClickCallback([=] {
if (const auto panel = _emojiStatusPanel.get()) {
panel->show(_controller, _badge->widget(), _badge->sizeTag());
} else {
::Settings::ShowEmojiStatusPremium(_controller, _peer);
}
});
_badge.updated() | rpl::start_with_next([=] {
_badge->updated() | rpl::start_with_next([=] {
refreshNameGeometry(width());
}, _name->lifetime());
@ -563,15 +264,15 @@ void Cover::refreshNameGeometry(int newWidth) {
auto nameWidth = newWidth
- nameLeft
- st::infoProfileNameRight;
if (const auto width = _badge.widget() ? _badge.widget()->width() : 0) {
nameWidth -= st::infoVerifiedCheckPosition.x() + width;
if (const auto widget = _badge->widget()) {
nameWidth -= st::infoVerifiedCheckPosition.x() + widget->width();
}
_name->resizeToNaturalWidth(nameWidth);
_name->moveToLeft(nameLeft, nameTop, newWidth);
const auto badgeLeft = nameLeft + _name->width();
const auto badgeTop = nameTop;
const auto badgeBottom = nameTop + _name->height();
_badge.move(badgeLeft, badgeTop, badgeBottom);
_badge->move(badgeLeft, badgeTop, badgeBottom);
}
void Cover::refreshStatusGeometry(int newWidth) {
@ -585,5 +286,4 @@ void Cover::refreshStatusGeometry(int newWidth) {
newWidth);
}
} // namespace Profile
} // namespace Info
} // namespace Info::Profile

View File

@ -8,25 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "ui/wrap/padding_wrap.h"
#include "ui/widgets/checkbox.h"
#include "base/timer.h"
#include "base/flags.h"
namespace Window {
class SessionController;
} // namespace Window
namespace style {
struct InfoToggle;
struct InfoPeerBadge;
} // namespace style
namespace ChatHelpers {
class TabbedPanel;
} // namespace ChatHelpers
namespace Ui {
class AbstractButton;
class UserpicButton;
class FlatLabel;
template <typename Widget>
@ -42,66 +30,10 @@ class Controller;
class Section;
} // namespace Info
namespace Info {
namespace Profile {
namespace Info::Profile {
enum class Badge {
None = 0x00,
Verified = 0x01,
Premium = 0x02,
Scam = 0x04,
Fake = 0x08,
};
inline constexpr bool is_flag_type(Badge) { return true; }
class BadgeView final {
public:
BadgeView(
not_null<QWidget*> parent,
const style::InfoPeerBadge &st,
not_null<PeerData*> peer,
Fn<bool()> animationPaused,
int customStatusLoopsLimit = 0,
base::flags<Badge> allowed = base::flags<Badge>::from_raw(-1));
[[nodiscard]] Ui::RpWidget *widget() const;
void setPremiumClickCallback(Fn<void()> callback);
[[nodiscard]] rpl::producer<> updated() const;
void move(int left, int top, int bottom);
private:
void setBadge(Badge badge, DocumentId emojiStatusId);
const not_null<QWidget*> _parent;
const style::InfoPeerBadge &_st;
const not_null<PeerData*> _peer;
const int _customStatusLoopsLimit = 0;
DocumentId _emojiStatusId = 0;
std::unique_ptr<Ui::Text::CustomEmoji> _emojiStatus;
std::unique_ptr<Ui::Text::CustomEmojiColored> _emojiStatusColored;
base::flags<Badge> _allowed;
Badge _badge = Badge();
Fn<void()> _premiumClickCallback;
Fn<bool()> _animationPaused;
object_ptr<Ui::AbstractButton> _view = { nullptr };
rpl::event_stream<> _updated;
rpl::lifetime _lifetime;
};
class EmojiStatusPanel final {
public:
void show(
not_null<Window::SessionController*> controller,
not_null<QWidget*> button);
private:
void create(not_null<Window::SessionController*> controller);
base::unique_qptr<ChatHelpers::TabbedPanel> _panel;
};
class EmojiStatusPanel;
class Badge;
class Cover final : public Ui::FixedHeightWidget {
public:
@ -114,15 +46,14 @@ public:
not_null<PeerData*> peer,
not_null<Window::SessionController*> controller,
rpl::producer<QString> title);
~Cover();
Cover *setOnlineCount(rpl::producer<int> &&count);
rpl::producer<Section> showSection() const {
[[nodiscard]] rpl::producer<Section> showSection() const {
return _showSection.events();
}
~Cover();
private:
void setupChildGeometry();
void initViewers(rpl::producer<QString> title);
@ -133,8 +64,8 @@ private:
const not_null<Window::SessionController*> _controller;
const not_null<PeerData*> _peer;
BadgeView _badge;
EmojiStatusPanel _emojiStatusPanel;
const std::unique_ptr<EmojiStatusPanel> _emojiStatusPanel;
const std::unique_ptr<Badge> _badge;
int _onlineCount = 0;
object_ptr<Ui::UserpicButton> _userpic;
@ -147,5 +78,4 @@ private:
};
} // namespace Profile
} // namespace Info
} // namespace Info::Profile

View File

@ -0,0 +1,322 @@
/*
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 "info/profile/info_profile_emoji_status_panel.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_emoji_statuses.h"
#include "data/stickers/data_custom_emoji.h"
#include "history/view/reactions/history_view_reactions_animation.h"
#include "lang/lang_keys.h"
#include "menu/menu_send.h" // SendMenu::Type.
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/time_picker_box.h"
#include "ui/text/format_values.h"
#include "base/unixtime.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
#include "main/main_session.h"
#include "mainwindow.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
#include "styles/style_chat.h"
namespace Info::Profile {
namespace {
void PickUntilBox(not_null<Ui::GenericBox*> box, Fn<void(TimeId)> callback) {
box->setTitle(tr::lng_emoji_status_for_title());
const auto seconds = Ui::DefaultTimePickerValues();
const auto phrases = ranges::views::all(
seconds
) | ranges::views::transform(Ui::FormatMuteFor) | ranges::to_vector;
const auto pickerCallback = Ui::TimePickerBox(box, seconds, phrases, 0);
Ui::ConfirmBox(box, {
.confirmed = [=] {
callback(pickerCallback());
box->closeBox();
},
.confirmText = tr::lng_emoji_status_for_submit(),
.cancelText = tr::lng_cancel(),
});
}
} // namespace
class EmojiStatusPanel::Animation {
public:
Animation(
not_null<Ui::RpWidget*> body,
not_null<Data::Reactions*> owner,
HistoryView::Reactions::AnimationArgs &&args,
Fn<void()> repaint,
Data::CustomEmojiSizeTag tag);
[[nodiscard]] not_null<Ui::RpWidget*> layer();
[[nodiscard]] bool finished() const;
void repaint();
bool paintBadgeFrame(not_null<Ui::RpWidget*> widget);
private:
const int _flySize = 0;
HistoryView::Reactions::Animation _fly;
Ui::RpWidget _layer;
QRect _area;
bool _areaUpdated = false;
QPointer<Ui::RpWidget> _target;
};
[[nodiscard]] int ComputeFlySize(Data::CustomEmojiSizeTag tag) {
using Tag = Data::CustomEmojiSizeTag;
if (tag == Tag::Normal) {
return st::reactionInlineImage;
}
return int(base::SafeRound(
(st::reactionInlineImage * Data::FrameSizeFromTag(tag)
/ float64(Data::FrameSizeFromTag(Tag::Normal)))));
}
EmojiStatusPanel::Animation::Animation(
not_null<Ui::RpWidget*> body,
not_null<Data::Reactions*> owner,
HistoryView::Reactions::AnimationArgs &&args,
Fn<void()> repaint,
Data::CustomEmojiSizeTag tag)
: _flySize(ComputeFlySize(tag))
, _fly(
owner,
std::move(args),
std::move(repaint),
_flySize,
tag)
, _layer(body) {
body->sizeValue() | rpl::start_with_next([=](QSize size) {
_layer.setGeometry(QRect(QPoint(), size));
}, _layer.lifetime());
_layer.paintRequest(
) | rpl::start_with_next([=](QRect clip) {
const auto target = _target.data();
if (!target || !target->isVisible()) {
return;
}
auto p = QPainter(&_layer);
const auto rect = Ui::MapFrom(&_layer, target, target->rect());
const auto skipx = (rect.width() - _flySize) / 2;
const auto skipy = (rect.height() - _flySize) / 2;
const auto area = _fly.paintGetArea(
p,
QPoint(),
QRect(
rect.topLeft() + QPoint(skipx, skipy),
QSize(_flySize, _flySize)),
st::infoPeerBadge.premiumFg->c,
clip,
crl::now());
if (_areaUpdated || _area.isEmpty()) {
_area = area;
} else {
_area = _area.united(area);
}
}, _layer.lifetime());
_layer.setAttribute(Qt::WA_TransparentForMouseEvents);
_layer.show();
}
not_null<Ui::RpWidget*> EmojiStatusPanel::Animation::layer() {
return &_layer;
}
bool EmojiStatusPanel::Animation::finished() const {
if (const auto target = _target.data()) {
return _fly.finished() || !target->isVisible();
}
return true;
}
void EmojiStatusPanel::Animation::repaint() {
if (_area.isEmpty()) {
_layer.update();
} else {
_layer.update(_area);
_areaUpdated = true;
}
}
bool EmojiStatusPanel::Animation::paintBadgeFrame(
not_null<Ui::RpWidget*> widget) {
_target = widget;
return !_fly.finished();
}
EmojiStatusPanel::EmojiStatusPanel() = default;
EmojiStatusPanel::~EmojiStatusPanel() = default;
void EmojiStatusPanel::show(
not_null<Window::SessionController*> controller,
not_null<QWidget*> button,
Data::CustomEmojiSizeTag animationSizeTag) {
const auto self = controller->session().user();
const auto &statuses = controller->session().data().emojiStatuses();
const auto &recent = statuses.list(Data::EmojiStatuses::Type::Recent);
const auto &other = statuses.list(Data::EmojiStatuses::Type::Default);
auto list = statuses.list(Data::EmojiStatuses::Type::Colored);
list.insert(begin(list), 0);
list.reserve(list.size() + recent.size() + other.size() + 1);
for (const auto &id : ranges::views::concat(recent, other)) {
if (!ranges::contains(list, id)) {
list.push_back(id);
}
}
if (!ranges::contains(list, self->emojiStatusId())) {
list.push_back(self->emojiStatusId());
}
if (!_panel) {
create(controller);
_panel->shownValue(
) | rpl::filter([=] {
return (_panelButton != nullptr);
}) | rpl::start_with_next([=](bool shown) {
if (shown) {
_panelButton->installEventFilter(_panel.get());
} else {
_panelButton->removeEventFilter(_panel.get());
}
}, _panel->lifetime());
}
if (const auto previous = _panelButton.data()) {
if (previous != button) {
previous->removeEventFilter(_panel.get());
}
}
_panelButton = button;
_animationSizeTag = animationSizeTag;
_panel->selector()->provideRecentEmoji(list);
const auto parent = _panel->parentWidget();
const auto global = button->mapToGlobal(QPoint());
const auto local = parent->mapFromGlobal(global);
_panel->moveTopRight(
local.y() + button->height(),
local.x() + button->width() * 3);
_panel->toggleAnimated();
}
bool EmojiStatusPanel::paintBadgeFrame(not_null<Ui::RpWidget*> widget) {
if (!_animation) {
return false;
} else if (_animation->paintBadgeFrame(widget)) {
return true;
}
InvokeQueued(_animation->layer(), [=] { _animation = nullptr; });
return false;
}
void EmojiStatusPanel::create(
not_null<Window::SessionController*> controller) {
using Selector = ChatHelpers::TabbedSelector;
const auto body = controller->window().widget()->bodyWidget();
_panel = base::make_unique_q<ChatHelpers::TabbedPanel>(
body,
controller,
object_ptr<Selector>(
nullptr,
controller,
Window::GifPauseReason::Layer,
ChatHelpers::TabbedSelector::Mode::EmojiStatus));
_panel->setDropDown(true);
_panel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
_panel->hide();
_panel->selector()->setAllowEmojiWithoutPremium(false);
struct Chosen {
DocumentId id = 0;
TimeId until = 0;
Ui::MessageSendingAnimationFrom animation;
};
_panel->selector()->contextMenuRequested(
) | rpl::start_with_next([=] {
_panel->selector()->showMenuWithType(SendMenu::Type::Scheduled);
}, _panel->lifetime());
auto statusChosen = _panel->selector()->customEmojiChosen(
) | rpl::map([=](Selector::FileChosen data) {
return Chosen{
.id = data.document->id,
.until = data.options.scheduled,
.animation = data.messageSendingFrom,
};
});
auto emojiChosen = _panel->selector()->emojiChosen(
) | rpl::map([=](Selector::EmojiChosen data) {
return Chosen{ .animation = data.messageSendingFrom };
});
const auto set = [=](Chosen chosen) {
Expects(chosen.until != Selector::kPickCustomTimeId);
const auto owner = &controller->session().data();
startAnimation(owner, body, chosen.id, chosen.animation);
owner->emojiStatuses().set(chosen.id, chosen.until);
};
rpl::merge(
std::move(statusChosen),
std::move(emojiChosen)
) | rpl::start_with_next([=](const Chosen chosen) {
if (chosen.until == Selector::kPickCustomTimeId) {
controller->show(Box(PickUntilBox, [=](TimeId seconds) {
set({ chosen.id, base::unixtime::now() + seconds });
}));
} else {
set(chosen);
_panel->hideAnimated();
}
}, _panel->lifetime());
_panel->selector()->showPromoForPremiumEmoji();
}
void EmojiStatusPanel::startAnimation(
not_null<Data::Session*> owner,
not_null<Ui::RpWidget*> body,
DocumentId statusId,
Ui::MessageSendingAnimationFrom from) {
if (!_panelButton) {
return;
}
auto args = HistoryView::Reactions::AnimationArgs{
.id = { { statusId } },
.flyIcon = from.frame,
.flyFrom = body->mapFromGlobal(from.globalStartGeometry),
};
_animation = std::make_unique<Animation>(
body,
&owner->reactions(),
std::move(args),
[=] { _animation->repaint(); },
_animationSizeTag);
}
} // namespace Info::Profile

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 "base/unique_qptr.h"
namespace Data {
class Session;
enum class CustomEmojiSizeTag : uchar;
} // namespace Data
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
struct MessageSendingAnimationFrom;
class RpWidget;
} // namespace Ui
namespace Ui::Text {
class CustomEmoji;
struct CustomEmojiPaintContext;
} // namespace Ui::Text
namespace ChatHelpers {
class TabbedPanel;
} // namespace ChatHelpers
namespace Info::Profile {
class EmojiStatusPanel final {
public:
EmojiStatusPanel();
~EmojiStatusPanel();
void show(
not_null<Window::SessionController*> controller,
not_null<QWidget*> button,
Data::CustomEmojiSizeTag animationSizeTag = {});
bool paintBadgeFrame(not_null<Ui::RpWidget*> widget);
private:
class Animation;
void create(not_null<Window::SessionController*> controller);
void startAnimation(
not_null<Data::Session*> owner,
not_null<Ui::RpWidget*> body,
DocumentId statusId,
Ui::MessageSendingAnimationFrom from);
base::unique_qptr<ChatHelpers::TabbedPanel> _panel;
QPointer<QWidget> _panelButton;
std::unique_ptr<Animation> _animation;
Data::CustomEmojiSizeTag _animationSizeTag = {};
};
} // namespace Info::Profile

View File

@ -93,12 +93,12 @@ object_ptr<Ui::RpWidget> InnerWidget::setupContent(
) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
auto min = (request.ymin < 0)
? request.ymin
: mapFromGlobal(_members->mapToGlobal({ 0, request.ymin })).y();
: MapFrom(this, _members, QPoint(0, request.ymin)).y();
auto max = (request.ymin < 0)
? mapFromGlobal(_members->mapToGlobal({ 0, 0 })).y()
? MapFrom(this, _members, QPoint()).y()
: (request.ymax < 0)
? request.ymax
: mapFromGlobal(_members->mapToGlobal({ 0, request.ymax })).y();
: MapFrom(this, _members, QPoint(0, request.ymax)).y();
_scrollToRequests.fire({ min, max });
}, _members->lifetime());
_cover->setOnlineCount(_members->onlineCountValue());

View File

@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "info/profile/info_profile_values.h"
#include "info/profile/info_profile_cover.h"
#include "info/profile/info_profile_badge.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "countries/countries_instance.h"
@ -456,7 +456,7 @@ rpl::producer<int> FullReactionsCountValue(
}
template <typename Flag, typename Peer>
rpl::producer<Badge> BadgeValueFromFlags(Peer peer) {
rpl::producer<BadgeType> BadgeValueFromFlags(Peer peer) {
return rpl::combine(
Data::PeerFlagsValue(
peer,
@ -464,24 +464,24 @@ rpl::producer<Badge> BadgeValueFromFlags(Peer peer) {
Data::PeerPremiumValue(peer)
) | rpl::map([=](base::flags<Flag> value, bool premium) {
return (value & Flag::Scam)
? Badge::Scam
? BadgeType::Scam
: (value & Flag::Fake)
? Badge::Fake
? BadgeType::Fake
: (value & Flag::Verified)
? Badge::Verified
? BadgeType::Verified
: premium
? Badge::Premium
: Badge::None;
? BadgeType::Premium
: BadgeType::None;
});
}
rpl::producer<Badge> BadgeValue(not_null<PeerData*> peer) {
rpl::producer<BadgeType> BadgeValue(not_null<PeerData*> peer) {
if (const auto user = peer->asUser()) {
return BadgeValueFromFlags<UserDataFlag>(user);
} else if (const auto channel = peer->asChannel()) {
return BadgeValueFromFlags<ChannelDataFlag>(channel);
}
return rpl::single(Badge::None);
return rpl::single(BadgeType::None);
}
rpl::producer<DocumentId> EmojiStatusIdValue(not_null<PeerData*> peer) {

View File

@ -90,8 +90,8 @@ rpl::producer<not_null<PeerData*>> MigratedOrMeValue(
[[nodiscard]] rpl::producer<int> FullReactionsCountValue(
not_null<Main::Session*> peer);
enum class Badge;
[[nodiscard]] rpl::producer<Badge> BadgeValue(not_null<PeerData*> peer);
enum class BadgeType;
[[nodiscard]] rpl::producer<BadgeType> BadgeValue(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<DocumentId> EmojiStatusIdValue(
not_null<PeerData*> peer);

View File

@ -36,7 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_premium_limits.h"
#include "dialogs/ui/dialogs_layout.h"
#include "info/profile/info_profile_values.h"
#include "info/profile/info_profile_cover.h"
#include "info/profile/info_profile_badge.h"
#include "lang/lang_keys.h"
#include "main/main_account.h"
#include "main/main_session.h"
@ -82,7 +82,7 @@ private:
rpl::event_stream<int> _premiumWidth;
QPointer<Ui::RpWidget> _unread;
Info::Profile::BadgeView _badge;
Info::Profile::Badge _badge;
};
@ -99,9 +99,10 @@ ComposedBadge::ComposedBadge(
this,
st::settingsInfoPeerBadge,
session->user(),
nullptr,
std::move(animationPaused),
kPlayStatusLimit,
Info::Profile::Badge::Premium) {
Info::Profile::BadgeType::Premium) {
if (hasUnread) {
_unread = CreateUnread(this, rpl::single(
rpl::empty

View File

@ -30,7 +30,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/special_buttons.h"
#include "info/profile/info_profile_cover.h"
#include "info/profile/info_profile_badge.h"
#include "info/profile/info_profile_emoji_status_panel.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "data/data_cloud_themes.h"
@ -85,8 +86,8 @@ private:
const not_null<Window::SessionController*> _controller;
const not_null<UserData*> _user;
Info::Profile::BadgeView _badge;
Info::Profile::EmojiStatusPanel _emojiStatusPanel;
Info::Profile::Badge _badge;
object_ptr<Ui::UserpicButton> _userpic;
object_ptr<Ui::FlatLabel> _name = { nullptr };
@ -110,12 +111,13 @@ Cover::Cover(
this,
st::infoPeerBadge,
user,
&_emojiStatusPanel,
[=] {
return controller->isGifPausedAtLeastFor(
Window::GifPauseReason::Layer);
},
0, // customStatusLoopsLimit
Info::Profile::Badge::Premium)
Info::Profile::BadgeType::Premium)
, _userpic(
this,
controller,
@ -145,7 +147,10 @@ Cover::Cover(
}, _userpic->lifetime());
_badge.setPremiumClickCallback([=] {
_emojiStatusPanel.show(_controller, _badge.widget());
_emojiStatusPanel.show(
_controller,
_badge.widget(),
_badge.sizeTag());
});
_badge.updated() | rpl::start_with_next([=] {
refreshNameGeometry(width());

View File

@ -208,7 +208,8 @@ using Order = std::vector<QString>;
u"faster_download"_q,
u"voice_to_text"_q,
u"no_ads"_q,
u"unique_reactions"_q,
u"emoji_status"_q,
u"infinite_reactions"_q,
u"premium_stickers"_q,
u"animated_emoji"_q,
u"advanced_chat_management"_q,
@ -264,12 +265,21 @@ using Order = std::vector<QString>;
},
},
{
u"unique_reactions"_q,
u"emoji_status"_q,
Entry{
&st::settingsPremiumIconLike,
tr::lng_premium_summary_subtitle_unique_reactions(),
tr::lng_premium_summary_about_unique_reactions(),
PremiumPreview::Reactions,
tr::lng_premium_summary_subtitle_emoji_status(),
tr::lng_premium_summary_about_emoji_status(),
PremiumPreview::EmojiStatus,
},
},
{
u"infinite_reactions"_q,
Entry{
&st::settingsPremiumIconLike,
tr::lng_premium_summary_subtitle_infinite_reactions(),
tr::lng_premium_summary_about_infinite_reactions(),
PremiumPreview::InfiniteReactions,
},
},
{

View File

@ -343,26 +343,6 @@ bool ShowSendPremiumError(
return true;
}
[[nodiscard]] auto ExtractDisabledReactions(
not_null<PeerData*> peer,
const std::vector<Data::Reaction> &list)
-> base::flat_map<Data::ReactionId, ReactionDisableType> {
auto result = base::flat_map<Data::ReactionId, ReactionDisableType>();
const auto type = peer->isBroadcast()
? ReactionDisableType::Channel
: ReactionDisableType::Group;
const auto &allowed = Data::PeerAllowedReactions(peer);
if (!allowed.some.empty()) {
for (const auto &reaction : list) {
if (reaction.premium
&& !ranges::contains(allowed.some, reaction.id)) {
result.emplace(reaction.id, type);
}
}
}
return result;
}
bool ShowReactPremiumError(
not_null<SessionController*> controller,
not_null<HistoryItem*> item,
@ -379,22 +359,8 @@ bool ShowReactPremiumError(
return false;
}
}
ShowPremiumPreviewBox(
controller,
PremiumPreview::Reactions,
ExtractDisabledReactions(item->history()->peer, list));
ShowPremiumPreviewBox(controller, PremiumPreview::InfiniteReactions);
return true;
}
void ShowPremiumPromoBox(
not_null<SessionController*> controller,
not_null<HistoryItem*> item) {
const auto &list = controller->session().data().reactions().list(
Data::Reactions::Type::Active);
ShowPremiumPreviewBox(
controller,
PremiumPreview::Reactions,
ExtractDisabledReactions(item->history()->peer, list));
}
} // namespace Window

View File

@ -219,8 +219,4 @@ private:
not_null<HistoryItem*> item,
const Data::ReactionId &id);
void ShowPremiumPromoBox(
not_null<SessionController*> controller,
not_null<HistoryItem*> item);
} // namespace Window

View File

@ -34,7 +34,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_common.h"
#include "settings/settings_calls.h"
#include "settings/settings_information.h"
#include "info/profile/info_profile_cover.h"
#include "info/profile/info_profile_badge.h"
#include "info/profile/info_profile_emoji_status_panel.h"
#include "base/qt_signal_producer.h"
#include "boxes/about_box.h"
#include "ui/boxes/confirm_box.h"
@ -338,14 +339,15 @@ MainMenu::MainMenu(
Ui::UserpicButton::Role::Custom,
st::mainMenuUserpic)
, _toggleAccounts(this)
, _badge(std::make_unique<Info::Profile::BadgeView>(
, _emojiStatusPanel(std::make_unique<Info::Profile::EmojiStatusPanel>())
, _badge(std::make_unique<Info::Profile::Badge>(
this,
st::settingsInfoPeerBadge,
controller->session().user(),
_emojiStatusPanel.get(),
[=] { return controller->isGifPausedAtLeastFor(GifPauseReason::Layer); },
kPlayStatusLimit,
Info::Profile::Badge::Premium))
, _emojiStatusPanel(std::make_unique<Info::Profile::EmojiStatusPanel>())
Info::Profile::BadgeType::Premium))
, _scroll(this, st::defaultSolidScroll)
, _inner(_scroll->setOwnedWidget(
object_ptr<Ui::VerticalLayout>(_scroll.data())))
@ -438,7 +440,10 @@ MainMenu::MainMenu(
moveBadge();
}, lifetime());
_badge->setPremiumClickCallback([=] {
_emojiStatusPanel->show(_controller, _badge->widget());
_emojiStatusPanel->show(
_controller,
_badge->widget(),
_badge->sizeTag());
});
_controller->session().downloaderTaskFinished(

View File

@ -29,7 +29,7 @@ class SlideWrap;
} // namespace Ui
namespace Info::Profile {
class BadgeView;
class Badge;
class EmojiStatusPanel;
} // namespace Info::Profile
@ -77,8 +77,8 @@ private:
Ui::Text::String _name;
int _nameVersion = 0;
object_ptr<ToggleAccountsButton> _toggleAccounts;
std::unique_ptr<Info::Profile::BadgeView> _badge;
std::unique_ptr<Info::Profile::EmojiStatusPanel> _emojiStatusPanel;
std::unique_ptr<Info::Profile::Badge> _badge;
object_ptr<ResetScaleButton> _resetScaleButton = { nullptr };
object_ptr<Ui::ScrollArea> _scroll;
not_null<Ui::VerticalLayout*> _inner;

@ -1 +1 @@
Subproject commit 4ec399f169e9308cd5da194b6fa2104578c39e45
Subproject commit 12b8b3804c1b458c124ca150de4320e3fda5035e