From 90179188b9b04e512988b5b6755965495cbfd7f1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 8 Mar 2018 00:25:03 +0300 Subject: [PATCH] Improve sticker by emoji ordering. First display recent by send/install date, then trending, then other. --- Telegram/SourceFiles/apiwrap.cpp | 16 +- Telegram/SourceFiles/apiwrap.h | 2 + .../chat_helpers/field_autocomplete.cpp | 33 +++-- .../chat_helpers/field_autocomplete.h | 12 +- .../SourceFiles/chat_helpers/stickers.cpp | 137 ++++++++++++++---- Telegram/SourceFiles/chat_helpers/stickers.h | 4 +- Telegram/SourceFiles/mainwidget.cpp | 28 +++- 7 files changed, 178 insertions(+), 54 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 18a402c92..9669855a5 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2200,6 +2200,10 @@ void ApiWrap::updateStickers() { requestSavedGifs(now); } +void ApiWrap::requestRecentStickersForce() { + requestRecentStickersWithHash(0); +} + void ApiWrap::setGroupStickerSet(not_null megagroup, const MTPInputStickerSet &set) { Expects(megagroup->mgInfo != nullptr); @@ -2283,13 +2287,19 @@ void ApiWrap::requestStickers(TimeId now) { } void ApiWrap::requestRecentStickers(TimeId now) { - if (!_session->data().recentStickersUpdateNeeded(now) - || _recentStickersUpdateRequest) { + if (!_session->data().recentStickersUpdateNeeded(now)) { + return; + } + requestRecentStickersWithHash(Local::countRecentStickersHash()); +} + +void ApiWrap::requestRecentStickersWithHash(int32 hash) { + if (_recentStickersUpdateRequest) { return; } _recentStickersUpdateRequest = request(MTPmessages_GetRecentStickers( MTP_flags(0), - MTP_int(Local::countRecentStickersHash()) + MTP_int(hash) )).done([=](const MTPmessages_RecentStickers &result) { _session->data().setLastRecentStickersUpdate(getms(true)); _recentStickersUpdateRequest = 0; diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 563087347..dc742a7bf 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -126,6 +126,7 @@ public: const Stickers::Order &localOrder, const Stickers::Order &localRemoved); void updateStickers(); + void requestRecentStickersForce(); void setGroupStickerSet( not_null megagroup, const MTPInputStickerSet &set); @@ -345,6 +346,7 @@ private: void requestStickers(TimeId now); void requestRecentStickers(TimeId now); + void requestRecentStickersWithHash(int32 hash); void requestFavedStickers(TimeId now); void requestFeaturedStickers(TimeId now); void requestSavedGifs(TimeId now); diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index 4eefa76cd..105a2fdf8 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -101,7 +101,7 @@ void FieldAutocomplete::showStickers(EmojiPtr emoji) { _emoji = emoji; _type = Type::Stickers; if (!emoji) { - rowsUpdated(_mrows, _hrows, _brows, Stickers::Pack(), false); + rowsUpdated(_mrows, _hrows, _brows, internal::StickerRows(), false); return; } @@ -135,9 +135,9 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { internal::MentionRows mrows; internal::HashtagRows hrows; internal::BotCommandRows brows; - Stickers::Pack srows; + internal::StickerRows srows; if (_emoji) { - srows = Stickers::GetListByEmoji(_emoji); + srows = Stickers::GetListByEmoji(_emoji, _stickersSeed); } else if (_type == Type::Mentions) { int maxListSize = _addInlineBots ? cRecentInlineBots().size() : 0; if (_chat) { @@ -319,8 +319,8 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { _inner->setRecentInlineBotsInRows(recentInlineBots); } -void FieldAutocomplete::rowsUpdated(const internal::MentionRows &mrows, const internal::HashtagRows &hrows, const internal::BotCommandRows &brows, const Stickers::Pack &srows, bool resetScroll) { - if (mrows.isEmpty() && hrows.isEmpty() && brows.isEmpty() && srows.isEmpty()) { +void FieldAutocomplete::rowsUpdated(const internal::MentionRows &mrows, const internal::HashtagRows &hrows, const internal::BotCommandRows &brows, const internal::StickerRows &srows, bool resetScroll) { + if (mrows.isEmpty() && hrows.isEmpty() && brows.isEmpty() && srows.empty()) { if (!isHidden()) { hideAnimated(); } @@ -356,7 +356,7 @@ void FieldAutocomplete::setBoundings(QRect boundings) { void FieldAutocomplete::recount(bool resetScroll) { int32 h = 0, oldst = _scroll->scrollTop(), st = oldst, maxh = 4.5 * st::mentionHeight; - if (!_srows.isEmpty()) { + if (!_srows.empty()) { int32 stickersPerRow = qMax(1, int32(_boundings.width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width())); int32 rows = rowscount(_srows.size(), stickersPerRow); h = st::stickerPanPadding + rows * st::stickerPanSize.height(); @@ -416,6 +416,7 @@ void FieldAutocomplete::showAnimated() { return; } if (_cache.isNull()) { + _stickersSeed = rand_value(); _scroll->show(); _cache = Ui::GrabWidget(this); } @@ -477,7 +478,7 @@ bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) { QKeyEvent *ev = static_cast(e); if (!(ev->modifiers() & (Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier | Qt::MetaModifier))) { if (!hidden) { - if (ev->key() == Qt::Key_Up || ev->key() == Qt::Key_Down || (!_srows.isEmpty() && (ev->key() == Qt::Key_Left || ev->key() == Qt::Key_Right))) { + if (ev->key() == Qt::Key_Up || ev->key() == Qt::Key_Down || (!_srows.empty() && (ev->key() == Qt::Key_Left || ev->key() == Qt::Key_Right))) { return _inner->moveSel(ev->key()); } else if (ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return) { return _inner->chooseSelected(ChooseMethod::ByEnter); @@ -498,7 +499,7 @@ FieldAutocomplete::~FieldAutocomplete() { namespace internal { -FieldAutocompleteInner::FieldAutocompleteInner(FieldAutocomplete *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, Stickers::Pack *srows) +FieldAutocompleteInner::FieldAutocompleteInner(FieldAutocomplete *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, StickerRows *srows) : _parent(parent) , _mrows(mrows) , _hrows(hrows) @@ -527,7 +528,7 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) { int32 mentionwidth = width() - mentionleft - 2 * st::mentionPadding.right(); int32 htagleft = st::historyAttach.width + st::historyComposeField.textMrg.left() - st::lineWidth, htagwidth = width() - st::mentionPadding.right() - htagleft - st::mentionScroll.width; - if (!_srows->isEmpty()) { + if (!_srows->empty()) { int32 rows = rowscount(_srows->size(), _stickersPerRow); int32 fromrow = floorclamp(r.y() - st::stickerPanPadding, st::stickerPanSize.height(), 0, rows); int32 torow = ceilclamp(r.y() + r.height() - st::stickerPanPadding, st::stickerPanSize.height(), 0, rows); @@ -697,7 +698,7 @@ bool FieldAutocompleteInner::moveSel(int key) { _mouseSel = false; int32 maxSel = (_mrows->isEmpty() ? (_hrows->isEmpty() ? (_brows->isEmpty() ? _srows->size() : _brows->size()) : _hrows->size()) : _mrows->size()); int32 direction = (key == Qt::Key_Up) ? -1 : (key == Qt::Key_Down ? 1 : 0); - if (!_srows->isEmpty()) { + if (!_srows->empty()) { if (key == Qt::Key_Left) { direction = -1; } else if (key == Qt::Key_Right) { @@ -721,7 +722,7 @@ bool FieldAutocompleteInner::moveSel(int key) { } bool FieldAutocompleteInner::chooseSelected(FieldAutocomplete::ChooseMethod method) const { - if (!_srows->isEmpty()) { + if (!_srows->empty()) { if (_sel >= 0 && _sel < _srows->size()) { emit stickerChosen(_srows->at(_sel), method); return true; @@ -791,7 +792,7 @@ void FieldAutocompleteInner::mousePressEvent(QMouseEvent *e) { _mouseSel = true; onUpdateSelected(true); - } else if (_srows->isEmpty()) { + } else if (_srows->empty()) { chooseSelected(FieldAutocomplete::ChooseMethod::ByClick); } else { _down = _sel; @@ -815,7 +816,7 @@ void FieldAutocompleteInner::mouseReleaseEvent(QMouseEvent *e) { return; } - if (_sel < 0 || _sel != pressed || _srows->isEmpty()) return; + if (_sel < 0 || _sel != pressed || _srows->empty()) return; chooseSelected(FieldAutocomplete::ChooseMethod::ByClick); } @@ -835,7 +836,7 @@ void FieldAutocompleteInner::leaveEventHook(QEvent *e) { void FieldAutocompleteInner::updateSelectedRow() { if (_sel >= 0) { - if (_srows->isEmpty()) { + if (_srows->empty()) { update(0, _sel * st::mentionHeight, width(), st::mentionHeight); } else { int32 row = _sel / _stickersPerRow, col = _sel % _stickersPerRow; @@ -850,7 +851,7 @@ void FieldAutocompleteInner::setSel(int sel, bool scroll) { updateSelectedRow(); if (scroll && _sel >= 0) { - if (_srows->isEmpty()) { + if (_srows->empty()) { emit mustScrollTo(_sel * st::mentionHeight, (_sel + 1) * st::mentionHeight); } else { int32 row = _sel / _stickersPerRow; @@ -866,7 +867,7 @@ void FieldAutocompleteInner::onUpdateSelected(bool force) { if (_down >= 0 && !_previewShown) return; int32 sel = -1, maxSel = 0; - if (!_srows->isEmpty()) { + if (!_srows->empty()) { int32 rows = rowscount(_srows->size(), _stickersPerRow); int32 row = (mouse.y() >= st::stickerPanPadding) ? ((mouse.y() - st::stickerPanPadding) / st::stickerPanSize.height()) : -1; int32 col = (mouse.x() >= st::stickerPanPadding) ? ((mouse.x() - st::stickerPanPadding) / st::stickerPanSize.width()) : -1; diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h index 030380fe7..1d5cf64df 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h @@ -19,6 +19,7 @@ namespace internal { using MentionRows = QList; using HashtagRows = QList; using BotCommandRows = QList>; +using StickerRows = std::vector>; class FieldAutocompleteInner; @@ -53,7 +54,7 @@ public: bool chooseSelected(ChooseMethod method) const; bool stickersShown() const { - return !_srows.isEmpty(); + return !_srows.empty(); } bool overlaps(const QRect &globalRect) { @@ -92,9 +93,9 @@ private: internal::MentionRows _mrows; internal::HashtagRows _hrows; internal::BotCommandRows _brows; - Stickers::Pack _srows; + internal::StickerRows _srows; - void rowsUpdated(const internal::MentionRows &mrows, const internal::HashtagRows &hrows, const internal::BotCommandRows &brows, const Stickers::Pack &srows, bool resetScroll); + void rowsUpdated(const internal::MentionRows &mrows, const internal::HashtagRows &hrows, const internal::BotCommandRows &brows, const internal::StickerRows &srows, bool resetScroll); object_ptr _scroll; QPointer _inner; @@ -103,6 +104,7 @@ private: UserData *_user = nullptr; ChannelData *_channel = nullptr; EmojiPtr _emoji; + uint64 _stickersSeed = 0; enum class Type { Mentions, Hashtags, @@ -129,7 +131,7 @@ class FieldAutocompleteInner final : public TWidget, private base::Subscriber { Q_OBJECT public: - FieldAutocompleteInner(FieldAutocomplete *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, Stickers::Pack *srows); + FieldAutocompleteInner(FieldAutocomplete *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, StickerRows *srows); void clearSel(bool hidden = false); bool moveSel(int key); @@ -167,7 +169,7 @@ private: MentionRows *_mrows; HashtagRows *_hrows; BotCommandRows *_brows; - Stickers::Pack *_srows; + StickerRows *_srows; int32 _stickersPerRow, _recentInlineBotsInRows; int32 _sel, _down; bool _mouseSel; diff --git a/Telegram/SourceFiles/chat_helpers/stickers.cpp b/Telegram/SourceFiles/chat_helpers/stickers.cpp index c7ade0d99..1f153972d 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers.cpp @@ -668,19 +668,84 @@ void GifsReceived(const QVector &items, int32 hash) { Auth().data().notifySavedGifsUpdated(); } -Pack GetListByEmoji(not_null emoji) { - auto original = emoji->original(); - auto result = Pack(); - auto setsToRequest = QMap(); - auto &sets = Auth().data().stickerSetsRef(); +std::vector> GetListByEmoji( + not_null emoji, + uint64 seed) { + const auto original = emoji->original(); - auto faved = Pack(); - auto favedIt = sets.find(Stickers::FavedSetId); - if (favedIt != sets.cend()) { - auto i = favedIt->emoji.constFind(original); - if (i != favedIt->emoji.cend()) { - faved = *i; - result = faved; + struct StickerWithDate { + not_null document; + TimeId date = 0; + }; + auto result = std::vector(); + auto &sets = Auth().data().stickerSetsRef(); + auto setsToRequest = base::flat_map(); + + const auto add = [&](not_null document, TimeId date) { + if (ranges::find(result, document, [](const StickerWithDate &data) { + return data.document; + }) == result.end()) { + result.push_back({ document, date }); + } + }; + + constexpr auto kSlice = 65536; + const auto CreateSortKey = [&]( + not_null document, + int base) { + return TimeId(base + int((document->id ^ seed) % kSlice)); + }; + const auto CreateRecentSortKey = [&](not_null document) { + return CreateSortKey(document, kSlice * 4); + }; + auto myCounter = 0; + const auto CreateMySortKey = [&] { + return (kSlice * 4 - (++myCounter)); + }; + const auto CreateFeaturedSortKey = [&](not_null document) { + return CreateSortKey(document, kSlice * 2); + }; + const auto CreateOtherSortKey = [&](not_null document) { + return CreateSortKey(document, kSlice); + }; + const auto InstallDate = [&](not_null document) { + Expects(document->sticker() != nullptr); + + const auto sticker = document->sticker(); + if (sticker->set.type() == mtpc_inputStickerSetID) { + const auto setId = sticker->set.c_inputStickerSetID().vid.v; + const auto setIt = sets.find(setId); + if (setIt != sets.end()) { + return setIt->installDate; + } + } + return TimeId(0); + }; + + auto recentIt = sets.find(Stickers::CloudRecentSetId); + if (recentIt != sets.cend()) { + auto i = recentIt->emoji.constFind(original); + if (i != recentIt->emoji.cend()) { + result.reserve(i->size()); + for (const auto document : *i) { + const auto usageDate = [&] { + if (recentIt->dates.empty()) { + return TimeId(0); + } + const auto index = recentIt->stickers.indexOf(document); + if (index < 0) { + return TimeId(0); + } + Assert(index < recentIt->dates.size()); + return recentIt->dates[index]; + }(); + const auto date = usageDate + ? usageDate + : InstallDate(document); + result.push_back({ + document, + date ? date : CreateRecentSortKey(document) }); + } } } const auto addList = [&](const Order &order, MTPDstickerSet::Flag skip) { @@ -690,7 +755,7 @@ Pack GetListByEmoji(not_null emoji) { continue; } if (it->emoji.isEmpty()) { - setsToRequest.insert(it->id, it->access); + setsToRequest.emplace(it->id, it->access); it->flags |= MTPDstickerSet_ClientFlag::f_not_loaded; continue; } @@ -698,11 +763,16 @@ Pack GetListByEmoji(not_null emoji) { if (i == it->emoji.cend()) { continue; } + const auto my = (it->flags & MTPDstickerSet::Flag::f_installed_date); result.reserve(result.size() + i->size()); - for_const (const auto document, *i) { - if (!faved.contains(document)) { - result.push_back(document); - } + for (const auto document : *i) { + const auto installDate = my ? it->installDate : TimeId(0); + const auto date = (installDate > 1) + ? installDate + : my + ? CreateMySortKey() + : CreateFeaturedSortKey(document); + add(document, date); } } }; @@ -714,21 +784,32 @@ Pack GetListByEmoji(not_null emoji) { Auth().data().featuredStickerSetsOrder(), MTPDstickerSet::Flag::f_installed_date); - if (!setsToRequest.isEmpty()) { - for (auto i = setsToRequest.cbegin(), e = setsToRequest.cend(); i != e; ++i) { - Auth().api().scheduleStickerSetRequest(i.key(), i.value()); + if (!setsToRequest.empty()) { + for (const auto [setId, accessHash] : setsToRequest) { + Auth().api().scheduleStickerSetRequest(setId, accessHash); } Auth().api().requestStickerSets(); } - if (const auto pack = Auth().api().stickersByEmoji(original)) { - for (const auto document : *pack) { - if (!base::contains(result, document)) { - result.push_back(document); - } - } - return result; + + const auto others = Auth().api().stickersByEmoji(original); + if (!others) { + return {}; } - return Pack(); + result.reserve(result.size() + others->size()); + for (const auto document : *others) { + add(document, CreateOtherSortKey(document)); + } + + ranges::action::sort( + result, + std::greater<>(), + [](const StickerWithDate &data) { return data.date; }); + + return ranges::view::all( + result + ) | ranges::view::transform([](const StickerWithDate &data) { + return data.document; + }) | ranges::to_vector; } base::optional>> GetEmojiListFromSet( diff --git a/Telegram/SourceFiles/chat_helpers/stickers.h b/Telegram/SourceFiles/chat_helpers/stickers.h index d406068fc..0d93db356 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers.h +++ b/Telegram/SourceFiles/chat_helpers/stickers.h @@ -86,7 +86,9 @@ void FeaturedSetsReceived( int32 hash); void GifsReceived(const QVector &items, int32 hash); -Pack GetListByEmoji(not_null emoji); +std::vector> GetListByEmoji( + not_null emoji, + uint64 seed); base::optional>> GetEmojiListFromSet( not_null document); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 787a16351..2aba72aff 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -4058,19 +4058,45 @@ void MainWidget::incrementSticker(DocumentData *sticker) { it->title = lang(lng_recent_stickers); } } + auto removedFromEmoji = std::vector>(); auto index = it->stickers.indexOf(sticker); if (index > 0) { - if (!it->dates.empty()) { + if (it->dates.empty()) { + Auth().api().requestRecentStickersForce(); + } else { Assert(it->dates.size() == it->stickers.size()); it->dates.erase(it->dates.begin() + index); } it->stickers.removeAt(index); + for (auto i = it->emoji.begin(); i != it->emoji.end();) { + if (const auto index = i->indexOf(sticker); index >= 0) { + removedFromEmoji.push_back(i.key()); + i->removeAt(index); + if (i->isEmpty()) { + i = it->emoji.erase(i); + continue; + } + } + ++i; + } } if (index) { if (it->dates.size() == it->stickers.size()) { it->dates.insert(it->dates.begin(), unixtime()); } it->stickers.push_front(sticker); + if (const auto emojiList = Stickers::GetEmojiListFromSet(sticker)) { + for (const auto emoji : *emojiList) { + it->emoji[emoji].push_front(sticker); + } + } else if (!removedFromEmoji.empty()) { + for (const auto emoji : removedFromEmoji) { + it->emoji[emoji].push_front(sticker); + } + } else { + Auth().api().requestRecentStickersForce(); + } + writeRecentStickers = true; }