diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 7d8e4125c2..846b14aef9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -790,6 +790,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_color_about" = "You can choose a color to tint your name, the links you send, and replies to your messages."; "lng_settings_color_about_channel" = "You can choose a color to tint your channel's name, the links it sends, and replies to its messages."; "lng_settings_color_emoji" = "Add icons to replies"; +"lng_settings_color_emoji_remove" = "Remove icon"; "lng_settings_color_emoji_off" = "Off"; "lng_settings_color_emoji_about" = "Make replies to your messages stand out by adding custom patterns to them."; "lng_settings_color_emoji_about_channel" = "Make replies to your channel's messages stand out by adding custom patterns to them."; diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp index 71512172b1..25a7065873 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.cpp +++ b/Telegram/SourceFiles/api/api_peer_photo.cpp @@ -510,40 +510,57 @@ void PeerPhoto::requestUserPhotos( _userPhotosRequests.emplace(user, requestId); } +auto PeerPhoto::emojiList(EmojiListType type) -> EmojiListData & { + switch (type) { + case EmojiListType::Profile: return _profileEmojiList; + case EmojiListType::Group: return _groupEmojiList; + case EmojiListType::Background: return _backgroundEmojiList; + } + Unexpected("Type in PeerPhoto::emojiList."); +} + +auto PeerPhoto::emojiList(EmojiListType type) const +-> const EmojiListData & { + return const_cast(this)->emojiList(type); +} + void PeerPhoto::requestEmojiList(EmojiListType type) { - if (_requestIdEmojiList) { + auto &list = emojiList(type); + if (list.requestId) { return; } - const auto isGroup = (type == EmojiListType::Group); - const auto d = [=](const MTPEmojiList &result) { - _requestIdEmojiList = 0; - result.match([](const MTPDemojiListNotModified &data) { - }, [&](const MTPDemojiList &data) { - auto &list = isGroup ? _profileEmojiList : _groupEmojiList; - list = ranges::views::all( - data.vdocument_id().v - ) | ranges::views::transform(&MTPlong::v) | ranges::to_vector; - }); + const auto send = [&](auto &&request) { + return _api.request( + std::move(request) + ).done([=](const MTPEmojiList &result) { + auto &list = emojiList(type); + list.requestId = 0; + result.match([](const MTPDemojiListNotModified &data) { + }, [&](const MTPDemojiList &data) { + list.list = ranges::views::all( + data.vdocument_id().v + ) | ranges::views::transform( + &MTPlong::v + ) | ranges::to_vector; + }); + }).fail([=] { + emojiList(type).requestId = 0; + }).send(); }; - const auto f = [=] { _requestIdEmojiList = 0; }; - _requestIdEmojiList = isGroup - ? _api.request( - MTPaccount_GetDefaultGroupPhotoEmojis() - ).done(d).fail(f).send() - : _api.request( - MTPaccount_GetDefaultProfilePhotoEmojis() - ).done(d).fail(f).send(); + list.requestId = (type == EmojiListType::Profile) + ? send(MTPaccount_GetDefaultProfilePhotoEmojis()) + : (type == EmojiListType::Group) + ? send(MTPaccount_GetDefaultGroupPhotoEmojis()) + : send(MTPaccount_GetDefaultBackgroundEmojis()); } rpl::producer PeerPhoto::emojiListValue( EmojiListType type) { - auto &list = (type == EmojiListType::Group) - ? _profileEmojiList - : _groupEmojiList; - if (list.current().empty() && !_requestIdEmojiList) { + auto &list = emojiList(type); + if (list.list.current().empty() && !list.requestId) { requestEmojiList(type); } - return list.value(); + return list.list.value(); } // Non-personal photo in case a personal photo is set. diff --git a/Telegram/SourceFiles/api/api_peer_photo.h b/Telegram/SourceFiles/api/api_peer_photo.h index 03920d3ef5..1d4bd1071e 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.h +++ b/Telegram/SourceFiles/api/api_peer_photo.h @@ -31,6 +31,7 @@ public: enum class EmojiListType { Profile, Group, + Background, }; struct UserPhoto { @@ -73,6 +74,10 @@ private: Suggestion, Fallback, }; + struct EmojiListData { + rpl::variable list; + mtpRequestId requestId = 0; + }; void ready( const FullMsgId &msgId, @@ -84,6 +89,9 @@ private: UploadType type, Fn done); + [[nodiscard]] EmojiListData &emojiList(EmojiListType type); + [[nodiscard]] const EmojiListData &emojiList(EmojiListType type) const; + const not_null _session; MTP::Sender _api; @@ -101,9 +109,9 @@ private: not_null, not_null> _nonPersonalPhotos; - mtpRequestId _requestIdEmojiList = 0; - rpl::variable _profileEmojiList; - rpl::variable _groupEmojiList; + EmojiListData _profileEmojiList; + EmojiListData _groupEmojiList; + EmojiListData _backgroundEmojiList; }; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 11c674565d..d5d0762214 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "chat_helpers/compose/compose_show.h" #include "data/data_changes.h" #include "data/data_channel.h" +#include "data/stickers/data_custom_emoji.h" #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_web_page.h" @@ -19,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "info/boosts/info_boosts_widget.h" +#include "info/profile/info_profile_emoji_status_panel.h" #include "info/info_memento.h" #include "lang/lang_keys.h" #include "main/main_account.h" @@ -105,7 +107,8 @@ public: std::shared_ptr style, std::shared_ptr theme, not_null peer, - rpl::producer colorIndexValue); + rpl::producer colorIndexValue, + rpl::producer backgroundEmojiId); ~PreviewWrap(); private: @@ -253,7 +256,8 @@ PreviewWrap::PreviewWrap( std::shared_ptr style, std::shared_ptr theme, not_null peer, - rpl::producer colorIndexValue) + rpl::producer colorIndexValue, + rpl::producer backgroundEmojiId) : RpWidget(box) , _box(box) , _peer(peer) @@ -329,6 +333,10 @@ PreviewWrap::PreviewWrap( _fake->changeColorIndex(index); update(); }, lifetime()); + std::move(backgroundEmojiId) | rpl::start_with_next([=](DocumentId id) { + _fake->changeBackgroundEmojiId(id); + update(); + }, lifetime()); const auto session = &_history->session(); session->data().viewRepaintRequest( @@ -423,22 +431,28 @@ HistoryView::Context PreviewDelegate::elementContext() { void Set( std::shared_ptr show, not_null peer, - uint8 colorIndex) { - const auto was = peer->colorIndex(); - peer->changeColorIndex(colorIndex); - peer->session().changes().peerUpdated( - peer, - Data::PeerUpdate::Flag::Color); + uint8 colorIndex, + DocumentId backgroundEmojiId) { + const auto wasIndex = peer->colorIndex(); + const auto wasEmojiId = peer->backgroundEmojiId(); + + const auto setLocal = [=](uint8 index, DocumentId emojiId) { + using UpdateFlag = Data::PeerUpdate::Flag; + peer->changeColorIndex(index); + peer->changeBackgroundEmojiId(emojiId); + peer->session().changes().peerUpdated( + peer, + UpdateFlag::Color | UpdateFlag::BackgroundEmoji); + }; + setLocal(colorIndex, backgroundEmojiId); + const auto done = [=] { show->showToast(peer->isSelf() ? tr::lng_settings_color_changed(tr::now) : tr::lng_settings_color_changed_channel(tr::now)); }; const auto fail = [=](const MTP::Error &error) { - peer->changeColorIndex(was); - peer->session().changes().peerUpdated( - peer, - Data::PeerUpdate::Flag::Color); + setLocal(wasIndex, wasEmojiId); show->showToast(error.type()); }; const auto send = [&](auto &&request) { @@ -451,14 +465,14 @@ void Set( MTP_flags( MTPaccount_UpdateColor::Flag::f_background_emoji_id), MTP_int(colorIndex), - MTP_long(peer->backgroundEmojiId()))); + MTP_long(backgroundEmojiId))); } else if (const auto channel = peer->asChannel()) { send(MTPchannels_UpdateColor( MTP_flags( MTPchannels_UpdateColor::Flag::f_background_emoji_id), channel->inputChannel, MTP_int(colorIndex), - MTP_long(peer->backgroundEmojiId()))); + MTP_long(backgroundEmojiId))); } else { Unexpected("Invalid peer type in Set(colorIndex)."); } @@ -468,10 +482,12 @@ void Apply( std::shared_ptr show, not_null peer, uint8 colorIndex, + DocumentId backgroundEmojiId, Fn close, Fn cancel) { const auto session = &peer->session(); - if (peer->colorIndex() == colorIndex) { + if (peer->colorIndex() == colorIndex + && peer->backgroundEmojiId() == backgroundEmojiId) { close(); } else if (peer->isSelf() && !session->premium()) { Settings::ShowPremiumPromoToast( @@ -486,7 +502,7 @@ void Apply( u"name_color"_q); cancel(); } else if (peer->isSelf()) { - Set(show, peer, colorIndex); + Set(show, peer, colorIndex, backgroundEmojiId); close(); } else { session->api().request(MTPpremium_GetBoostsStatus( @@ -497,7 +513,7 @@ void Apply( "channel_color_level_min", 5); if (data.vlevel().v >= required) { - Set(show, peer, colorIndex); + Set(show, peer, colorIndex, backgroundEmojiId); close(); return; } @@ -635,6 +651,121 @@ int ColorSelector::resizeGetHeight(int newWidth) { return (top - skip) + ((count % columns) ? (isize + skip) : 0); } +[[nodiscard]] object_ptr CreateEmojiIconButton( + not_null parent, + std::shared_ptr show, + std::shared_ptr style, + rpl::producer colorIndexValue, + rpl::producer emojiIdValue, + Fn emojiIdChosen) { + const auto &basicSt = st::settingsButtonNoIcon; + const auto ratio = style::DevicePixelRatio(); + const auto added = st::normalFont->spacew; + const auto emojiSize = Data::FrameSizeFromTag({}) / ratio; + const auto noneWidth = added + + st::normalFont->width(tr::lng_settings_color_emoji_off(tr::now)); + const auto emojiWidth = added + emojiSize; + const auto rightPadding = std::max(noneWidth, emojiWidth) + + basicSt.padding.right(); + const auto st = parent->lifetime().make_state( + basicSt); + st->padding.setRight(rightPadding); + auto result = CreateButton( + parent, + tr::lng_settings_color_emoji(), + *st, + {}); + const auto raw = result.data(); + + const auto right = Ui::CreateChild(raw); + right->show(); + + struct State { + Info::Profile::EmojiStatusPanel panel; + std::unique_ptr emoji; + DocumentId emojiId = 0; + uint8 index = 0; + }; + const auto state = right->lifetime().make_state(); + state->panel.backgroundEmojiChosen( + ) | rpl::start_with_next(emojiIdChosen, raw->lifetime()); + + std::move(colorIndexValue) | rpl::start_with_next([=](uint8 index) { + state->index = index; + if (state->emoji) { + right->update(); + } + }, right->lifetime()); + + const auto session = &show->session(); + std::move(emojiIdValue) | rpl::start_with_next([=](DocumentId emojiId) { + state->emojiId = emojiId; + state->emoji = emojiId + ? session->data().customEmojiManager().create( + emojiId, + [=] { right->update(); }) + : nullptr; + right->resize( + (emojiId ? emojiWidth : noneWidth) + added, + right->height()); + right->update(); + }, right->lifetime()); + + rpl::combine( + raw->sizeValue(), + right->widthValue() + ) | rpl::start_with_next([=](QSize outer, int width) { + right->resize(width, outer.height()); + const auto skip = st::settingsButton.padding.right(); + right->moveToRight(skip - added, 0, outer.width()); + }, right->lifetime()); + + right->paintRequest( + ) | rpl::start_with_next([=] { + if (state->panel.paintBadgeFrame(right)) { + return; + } + auto p = QPainter(right); + const auto height = right->height(); + if (state->emoji) { + const auto colors = style->coloredValues(false, state->index); + state->emoji->paint(p, { + .textColor = colors.name, + .position = QPoint(added, (height - emojiSize) / 2), + .internal = { + .forceFirstFrame = true, + }, + }); + } else { + const auto &font = st::normalFont; + p.setFont(font); + p.setPen(style->windowActiveTextFg()); + p.drawText( + QPoint(added, (height - font->height) / 2 + font->ascent), + tr::lng_settings_color_emoji_off(tr::now)); + } + }, right->lifetime()); + + raw->setClickedCallback([=] { + const auto customTextColor = [=] { + return style->coloredValues(false, state->index).name; + }; + const auto controller = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo); + if (controller) { + state->panel.show({ + .controller = controller, + .button = right, + .currentBackgroundEmojiId = state->emojiId, + .customTextColor = customTextColor, + .backgroundEmojiMode = true, + }); + } + }); + + return result; +} + } // namespace void EditPeerColorBox( @@ -648,18 +779,21 @@ void EditPeerColorBox( struct State { rpl::variable index; + rpl::variable emojiId; bool changing = false; bool applying = false; }; const auto state = box->lifetime().make_state(); state->index = peer->colorIndex(); + state->emojiId = peer->backgroundEmojiId(); box->addRow(object_ptr( box, style, theme, peer, - state->index.value() + state->index.value(), + state->emojiId.value() ), {}); const auto appConfig = &peer->session().account().appConfig(); @@ -691,12 +825,29 @@ void EditPeerColorBox( ? tr::lng_settings_color_about() : tr::lng_settings_color_about_channel()); + AddSkip(container, st::settingsColorSampleSkip); + + container->add(CreateEmojiIconButton( + container, + show, + style, + state->index.value(), + state->emojiId.value(), + [=](DocumentId id) { state->emojiId = id; })); + + AddSkip(container, st::settingsColorSampleSkip); + AddDividerText(container, peer->isSelf() + ? tr::lng_settings_color_emoji_about() + : tr::lng_settings_color_emoji_about_channel()); + box->addButton(tr::lng_settings_apply(), [=] { if (state->applying) { return; } state->applying = true; - Apply(show, peer, state->index.current(), crl::guard(box, [=] { + const auto index = state->index.current(); + const auto emojiId = state->emojiId.current(); + Apply(show, peer, index, emojiId, crl::guard(box, [=] { box->closeBox(); }), crl::guard(box, [=] { state->applying = false; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 81ff92cf01..d100850ee2 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -662,6 +662,9 @@ statusEmojiPan: EmojiPan(defaultEmojiPan) { fadeLeft: icon {{ "fade_horizontal-flip_horizontal", windowBg }}; fadeRight: icon {{ "fade_horizontal", windowBg }}; } +backgroundEmojiPan: EmojiPan(defaultEmojiPan) { + padding: margins(7px, 7px, 4px, 0px); +} inlineBotsScroll: ScrollArea(defaultSolidScroll) { deltat: stickerPanPadding; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index cb3b034ce6..411e1638e8 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -467,6 +467,7 @@ EmojiListWidget::EmojiListWidget( , _localSetsManager( std::make_unique(&session())) , _customRecentFactory(std::move(descriptor.customRecentFactory)) +, _customTextColor(std::move(descriptor.customTextColor)) , _overBg(st::emojiPanRadius, st().overBg) , _collapsedBg(st::emojiPanExpand.height / 2, st().headerFg) , _picker(this, st()) @@ -476,7 +477,7 @@ EmojiListWidget::EmojiListWidget( setAttribute(Qt::WA_OpaquePaintEvent); } - if (_mode != Mode::RecentReactions) { + if (_mode != Mode::RecentReactions && _mode != Mode::BackgroundEmoji) { setupSearch(); } @@ -791,10 +792,12 @@ object_ptr EmojiListWidget::createFooter() { }; auto result = object_ptr(FooterDescriptor{ .session = &session(), + .customTextColor = _customTextColor, .paused = footerPaused, .parent = this, .st = &st(), .features = { .stickersSettings = false }, + .forceFirstFrame = (_mode == Mode::BackgroundEmoji), }); _footer = result; @@ -1027,6 +1030,14 @@ void EmojiListWidget::fillRecentFrom(const std::vector &list) { if (!id && _mode == Mode::EmojiStatus) { const auto star = QString::fromUtf8("\xe2\xad\x90\xef\xb8\x8f"); _recent.push_back({ .id = { Ui::Emoji::Find(star) } }); + } else if (!id && _mode == Mode::BackgroundEmoji) { + const auto fakeId = DocumentId(5246772116543512028ULL); + const auto no = QString::fromUtf8("\xe2\x9b\x94\xef\xb8\x8f"); + _recent.push_back({ + .custom = resolveCustomRecent(fakeId), + .id = { Ui::Emoji::Find(no) }, + }); + _recentCustomIds.emplace(fakeId); } else { _recent.push_back({ .custom = resolveCustomRecent(id), @@ -1188,7 +1199,9 @@ void EmojiListWidget::paintEvent(QPaintEvent *e) { void EmojiListWidget::validateEmojiPaintContext( const ExpandingContext &context) { auto value = Ui::Text::CustomEmojiPaintContext{ - .textColor = (_mode == Mode::EmojiStatus + .textColor = (_customTextColor + ? _customTextColor() + : (_mode == Mode::EmojiStatus) ? anim::color( st::stickerPanPremium1, st::stickerPanPremium2, @@ -1199,6 +1212,7 @@ void EmojiListWidget::validateEmojiPaintContext( .scale = context.progress, .paused = On(powerSavingFlag()) || paused(), .scaled = context.expanding, + .internal = { .forceFirstFrame = (_mode == Mode::BackgroundEmoji) }, }; if (!_emojiPaintContext) { _emojiPaintContext = std::make_unique< @@ -1384,7 +1398,17 @@ void EmojiListWidget::drawRecent( _emojiPaintContext->position = position + _innerPosition + _customPosition; + const auto isResetIcon = (_mode == Mode::BackgroundEmoji) + && v::is(recent.id.data); + if (isResetIcon) { + _emojiPaintContext->textColor = st::windowSubTextFg->c; + } custom->paint(p, *_emojiPaintContext); + if (isResetIcon) { + _emojiPaintContext->textColor = _customTextColor + ? _customTextColor() + : st().textFg->c; + } } else if (const auto emoji = std::get_if(&recent.id.data)) { if (_mode == Mode::EmojiStatus) { position += QPoint( @@ -1629,6 +1653,9 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { case Mode::TopicIcon: Settings::ShowPremium(resolved, u"forum_topic_icon"_q); break; + case Mode::BackgroundEmoji: + Settings::ShowPremium(resolved, u"name_color"_q); + break; } } } @@ -1995,7 +2022,10 @@ void EmojiListWidget::refreshCustom() { const auto &sets = owner->stickers().sets(); const auto push = [&](uint64 setId, bool installed) { auto it = sets.find(setId); - if (it == sets.cend() || it->second->stickers.isEmpty()) { + if (it == sets.cend() + || it->second->stickers.isEmpty() + || (_mode == Mode::BackgroundEmoji + && !it->second->textColor())) { return; } const auto canRemove = !!(it->second->flags diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 5cc197995e..5735d7a55d 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -74,11 +74,13 @@ enum class EmojiListMode { FullReactions, RecentReactions, UserpicBuilder, + BackgroundEmoji, }; struct EmojiListDescriptor { std::shared_ptr show; EmojiListMode mode = EmojiListMode::Full; + Fn customTextColor; Fn paused; std::vector customRecentList; Fn( @@ -386,6 +388,7 @@ private: base::flat_map< DocumentId, std::unique_ptr> _customRecent; + Fn _customTextColor; int _customSingleSize = 0; bool _allowWithoutPremium = false; Ui::RoundRect _overBg; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp index 0c09b6dd3c..66784f7748 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp @@ -291,12 +291,14 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor) descriptor.parent, descriptor.st ? *descriptor.st : st::defaultEmojiPan) , _session(descriptor.session) -, _paused(descriptor.paused) +, _customTextColor(std::move(descriptor.customTextColor)) +, _paused(std::move(descriptor.paused)) , _features(descriptor.features) , _iconState([=] { update(); }) , _subiconState([=] { update(); }) , _selectionBg(st::emojiPanRadius, st().categoriesBgOver) -, _subselectionBg(st().iconArea / 2, st().categoriesBgOver) { +, _subselectionBg(st().iconArea / 2, st().categoriesBgOver) +, _forceFirstFrame(descriptor.forceFirstFrame) { setMouseTracking(true); _iconsLeft = st().iconSkip @@ -1345,13 +1347,16 @@ void StickersListFooter::paintSetIconToCache( const auto y = (st().footer - icon.pixh) / 2; if (icon.custom) { icon.custom->paint(p, Ui::Text::CustomEmoji::Context{ - .textColor = st().textFg->c, + .textColor = (_customTextColor + ? _customTextColor() + : st().textFg->c), .size = QSize(icon.pixw, icon.pixh), .now = now, .scale = context.progress, .position = { x, y }, .paused = paused, .scaled = context.expanding, + .internal = { .forceFirstFrame = _forceFirstFrame }, }); } else if (icon.lottie && icon.lottie->ready()) { const auto frame = icon.lottie->frame(); @@ -1428,11 +1433,13 @@ void StickersListFooter::paintSetIconToCache( return icons[index]; }; const auto paintOne = [&](int left, const style::icon *icon) { - icon->paint( - p, - left + (_singleWidth - icon->width()) / 2, - (st().footer - icon->height()) / 2, - width()); + left += (_singleWidth - icon->width()) / 2; + const auto top = (st().footer - icon->height()) / 2; + if (_customTextColor) { + icon->paint(p, left, top, width(), _customTextColor()); + } else { + icon->paint(p, left, top, width()); + } }; if (_icons[info.index].setId == AllEmojiSectionSetId() && info.width > _singleWidth) { diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h index f7a0afc1d4..5dbdd30f9d 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h @@ -115,10 +115,12 @@ class StickersListFooter final : public TabbedSelector::InnerFooter { public: struct Descriptor { not_null session; + Fn customTextColor; Fn paused; not_null parent; const style::EmojiPan *st = nullptr; ComposeFeatures features; + bool forceFirstFrame = false; }; explicit StickersListFooter(Descriptor &&descriptor); @@ -269,6 +271,7 @@ private: void clipCallback(Media::Clip::Notification notification, uint64 setId); const not_null _session; + const Fn _customTextColor; const Fn _paused; const ComposeFeatures _features; @@ -303,6 +306,7 @@ private: int _subiconsWidth = 0; bool _subiconsExpanded = false; bool _repaintScheduled = false; + bool _forceFirstFrame = false; rpl::event_stream<> _openSettingsRequests; rpl::event_stream _setChosen; diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp index 43b08c4a42..4860481e70 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp @@ -331,7 +331,7 @@ TabbedSelector::TabbedSelector( Mode mode) : TabbedSelector(parent, { .show = std::move(show), - .st = (mode == Mode::EmojiStatus + .st = ((mode == Mode::EmojiStatus || mode == Mode::BackgroundEmoji) ? st::statusEmojiPan : st::defaultEmojiPan), .level = level, @@ -347,6 +347,7 @@ TabbedSelector::TabbedSelector( , _features(descriptor.features) , _show(std::move(descriptor.show)) , _level(descriptor.level) +, _customTextColor(std::move(descriptor.customTextColor)) , _mode(descriptor.mode) , _panelRounding(Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.bg)) , _categoriesRounding( @@ -512,7 +513,10 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) { .show = _show, .mode = (_mode == Mode::EmojiStatus ? EmojiMode::EmojiStatus + : _mode == Mode::BackgroundEmoji + ? EmojiMode::BackgroundEmoji : EmojiMode::Full), + .customTextColor = _customTextColor, .paused = paused, .st = &_st, .features = _features, diff --git a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h index d41aebb809..109ccead60 100644 --- a/Telegram/SourceFiles/chat_helpers/tabbed_selector.h +++ b/Telegram/SourceFiles/chat_helpers/tabbed_selector.h @@ -81,6 +81,7 @@ enum class TabbedSelectorMode { EmojiOnly, MediaEditor, EmojiStatus, + BackgroundEmoji, }; struct TabbedSelectorDescriptor { @@ -88,6 +89,7 @@ struct TabbedSelectorDescriptor { const style::EmojiPan &st; PauseReason level = {}; TabbedSelectorMode mode = TabbedSelectorMode::Full; + Fn customTextColor; ComposeFeatures features; }; @@ -272,6 +274,7 @@ private: const ComposeFeatures _features; const std::shared_ptr _show; const PauseReason _level = {}; + const Fn _customTextColor; Mode _mode = Mode::Full; int _roundRadius = 0; diff --git a/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp b/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp index f9db8da020..347a3f13ca 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp +++ b/Telegram/SourceFiles/data/stickers/data_stickers_set.cpp @@ -53,7 +53,8 @@ StickersSetFlags ParseStickersSetFlags(const MTPDstickerSet &data) { | (data.is_masks() ? Flag::Masks : Flag()) | (data.is_emojis() ? Flag::Emoji : Flag()) | (data.vinstalled_date() ? Flag::Installed : Flag()) - | (data.is_videos() ? Flag::Webm : Flag()); + | (data.is_videos() ? Flag::Webm : Flag()) + | (data.is_text_color() ? Flag::TextColor : Flag()); } StickersSet::StickersSet( @@ -108,6 +109,10 @@ StickersType StickersSet::type() const { : StickersType::Stickers; } +bool StickersSet::textColor() const { + return flags & StickersSetFlag::TextColor; +} + void StickersSet::setThumbnail(const ImageWithLocation &data) { Data::UpdateCloudFile( _thumbnail, diff --git a/Telegram/SourceFiles/data/stickers/data_stickers_set.h b/Telegram/SourceFiles/data/stickers/data_stickers_set.h index 4fb30b3d10..bf9a6e4177 100644 --- a/Telegram/SourceFiles/data/stickers/data_stickers_set.h +++ b/Telegram/SourceFiles/data/stickers/data_stickers_set.h @@ -57,6 +57,7 @@ enum class StickersSetFlag { Special = (1 << 7), Webm = (1 << 8), Emoji = (1 << 9), + TextColor = (1 << 10), }; inline constexpr bool is_flag_type(StickersSetFlag) { return true; }; using StickersSetFlags = base::flags; @@ -84,6 +85,7 @@ public: [[nodiscard]] MTPInputStickerSet mtpInput() const; [[nodiscard]] StickerSetIdentifier identifier() const; [[nodiscard]] StickersType type() const; + [[nodiscard]] bool textColor() const; void setThumbnail(const ImageWithLocation &data); diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp index 7a9e18b927..db8714c705 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.cpp @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/profile/info_profile_emoji_status_panel.h" +#include "api/api_peer_photo.h" +#include "apiwrap.h" #include "data/data_user.h" #include "data/data_session.h" #include "data/data_document.h" @@ -66,26 +68,17 @@ void EmojiStatusPanel::show( not_null controller, not_null 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); - if (list.size() > kLimitFirstRow) { - list.erase(begin(list) + kLimitFirstRow, end(list)); - } - 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()); - } + show({ + .controller = controller, + .button = button, + .animationSizeTag = animationSizeTag, + }); +} + +void EmojiStatusPanel::show(Descriptor &&descriptor) { + const auto controller = descriptor.controller; if (!_panel) { - create(controller); + create(descriptor); _panel->shownValue( ) | rpl::filter([=] { @@ -98,23 +91,67 @@ void EmojiStatusPanel::show( } }, _panel->lifetime()); } + const auto button = descriptor.button; if (const auto previous = _panelButton.data()) { if (previous != button) { previous->removeEventFilter(_panel.get()); } } _panelButton = button; - _animationSizeTag = animationSizeTag; - _panel->selector()->provideRecentEmoji(list); + _animationSizeTag = descriptor.animationSizeTag; + auto list = std::vector(); + if (descriptor.backgroundEmojiMode) { + controller->session().api().peerPhoto().emojiListValue( + Api::PeerPhoto::EmojiListType::Background + ) | rpl::start_with_next([=](std::vector &&list) { + list.insert(begin(list), 0); + if (const auto now = descriptor.currentBackgroundEmojiId) { + if (!ranges::contains(list, now)) { + list.push_back(now); + } + } + _panel->selector()->provideRecentEmoji(list); + }, _panel->lifetime()); + } else { + 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); + if (list.size() > kLimitFirstRow) { + list.erase(begin(list) + kLimitFirstRow, end(list)); + } + 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()); + } + _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() - (st::normalFont->height / 2), - local.x() + button->width() * 3); + if (descriptor.backgroundEmojiMode) { + _panel->moveBottomRight( + local.y() + (st::normalFont->height / 2), + local.x() + button->width() * 3); + } else { + _panel->moveTopRight( + local.y() + button->height() - (st::normalFont->height / 2), + local.x() + button->width() * 3); + } _panel->toggleAnimated(); } +void EmojiStatusPanel::repaint() { + _panel->selector()->update(); +} + bool EmojiStatusPanel::paintBadgeFrame(not_null widget) { if (!_animation) { return false; @@ -125,19 +162,31 @@ bool EmojiStatusPanel::paintBadgeFrame(not_null widget) { return false; } -void EmojiStatusPanel::create( - not_null controller) { +void EmojiStatusPanel::create(const Descriptor &descriptor) { using Selector = ChatHelpers::TabbedSelector; + using Descriptor = ChatHelpers::TabbedSelectorDescriptor; + using Mode = ChatHelpers::TabbedSelector::Mode; + const auto controller = descriptor.controller; const auto body = controller->window().widget()->bodyWidget(); _panel = base::make_unique_q( body, controller, object_ptr( nullptr, - controller->uiShow(), - Window::GifPauseReason::Layer, - ChatHelpers::TabbedSelector::Mode::EmojiStatus)); - _panel->setDropDown(true); + Descriptor{ + .show = controller->uiShow(), + .st = (descriptor.backgroundEmojiMode + ? st::backgroundEmojiPan + : st::statusEmojiPan), + .level = Window::GifPauseReason::Layer, + .mode = (descriptor.backgroundEmojiMode + ? Mode::BackgroundEmoji + : Mode::EmojiStatus), + .customTextColor = descriptor.customTextColor, + })); + _customTextColor = descriptor.customTextColor; + _backgroundEmojiMode = descriptor.backgroundEmojiMode; + _panel->setDropDown(!_backgroundEmojiMode); _panel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, @@ -169,34 +218,46 @@ void EmojiStatusPanel::create( return Chosen{ .animation = data.messageSendingFrom }; }); - const auto weak = Ui::MakeWeak(_panel.get()); - const auto accept = [=](Chosen chosen) { - Expects(chosen.until != Selector::kPickCustomTimeId); - - // From PickUntilBox is called after EmojiStatusPanel is destroyed! - const auto owner = &controller->session().data(); - if (weak) { + if (descriptor.backgroundEmojiMode) { + rpl::merge( + std::move(statusChosen), + std::move(emojiChosen) + ) | rpl::start_with_next([=](const Chosen &chosen) { + const auto owner = &controller->session().data(); startAnimation(owner, body, chosen.id, chosen.animation); - } - owner->emojiStatuses().set(chosen.id, chosen.until); - }; + _backgroundEmojiChosen.fire_copy(chosen.id); + _panel->hideAnimated(); + }, _panel->lifetime()); + } else { + const auto weak = Ui::MakeWeak(_panel.get()); + const auto accept = [=](Chosen chosen) { + Expects(chosen.until != Selector::kPickCustomTimeId); - rpl::merge( - std::move(statusChosen), - std::move(emojiChosen) - ) | rpl::filter([=](const Chosen &chosen) { - return filter(controller, chosen.id); - }) | rpl::start_with_next([=](const Chosen &chosen) { - if (chosen.until == Selector::kPickCustomTimeId) { - _panel->hideAnimated(); - controller->show(Box(PickUntilBox, [=](TimeId seconds) { - accept({ chosen.id, base::unixtime::now() + seconds }); - })); - } else { - accept(chosen); - _panel->hideAnimated(); - } - }, _panel->lifetime()); + // PickUntilBox calls this after EmojiStatusPanel is destroyed! + const auto owner = &controller->session().data(); + if (weak) { + startAnimation(owner, body, chosen.id, chosen.animation); + } + owner->emojiStatuses().set(chosen.id, chosen.until); + }; + + rpl::merge( + std::move(statusChosen), + std::move(emojiChosen) + ) | rpl::filter([=](const Chosen &chosen) { + return filter(controller, chosen.id); + }) | rpl::start_with_next([=](const Chosen &chosen) { + if (chosen.until == Selector::kPickCustomTimeId) { + _panel->hideAnimated(); + controller->show(Box(PickUntilBox, [=](TimeId seconds) { + accept({ chosen.id, base::unixtime::now() + seconds }); + })); + } else { + accept(chosen); + _panel->hideAnimated(); + } + }, _panel->lifetime()); + } } bool EmojiStatusPanel::filter( @@ -223,13 +284,17 @@ void EmojiStatusPanel::startAnimation( .id = { { statusId } }, .flyIcon = from.frame, .flyFrom = body->mapFromGlobal(from.globalStartGeometry), + .forceFirstFrame = _backgroundEmojiMode, }; + const auto color = _customTextColor + ? _customTextColor + : [] { return st::profileVerifiedCheckBg->c; }; _animation = std::make_unique( body, &owner->reactions(), std::move(args), [=] { _animation->repaint(); }, - [] { return st::profileVerifiedCheckBg->c; }, + _customTextColor, _animationSizeTag); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h index 3d4373665f..246b171fe0 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h +++ b/Telegram/SourceFiles/info/profile/info_profile_emoji_status_panel.h @@ -47,10 +47,25 @@ public: not_null button, Data::CustomEmojiSizeTag animationSizeTag = {}); + struct Descriptor { + not_null controller; + not_null button; + Data::CustomEmojiSizeTag animationSizeTag = {}; + DocumentId currentBackgroundEmojiId = 0; + Fn customTextColor; + bool backgroundEmojiMode = false; + }; + void show(Descriptor &&descriptor); + void repaint(); + + [[nodiscard]] rpl::producer backgroundEmojiChosen() const { + return _backgroundEmojiChosen.events(); + } + bool paintBadgeFrame(not_null widget); private: - void create(not_null controller); + void create(const Descriptor &descriptor); [[nodiscard]] bool filter( not_null controller, DocumentId chosenId) const; @@ -62,10 +77,13 @@ private: Ui::MessageSendingAnimationFrom from); base::unique_qptr _panel; + Fn _customTextColor; Fn _chooseFilter; QPointer _panelButton; std::unique_ptr _animation; + rpl::event_stream _backgroundEmojiChosen; Data::CustomEmojiSizeTag _animationSizeTag = {}; + bool _backgroundEmojiMode = false; }; diff --git a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp index 8a271233d8..395e38faeb 100644 --- a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp +++ b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.cpp @@ -69,7 +69,8 @@ ReactionFlyAnimation::ReactionFlyAnimation( , _repaint(std::move(repaint)) , _flyFrom(args.flyFrom) , _scaleOutDuration(args.scaleOutDuration) -, _scaleOutTarget(args.scaleOutTarget) { +, _scaleOutTarget(args.scaleOutTarget) +, _forceFirstFrame(args.forceFirstFrame) { const auto &list = owner->list(::Data::Reactions::Type::All); auto centerIcon = (DocumentData*)nullptr; auto aroundAnimation = (DocumentData*)nullptr; @@ -251,6 +252,7 @@ void ReactionFlyAnimation::paintCenterFrame( target.x() + (target.width() - _customSize) / 2, target.y() + (target.height() - _customSize) / 2), .scaled = scaled, + .internal = { .forceFirstFrame = _forceFirstFrame }, }); } } @@ -278,6 +280,7 @@ void ReactionFlyAnimation::paintMiniCopies( .size = size, .now = now, .scaled = true, + .internal = { .forceFirstFrame = _forceFirstFrame }, }; for (const auto &mini : _miniCopies) { if (progress >= mini.duration) { diff --git a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h index 50aae6dc8d..0ea353f3bf 100644 --- a/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h +++ b/Telegram/SourceFiles/ui/effects/reaction_fly_animation.h @@ -31,6 +31,7 @@ struct ReactionFlyAnimationArgs { float64 scaleOutTarget = 0.; float64 miniCopyMultiplier = 1.; bool effectOnly = false; + bool forceFirstFrame = false; [[nodiscard]] ReactionFlyAnimationArgs translated(QPoint point) const; }; @@ -42,6 +43,7 @@ struct ReactionFlyCenter { float64 centerSizeMultiplier = 0.; int customSize = 0; int size = 0; + bool forceFirstFrame = false; }; class ReactionFlyAnimation final { @@ -121,6 +123,7 @@ private: crl::time _scaleOutDuration = 0; float64 _scaleOutTarget = 0.; bool _noEffectScaleStarted = false; + bool _forceFirstFrame = false; bool _effectOnly = false; bool _valid = false;