Implement emoji search / categories.
BIN
Telegram/Resources/icons/emoji/emoji_activities.png
Normal file
After Width: | Height: | Size: 777 B |
BIN
Telegram/Resources/icons/emoji/emoji_activities@2x.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Telegram/Resources/icons/emoji/emoji_activities@3x.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 731 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2 KiB |
BIN
Telegram/Resources/icons/emoji/emoji_back.png
Normal file
After Width: | Height: | Size: 340 B |
BIN
Telegram/Resources/icons/emoji/emoji_back@2x.png
Normal file
After Width: | Height: | Size: 520 B |
BIN
Telegram/Resources/icons/emoji/emoji_back@3x.png
Normal file
After Width: | Height: | Size: 787 B |
Before Width: | Height: | Size: 739 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 859 B After Width: | Height: | Size: 738 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2 KiB |
BIN
Telegram/Resources/icons/emoji/emoji_love.png
Normal file
After Width: | Height: | Size: 575 B |
BIN
Telegram/Resources/icons/emoji/emoji_love@2x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/emoji/emoji_love@3x.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 836 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 785 B After Width: | Height: | Size: 652 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 710 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 647 B After Width: | Height: | Size: 484 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 906 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
Telegram/Resources/icons/emoji/emoji_search_input.png
Normal file
After Width: | Height: | Size: 471 B |
BIN
Telegram/Resources/icons/emoji/emoji_search_input@2x.png
Normal file
After Width: | Height: | Size: 844 B |
BIN
Telegram/Resources/icons/emoji/emoji_search_input@3x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 700 B After Width: | Height: | Size: 632 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
Telegram/Resources/icons/emoji/emoji_smile.png
Normal file
After Width: | Height: | Size: 541 B |
BIN
Telegram/Resources/icons/emoji/emoji_smile@2x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/emoji/emoji_smile@3x.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 270 B |
Before Width: | Height: | Size: 384 B |
Before Width: | Height: | Size: 520 B |
Before Width: | Height: | Size: 697 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 663 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 547 B After Width: | Height: | Size: 470 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 899 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 247 B After Width: | Height: | Size: 255 B |
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 312 B |
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 494 B |
Before Width: | Height: | Size: 589 B After Width: | Height: | Size: 527 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1,007 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 575 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 983 B |
Before Width: | Height: | Size: 1.8 KiB |
|
@ -10,6 +10,22 @@ using "ui/basic.style";
|
|||
using "boxes/boxes.style";
|
||||
using "ui/widgets/widgets.style";
|
||||
|
||||
TabbedSearch {
|
||||
outer: color;
|
||||
bg: color;
|
||||
fg: color;
|
||||
fgActive: color;
|
||||
fadeLeft: icon;
|
||||
fadeRight: icon;
|
||||
field: InputField;
|
||||
search: IconButton;
|
||||
back: IconButton;
|
||||
cancel: CrossButton;
|
||||
defaultFieldWidth: pixels;
|
||||
groupWidth: pixels;
|
||||
height: pixels;
|
||||
}
|
||||
|
||||
EmojiPan {
|
||||
margin: margins;
|
||||
padding: margins;
|
||||
|
@ -28,6 +44,8 @@ EmojiPan {
|
|||
overBg: color;
|
||||
fadeLeft: icon;
|
||||
fadeRight: icon;
|
||||
search: TabbedSearch;
|
||||
searchMargin: margins;
|
||||
}
|
||||
|
||||
switchPmButton: RoundButton(defaultBoxButton) {
|
||||
|
@ -132,8 +150,7 @@ stickersTrendingUnread: icon {
|
|||
{ "emoji/stickers_add_dot", dialogsUnreadBg }
|
||||
};
|
||||
stickersRecent: icon {{ "emoji/emoji_recent", emojiIconFg }};
|
||||
stickersSearch: icon {{ "emoji/stickers_search", emojiIconFg }};
|
||||
stickersPremium: icon {{ "emoji/stickers_premium", emojiIconFg }};
|
||||
emojiStatusDefault: icon {{ "emoji/stickers_premium", emojiIconFg }};
|
||||
|
||||
stickersSettingsUnreadSize: 6px;
|
||||
stickersSettingsUnreadPosition: point(6px, 10px);
|
||||
|
@ -145,27 +162,27 @@ filtersRemove: IconButton(stickersRemove) {
|
|||
emojiPanMargins: margins(10px, 10px, 10px, 10px);
|
||||
|
||||
emojiTabs: SettingsSlider(defaultTabsSlider) {
|
||||
height: 55px;
|
||||
barTop: 52px;
|
||||
labelTop: 19px;
|
||||
height: 45px;
|
||||
barTop: 42px;
|
||||
labelTop: 13px;
|
||||
}
|
||||
emojiScroll: defaultSolidScroll;
|
||||
emojiRecent: icon {{ "emoji/emoji_recent", emojiIconFg }};
|
||||
emojiRecentActive: icon {{ "emoji/emoji_recent", emojiSubIconFgActive }};
|
||||
emojiPeople: icon {{ "emoji/emoji_people", emojiIconFg }};
|
||||
emojiPeopleActive: icon {{ "emoji/emoji_people", emojiSubIconFgActive }};
|
||||
emojiPeople: icon {{ "emoji/emoji_smile", emojiIconFg }};
|
||||
emojiPeopleActive: icon {{ "emoji/emoji_smile", emojiSubIconFgActive }};
|
||||
emojiNature: icon {{ "emoji/emoji_nature", emojiIconFg }};
|
||||
emojiNatureActive: icon {{ "emoji/emoji_nature", emojiSubIconFgActive }};
|
||||
emojiFood: icon {{ "emoji/emoji_food", emojiIconFg }};
|
||||
emojiFoodActive: icon {{ "emoji/emoji_food", emojiSubIconFgActive }};
|
||||
emojiActivity: icon {{ "emoji/emoji_activity", emojiIconFg }};
|
||||
emojiActivityActive: icon {{ "emoji/emoji_activity", emojiSubIconFgActive }};
|
||||
emojiActivity: icon {{ "emoji/emoji_activities", emojiIconFg }};
|
||||
emojiActivityActive: icon {{ "emoji/emoji_activities", emojiSubIconFgActive }};
|
||||
emojiTravel: icon {{ "emoji/emoji_travel", emojiIconFg }};
|
||||
emojiTravelActive: icon {{ "emoji/emoji_travel", emojiSubIconFgActive }};
|
||||
emojiObjects: icon {{ "emoji/emoji_objects", emojiIconFg }};
|
||||
emojiObjectsActive: icon {{ "emoji/emoji_objects", emojiSubIconFgActive }};
|
||||
emojiSymbols: icon {{ "emoji/emoji_symbols", emojiIconFg }};
|
||||
emojiSymbolsActive: icon {{ "emoji/emoji_symbols", emojiSubIconFgActive }};
|
||||
emojiSymbols: icon {{ "emoji/emoji_love", emojiIconFg }};
|
||||
emojiSymbolsActive: icon {{ "emoji/emoji_love", emojiSubIconFgActive }};
|
||||
|
||||
emojiCategoryIconTop: 6px;
|
||||
emojiPanAnimation: PanelAnimation(defaultPanelAnimation) {
|
||||
|
@ -185,6 +202,37 @@ emojiPanLeft: 13px;
|
|||
emojiPanRight: 17px;
|
||||
emojiPanRadius: 8px;
|
||||
|
||||
defaultTabbedSearch: TabbedSearch {
|
||||
outer: emojiPanBg;
|
||||
bg: emojiPanCategories;
|
||||
fg: emojiIconFg;
|
||||
fgActive: emojiSubIconFgActive;
|
||||
fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }};
|
||||
fadeRight: icon {{ "fade_horizontal", emojiPanCategories }};
|
||||
field: InputField(defaultMultiSelectSearchField) {
|
||||
textMargins: margins(2px, 7px, 2px, 0px);
|
||||
}
|
||||
search: IconButton(defaultIconButton) {
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
icon: icon{{ "emoji/emoji_search_input", emojiIconFg }};
|
||||
iconOver: icon{{ "emoji/emoji_search_input", emojiIconFg }};
|
||||
iconPosition: point(12px, -1px);
|
||||
ripple: emptyRippleAnimation;
|
||||
}
|
||||
back: IconButton(defaultIconButton) {
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
icon: icon{{ "emoji/emoji_back", menuIconFg }};
|
||||
iconOver: icon{{ "emoji/emoji_back", menuIconFg }};
|
||||
iconPosition: point(12px, -1px);
|
||||
ripple: emptyRippleAnimation;
|
||||
}
|
||||
cancel: defaultMultiSelectSearchCancel;
|
||||
defaultFieldWidth: 95px;
|
||||
groupWidth: 30px;
|
||||
height: 33px;
|
||||
}
|
||||
defaultEmojiPan: EmojiPan {
|
||||
margin: margins(roundRadiusSmall, 0px, 14px, 0px);
|
||||
padding: margins(13px, 12px, 17px, 12px);
|
||||
|
@ -203,6 +251,8 @@ defaultEmojiPan: EmojiPan {
|
|||
overBg: emojiPanHover;
|
||||
fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }};
|
||||
fadeRight: icon {{ "fade_horizontal", emojiPanCategories }};
|
||||
search: defaultTabbedSearch;
|
||||
searchMargin: margins(6px, 10px, 6px, 10px);
|
||||
}
|
||||
|
||||
inlineResultsMinHeight: 278px;
|
||||
|
@ -215,13 +265,6 @@ emojiColorsPadding: 5px;
|
|||
emojiColorsSep: 1px;
|
||||
emojiColorsSepColor: shadowFg;
|
||||
|
||||
emojiSwitchSkip: 27px;
|
||||
emojiSwitchImgSkip: 21px;
|
||||
emojiSwitchColor: windowActiveTextFg;
|
||||
emojiSwitchStickers: icon {{ "emoji/emoji_switch", emojiSwitchColor }};
|
||||
emojiSwitchEmoji: icon {{ "emoji/emoji_switch-flip_horizontal", emojiSwitchColor }};
|
||||
|
||||
emojiIconPadding: 7px;
|
||||
emojiIconSelectSkip: 3px;
|
||||
emojiPremiumRequired: icon{{ "emoji/premium_lock", windowSubTextFg }};
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "main/main_domain.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
|
@ -639,6 +641,52 @@ std::vector<Result> EmojiKeywords::query(
|
|||
return result;
|
||||
}
|
||||
|
||||
std::vector<Result> EmojiKeywords::queryMine(
|
||||
const QString &query,
|
||||
bool exact) const {
|
||||
return ApplyVariants(PrioritizeRecent(this->query(query, exact)));
|
||||
}
|
||||
|
||||
std::vector<Result> EmojiKeywords::PrioritizeRecent(
|
||||
std::vector<Result> list) {
|
||||
using Entry = Result;
|
||||
auto lastRecent = begin(list);
|
||||
const auto &recent = Core::App().settings().recentEmoji();
|
||||
for (const auto &item : recent) {
|
||||
const auto emoji = std::get_if<EmojiPtr>(&item.id.data);
|
||||
if (!emoji) {
|
||||
continue;
|
||||
}
|
||||
const auto original = (*emoji)->original()
|
||||
? (*emoji)->original()
|
||||
: (*emoji);
|
||||
const auto it = ranges::find(list, original, [](const Entry &entry) {
|
||||
return entry.emoji;
|
||||
});
|
||||
if (it > lastRecent && it != end(list)) {
|
||||
std::rotate(lastRecent, it, it + 1);
|
||||
++lastRecent;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
std::vector<Result> EmojiKeywords::ApplyVariants(std::vector<Result> list) {
|
||||
for (auto &item : list) {
|
||||
item.emoji = [&] {
|
||||
const auto result = item.emoji;
|
||||
const auto &variants = Core::App().settings().emojiVariants();
|
||||
const auto i = result->hasVariants()
|
||||
? variants.find(result->nonColoredId())
|
||||
: end(variants);
|
||||
return (i != end(variants))
|
||||
? result->variant(i->second)
|
||||
: result;
|
||||
}();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
int EmojiKeywords::maxQueryLength() const {
|
||||
if (_data.empty()) {
|
||||
return 0;
|
||||
|
|
|
@ -43,6 +43,9 @@ public:
|
|||
[[nodiscard]] std::vector<Result> query(
|
||||
const QString &query,
|
||||
bool exact = false) const;
|
||||
[[nodiscard]] std::vector<Result> queryMine(
|
||||
const QString &query,
|
||||
bool exact = false) const;
|
||||
[[nodiscard]] int maxQueryLength() const;
|
||||
|
||||
private:
|
||||
|
@ -52,6 +55,11 @@ private:
|
|||
ApiWrap *api() override;
|
||||
void langPackRefreshed() override;
|
||||
|
||||
[[nodiscard]] static std::vector<Result> PrioritizeRecent(
|
||||
std::vector<Result> list);
|
||||
[[nodiscard]] static std::vector<Result> ApplyVariants(
|
||||
std::vector<Result> list);
|
||||
|
||||
void handleSessionChanges();
|
||||
void apiChanged(ApiWrap *api);
|
||||
void refreshInputLanguages();
|
||||
|
|
|
@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "chat_helpers/emoji_list_widget.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "ui/controls/tabbed_search.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
|
@ -23,11 +24,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "boxes/sticker_set_box.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "layout/layout_position.h"
|
||||
#include "data/data_emoji_statuses.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "chat_helpers/emoji_keywords.h"
|
||||
#include "chat_helpers/stickers_list_widget.h"
|
||||
#include "chat_helpers/stickers_list_footer.h"
|
||||
#include "emoji_suggestions_data.h"
|
||||
|
@ -44,6 +47,8 @@ namespace {
|
|||
|
||||
constexpr auto kCollapsedRows = 3;
|
||||
constexpr auto kAppearDuration = 0.3;
|
||||
constexpr auto kPlainSearchLimit = 32;
|
||||
constexpr auto kCustomSearchLimit = 256;
|
||||
|
||||
using Core::RecentEmojiId;
|
||||
using Core::RecentEmojiDocument;
|
||||
|
@ -402,6 +407,10 @@ EmojiListWidget::EmojiListWidget(
|
|||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
}
|
||||
|
||||
if (_mode != Mode::RecentReactions) {
|
||||
setupSearch();
|
||||
}
|
||||
|
||||
_customSingleSize = Data::FrameSizeFromTag(
|
||||
Data::CustomEmojiManager::SizeTag::Large
|
||||
) / style::DevicePixelRatio();
|
||||
|
@ -457,6 +466,142 @@ EmojiListWidget::~EmojiListWidget() {
|
|||
base::take(_customEmoji);
|
||||
}
|
||||
|
||||
void EmojiListWidget::setupSearch() {
|
||||
using Descriptor = Ui::SearchDescriptor;
|
||||
_search = std::make_unique<Ui::TabbedSearch>(this, st(), Descriptor{
|
||||
.st = st().search,
|
||||
.groups = (_mode == Mode::EmojiStatus
|
||||
? session().data().emojiStatuses().statusGroupsValue()
|
||||
: session().data().emojiStatuses().emojiGroupsValue()),
|
||||
.customEmojiFactory = session().data().customEmojiManager().factory()
|
||||
});
|
||||
_search->queryValue(
|
||||
) | rpl::start_with_next([=](std::vector<QString> &&query) {
|
||||
_nextSearchQuery = std::move(query);
|
||||
InvokeQueued(this, [=] {
|
||||
applyNextSearchQuery();
|
||||
});
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void EmojiListWidget::applyNextSearchQuery() {
|
||||
if (_searchQuery == _nextSearchQuery) {
|
||||
return;
|
||||
}
|
||||
_searchQuery = _nextSearchQuery;
|
||||
std::swap(_searchEmoji, _searchEmojiPrevious);
|
||||
_searchEmoji.clear();
|
||||
const auto finish = [&](bool searching = true) {
|
||||
if (!_searchMode && !searching) {
|
||||
return;
|
||||
}
|
||||
_searchMode = searching;
|
||||
if (!searching) {
|
||||
_searchResults.clear();
|
||||
_searchCustomIds.clear();
|
||||
}
|
||||
clearSelection();
|
||||
resizeToWidth(width());
|
||||
update();
|
||||
};
|
||||
if (_searchQuery.empty()) {
|
||||
finish(false);
|
||||
return;
|
||||
}
|
||||
const auto guard = gsl::finally([&] { finish(); });
|
||||
auto plain = collectPlainSearchResults();
|
||||
if (_searchEmoji == _searchEmojiPrevious) {
|
||||
return;
|
||||
}
|
||||
_searchResults.clear();
|
||||
_searchCustomIds.clear();
|
||||
if (_mode == Mode::EmojiStatus || session().premium()) {
|
||||
appendPremiumSearchResults();
|
||||
}
|
||||
if (_mode != Mode::EmojiStatus) {
|
||||
for (const auto emoji : plain) {
|
||||
_searchResults.push_back({
|
||||
.id = { emoji },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EmojiPtr> EmojiListWidget::collectPlainSearchResults() {
|
||||
auto result = std::vector<EmojiPtr>();
|
||||
const auto pushPlain = [&](EmojiPtr emoji) {
|
||||
if (result.size() < kPlainSearchLimit
|
||||
&& _searchEmoji.emplace(emoji).second) {
|
||||
result.push_back(emoji);
|
||||
}
|
||||
if (const auto original = emoji->original(); original != emoji) {
|
||||
_searchEmoji.emplace(original);
|
||||
}
|
||||
};
|
||||
auto refreshed = false;
|
||||
auto &keywords = Core::App().emojiKeywords();
|
||||
for (const auto &entry : _searchQuery) {
|
||||
if (const auto emoji = Ui::Emoji::Find(entry)) {
|
||||
pushPlain(emoji);
|
||||
if (result.size() >= kPlainSearchLimit) {
|
||||
return result;
|
||||
}
|
||||
} else if (!entry.isEmpty()) {
|
||||
if (!refreshed) {
|
||||
refreshed = true;
|
||||
keywords.refresh();
|
||||
}
|
||||
const auto list = keywords.queryMine(entry);
|
||||
for (const auto &entry : list) {
|
||||
pushPlain(entry.emoji);
|
||||
if (result.size() >= kPlainSearchLimit) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void EmojiListWidget::appendPremiumSearchResults() {
|
||||
const auto test = session().isTestMode();
|
||||
auto &owner = session().data();
|
||||
const auto checkCustom = [&](EmojiPtr emoji, DocumentId id) {
|
||||
return emoji
|
||||
&& _searchEmoji.contains(emoji)
|
||||
&& (_searchResults.size() < kCustomSearchLimit)
|
||||
&& _searchCustomIds.emplace(id).second;
|
||||
};
|
||||
for (const auto &recent : _recent) {
|
||||
if (!recent.custom) {
|
||||
continue;
|
||||
}
|
||||
const auto &idData = recent.id.data;
|
||||
const auto id = std::get_if<Core::RecentEmojiDocument>(&idData);
|
||||
if (!id || id->test != test) {
|
||||
continue;
|
||||
}
|
||||
const auto sticker = owner.document(id->id)->sticker();
|
||||
const auto emoji = sticker
|
||||
? Ui::Emoji::Find(sticker->alt)
|
||||
: nullptr;
|
||||
if (checkCustom(emoji, id->id)) {
|
||||
_searchResults.push_back(recent);
|
||||
}
|
||||
}
|
||||
for (const auto &set : _custom) {
|
||||
for (const auto &one : set.list) {
|
||||
const auto id = one.document->id;
|
||||
if (checkCustom(one.emoji, id)) {
|
||||
_searchResults.push_back({
|
||||
.custom = one.custom,
|
||||
.id = { RecentEmojiDocument{ .id = id, .test = test } },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EmojiListWidget::provideRecent(
|
||||
const std::vector<DocumentId> &customRecentList) {
|
||||
clearSelection();
|
||||
|
@ -468,6 +613,13 @@ void EmojiListWidget::repaintCustom(uint64 setId) {
|
|||
if (!_repaintsScheduled.emplace(setId).second) {
|
||||
return;
|
||||
}
|
||||
const auto repaintSearch = (setId == SearchEmojiSectionSetId());
|
||||
if (_searchMode) {
|
||||
if (repaintSearch) {
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto repaintRecent = (setId == RecentEmojiSectionSetId());
|
||||
enumerateSections([&](const SectionInfo &info) {
|
||||
const auto repaint1 = repaintRecent
|
||||
|
@ -600,7 +752,7 @@ bool EmojiListWidget::enumerateSections(Callback callback) const {
|
|||
? kCollapsedRows
|
||||
: (info.count + _columnCount - 1) / _columnCount;
|
||||
info.rowsTop = info.top
|
||||
+ (i == 0 ? st().padding.top() : st().header);
|
||||
+ (i == 0 ? _rowsTop : st().header);
|
||||
info.rowsBottom = info.rowsTop
|
||||
+ (info.rowsCount * _singleSize.height())
|
||||
+ st::roundRadiusSmall;
|
||||
|
@ -610,6 +762,11 @@ bool EmojiListWidget::enumerateSections(Callback callback) const {
|
|||
info.top = info.rowsBottom;
|
||||
return true;
|
||||
};
|
||||
if (_searchMode) {
|
||||
info.section = i;
|
||||
info.count = _searchResults.size();
|
||||
return next();
|
||||
}
|
||||
for (; i != _staticCount; ++i) {
|
||||
info.section = i;
|
||||
info.count = i ? _counts[i] : _recent.size();
|
||||
|
@ -660,7 +817,7 @@ EmojiListWidget::SectionInfo EmojiListWidget::sectionInfoByOffset(
|
|||
}
|
||||
|
||||
int EmojiListWidget::sectionsCount() const {
|
||||
return _staticCount + int(_custom.size());
|
||||
return _searchMode ? 1 : (_staticCount + int(_custom.size()));
|
||||
}
|
||||
|
||||
void EmojiListWidget::setSingleSize(QSize size) {
|
||||
|
@ -686,6 +843,7 @@ int EmojiListWidget::countDesiredHeight(int newWidth) {
|
|||
const auto innerWidth = fullWidth - padding.left() - padding.right();
|
||||
_columnCount = std::max(innerWidth / st().desiredSize, 1);
|
||||
const auto singleWidth = innerWidth / _columnCount;
|
||||
_rowsTop = _search ? _search->height() : padding.top();
|
||||
_rowsLeft = padding.left()
|
||||
+ (innerWidth - _columnCount * singleWidth) / 2
|
||||
- st().margin.left();
|
||||
|
@ -698,9 +856,12 @@ int EmojiListWidget::countDesiredHeight(int newWidth) {
|
|||
return info.top
|
||||
+ qMax(info.rowsBottom - info.top, minimalLastHeight);
|
||||
};
|
||||
const auto minimalLastHeight = minimalHeight;
|
||||
return qMax(minimalHeight, countResult(minimalLastHeight))
|
||||
+ padding.bottom();
|
||||
const auto minimalLastHeight = std::max(
|
||||
minimalHeight - padding.bottom(),
|
||||
0);
|
||||
return qMax(
|
||||
minimalHeight,
|
||||
countResult(minimalLastHeight) + padding.bottom());
|
||||
}
|
||||
|
||||
int EmojiListWidget::defaultMinimalHeight() const {
|
||||
|
@ -953,7 +1114,7 @@ void EmojiListWidget::paint(
|
|||
);
|
||||
const auto w = position + _areaPosition;
|
||||
if (context.expanding) {
|
||||
const auto y = (position.y() - st().padding.top());
|
||||
const auto y = (position.y() - _rowsTop);
|
||||
const auto x = (position.x() - _rowsLeft);
|
||||
const auto sum = y
|
||||
+ std::max(std::min(y, width()) - x, 0);
|
||||
|
@ -981,8 +1142,10 @@ void EmojiListWidget::paint(
|
|||
}
|
||||
_overBg.paint(p, QRect(tl, st::emojiPanArea));
|
||||
}
|
||||
if (info.section == int(Section::Recent)) {
|
||||
drawRecent(p, context, w, index);
|
||||
if (_searchMode) {
|
||||
drawRecent(p, context, w, _searchResults[index]);
|
||||
} else if (info.section == int(Section::Recent)) {
|
||||
drawRecent(p, context, w, _recent[index]);
|
||||
} else if (info.section < _staticCount) {
|
||||
drawEmoji(p, context, w, _emoji[info.section][index]);
|
||||
} else {
|
||||
|
@ -1020,9 +1183,8 @@ void EmojiListWidget::drawRecent(
|
|||
QPainter &p,
|
||||
const ExpandingContext &context,
|
||||
QPoint position,
|
||||
int index) {
|
||||
const RecentOne &recent) {
|
||||
_recentPainted = true;
|
||||
auto &recent = _recent[index];
|
||||
if (const auto custom = recent.custom) {
|
||||
_emojiPaintContext->scale = context.progress;
|
||||
_emojiPaintContext->position = position
|
||||
|
@ -1032,8 +1194,8 @@ void EmojiListWidget::drawRecent(
|
|||
} else if (const auto emoji = std::get_if<EmojiPtr>(&recent.id.data)) {
|
||||
if (_mode == Mode::EmojiStatus) {
|
||||
position += QPoint(
|
||||
(_singleSize.width() - st::stickersPremium.width()) / 2,
|
||||
(_singleSize.height() - st::stickersPremium.height()) / 2
|
||||
(_singleSize.width() - st::emojiStatusDefault.width()) / 2,
|
||||
(_singleSize.height() - st::emojiStatusDefault.height()) / 2
|
||||
) - _areaPosition;
|
||||
p.drawImage(position, _premiumIcon->image());
|
||||
} else {
|
||||
|
@ -1087,7 +1249,16 @@ bool EmojiListWidget::checkPickerHide() {
|
|||
DocumentData *EmojiListWidget::lookupCustomEmoji(
|
||||
int index,
|
||||
int section) const {
|
||||
if (section == int(Section::Recent) && index < _recent.size()) {
|
||||
if (_searchMode) {
|
||||
if (index < _searchResults.size()) {
|
||||
const auto document = std::get_if<RecentEmojiDocument>(
|
||||
&_searchResults[index].id.data);
|
||||
if (document) {
|
||||
return session().data().document(document->id);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
} else if (section == int(Section::Recent) && index < _recent.size()) {
|
||||
const auto document = std::get_if<RecentEmojiDocument>(
|
||||
&_recent[index].id.data);
|
||||
if (document) {
|
||||
|
@ -1104,9 +1275,14 @@ DocumentData *EmojiListWidget::lookupCustomEmoji(
|
|||
EmojiPtr EmojiListWidget::lookupOverEmoji(const OverEmoji *over) const {
|
||||
const auto section = over ? over->section : -1;
|
||||
const auto index = over ? over->index : -1;
|
||||
return (section == int(Section::Recent)
|
||||
&& index < _recent.size()
|
||||
&& v::is<EmojiPtr>(_recent[index].id.data))
|
||||
return _searchMode
|
||||
? ((index < _searchResults.size()
|
||||
&& v::is<EmojiPtr>(_searchResults[index].id.data))
|
||||
? v::get<EmojiPtr>(_searchResults[index].id.data)
|
||||
: nullptr)
|
||||
: (section == int(Section::Recent)
|
||||
&& index < _recent.size()
|
||||
&& v::is<EmojiPtr>(_recent[index].id.data))
|
||||
? v::get<EmojiPtr>(_recent[index].id.data)
|
||||
: (section > int(Section::Recent)
|
||||
&& section < _staticCount
|
||||
|
@ -1119,9 +1295,10 @@ EmojiChosen EmojiListWidget::lookupChosen(
|
|||
EmojiPtr emoji,
|
||||
not_null<const OverEmoji*> over) {
|
||||
const auto rect = emojiRect(over->section, over->index);
|
||||
const auto size = st::emojiStatusDefault.size();
|
||||
const auto icon = QRect(
|
||||
rect.x() + (_singleSize.width() - st::stickersPremium.width()) / 2,
|
||||
rect.y() + (_singleSize.height() - st::stickersPremium.height()) / 2,
|
||||
rect.x() + (_singleSize.width() - size.width()) / 2,
|
||||
rect.y() + (_singleSize.height() - size.height()) / 2,
|
||||
rect.width(),
|
||||
rect.height());
|
||||
return {
|
||||
|
@ -1622,10 +1799,11 @@ void EmojiListWidget::refreshCustom() {
|
|||
auto set = std::vector<CustomOne>();
|
||||
set.reserve(list.size());
|
||||
for (const auto document : list) {
|
||||
if (document->sticker()) {
|
||||
if (const auto sticker = document->sticker()) {
|
||||
set.push_back({
|
||||
.custom = resolveCustomEmoji(document, setId),
|
||||
.document = document,
|
||||
.emoji = Ui::Emoji::Find(sticker->alt),
|
||||
});
|
||||
if (!premium && document->isPremiumEmoji()) {
|
||||
premium = true;
|
||||
|
@ -1668,6 +1846,9 @@ Fn<void()> EmojiListWidget::repaintCallback(
|
|||
if (_recentCustomIds.contains(documentId)) {
|
||||
repaintCustom(RecentEmojiSectionSetId());
|
||||
}
|
||||
if (_searchCustomIds.contains(documentId)) {
|
||||
repaintCustom(SearchEmojiSectionSetId());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ struct phrase;
|
|||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
class TabbedSearch;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Emoji {
|
||||
|
@ -162,6 +163,7 @@ private:
|
|||
struct CustomOne {
|
||||
not_null<Ui::Text::CustomEmoji*> custom;
|
||||
not_null<DocumentData*> document;
|
||||
EmojiPtr emoji = nullptr;
|
||||
};
|
||||
struct CustomSet {
|
||||
uint64 id = 0;
|
||||
|
@ -243,6 +245,9 @@ private:
|
|||
void unloadAllCustom();
|
||||
void unloadCustomIn(const SectionInfo &info);
|
||||
|
||||
void setupSearch();
|
||||
[[nodiscard]] std::vector<EmojiPtr> collectPlainSearchResults();
|
||||
void appendPremiumSearchResults();
|
||||
void ensureLoaded(int section);
|
||||
void updateSelected();
|
||||
void setSelected(OverState newSelected);
|
||||
|
@ -267,7 +272,7 @@ private:
|
|||
QPainter &p,
|
||||
const ExpandingContext &context,
|
||||
QPoint position,
|
||||
int index);
|
||||
const RecentOne &recent);
|
||||
void drawEmoji(
|
||||
QPainter &p,
|
||||
const ExpandingContext &context,
|
||||
|
@ -307,7 +312,6 @@ private:
|
|||
void displaySet(uint64 setId);
|
||||
void removeSet(uint64 setId);
|
||||
|
||||
void refreshColoredStatuses();
|
||||
void initButton(RightButton &button, const QString &text, bool gradient);
|
||||
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(
|
||||
int section);
|
||||
|
@ -328,8 +332,11 @@ private:
|
|||
DocumentId documentId,
|
||||
uint64 setId);
|
||||
|
||||
void applyNextSearchQuery();
|
||||
|
||||
Window::SessionController *_controller = nullptr;
|
||||
Mode _mode = Mode::Full;
|
||||
std::unique_ptr<Ui::TabbedSearch> _search;
|
||||
const int _staticCount = 0;
|
||||
StickersListFooter *_footer = nullptr;
|
||||
std::unique_ptr<GradientPremiumStar> _premiumIcon;
|
||||
|
@ -355,6 +362,15 @@ private:
|
|||
bool _allowWithoutPremium = false;
|
||||
Ui::RoundRect _overBg;
|
||||
|
||||
std::vector<QString> _nextSearchQuery;
|
||||
std::vector<QString> _searchQuery;
|
||||
base::flat_set<EmojiPtr> _searchEmoji;
|
||||
base::flat_set<EmojiPtr> _searchEmojiPrevious;
|
||||
base::flat_set<DocumentId> _searchCustomIds;
|
||||
std::vector<RecentOne> _searchResults;
|
||||
bool _searchMode = false;
|
||||
|
||||
int _rowsTop = 0;
|
||||
int _rowsLeft = 0;
|
||||
int _columnCount = 1;
|
||||
QSize _singleSize;
|
||||
|
|
|
@ -213,49 +213,13 @@ auto SuggestionsWidget::getRowsByQuery(const QString &text) const
|
|||
return ranges::none_of(text, [](QChar ch) { return ch.isLower(); });
|
||||
}();
|
||||
const auto exact = !middle || simple;
|
||||
const auto list = Core::App().emojiKeywords().query(real, exact);
|
||||
if (list.empty()) {
|
||||
return {};
|
||||
}
|
||||
const auto list = Core::App().emojiKeywords().queryMine(real, exact);
|
||||
using Entry = ChatHelpers::EmojiKeywords::Result;
|
||||
auto result = ranges::views::all(
|
||||
return ranges::views::all(
|
||||
list
|
||||
) | ranges::views::transform([](const Entry &result) {
|
||||
return Row(result.emoji, result.replacement);
|
||||
}) | ranges::to_vector;
|
||||
|
||||
auto lastRecent = begin(result);
|
||||
const auto &recent = Core::App().settings().recentEmoji();
|
||||
for (const auto &item : recent) {
|
||||
const auto emoji = std::get_if<EmojiPtr>(&item.id.data);
|
||||
if (!emoji) {
|
||||
continue;
|
||||
}
|
||||
const auto original = (*emoji)->original()
|
||||
? (*emoji)->original()
|
||||
: (*emoji);
|
||||
const auto it = ranges::find(result, original, [](const Row &row) {
|
||||
return row.emoji.get();
|
||||
});
|
||||
if (it > lastRecent && it != end(result)) {
|
||||
std::rotate(lastRecent, it, it + 1);
|
||||
++lastRecent;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &item : result) {
|
||||
item.emoji = [&] {
|
||||
const auto result = item.emoji;
|
||||
const auto &variants = Core::App().settings().emojiVariants();
|
||||
const auto i = result->hasVariants()
|
||||
? variants.find(result->nonColoredId())
|
||||
: end(variants);
|
||||
return (i != end(variants))
|
||||
? result->variant(i->second)
|
||||
: result.get();
|
||||
}();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void SuggestionsWidget::resizeToRows() {
|
||||
|
|
|
@ -322,11 +322,9 @@ void InitMessageFieldHandlers(
|
|||
const style::InputField *fieldStyle) {
|
||||
field->setTagMimeProcessor(
|
||||
FieldTagMimeProcessor(session, allowPremiumEmoji));
|
||||
field->setCustomEmojiFactory([=](QStringView data, Fn<void()> update) {
|
||||
return session->data().customEmojiManager().create(
|
||||
data,
|
||||
std::move(update));
|
||||
}, std::move(customEmojiPaused));
|
||||
field->setCustomEmojiFactory(
|
||||
session->data().customEmojiManager().factory(),
|
||||
std::move(customEmojiPaused));
|
||||
field->setInstantReplaces(Ui::InstantReplaces::Default());
|
||||
field->setInstantReplacesEnabled(
|
||||
Core::App().settings().replaceEmojiValue());
|
||||
|
|
|
@ -71,6 +71,12 @@ uint64 AllEmojiSectionSetId() {
|
|||
return kEmojiSectionSetIdBase;
|
||||
}
|
||||
|
||||
uint64 SearchEmojiSectionSetId() {
|
||||
return kEmojiSectionSetIdBase
|
||||
+ static_cast<uint64>(EmojiSection::Symbols)
|
||||
+ 2;
|
||||
}
|
||||
|
||||
std::optional<EmojiSection> SetIdEmojiSection(uint64 id) {
|
||||
const auto base = RecentEmojiSectionSetId();
|
||||
if (id < base) {
|
||||
|
@ -161,8 +167,8 @@ QImage GradientPremiumStar::image() const {
|
|||
}
|
||||
|
||||
void GradientPremiumStar::renderOnDemand() const {
|
||||
const auto size = st::stickersPremium.size();
|
||||
const auto mask = st::stickersPremium.instance(Qt::white);
|
||||
const auto size = st::emojiStatusDefault.size();
|
||||
const auto mask = st::emojiStatusDefault.instance(Qt::white);
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
_image = QImage(
|
||||
size * factor,
|
||||
|
@ -188,7 +194,6 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor)
|
|||
descriptor.st ? *descriptor.st : st::defaultEmojiPan)
|
||||
, _session(descriptor.session)
|
||||
, _paused(descriptor.paused)
|
||||
, _searchButtonVisible(descriptor.searchButtonVisible)
|
||||
, _settingsButtonVisible(descriptor.settingsButtonVisible)
|
||||
, _iconState([=] { update(); })
|
||||
, _subiconState([=] { update(); })
|
||||
|
@ -197,9 +202,7 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor)
|
|||
, _barSelection(descriptor.barSelection) {
|
||||
setMouseTracking(true);
|
||||
|
||||
_iconsLeft = st().iconSkip + (_searchButtonVisible
|
||||
? st::stickerIconWidth
|
||||
: 0);
|
||||
_iconsLeft = st().iconSkip;
|
||||
_iconsRight = st().iconSkip + (_settingsButtonVisible
|
||||
? st::stickerIconWidth
|
||||
: 0);
|
||||
|
@ -568,9 +571,6 @@ void StickersListFooter::paintEvent(QPaintEvent *e) {
|
|||
void StickersListFooter::paint(
|
||||
Painter &p,
|
||||
const ExpandingContext &context) const {
|
||||
if (_searchButtonVisible) {
|
||||
paintSearchIcon(p);
|
||||
}
|
||||
if (_icons.empty() || _searchShown) {
|
||||
return;
|
||||
}
|
||||
|
@ -751,8 +751,6 @@ void StickersListFooter::mousePressEvent(QMouseEvent *e) {
|
|||
|
||||
if (_selected == SpecialOver::Settings) {
|
||||
_openSettingsRequests.fire({});
|
||||
} else if (_selected == SpecialOver::Search) {
|
||||
toggleSearch(true);
|
||||
} else {
|
||||
_pressed = _selected;
|
||||
_iconsMouseDown = _iconsMousePos;
|
||||
|
@ -940,13 +938,7 @@ void StickersListFooter::updateSelected() {
|
|||
const auto settingsLeft = width() - _iconsRight;
|
||||
const auto searchLeft = _iconsLeft - _singleWidth;
|
||||
auto newOver = OverState(SpecialOver::None);
|
||||
if (_searchButtonVisible
|
||||
&& x >= searchLeft
|
||||
&& x < searchLeft + _singleWidth
|
||||
&& y >= _iconsTop
|
||||
&& y < _iconsTop + st().footer) {
|
||||
newOver = SpecialOver::Search;
|
||||
} else if (_settingsButtonVisible
|
||||
if (_settingsButtonVisible
|
||||
&& x >= settingsLeft
|
||||
&& x < settingsLeft + _singleWidth
|
||||
&& y >= _iconsTop
|
||||
|
@ -1115,15 +1107,6 @@ void StickersListFooter::paintStickerSettingsIcon(QPainter &p) const {
|
|||
width());
|
||||
}
|
||||
|
||||
void StickersListFooter::paintSearchIcon(QPainter &p) const {
|
||||
const auto searchLeft = _iconsLeft - _singleWidth;
|
||||
st::stickersSearch.paint(
|
||||
p,
|
||||
searchLeft + (_singleWidth - st::stickersSearch.width()) / 2,
|
||||
_iconsTop + st::emojiCategoryIconTop,
|
||||
width());
|
||||
}
|
||||
|
||||
void StickersListFooter::customEmojiRepaint() {
|
||||
if (!_repaintScheduled) {
|
||||
_repaintScheduled = true;
|
||||
|
@ -1309,7 +1292,7 @@ void StickersListFooter::paintSetIcon(
|
|||
width(),
|
||||
st::stickerGroupCategorySize);
|
||||
} else if (icon.setId == Data::Stickers::PremiumSetId) {
|
||||
const auto size = st::stickersPremium.size();
|
||||
const auto size = st::emojiStatusDefault.size();
|
||||
p.drawImage(
|
||||
info.adjustedLeft + (_singleWidth - size.width()) / 2,
|
||||
_iconsTop + (st().footer - size.height()) / 2,
|
||||
|
|
|
@ -48,6 +48,7 @@ enum class ValidateIconAnimations {
|
|||
[[nodiscard]] uint64 EmojiSectionSetId(Ui::Emoji::Section section);
|
||||
[[nodiscard]] uint64 RecentEmojiSectionSetId();
|
||||
[[nodiscard]] uint64 AllEmojiSectionSetId();
|
||||
[[nodiscard]] uint64 SearchEmojiSectionSetId();
|
||||
[[nodiscard]] std::optional<Ui::Emoji::Section> SetIdEmojiSection(uint64 id);
|
||||
|
||||
struct StickerIcon {
|
||||
|
@ -99,7 +100,6 @@ public:
|
|||
not_null<Main::Session*> session;
|
||||
Fn<bool()> paused;
|
||||
not_null<RpWidget*> parent;
|
||||
bool searchButtonVisible = false;
|
||||
bool settingsButtonVisible = false;
|
||||
bool barSelection = false;
|
||||
const style::EmojiPan *st = nullptr;
|
||||
|
@ -153,7 +153,6 @@ protected:
|
|||
private:
|
||||
enum class SpecialOver {
|
||||
None,
|
||||
Search,
|
||||
Settings,
|
||||
};
|
||||
struct IconId {
|
||||
|
@ -227,7 +226,6 @@ private:
|
|||
|
||||
void paint(Painter &p, const ExpandingContext &context) const;
|
||||
void paintStickerSettingsIcon(QPainter &p) const;
|
||||
void paintSearchIcon(QPainter &p) const;
|
||||
void paintSetIcon(
|
||||
Painter &p,
|
||||
const ExpandingContext &context,
|
||||
|
@ -254,7 +252,6 @@ private:
|
|||
|
||||
const not_null<Main::Session*> _session;
|
||||
const Fn<bool()> _paused;
|
||||
const bool _searchButtonVisible = false;
|
||||
const bool _settingsButtonVisible = false;
|
||||
|
||||
static constexpr auto kVisibleIconsCount = 8;
|
||||
|
|
|
@ -264,7 +264,6 @@ object_ptr<TabbedSelector::InnerFooter> StickersListWidget::createFooter() {
|
|||
.session = &session(),
|
||||
.paused = pausedMethod(),
|
||||
.parent = this,
|
||||
.searchButtonVisible = !_isMasks,
|
||||
.settingsButtonVisible = true,
|
||||
.barSelection = true,
|
||||
});
|
||||
|
|
|
@ -1020,7 +1020,7 @@ void TabbedSelector::fillTabsSliderSections() {
|
|||
return tr::lng_switch_masks;
|
||||
}
|
||||
Unexpected("SelectorTab value in fillTabsSliderSections.");
|
||||
}()(tr::now).toUpper();
|
||||
}()(tr::now);
|
||||
}) | ranges::to_vector;
|
||||
_tabsSlider->setSections(sections);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "base/timer_rpl.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "apiwrap.h"
|
||||
#include "ui/controls/tabbed_search.h"
|
||||
|
||||
namespace Data {
|
||||
namespace {
|
||||
|
@ -126,6 +127,70 @@ void EmojiStatuses::registerAutomaticClear(
|
|||
}
|
||||
}
|
||||
|
||||
auto EmojiStatuses::emojiGroupsValue() const -> rpl::producer<Groups> {
|
||||
const_cast<EmojiStatuses*>(this)->requestEmojiGroups();
|
||||
return _emojiGroups.data.value();
|
||||
}
|
||||
|
||||
auto EmojiStatuses::statusGroupsValue() const -> rpl::producer<Groups> {
|
||||
const_cast<EmojiStatuses*>(this)->requestStatusGroups();
|
||||
return _statusGroups.data.value();
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestEmojiGroups() {
|
||||
requestGroups(
|
||||
&_emojiGroups,
|
||||
MTPmessages_GetEmojiGroups(MTP_int(_emojiGroups.hash)));
|
||||
|
||||
}
|
||||
|
||||
void EmojiStatuses::requestStatusGroups() {
|
||||
requestGroups(
|
||||
&_statusGroups,
|
||||
MTPmessages_GetEmojiStatusGroups(MTP_int(_statusGroups.hash)));
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<Ui::EmojiGroup> GroupsFromTL(
|
||||
const MTPDmessages_emojiGroups &data) {
|
||||
const auto &list = data.vgroups().v;
|
||||
auto result = std::vector<Ui::EmojiGroup>();
|
||||
result.reserve(list.size());
|
||||
for (const auto &group : list) {
|
||||
const auto &data = group.data();
|
||||
auto emoticons = ranges::views::all(
|
||||
data.vemoticons().v
|
||||
) | ranges::view::transform([](const MTPstring &emoticon) {
|
||||
return qs(emoticon);
|
||||
}) | ranges::to_vector;
|
||||
result.push_back({
|
||||
.iconId = QString::number(data.vicon_emoji_id().v),
|
||||
.emoticons = std::move(emoticons),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Request>
|
||||
void EmojiStatuses::requestGroups(
|
||||
not_null<GroupsType*> type,
|
||||
Request &&request) {
|
||||
if (type->requestId) {
|
||||
return;
|
||||
}
|
||||
type->requestId = _owner->session().api().request(
|
||||
std::forward<Request>(request)
|
||||
).done([=](const MTPmessages_EmojiGroups &result) {
|
||||
type->requestId = 0;
|
||||
result.match([&](const MTPDmessages_emojiGroups &data) {
|
||||
type->hash = data.vhash().v;
|
||||
type->data = GroupsFromTL(data);
|
||||
}, [](const MTPDmessages_emojiGroupsNotModified&) {
|
||||
});
|
||||
}).fail([=] {
|
||||
type->requestId = 0;
|
||||
}).send();
|
||||
}
|
||||
|
||||
void EmojiStatuses::processClearing() {
|
||||
auto minWait = TimeId(0);
|
||||
const auto now = base::unixtime::now();
|
||||
|
|
|
@ -13,6 +13,10 @@ namespace Main {
|
|||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
struct EmojiGroup;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Data {
|
||||
|
||||
class DocumentMedia;
|
||||
|
@ -49,7 +53,19 @@ public:
|
|||
|
||||
void registerAutomaticClear(not_null<UserData*> user, TimeId until);
|
||||
|
||||
using Groups = std::vector<Ui::EmojiGroup>;
|
||||
[[nodiscard]] rpl::producer<Groups> emojiGroupsValue() const;
|
||||
[[nodiscard]] rpl::producer<Groups> statusGroupsValue() const;
|
||||
void requestEmojiGroups();
|
||||
void requestStatusGroups();
|
||||
|
||||
private:
|
||||
struct GroupsType {
|
||||
rpl::variable<Groups> data;
|
||||
mtpRequestId requestId = 0;
|
||||
int32 hash = 0;
|
||||
};
|
||||
|
||||
void requestRecent();
|
||||
void requestDefault();
|
||||
void requestColored();
|
||||
|
@ -61,6 +77,9 @@ private:
|
|||
void processClearingIn(TimeId wait);
|
||||
void processClearing();
|
||||
|
||||
template <typename Request>
|
||||
void requestGroups(not_null<GroupsType*> type, Request &&request);
|
||||
|
||||
const not_null<Session*> _owner;
|
||||
|
||||
std::vector<DocumentId> _recent;
|
||||
|
@ -84,6 +103,9 @@ private:
|
|||
base::flat_map<not_null<UserData*>, TimeId> _clearing;
|
||||
base::Timer _clearingTimer;
|
||||
|
||||
GroupsType _emojiGroups;
|
||||
GroupsType _statusGroups;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
|
|
@ -474,6 +474,14 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
|
|||
std::move(update));
|
||||
}
|
||||
|
||||
Ui::Text::CustomEmojiFactory CustomEmojiManager::factory(
|
||||
SizeTag tag,
|
||||
int sizeOverride) {
|
||||
return [=](QStringView data, Fn<void()> update) {
|
||||
return create(data, std::move(update), tag, sizeOverride);
|
||||
};
|
||||
}
|
||||
|
||||
Ui::CustomEmoji::Preview CustomEmojiManager::prepareNonExactPreview(
|
||||
DocumentId documentId,
|
||||
SizeTag tag,
|
||||
|
|
|
@ -52,6 +52,10 @@ public:
|
|||
SizeTag tag = SizeTag::Normal,
|
||||
int sizeOverride = 0);
|
||||
|
||||
[[nodiscard]] Ui::Text::CustomEmojiFactory factory(
|
||||
SizeTag tag = SizeTag::Normal,
|
||||
int sizeOverride = 0);
|
||||
|
||||
class Listener {
|
||||
public:
|
||||
virtual void customEmojiResolveDone(
|
||||
|
|
419
Telegram/SourceFiles/ui/controls/tabbed_search.cpp
Normal file
|
@ -0,0 +1,419 @@
|
|||
/*
|
||||
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 "ui/controls/tabbed_search.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/input_fields.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDebounceTimeout = crl::time(400);
|
||||
|
||||
class GroupsStrip final : public RpWidget {
|
||||
public:
|
||||
GroupsStrip(
|
||||
QWidget *parent,
|
||||
const style::TabbedSearch &st,
|
||||
rpl::producer<std::vector<EmojiGroup>> groups,
|
||||
Text::CustomEmojiFactory factory);
|
||||
|
||||
[[nodiscard]] rpl::producer<EmojiGroup> chosen() const;
|
||||
void clearChosen();
|
||||
|
||||
private:
|
||||
struct Button {
|
||||
EmojiGroup group;
|
||||
QString iconId;
|
||||
std::unique_ptr<Ui::Text::CustomEmoji> icon;
|
||||
};
|
||||
|
||||
void init(rpl::producer<std::vector<EmojiGroup>> groups);
|
||||
void set(std::vector<EmojiGroup> list);
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
static inline auto FindById(auto &&buttons, QStringView id) {
|
||||
return ranges::find(buttons, id, &Button::iconId);
|
||||
}
|
||||
|
||||
const style::TabbedSearch &_st;
|
||||
const Text::CustomEmojiFactory _factory;
|
||||
|
||||
std::vector<Button> _buttons;
|
||||
rpl::event_stream<EmojiGroup> _chosenGroup;
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
int _chosen = -1;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<QString> FieldQuery(not_null<InputField*> field) {
|
||||
if (const auto last = field->getLastText(); !last.isEmpty()) {
|
||||
return { last };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
GroupsStrip::GroupsStrip(
|
||||
QWidget *parent,
|
||||
const style::TabbedSearch &st,
|
||||
rpl::producer<std::vector<EmojiGroup>> groups,
|
||||
Text::CustomEmojiFactory factory)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _factory(std::move(factory)) {
|
||||
init(std::move(groups));
|
||||
}
|
||||
|
||||
rpl::producer<EmojiGroup> GroupsStrip::chosen() const {
|
||||
return _chosenGroup.events();
|
||||
}
|
||||
|
||||
void GroupsStrip::clearChosen() {
|
||||
if (const auto chosen = std::exchange(_chosen, -1); chosen >= 0) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void GroupsStrip::init(rpl::producer<std::vector<EmojiGroup>> groups) {
|
||||
std::move(
|
||||
groups
|
||||
) | rpl::start_with_next([=](std::vector<EmojiGroup> &&list) {
|
||||
set(std::move(list));
|
||||
}, lifetime());
|
||||
|
||||
setCursor(style::cur_pointer);
|
||||
}
|
||||
|
||||
void GroupsStrip::set(std::vector<EmojiGroup> list) {
|
||||
const auto chosen = (_chosen >= 0)
|
||||
? _buttons[_chosen].group.iconId
|
||||
: QString();
|
||||
auto existing = std::move(_buttons);
|
||||
const auto updater = [=](const QString &iconId) {
|
||||
return [=] {
|
||||
const auto i = FindById(_buttons, iconId);
|
||||
if (i != end(_buttons)) {
|
||||
const auto index = i - begin(_buttons);
|
||||
const auto single = _st.groupWidth;
|
||||
update(index * single, 0, single, height());
|
||||
}
|
||||
};
|
||||
};
|
||||
for (auto &group : list) {
|
||||
const auto i = FindById(existing, group.iconId);
|
||||
if (i != end(existing)) {
|
||||
_buttons.push_back(std::move(*i));
|
||||
existing.erase(i);
|
||||
} else {
|
||||
const auto loopCount = 1;
|
||||
const auto stopAtLastFrame = true;
|
||||
_buttons.push_back({
|
||||
.iconId = group.iconId,
|
||||
.icon = std::make_unique<Text::LimitedLoopsEmoji>(
|
||||
_factory(
|
||||
group.iconId,
|
||||
updater(group.iconId)),
|
||||
loopCount,
|
||||
stopAtLastFrame),
|
||||
});
|
||||
}
|
||||
_buttons.back().group = std::move(group);
|
||||
}
|
||||
resize(_buttons.size() * _st.groupWidth, height());
|
||||
if (!chosen.isEmpty()) {
|
||||
const auto i = FindById(_buttons, chosen);
|
||||
if (i != end(_buttons)) {
|
||||
_chosen = (i - begin(_buttons));
|
||||
_chosenGroup.fire_copy(i->group);
|
||||
} else {
|
||||
_chosen = -1;
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void GroupsStrip::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
auto index = 0;
|
||||
const auto single = _st.groupWidth;
|
||||
const auto height = this->height();
|
||||
const auto clip = e->rect();
|
||||
const auto now = crl::now();
|
||||
for (const auto &button : _buttons) {
|
||||
const auto left = index * single;
|
||||
const auto top = 0;
|
||||
const auto size = Ui::Text::AdjustCustomEmojiSize(st::emojiSize);
|
||||
if (_chosen == index) {
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::windowBgRipple);
|
||||
p.drawEllipse(left, top + (height - single) / 2, single, single);
|
||||
}
|
||||
if (QRect(left, top, single, height).intersects(clip)) {
|
||||
button.icon->paint(p, {
|
||||
.textColor = (_chosen == index ? _st.fgActive : _st.fg)->c,
|
||||
.now = now,
|
||||
.position = QPoint(left, top) + QPoint(
|
||||
(single - size) / 2,
|
||||
(height - size) / 2),
|
||||
});
|
||||
}
|
||||
++index;
|
||||
}
|
||||
}
|
||||
|
||||
void GroupsStrip::mousePressEvent(QMouseEvent *e) {
|
||||
const auto index = e->pos().x() / _st.groupWidth;
|
||||
const auto chosen = (index < 0 || index >= _buttons.size())
|
||||
? -1
|
||||
: index;
|
||||
_pressed = chosen;
|
||||
}
|
||||
|
||||
void GroupsStrip::mouseReleaseEvent(QMouseEvent *e) {
|
||||
const auto index = e->pos().x() / _st.groupWidth;
|
||||
const auto chosen = (index < 0 || index >= _buttons.size())
|
||||
? -1
|
||||
: index;
|
||||
const auto pressed = std::exchange(_pressed, -1);
|
||||
if (pressed == index && index >= 0) {
|
||||
_chosen = pressed;
|
||||
_chosenGroup.fire_copy(_buttons[index].group);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SearchWithGroups::SearchWithGroups(
|
||||
QWidget *parent,
|
||||
SearchDescriptor descriptor)
|
||||
: RpWidget(parent)
|
||||
, _st(descriptor.st)
|
||||
, _search(CreateChild<FadeWrap<IconButton>>(
|
||||
this,
|
||||
object_ptr<IconButton>(this, _st.search)))
|
||||
, _back(CreateChild<FadeWrap<IconButton>>(
|
||||
this,
|
||||
object_ptr<IconButton>(this, _st.back)))
|
||||
, _cancel(CreateChild<CrossButton>(this, _st.cancel))
|
||||
, _field(CreateChild<InputField>(this, _st.field, tr::lng_dlg_filter()))
|
||||
, _groups(CreateChild<FadeWrap<RpWidget>>(
|
||||
this,
|
||||
object_ptr<GroupsStrip>(
|
||||
this,
|
||||
_st,
|
||||
std::move(descriptor.groups),
|
||||
std::move(descriptor.customEmojiFactory))))
|
||||
, _fadeLeft(CreateChild<FadeWrap<RpWidget>>(
|
||||
this,
|
||||
object_ptr<RpWidget>(this)))
|
||||
, _fadeRight(CreateChild<FadeWrap<RpWidget>>(
|
||||
this,
|
||||
object_ptr<RpWidget>(this)))
|
||||
, _debounceTimer([=] { _debouncedQuery = _query.current(); }) {
|
||||
initField();
|
||||
initGroups();
|
||||
initEdges();
|
||||
}
|
||||
|
||||
anim::type SearchWithGroups::animated() const {
|
||||
return _inited ? anim::type::normal : anim::type::instant;
|
||||
}
|
||||
|
||||
void SearchWithGroups::initField() {
|
||||
connect(_field, &InputField::changed, [=] {
|
||||
const auto last = FieldQuery(_field);
|
||||
_query = last;
|
||||
const auto empty = last.empty();
|
||||
_cancel->toggle(!empty, animated());
|
||||
_groups->toggle(empty, animated());
|
||||
if (empty) {
|
||||
_debounceTimer.cancel();
|
||||
_debouncedQuery = last;
|
||||
} else {
|
||||
_debounceTimer.callOnce(kDebounceTimeout);
|
||||
_chosenGroup = QString();
|
||||
}
|
||||
});
|
||||
|
||||
_fieldPlaceholderWidth = tr::lng_dlg_filter(
|
||||
) | rpl::map([=](const QString &value) {
|
||||
return _st.field.placeholderFont->width(value);
|
||||
}) | rpl::after_next([=] {
|
||||
resizeToWidth(width());
|
||||
});
|
||||
|
||||
const auto last = FieldQuery(_field);
|
||||
_query = last;
|
||||
_debouncedQuery = last;
|
||||
}
|
||||
|
||||
void SearchWithGroups::initGroups() {
|
||||
const auto widget = static_cast<GroupsStrip*>(_groups->entity());
|
||||
|
||||
_groups->move(_search->entity()->width() + _st.defaultFieldWidth, 0);
|
||||
widget->resize(widget->width(), _st.height);
|
||||
widget->widthValue(
|
||||
) | rpl::filter([=] {
|
||||
return (width() > 0);
|
||||
}) | rpl::start_with_next([=] {
|
||||
resizeToWidth(width());
|
||||
}, widget->lifetime());
|
||||
|
||||
widget->chosen(
|
||||
) | rpl::start_with_next([=](const EmojiGroup &group) {
|
||||
_chosenGroup = group.iconId;
|
||||
_query = group.emoticons;
|
||||
_debouncedQuery = group.emoticons;
|
||||
_debounceTimer.cancel();
|
||||
}, lifetime());
|
||||
|
||||
_chosenGroup.value(
|
||||
) | rpl::map([=](const QString &id) {
|
||||
return id.isEmpty();
|
||||
}) | rpl::start_with_next([=](bool empty) {
|
||||
_search->toggle(empty, animated());
|
||||
_back->toggle(!empty, animated());
|
||||
if (empty) {
|
||||
widget->clearChosen();
|
||||
} else {
|
||||
_field->setText({});
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void SearchWithGroups::initEdges() {
|
||||
paintRequest() | rpl::start_with_next([=](QRect clip) {
|
||||
QPainter(this).fillRect(clip, _st.bg);
|
||||
}, lifetime());
|
||||
|
||||
const auto makeEdge = [&](bool left) {
|
||||
const auto edge = CreateChild<RpWidget>(this);
|
||||
const auto size = QSize(height() / 2, height());
|
||||
edge->resize(size);
|
||||
if (left) {
|
||||
edge->move(0, 0);
|
||||
} else {
|
||||
widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
edge->move(width - edge->width(), 0);
|
||||
}, edge->lifetime());
|
||||
}
|
||||
edge->paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto ratio = edge->devicePixelRatioF();
|
||||
ensureRounding(height(), ratio);
|
||||
const auto size = _rounding.height();
|
||||
const auto half = size / 2;
|
||||
QPainter(edge).drawImage(
|
||||
QPoint(),
|
||||
_rounding,
|
||||
QRect(left ? 0 : _rounding.width() - half, 0, half, size));
|
||||
}, edge->lifetime());
|
||||
};
|
||||
makeEdge(true);
|
||||
makeEdge(false);
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
_rounding = QImage();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void SearchWithGroups::ensureRounding(int size, float64 ratio) {
|
||||
const auto rounded = qRound(size * ratio);
|
||||
const auto full = QSize(rounded + 4, rounded);
|
||||
if (_rounding.size() != full) {
|
||||
_rounding = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
_rounding.fill(_st.outer->c);
|
||||
auto p = QPainter(&_rounding);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.setBrush(Qt::transparent);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(QRect(QPoint(), full), rounded / 2., rounded / 2.);
|
||||
}
|
||||
_rounding.setDevicePixelRatio(ratio);
|
||||
}
|
||||
|
||||
rpl::producer<std::vector<QString>> SearchWithGroups::queryValue() const {
|
||||
return _query.value();
|
||||
}
|
||||
|
||||
auto SearchWithGroups::debouncedQueryValue() const
|
||||
-> rpl::producer<std::vector<QString>> {
|
||||
return _debouncedQuery.value();
|
||||
}
|
||||
|
||||
int SearchWithGroups::resizeGetHeight(int newWidth) {
|
||||
_back->moveToLeft(0, 0, newWidth);
|
||||
_search->moveToLeft(0, 0, newWidth);
|
||||
_cancel->moveToRight(0, 0, newWidth);
|
||||
|
||||
const auto searchWidth = _search->entity()->width();
|
||||
const auto groupsLeftDefault = searchWidth + _st.defaultFieldWidth;
|
||||
const auto groupsLeftMin = newWidth - _groups->entity()->width();
|
||||
const auto groupsLeftMax = std::max(groupsLeftDefault, groupsLeftMin);
|
||||
const auto groupsLeft = std::clamp(
|
||||
_groups->x(),
|
||||
groupsLeftMin,
|
||||
groupsLeftMax);
|
||||
_groups->move(groupsLeft, 0);
|
||||
|
||||
const auto placeholderMargins = _st.field.textMargins
|
||||
+ _st.field.placeholderMargins;
|
||||
const auto placeholderWidth = _fieldPlaceholderWidth.current();
|
||||
const auto fieldWidthMin = std::min(
|
||||
rect::m::sum::h(placeholderMargins) + placeholderWidth,
|
||||
_st.defaultFieldWidth);
|
||||
const auto fieldWidth = std::max(
|
||||
groupsLeft - searchWidth,
|
||||
fieldWidthMin);
|
||||
_field->resizeToWidth(fieldWidth);
|
||||
_field->moveToLeft(groupsLeft - fieldWidth, 0);
|
||||
|
||||
return _st.height;
|
||||
}
|
||||
|
||||
TabbedSearch::TabbedSearch(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::EmojiPan &st,
|
||||
SearchDescriptor &&descriptor)
|
||||
: _st(st)
|
||||
, _search(parent, std::move(descriptor)) {
|
||||
_search.move(_st.searchMargin.left(), _st.searchMargin.top());
|
||||
|
||||
parent->widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
_search.resizeToWidth(width - rect::m::sum::h(_st.searchMargin));
|
||||
}, _search.lifetime());
|
||||
}
|
||||
|
||||
int TabbedSearch::height() const {
|
||||
return _search.height() + rect::m::sum::v(_st.searchMargin);
|
||||
}
|
||||
|
||||
rpl::producer<std::vector<QString>> TabbedSearch::queryValue() const {
|
||||
return _search.queryValue();
|
||||
}
|
||||
|
||||
auto TabbedSearch::debouncedQueryValue() const
|
||||
-> rpl::producer<std::vector<QString>> {
|
||||
return _search.debouncedQueryValue();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
105
Telegram/SourceFiles/ui/controls/tabbed_search.h
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
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/timer.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
|
||||
namespace style {
|
||||
struct EmojiPan;
|
||||
struct TabbedSearch;
|
||||
} // namespace style
|
||||
|
||||
namespace anim {
|
||||
enum class type : uchar;
|
||||
} // namespace anim
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class InputField;
|
||||
class IconButton;
|
||||
class CrossButton;
|
||||
class RpWidget;
|
||||
template <typename Widget>
|
||||
class FadeWrap;
|
||||
|
||||
struct EmojiGroup {
|
||||
QString iconId;
|
||||
std::vector<QString> emoticons;
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const EmojiGroup &a,
|
||||
const EmojiGroup &b) = default;
|
||||
};
|
||||
|
||||
struct SearchDescriptor {
|
||||
const style::TabbedSearch &st;
|
||||
rpl::producer<std::vector<EmojiGroup>> groups;
|
||||
Text::CustomEmojiFactory customEmojiFactory;
|
||||
};
|
||||
|
||||
class SearchWithGroups final : public RpWidget {
|
||||
public:
|
||||
SearchWithGroups(QWidget *parent, SearchDescriptor descriptor);
|
||||
|
||||
[[nodiscard]] rpl::producer<std::vector<QString>> queryValue() const;
|
||||
[[nodiscard]] auto debouncedQueryValue() const
|
||||
-> rpl::producer<std::vector<QString>>;
|
||||
|
||||
private:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
[[nodiscard]] anim::type animated() const;
|
||||
void initField();
|
||||
void initGroups();
|
||||
void initEdges();
|
||||
|
||||
void ensureRounding(int size, float64 rounding);
|
||||
|
||||
const style::TabbedSearch &_st;
|
||||
not_null<FadeWrap<IconButton>*> _search;
|
||||
not_null<FadeWrap<IconButton>*> _back;
|
||||
not_null<CrossButton*> _cancel;
|
||||
not_null<InputField*> _field;
|
||||
not_null<FadeWrap<RpWidget>*> _groups;
|
||||
not_null<FadeWrap<RpWidget>*> _fadeLeft;
|
||||
not_null<FadeWrap<RpWidget>*> _fadeRight;
|
||||
|
||||
rpl::variable<int> _fieldPlaceholderWidth;
|
||||
|
||||
QImage _rounding;
|
||||
|
||||
rpl::variable<std::vector<QString>> _query;
|
||||
rpl::variable<std::vector<QString>> _debouncedQuery;
|
||||
rpl::variable<QString> _chosenGroup;
|
||||
base::Timer _debounceTimer;
|
||||
bool _inited = false;
|
||||
|
||||
};
|
||||
|
||||
class TabbedSearch final {
|
||||
public:
|
||||
TabbedSearch(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::EmojiPan &st,
|
||||
SearchDescriptor &&descriptor);
|
||||
|
||||
[[nodiscard]] int height() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<std::vector<QString>> queryValue() const;
|
||||
[[nodiscard]] auto debouncedQueryValue() const
|
||||
->rpl::producer<std::vector<QString>>;
|
||||
|
||||
private:
|
||||
const style::EmojiPan &_st;
|
||||
SearchWithGroups _search;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
|
@ -235,6 +235,8 @@ PRIVATE
|
|||
ui/controls/send_as_button.h
|
||||
ui/controls/send_button.cpp
|
||||
ui/controls/send_button.h
|
||||
ui/controls/tabbed_search.cpp
|
||||
ui/controls/tabbed_search.h
|
||||
ui/controls/who_reacted_context_action.cpp
|
||||
ui/controls/who_reacted_context_action.h
|
||||
ui/controls/window_outdated_bar.cpp
|
||||
|
|