diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 26b51af33..0624ead80 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1034,6 +1034,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_emoji_category6" = "Objects"; "lng_emoji_category7" = "Symbols & Flags"; "lng_emoji_hide_panel" = "Click here to hide the emoji sidebar"; +"lng_emoji_manage_sets" = "Choose emoji set"; +"lng_emoji_set_available" = "Download {size}"; +"lng_emoji_set_ready" = "Ready"; +"lng_emoji_set_active" = "Active"; +"lng_emoji_set_loading" = "Downloading {progress}"; "lng_recent_stickers" = "Frequently used"; "lng_faved_stickers_add" = "Add to Favorites"; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index de34ca354..f1f68c540 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -265,3 +265,9 @@ autocompleteRowPadding: margins(16px, 5px, 16px, 5px); autocompleteRowTitle: semiboldTextStyle; autocompleteRowKeys: defaultTextStyle; autocompleteRowAnswer: defaultTextStyle; + +manageEmojiCheck: IconButton(defaultIconButton) { + width: 18px; + height: 13px; + icon: stickersFeaturedInstalled; +} diff --git a/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp new file mode 100644 index 000000000..286c6f74a --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp @@ -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 "chat_helpers/emoji_sets_manager.h" + +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/fade_wrap.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/effects/radial_animation.h" +#include "ui/emoji_config.h" +#include "lang/lang_keys.h" +#include "layout.h" +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" + +namespace Ui { +namespace Emoji { +namespace { + +struct Available { + int size = 0; + + inline bool operator<(const Available &other) const { + return size < other.size; + } + inline bool operator==(const Available &other) const { + return size == other.size; + } +}; +struct Ready { + inline bool operator<(const Ready &other) const { + return false; + } + inline bool operator==(const Ready &other) const { + return true; + } +}; +struct Active { + inline bool operator<(const Active &other) const { + return false; + } + inline bool operator==(const Active &other) const { + return true; + } +}; +struct Loading { + int offset = 0; + int size = 0; + + inline bool operator<(const Loading &other) const { + return (offset < other.offset) + || (offset == other.offset && size < other.size); + } + inline bool operator==(const Loading &other) const { + return (offset == other.offset) && (size == other.size); + } +}; +struct Failed { + inline bool operator<(const Failed &other) const { + return false; + } + inline bool operator==(const Failed &other) const { + return true; + } +}; +using SetState = base::variant< + Available, + Ready, + Active, + Loading, + Failed>; + +class Loader : public QObject { +public: + explicit Loader(int id); + + int id() const; + + rpl::producer state() const; + +private: + int _id = 0; + int _size = 0; + rpl::variable _state; + +}; + +class Inner : public Ui::RpWidget { +public: + Inner(QWidget *parent); + +private: + void setupContent(); + +}; + +class Row : public Ui::RippleButton { +public: + Row(QWidget *widget, const Set &set); + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + void setupContent(const Set &set); + void setupCheck(); + void setupLabels(const Set &set); + void setupHandler(); + + int _id = 0; + rpl::variable _state; + std::unique_ptr _loading; + +}; + +QPointer GlobalLoader; +rpl::event_stream GlobalLoaderValues; + +int GetDownloadSize(int id) { + const auto sets = Sets(); + return ranges::find(sets, id, &Set::id)->size; +} + +SetState ComputeState(int id) { + if (id == CurrentSetId()) { + return Active(); + } else if (SetIsReady(id)) { + return Ready(); + } + return Available{ GetDownloadSize(id) }; +} + +QString StateDescription(const SetState &state) { + return state.match([](const Available &data) { + return lng_emoji_set_available(lt_size, formatSizeText(data.size)); + }, [](const Ready &data) -> QString { + return lang(lng_emoji_set_ready); + }, [](const Active &data) -> QString { + return lang(lng_emoji_set_active); + }, [](const Loading &data) { + return lng_emoji_set_loading( + lt_progress, + formatDownloadText(data.offset, data.size)); + }, [](const Failed &data) { + return lang(lng_attach_failed); + }); +} + +Loader::Loader(int id) +: _id(id) +, _size(GetDownloadSize(_id)) +, _state(Loading{ 0, _size }) { +} + +int Loader::id() const { + return _id; +} + +rpl::producer Loader::state() const { + return _state.value(); +} + +Inner::Inner(QWidget *parent) : RpWidget(parent) { + setupContent(); +} + +void Inner::setupContent() { + const auto content = Ui::CreateChild(this); + + const auto sets = Sets(); + for (const auto &set : sets) { + content->add(object_ptr(content, set)); + } + + content->resizeToWidth(st::boxWidth); + Ui::ResizeFitChild(this, content); +} + +Row::Row(QWidget *widget, const Set &set) +: RippleButton(widget, st::contactsRipple) +, _id(set.id) +, _state(Available{ set.size }) { + setupContent(set); + setupHandler(); +} + +void Row::paintEvent(QPaintEvent *e) { + Painter p(this); + + if (isDisabled()) { + return; + } + const auto over = isOver() || isDown(); + const auto bg = over ? st::windowBgOver : st::windowBg; + p.fillRect(rect(), bg); + + const auto ms = getms(); + paintRipple(p, 0, 0, ms); +} + +void Row::setupContent(const Set &set) { + _state = GlobalLoaderValues.events_starting_with( + GlobalLoader.data() + ) | rpl::map([=](Loader *loader) { + return (loader && loader->id() == _id) + ? loader->state() + : rpl::single( + rpl::empty_value() + ) | rpl::then( + Updated() + ) | rpl::map([=] { + return ComputeState(_id); + }); + }) | rpl::flatten_latest( + ) | rpl::filter([=](const SetState &state) { + return !_state.current().is() || !state.is(); + }); + + setupCheck(); + setupLabels(set); + + resize(width(), st::defaultPeerList.item.height); +} + +void Row::setupHandler() { + clicks( + ) | rpl::filter([=] { + const auto &state = _state.current(); + return state.is() || state.is(); + }) | rpl::start_with_next([=] { + App::CallDelayed(st::defaultRippleAnimation.hideDuration, this, [=] { + if (!SwitchToSet(_id)) { + // load + } else { + delete GlobalLoader; + } + }); + }, lifetime()); + + _state.value( + ) | rpl::map([](const SetState &state) { + return state.is() || state.is(); + }) | rpl::start_with_next([=](bool active) { + setDisabled(!active); + setPointerCursor(active); + }, lifetime()); +} + +void Row::setupCheck() { + using namespace rpl::mappers; + + const auto check = Ui::CreateChild>( + this, + object_ptr( + this, + st::manageEmojiCheck)); + sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto checkx = size.width() + - (st::contactsPadding.right() + + st::contactsCheckPosition.x() + + check->width()); + const auto checky = st::contactsPadding.top() + + (st::contactsPhotoSize - check->height()) / 2; + check->moveToLeft(checkx, checky); + }, check->lifetime()); + + check->toggleOn(_state.value( + ) | rpl::map( + _1 == Active() + ) | rpl::distinct_until_changed()); + + check->setAttribute(Qt::WA_TransparentForMouseEvents); +} + +void Row::setupLabels(const Set &set) { + using namespace rpl::mappers; + + const auto name = Ui::CreateChild( + this, + set.name, + Ui::FlatLabel::InitType::Simple, + st::localStorageRowTitle); + const auto status = Ui::CreateChild( + this, + _state.value() | rpl::map(StateDescription), + st::localStorageRowSize); + + sizeValue( + ) | rpl::start_with_next([=](QSize size) { + const auto left = st::contactsPadding.left(); + const auto namey = st::contactsPadding.top() + + st::contactsNameTop; + const auto statusy = st::contactsPadding.top() + + st::contactsStatusTop; + name->moveToLeft(left, namey); + status->moveToLeft(left, statusy); + }, name->lifetime()); +} + +} // namespace + +ManageSetsBox::ManageSetsBox(QWidget*) { +} + +void ManageSetsBox::prepare() { + const auto inner = setInnerWidget(object_ptr(this)); + + setTitle(langFactory(lng_emoji_manage_sets)); + + addButton(langFactory(lng_close), [=] { closeBox(); }); + + setDimensionsToContent(st::boxWidth, inner); +} + +} // namespace Emoji +} // namespace Ui diff --git a/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.h b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.h new file mode 100644 index 000000000..000af1c1f --- /dev/null +++ b/Telegram/SourceFiles/chat_helpers/emoji_sets_manager.h @@ -0,0 +1,27 @@ +/* +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 "boxes/abstract_box.h" + +namespace Ui { +namespace Emoji { + +class ManageSetsBox : public BoxContent { +public: + explicit ManageSetsBox(QWidget*); + +protected: + void prepare() override; + +private: + +}; + +} // namespace Emoji +} // namespace Ui diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index 899ae7071..8c28d56ff 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localstorage.h" #include "core/file_utilities.h" #include "data/data_session.h" +#include "chat_helpers/emoji_sets_manager.h" #include "support/support_common.h" #include "support/support_templates.h" #include "auth_session.h" @@ -485,6 +486,16 @@ void SetupStickersEmoji(not_null container) { Ui::show(Box(StickersBox::Section::Installed)); }); + AddButton( + container, + lng_emoji_manage_sets, + st::settingsChatButton, + &st::settingsIconStickers, + st::settingsChatIconLeft + )->addClickHandler([] { + Ui::show(Box()); + }); + AddSkip(container, st::settingsCheckboxesSkip); } diff --git a/Telegram/SourceFiles/ui/emoji_config.cpp b/Telegram/SourceFiles/ui/emoji_config.cpp index d41429979..b1c23cba6 100644 --- a/Telegram/SourceFiles/ui/emoji_config.cpp +++ b/Telegram/SourceFiles/ui/emoji_config.cpp @@ -30,10 +30,10 @@ constexpr auto kCacheVersion = uint32(3); constexpr auto kMaxId = uint32(1 << 8); const auto kSets = { - Set{ 0, 0, "Mac" }, - Set{ 1, 205, "Android" }, - Set{ 2, 206, "Twemoji" }, - Set{ 3, 237, "EmojiOne" }, + Set{ 0, 0, 0, "Mac" }, + Set{ 1, 205, 7'232'542, "Android" }, + Set{ 2, 206, 5'038'738, "Twemoji" }, + Set{ 3, 238, 6'992'260, "EmojiOne" }, }; // Right now we can't allow users of Ui::Emoji to create custom sizes. @@ -552,6 +552,27 @@ bool SwitchToSet(int id) { return true; } +bool SetIsReady(int id) { + Expects(IsValidSetId(id)); + + if (!id) { + return true; + } + const auto folder = SetDataPath(id) + '/'; + auto names = ranges::view::ints( + 0, + SpritesCount + 1 + ) | ranges::view::transform([](int index) { + return index + ? "emoji_" + QString::number(index) + ".webp" + : QString("config.json"); + }); + const auto bad = ranges::find_if(names, [&](const QString &name) { + return !QFile(folder + name).exists(); + }); + return (bad == names.end()); +} + rpl::producer<> Updated() { return Updates.events(); } diff --git a/Telegram/SourceFiles/ui/emoji_config.h b/Telegram/SourceFiles/ui/emoji_config.h index 99bb647c6..91bc4f820 100644 --- a/Telegram/SourceFiles/ui/emoji_config.h +++ b/Telegram/SourceFiles/ui/emoji_config.h @@ -23,12 +23,14 @@ void ClearIrrelevantCache(); struct Set { int id = 0; int postId = 0; + int size = 0; QString name; }; std::vector Sets(); int CurrentSetId(); bool SwitchToSet(int id); +bool SetIsReady(int id); rpl::producer<> Updated(); int GetSizeNormal(); diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt index 974712e40..85082fcc6 100644 --- a/Telegram/gyp/telegram_sources.txt +++ b/Telegram/gyp/telegram_sources.txt @@ -86,6 +86,8 @@ <(src_loc)/chat_helpers/bot_keyboard.h <(src_loc)/chat_helpers/emoji_list_widget.cpp <(src_loc)/chat_helpers/emoji_list_widget.h +<(src_loc)/chat_helpers/emoji_sets_manager.cpp +<(src_loc)/chat_helpers/emoji_sets_manager.h <(src_loc)/chat_helpers/emoji_suggestions_helper.h <(src_loc)/chat_helpers/emoji_suggestions_widget.cpp <(src_loc)/chat_helpers/emoji_suggestions_widget.h