Implement background emoji selector.

This commit is contained in:
John Preston 2023-10-27 23:40:39 +04:00
parent bcdb1bdfd2
commit cb6698cf4a
17 changed files with 445 additions and 118 deletions

View File

@ -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.";

View File

@ -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<PeerPhoto*>(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::EmojiList> 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.

View File

@ -31,6 +31,7 @@ public:
enum class EmojiListType {
Profile,
Group,
Background,
};
struct UserPhoto {
@ -73,6 +74,10 @@ private:
Suggestion,
Fallback,
};
struct EmojiListData {
rpl::variable<EmojiList> list;
mtpRequestId requestId = 0;
};
void ready(
const FullMsgId &msgId,
@ -84,6 +89,9 @@ private:
UploadType type,
Fn<void()> done);
[[nodiscard]] EmojiListData &emojiList(EmojiListType type);
[[nodiscard]] const EmojiListData &emojiList(EmojiListType type) const;
const not_null<Main::Session*> _session;
MTP::Sender _api;
@ -101,9 +109,9 @@ private:
not_null<UserData*>,
not_null<PhotoData*>> _nonPersonalPhotos;
mtpRequestId _requestIdEmojiList = 0;
rpl::variable<EmojiList> _profileEmojiList;
rpl::variable<EmojiList> _groupEmojiList;
EmojiListData _profileEmojiList;
EmojiListData _groupEmojiList;
EmojiListData _backgroundEmojiList;
};

View File

@ -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<Ui::ChatStyle> style,
std::shared_ptr<Ui::ChatTheme> theme,
not_null<PeerData*> peer,
rpl::producer<uint8> colorIndexValue);
rpl::producer<uint8> colorIndexValue,
rpl::producer<DocumentId> backgroundEmojiId);
~PreviewWrap();
private:
@ -253,7 +256,8 @@ PreviewWrap::PreviewWrap(
std::shared_ptr<Ui::ChatStyle> style,
std::shared_ptr<Ui::ChatTheme> theme,
not_null<PeerData*> peer,
rpl::producer<uint8> colorIndexValue)
rpl::producer<uint8> colorIndexValue,
rpl::producer<DocumentId> 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<ChatHelpers::Show> show,
not_null<PeerData*> 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<ChatHelpers::Show> show,
not_null<PeerData*> peer,
uint8 colorIndex,
DocumentId backgroundEmojiId,
Fn<void()> close,
Fn<void()> 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<Ui::SettingsButton> CreateEmojiIconButton(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
std::shared_ptr<Ui::ChatStyle> style,
rpl::producer<uint8> colorIndexValue,
rpl::producer<DocumentId> emojiIdValue,
Fn<void(DocumentId)> 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<style::SettingsButton>(
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<Ui::RpWidget>(raw);
right->show();
struct State {
Info::Profile::EmojiStatusPanel panel;
std::unique_ptr<Ui::Text::CustomEmoji> emoji;
DocumentId emojiId = 0;
uint8 index = 0;
};
const auto state = right->lifetime().make_state<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<uint8> index;
rpl::variable<DocumentId> emojiId;
bool changing = false;
bool applying = false;
};
const auto state = box->lifetime().make_state<State>();
state->index = peer->colorIndex();
state->emojiId = peer->backgroundEmojiId();
box->addRow(object_ptr<PreviewWrap>(
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;

View File

@ -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;

View File

@ -467,6 +467,7 @@ EmojiListWidget::EmojiListWidget(
, _localSetsManager(
std::make_unique<LocalStickersManager>(&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<TabbedSelector::InnerFooter> EmojiListWidget::createFooter() {
};
auto result = object_ptr<StickersListFooter>(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<DocumentId> &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<EmojiPtr>(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<EmojiPtr>(&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

View File

@ -74,11 +74,13 @@ enum class EmojiListMode {
FullReactions,
RecentReactions,
UserpicBuilder,
BackgroundEmoji,
};
struct EmojiListDescriptor {
std::shared_ptr<Show> show;
EmojiListMode mode = EmojiListMode::Full;
Fn<QColor()> customTextColor;
Fn<bool()> paused;
std::vector<DocumentId> customRecentList;
Fn<std::unique_ptr<Ui::Text::CustomEmoji>(
@ -386,6 +388,7 @@ private:
base::flat_map<
DocumentId,
std::unique_ptr<Ui::Text::CustomEmoji>> _customRecent;
Fn<QColor()> _customTextColor;
int _customSingleSize = 0;
bool _allowWithoutPremium = false;
Ui::RoundRect _overBg;

View File

@ -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) {

View File

@ -115,10 +115,12 @@ class StickersListFooter final : public TabbedSelector::InnerFooter {
public:
struct Descriptor {
not_null<Main::Session*> session;
Fn<QColor()> customTextColor;
Fn<bool()> paused;
not_null<RpWidget*> 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<Main::Session*> _session;
const Fn<QColor()> _customTextColor;
const Fn<bool()> _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<uint64> _setChosen;

View File

@ -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,

View File

@ -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<QColor()> customTextColor;
ComposeFeatures features;
};
@ -272,6 +274,7 @@ private:
const ComposeFeatures _features;
const std::shared_ptr<Show> _show;
const PauseReason _level = {};
const Fn<QColor()> _customTextColor;
Mode _mode = Mode::Full;
int _roundRadius = 0;

View File

@ -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,

View File

@ -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<StickersSetFlag>;
@ -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);

View File

@ -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<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);
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<DocumentId>();
if (descriptor.backgroundEmojiMode) {
controller->session().api().peerPhoto().emojiListValue(
Api::PeerPhoto::EmojiListType::Background
) | rpl::start_with_next([=](std::vector<DocumentId> &&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<Ui::RpWidget*> widget) {
if (!_animation) {
return false;
@ -125,19 +162,31 @@ bool EmojiStatusPanel::paintBadgeFrame(not_null<Ui::RpWidget*> widget) {
return false;
}
void EmojiStatusPanel::create(
not_null<Window::SessionController*> 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<ChatHelpers::TabbedPanel>(
body,
controller,
object_ptr<Selector>(
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<Ui::EmojiFlyAnimation>(
body,
&owner->reactions(),
std::move(args),
[=] { _animation->repaint(); },
[] { return st::profileVerifiedCheckBg->c; },
_customTextColor,
_animationSizeTag);
}

View File

@ -47,10 +47,25 @@ public:
not_null<QWidget*> button,
Data::CustomEmojiSizeTag animationSizeTag = {});
struct Descriptor {
not_null<Window::SessionController*> controller;
not_null<QWidget*> button;
Data::CustomEmojiSizeTag animationSizeTag = {};
DocumentId currentBackgroundEmojiId = 0;
Fn<QColor()> customTextColor;
bool backgroundEmojiMode = false;
};
void show(Descriptor &&descriptor);
void repaint();
[[nodiscard]] rpl::producer<DocumentId> backgroundEmojiChosen() const {
return _backgroundEmojiChosen.events();
}
bool paintBadgeFrame(not_null<Ui::RpWidget*> widget);
private:
void create(not_null<Window::SessionController*> controller);
void create(const Descriptor &descriptor);
[[nodiscard]] bool filter(
not_null<Window::SessionController*> controller,
DocumentId chosenId) const;
@ -62,10 +77,13 @@ private:
Ui::MessageSendingAnimationFrom from);
base::unique_qptr<ChatHelpers::TabbedPanel> _panel;
Fn<QColor()> _customTextColor;
Fn<bool(DocumentId)> _chooseFilter;
QPointer<QWidget> _panelButton;
std::unique_ptr<Ui::EmojiFlyAnimation> _animation;
rpl::event_stream<DocumentId> _backgroundEmojiChosen;
Data::CustomEmojiSizeTag _animationSizeTag = {};
bool _backgroundEmojiMode = false;
};

View File

@ -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) {

View File

@ -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;