mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-08-22 10:17:10 +00:00
Initial global posts search implementation.
This commit is contained in:
parent
79650cf318
commit
4aab5da7ae
@ -709,6 +709,8 @@ PRIVATE
|
|||||||
dialogs/dialogs_search_from_controllers.h
|
dialogs/dialogs_search_from_controllers.h
|
||||||
dialogs/dialogs_search_tags.cpp
|
dialogs/dialogs_search_tags.cpp
|
||||||
dialogs/dialogs_search_tags.h
|
dialogs/dialogs_search_tags.h
|
||||||
|
dialogs/dialogs_search_posts.cpp
|
||||||
|
dialogs/dialogs_search_posts.h
|
||||||
dialogs/dialogs_top_bar_suggestion.cpp
|
dialogs/dialogs_top_bar_suggestion.cpp
|
||||||
dialogs/dialogs_top_bar_suggestion.h
|
dialogs/dialogs_top_bar_suggestion.h
|
||||||
dialogs/dialogs_widget.cpp
|
dialogs/dialogs_widget.cpp
|
||||||
|
@ -6745,6 +6745,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_recent_chats" = "Chats";
|
"lng_recent_chats" = "Chats";
|
||||||
"lng_recent_channels" = "Channels";
|
"lng_recent_channels" = "Channels";
|
||||||
"lng_recent_apps" = "Apps";
|
"lng_recent_apps" = "Apps";
|
||||||
|
"lng_recent_posts" = "Posts";
|
||||||
"lng_all_photos" = "Photos";
|
"lng_all_photos" = "Photos";
|
||||||
"lng_all_videos" = "Videos";
|
"lng_all_videos" = "Videos";
|
||||||
"lng_all_downloads" = "Downloads";
|
"lng_all_downloads" = "Downloads";
|
||||||
@ -6762,6 +6763,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_bot_apps_popular" = "Grossing apps";
|
"lng_bot_apps_popular" = "Grossing apps";
|
||||||
"lng_bot_apps_which" = "Which apps are included here? {link}";
|
"lng_bot_apps_which" = "Which apps are included here? {link}";
|
||||||
"lng_bot_apps_which_link" = "Learn >";
|
"lng_bot_apps_which_link" = "Learn >";
|
||||||
|
"lng_posts_title" = "Global Search";
|
||||||
|
"lng_posts_start" = "Type a keyword to search all posts from public channels.";
|
||||||
|
"lng_posts_subscribe" = "Subscribe to Premium";
|
||||||
|
"lng_posts_need_subscribe" = "Global search is a Premium feature.";
|
||||||
|
"lng_posts_search_button" = "Search {query}";
|
||||||
|
"lng_posts_remaining#one" = "{count} free search remaining today";
|
||||||
|
"lng_posts_remaining#other" = "{count} free searches remaining today";
|
||||||
|
"lng_posts_subtitle_empty" = "Telegram News";
|
||||||
|
"lng_posts_subtitle" = "Public posts";
|
||||||
|
"lng_posts_limit_reached" = "Limit Reached";
|
||||||
|
"lng_posts_limit_about#one" = "You can make up to {count} search query per day.";
|
||||||
|
"lng_posts_limit_about#other" = "You can make up to {count} search queries per day.";
|
||||||
|
"lng_posts_limit_search_paid" = "Search for {cost}";
|
||||||
|
"lng_posts_limit_unlocks" = "free search unlocks in {duration}";
|
||||||
|
"lng_posts_paid_spent#one" = "**{count} Star** spent on extra search.";
|
||||||
|
"lng_posts_paid_spent#other" = "**{count} Stars** spent on extra search.";
|
||||||
|
|
||||||
"lng_popular_apps_info_title" = "Top Mini Apps";
|
"lng_popular_apps_info_title" = "Top Mini Apps";
|
||||||
"lng_popular_apps_info_text" = "This catalogue ranks mini apps based on their daily revenue, measured in Stars. To be listed, developers must set their main mini apps in {bot} (as described {link}), have over **1,000** daily users, and earn a daily revenue above **1,000** Stars, based on the weekly average.";
|
"lng_popular_apps_info_text" = "This catalogue ranks mini apps based on their daily revenue, measured in Stars. To be listed, developers must set their main mini apps in {bot} (as described {link}), have over **1,000** daily users, and earn a daily revenue above **1,000** Stars, based on the weekly average.";
|
||||||
|
@ -567,49 +567,6 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
|
|||||||
}), st, textFg);
|
}), st, textFg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetButtonTwoLabels(
|
|
||||||
not_null<Ui::RpWidget*> button,
|
|
||||||
rpl::producer<TextWithEntities> title,
|
|
||||||
rpl::producer<TextWithEntities> subtitle,
|
|
||||||
const style::FlatLabel &st,
|
|
||||||
const style::FlatLabel &subst,
|
|
||||||
const style::color *textFg) {
|
|
||||||
const auto buttonTitle = Ui::CreateChild<Ui::FlatLabel>(
|
|
||||||
button,
|
|
||||||
std::move(title),
|
|
||||||
st);
|
|
||||||
const auto buttonSubtitle = Ui::CreateChild<Ui::FlatLabel>(
|
|
||||||
button,
|
|
||||||
std::move(subtitle),
|
|
||||||
subst);
|
|
||||||
buttonSubtitle->setOpacity(0.6);
|
|
||||||
if (textFg) {
|
|
||||||
buttonTitle->setTextColorOverride((*textFg)->c);
|
|
||||||
buttonSubtitle->setTextColorOverride((*textFg)->c);
|
|
||||||
style::PaletteChanged() | rpl::start_with_next([=] {
|
|
||||||
buttonTitle->setTextColorOverride((*textFg)->c);
|
|
||||||
buttonSubtitle->setTextColorOverride((*textFg)->c);
|
|
||||||
}, buttonTitle->lifetime());
|
|
||||||
}
|
|
||||||
rpl::combine(
|
|
||||||
button->sizeValue(),
|
|
||||||
buttonTitle->sizeValue(),
|
|
||||||
buttonSubtitle->sizeValue()
|
|
||||||
) | rpl::start_with_next([=](QSize outer, QSize title, QSize subtitle) {
|
|
||||||
const auto two = title.height() + subtitle.height();
|
|
||||||
const auto titleTop = (outer.height() - two) / 2;
|
|
||||||
const auto subtitleTop = titleTop + title.height();
|
|
||||||
buttonTitle->moveToLeft(
|
|
||||||
(outer.width() - title.width()) / 2,
|
|
||||||
titleTop);
|
|
||||||
buttonSubtitle->moveToLeft(
|
|
||||||
(outer.width() - subtitle.width()) / 2,
|
|
||||||
subtitleTop);
|
|
||||||
}, buttonTitle->lifetime());
|
|
||||||
buttonTitle->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
buttonSubtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SendStarsForm(
|
void SendStarsForm(
|
||||||
not_null<Main::Session*> session,
|
not_null<Main::Session*> session,
|
||||||
std::shared_ptr<Payments::CreditsFormData> data,
|
std::shared_ptr<Payments::CreditsFormData> data,
|
||||||
|
@ -52,14 +52,6 @@ not_null<FlatLabel*> SetButtonMarkedLabel(
|
|||||||
const style::FlatLabel &st,
|
const style::FlatLabel &st,
|
||||||
const style::color *textFg = nullptr);
|
const style::color *textFg = nullptr);
|
||||||
|
|
||||||
void SetButtonTwoLabels(
|
|
||||||
not_null<Ui::RpWidget*> button,
|
|
||||||
rpl::producer<TextWithEntities> title,
|
|
||||||
rpl::producer<TextWithEntities> subtitle,
|
|
||||||
const style::FlatLabel &st,
|
|
||||||
const style::FlatLabel &subst,
|
|
||||||
const style::color *textFg = nullptr);
|
|
||||||
|
|
||||||
void SendStarsForm(
|
void SendStarsForm(
|
||||||
not_null<Main::Session*> session,
|
not_null<Main::Session*> session,
|
||||||
std::shared_ptr<Payments::CreditsFormData> data,
|
std::shared_ptr<Payments::CreditsFormData> data,
|
||||||
|
@ -832,3 +832,34 @@ dialogsTopBarSuggestionTitleStyle: TextStyle(defaultTextStyle) {
|
|||||||
dialogsTopBarSuggestionAboutStyle: TextStyle(defaultTextStyle) {
|
dialogsTopBarSuggestionAboutStyle: TextStyle(defaultTextStyle) {
|
||||||
font: font(11px);
|
font: font(11px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postsSearchIntroTitle: FlatLabel(defaultFlatLabel) {
|
||||||
|
textFg: windowBoldFg;
|
||||||
|
minWidth: 64px;
|
||||||
|
style: TextStyle(semiboldTextStyle) {
|
||||||
|
font: font(semibold 16px);
|
||||||
|
}
|
||||||
|
align: align(top);
|
||||||
|
}
|
||||||
|
postsSearchIntroTitleMargin: margins(20px, 0px, 20px, 12px);
|
||||||
|
postsSearchIntroSubtitle: FlatLabel(defaultFlatLabel) {
|
||||||
|
textFg: windowSubTextFg;
|
||||||
|
minWidth: 64px;
|
||||||
|
align: align(top);
|
||||||
|
}
|
||||||
|
postsSearchIntroSubtitleMargin: margins(20px, 4px, 20px, 12px);
|
||||||
|
postsSearchIntroButton: RoundButton(defaultActiveButton) {
|
||||||
|
width: 200px;
|
||||||
|
height: 42px;
|
||||||
|
textTop: 12px;
|
||||||
|
style: semiboldTextStyle;
|
||||||
|
}
|
||||||
|
postsSearchIntroFooter: FlatLabel(defaultFlatLabel) {
|
||||||
|
textFg: windowSubTextFg;
|
||||||
|
minWidth: 64px;
|
||||||
|
align: align(top);
|
||||||
|
style: TextStyle(defaultTextStyle) {
|
||||||
|
font: font(12px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postsSearchIntroFooterMargin: margins(20px, 12px, 20px, 0px);
|
||||||
|
@ -1276,6 +1276,10 @@ void InnerWidget::paintEvent(QPaintEvent *e) {
|
|||||||
if (!_searchResults.empty()) {
|
if (!_searchResults.empty()) {
|
||||||
const auto text = showUnreadInSearchResults
|
const auto text = showUnreadInSearchResults
|
||||||
? u"Search results"_q
|
? u"Search results"_q
|
||||||
|
: (_searchState.tab == ChatSearchTab::PublicPosts && !_searchIn)
|
||||||
|
? (_searchState.query.isEmpty()
|
||||||
|
? tr::lng_posts_subtitle_empty(tr::now)
|
||||||
|
: tr::lng_posts_subtitle(tr::now))
|
||||||
: tr::lng_search_found_results(
|
: tr::lng_search_found_results(
|
||||||
tr::now,
|
tr::now,
|
||||||
lt_count,
|
lt_count,
|
||||||
@ -3089,7 +3093,8 @@ void InnerWidget::fillArchiveSearchMenu(not_null<Ui::PopupMenu*> menu) {
|
|||||||
|| (_searchState.tab == ChatSearchTab::PublicPosts);
|
|| (_searchState.tab == ChatSearchTab::PublicPosts);
|
||||||
if (!folder
|
if (!folder
|
||||||
|| !folder->chatsList()->fullSize().current()
|
|| !folder->chatsList()->fullSize().current()
|
||||||
|| (!globalSearch && _searchState.inChat)) {
|
|| (!globalSearch && _searchState.inChat)
|
||||||
|
|| (_searchState.tab == ChatSearchTab::PublicPosts)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto skip = session().settings().skipArchiveInSearch();
|
const auto skip = session().settings().skipArchiveInSearch();
|
||||||
@ -3468,7 +3473,8 @@ void InnerWidget::applySearchState(SearchState state) {
|
|||||||
_filter = newFilter;
|
_filter = newFilter;
|
||||||
if (_filter.isEmpty()
|
if (_filter.isEmpty()
|
||||||
&& !_searchState.fromPeer
|
&& !_searchState.fromPeer
|
||||||
&& _searchState.tags.empty()) {
|
&& _searchState.tags.empty()
|
||||||
|
&& _searchState.tab != ChatSearchTab::PublicPosts) {
|
||||||
clearFilter();
|
clearFilter();
|
||||||
} else {
|
} else {
|
||||||
setState(WidgetState::Filtered);
|
setState(WidgetState::Filtered);
|
||||||
|
283
Telegram/SourceFiles/dialogs/dialogs_search_posts.cpp
Normal file
283
Telegram/SourceFiles/dialogs/dialogs_search_posts.cpp
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
/*
|
||||||
|
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 "dialogs/dialogs_search_posts.h"
|
||||||
|
|
||||||
|
#include "apiwrap.h"
|
||||||
|
#include "base/unixtime.h"
|
||||||
|
#include "data/data_session.h"
|
||||||
|
#include "data/data_peer_values.h"
|
||||||
|
#include "history/history.h"
|
||||||
|
#include "main/main_session.h"
|
||||||
|
|
||||||
|
namespace Dialogs {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kQueryDelay = crl::time(500);
|
||||||
|
constexpr auto kPerPage = 50;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PostsSearch::PostsSearch(not_null<Main::Session*> session)
|
||||||
|
: _session(session)
|
||||||
|
, _api(&_session->api().instance())
|
||||||
|
, _timer([=] { applyQuery(); })
|
||||||
|
, _recheckTimer([=] { recheck(); }) {
|
||||||
|
Data::AmPremiumValue(_session) | rpl::start_with_next([=] {
|
||||||
|
maybePushPremiumUpdate();
|
||||||
|
}, _lifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<PostsSearchState> PostsSearch::stateUpdates() const {
|
||||||
|
return _stateUpdates.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearch::requestMore() {
|
||||||
|
if (_query) {
|
||||||
|
requestSearch(*_query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearch::setQuery(const QString &query) {
|
||||||
|
if (_query == query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_query = query;
|
||||||
|
const auto i = _entries.find(query);
|
||||||
|
if (i != end(_entries)) {
|
||||||
|
pushStateUpdate(i->second);
|
||||||
|
} else if (query.isEmpty()) {
|
||||||
|
applyQuery();
|
||||||
|
} else {
|
||||||
|
_timer.callOnce(kQueryDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearch::setAllowedStars(int stars) {
|
||||||
|
if (_query) {
|
||||||
|
_entries[*_query].allowedStars = stars;
|
||||||
|
requestSearch(*_query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearch::pushStateUpdate(const Entry &entry) {
|
||||||
|
if (!entry.pages.empty() || entry.loaded) {
|
||||||
|
_stateUpdates.fire(PostsSearchState{
|
||||||
|
.first = (entry.pages.empty()
|
||||||
|
? std::vector<not_null<HistoryItem*>>()
|
||||||
|
: entry.pages.front()),
|
||||||
|
.totalCount = entry.totalCount,
|
||||||
|
});
|
||||||
|
} else if (entry.checkId || entry.searchId) {
|
||||||
|
_stateUpdates.fire(PostsSearchState{
|
||||||
|
.loading = true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Assert(_floodState.has_value());
|
||||||
|
auto copy = _floodState;
|
||||||
|
copy->query = *_query;
|
||||||
|
copy->needsPremium = !_session->premium();
|
||||||
|
_stateUpdates.fire(PostsSearchState{
|
||||||
|
.intro = std::move(copy),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearch::maybePushPremiumUpdate() {
|
||||||
|
if (!_floodState || !_query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto &entry = _entries[*_query];
|
||||||
|
if (!entry.pages.empty()
|
||||||
|
|| entry.loaded
|
||||||
|
|| entry.checkId
|
||||||
|
|| entry.searchId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pushStateUpdate(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearch::applyQuery() {
|
||||||
|
Expects(_query.has_value());
|
||||||
|
|
||||||
|
_timer.cancel();
|
||||||
|
if (_query->isEmpty()) {
|
||||||
|
requestSearch(QString());
|
||||||
|
} else {
|
||||||
|
requestState(*_query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearch::requestSearch(const QString &query) {
|
||||||
|
auto &entry = _entries[query];
|
||||||
|
if (entry.searchId || entry.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using Flag = MTPchannels_SearchPosts::Flag;
|
||||||
|
entry.searchId = _api.request(MTPchannels_SearchPosts(
|
||||||
|
MTP_flags(Flag::f_query
|
||||||
|
| (entry.allowedStars ? Flag::f_allow_paid_stars : Flag())),
|
||||||
|
MTP_string(), // hashtag
|
||||||
|
MTP_string(query),
|
||||||
|
MTP_int(entry.offsetRate),
|
||||||
|
(entry.offsetPeer ? entry.offsetPeer->input : MTP_inputPeerEmpty()),
|
||||||
|
MTP_int(entry.offsetId),
|
||||||
|
MTP_int(kPerPage),
|
||||||
|
MTP_long(entry.allowedStars)
|
||||||
|
)).done([=](const MTPmessages_Messages &result) {
|
||||||
|
auto &entry = _entries[query];
|
||||||
|
entry.searchId = 0;
|
||||||
|
|
||||||
|
const auto initial = !entry.offsetId;
|
||||||
|
const auto owner = &_session->data();
|
||||||
|
const auto processList = [&](const MTPVector<MTPMessage> &messages) {
|
||||||
|
auto result = std::vector<not_null<HistoryItem*>>();
|
||||||
|
for (const auto &message : messages.v) {
|
||||||
|
const auto msgId = IdFromMessage(message);
|
||||||
|
const auto peerId = PeerFromMessage(message);
|
||||||
|
const auto lastDate = DateFromMessage(message);
|
||||||
|
if (const auto peer = owner->peerLoaded(peerId)) {
|
||||||
|
if (lastDate) {
|
||||||
|
const auto item = owner->addNewMessage(
|
||||||
|
message,
|
||||||
|
MessageFlags(),
|
||||||
|
NewMessageType::Existing);
|
||||||
|
result.push_back(item);
|
||||||
|
}
|
||||||
|
entry.offsetPeer = peer;
|
||||||
|
} else {
|
||||||
|
LOG(("API Error: a search results with not loaded peer %1"
|
||||||
|
).arg(peerId.value));
|
||||||
|
}
|
||||||
|
entry.offsetId = msgId;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
auto totalCount = 0;
|
||||||
|
auto messages = result.match([&](const MTPDmessages_messages &data) {
|
||||||
|
owner->processUsers(data.vusers());
|
||||||
|
owner->processChats(data.vchats());
|
||||||
|
entry.loaded = true;
|
||||||
|
auto list = processList(data.vmessages());
|
||||||
|
totalCount = list.size();
|
||||||
|
return list;
|
||||||
|
}, [&](const MTPDmessages_messagesSlice &data) {
|
||||||
|
owner->processUsers(data.vusers());
|
||||||
|
owner->processChats(data.vchats());
|
||||||
|
auto list = processList(data.vmessages());
|
||||||
|
const auto nextRate = data.vnext_rate();
|
||||||
|
const auto rateUpdated = nextRate
|
||||||
|
&& (nextRate->v != entry.offsetRate);
|
||||||
|
const auto finished = list.empty();
|
||||||
|
if (rateUpdated) {
|
||||||
|
entry.offsetRate = nextRate->v;
|
||||||
|
}
|
||||||
|
if (finished) {
|
||||||
|
entry.loaded = true;
|
||||||
|
}
|
||||||
|
totalCount = data.vcount().v;
|
||||||
|
if (const auto flood = data.vsearch_flood()) {
|
||||||
|
setFloodStateFrom(flood->data());
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}, [&](const MTPDmessages_channelMessages &data) {
|
||||||
|
LOG(("API Error: "
|
||||||
|
"received messages.channelMessages when no channel "
|
||||||
|
"was passed! (PostsSearch::performSearch)"));
|
||||||
|
owner->processUsers(data.vusers());
|
||||||
|
owner->processChats(data.vchats());
|
||||||
|
auto list = processList(data.vmessages());
|
||||||
|
if (list.empty()) {
|
||||||
|
entry.loaded = true;
|
||||||
|
}
|
||||||
|
totalCount = data.vcount().v;
|
||||||
|
return list;
|
||||||
|
}, [&](const MTPDmessages_messagesNotModified &) {
|
||||||
|
LOG(("API Error: received messages.messagesNotModified! "
|
||||||
|
"(PostsSearch::performSearch)"));
|
||||||
|
entry.loaded = true;
|
||||||
|
return std::vector<not_null<HistoryItem*>>();
|
||||||
|
});
|
||||||
|
if (initial) {
|
||||||
|
entry.pages.clear();
|
||||||
|
}
|
||||||
|
entry.pages.push_back(std::move(messages));
|
||||||
|
const auto count = int(entry.pages.size());
|
||||||
|
const auto full = entry.loaded ? count : std::max(count, totalCount);
|
||||||
|
entry.totalCount = full;
|
||||||
|
if (initial && _query == query) {
|
||||||
|
pushStateUpdate(entry);
|
||||||
|
}
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
auto &entry = _entries[query];
|
||||||
|
entry.searchId = 0;
|
||||||
|
|
||||||
|
const auto initial = !entry.offsetId;
|
||||||
|
const auto &type = error.type();
|
||||||
|
if (initial && type.startsWith(u"FLOOD_WAIT_"_q)) {
|
||||||
|
requestState(query);
|
||||||
|
} else {
|
||||||
|
entry.loaded = true;
|
||||||
|
}
|
||||||
|
}).handleFloodErrors().send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearch::setFloodStateFrom(const MTPDsearchPostsFlood &data) {
|
||||||
|
_recheckTimer.cancel();
|
||||||
|
const auto left = data.vremains().v;
|
||||||
|
const auto next = data.vwait_till().value_or_empty();
|
||||||
|
if (!left && next > 0) {
|
||||||
|
const auto now = base::unixtime::now();
|
||||||
|
const auto delay = std::clamp(next - now, 1, 86401);
|
||||||
|
_recheckTimer.callOnce(delay * crl::time(1000));
|
||||||
|
}
|
||||||
|
_floodState = PostsSearchIntroState{
|
||||||
|
.freeSearchesPerDay = data.vtotal_daily().v,
|
||||||
|
.freeSearchesLeft = left,
|
||||||
|
.nextFreeSearchTime = next,
|
||||||
|
.starsPerPaidSearch = uint32(data.vstars_amount().v),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearch::recheck() {
|
||||||
|
requestState(*_query, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearch::requestState(const QString &query, bool force) {
|
||||||
|
auto &entry = _entries[query];
|
||||||
|
if (force) {
|
||||||
|
_api.request(base::take(entry.checkId)).cancel();
|
||||||
|
} else if (entry.checkId || entry.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using Flag = MTPchannels_CheckSearchPostsFlood::Flag;
|
||||||
|
entry.checkId = _api.request(MTPchannels_CheckSearchPostsFlood(
|
||||||
|
MTP_flags(Flag::f_query),
|
||||||
|
MTP_string(query)
|
||||||
|
)).done([=](const MTPSearchPostsFlood &result) {
|
||||||
|
auto &entry = _entries[query];
|
||||||
|
entry.checkId = 0;
|
||||||
|
|
||||||
|
const auto &data = result.data();
|
||||||
|
setFloodStateFrom(data);
|
||||||
|
if (data.is_query_is_free()) {
|
||||||
|
if (!entry.loaded) {
|
||||||
|
requestSearch(query);
|
||||||
|
}
|
||||||
|
} else if (_query == query) {
|
||||||
|
pushStateUpdate(entry);
|
||||||
|
}
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
auto &entry = _entries[query];
|
||||||
|
entry.checkId = 0;
|
||||||
|
entry.loaded = true;
|
||||||
|
}).handleFloodErrors().send();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Dialogs
|
75
Telegram/SourceFiles/dialogs/dialogs_search_posts.h
Normal file
75
Telegram/SourceFiles/dialogs/dialogs_search_posts.h
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
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 "dialogs/ui/posts_search_intro.h"
|
||||||
|
#include "mtproto/sender.h"
|
||||||
|
|
||||||
|
namespace Main {
|
||||||
|
class Session;
|
||||||
|
} // namespace Main
|
||||||
|
|
||||||
|
namespace Dialogs {
|
||||||
|
|
||||||
|
struct PostsSearchState {
|
||||||
|
std::optional<PostsSearchIntroState> intro;
|
||||||
|
std::vector<not_null<HistoryItem*>> first;
|
||||||
|
int totalCount = 0;
|
||||||
|
bool loading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PostsSearch final {
|
||||||
|
public:
|
||||||
|
explicit PostsSearch(not_null<Main::Session*> session);
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::producer<PostsSearchState> stateUpdates() const;
|
||||||
|
|
||||||
|
void setQuery(const QString &query);
|
||||||
|
void setAllowedStars(int stars);
|
||||||
|
void requestMore();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry {
|
||||||
|
std::vector<std::vector<not_null<HistoryItem*>>> pages;
|
||||||
|
int totalCount = 0;
|
||||||
|
mtpRequestId searchId = 0;
|
||||||
|
mtpRequestId checkId = 0;
|
||||||
|
PeerData *offsetPeer = nullptr;
|
||||||
|
MsgId offsetId = 0;
|
||||||
|
int offsetRate = 0;
|
||||||
|
int allowedStars = 0;
|
||||||
|
bool loaded = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
void recheck();
|
||||||
|
void applyQuery();
|
||||||
|
void requestSearch(const QString &query);
|
||||||
|
void requestState(const QString &query, bool force = false);
|
||||||
|
void setFloodStateFrom(const MTPDsearchPostsFlood &data);
|
||||||
|
void pushStateUpdate(const Entry &entry);
|
||||||
|
void maybePushPremiumUpdate();
|
||||||
|
|
||||||
|
const not_null<Main::Session*> _session;
|
||||||
|
|
||||||
|
MTP::Sender _api;
|
||||||
|
|
||||||
|
base::Timer _timer;
|
||||||
|
base::Timer _recheckTimer;
|
||||||
|
base::flat_map<QString, Entry> _entries;
|
||||||
|
std::optional<QString> _query;
|
||||||
|
|
||||||
|
std::optional<PostsSearchIntroState> _floodState;
|
||||||
|
|
||||||
|
rpl::event_stream<PostsSearchState> _stateUpdates;
|
||||||
|
|
||||||
|
rpl::lifetime _lifetime;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Dialogs
|
@ -24,6 +24,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "data/data_session.h"
|
#include "data/data_session.h"
|
||||||
#include "data/data_user.h"
|
#include "data/data_user.h"
|
||||||
#include "dialogs/ui/chat_search_empty.h"
|
#include "dialogs/ui/chat_search_empty.h"
|
||||||
|
#include "dialogs/ui/chat_search_in.h"
|
||||||
|
#include "dialogs/ui/posts_search_intro.h"
|
||||||
|
#include "dialogs/dialogs_inner_widget.h"
|
||||||
|
#include "dialogs/dialogs_search_posts.h"
|
||||||
#include "history/history.h"
|
#include "history/history.h"
|
||||||
#include "info/downloads/info_downloads_widget.h"
|
#include "info/downloads/info_downloads_widget.h"
|
||||||
#include "info/media/info_media_widget.h"
|
#include "info/media/info_media_widget.h"
|
||||||
@ -34,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "settings/settings_common.h"
|
#include "settings/settings_common.h"
|
||||||
|
#include "settings/settings_premium.h"
|
||||||
#include "storage/storage_shared_media.h"
|
#include "storage/storage_shared_media.h"
|
||||||
#include "ui/boxes/confirm_box.h"
|
#include "ui/boxes/confirm_box.h"
|
||||||
#include "ui/controls/swipe_handler.h"
|
#include "ui/controls/swipe_handler.h"
|
||||||
@ -1331,6 +1336,8 @@ Suggestions::Suggestions(
|
|||||||
, _appsScroll(std::make_unique<Ui::ElasticScroll>(this))
|
, _appsScroll(std::make_unique<Ui::ElasticScroll>(this))
|
||||||
, _appsContent(
|
, _appsContent(
|
||||||
_appsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
|
_appsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
|
||||||
|
, _postsScroll(std::make_unique<Ui::ElasticScroll>(this))
|
||||||
|
, _postsWrap(_postsScroll->setOwnedWidget(object_ptr<Ui::RpWidget>(this)))
|
||||||
, _recentApps(setupRecentApps())
|
, _recentApps(setupRecentApps())
|
||||||
, _popularApps(setupPopularApps())
|
, _popularApps(setupPopularApps())
|
||||||
, _searchQueryTimer([=] { applySearchQuery(); }) {
|
, _searchQueryTimer([=] { applySearchQuery(); }) {
|
||||||
@ -1397,6 +1404,7 @@ void Suggestions::setupTabs() {
|
|||||||
{ Key{ Tab::Chats }, tr::lng_recent_chats(tr::now) },
|
{ Key{ Tab::Chats }, tr::lng_recent_chats(tr::now) },
|
||||||
{ Key{ Tab::Channels }, tr::lng_recent_channels(tr::now) },
|
{ Key{ Tab::Channels }, tr::lng_recent_channels(tr::now) },
|
||||||
{ Key{ Tab::Apps }, tr::lng_recent_apps(tr::now) },
|
{ Key{ Tab::Apps }, tr::lng_recent_apps(tr::now) },
|
||||||
|
{ Key{ Tab::Posts }, tr::lng_recent_posts(tr::now) },
|
||||||
{ Key{ Tab::Media, MediaType::Photo }, tr::lng_all_photos(tr::now) },
|
{ Key{ Tab::Media, MediaType::Photo }, tr::lng_all_photos(tr::now) },
|
||||||
{ Key{ Tab::Media, MediaType::Video }, tr::lng_all_videos(tr::now) },
|
{ Key{ Tab::Media, MediaType::Video }, tr::lng_all_videos(tr::now) },
|
||||||
{ Key{ Tab::Downloads }, tr::lng_all_downloads(tr::now) },
|
{ Key{ Tab::Downloads }, tr::lng_all_downloads(tr::now) },
|
||||||
@ -1478,7 +1486,7 @@ void Suggestions::setupChats() {
|
|||||||
Ui::Text::FixAmpersandInAction),
|
Ui::Text::FixAmpersandInAction),
|
||||||
.removeAllConfirm = tr::lng_recent_hide_sure(tr::now),
|
.removeAllConfirm = tr::lng_recent_hide_sure(tr::now),
|
||||||
.removeAll = removeAll,
|
.removeAll = removeAll,
|
||||||
});
|
});
|
||||||
}, _topPeers->lifetime());
|
}, _topPeers->lifetime());
|
||||||
|
|
||||||
_topPeers->scrollToRequests(
|
_topPeers->scrollToRequests(
|
||||||
@ -1496,8 +1504,8 @@ void Suggestions::setupChats() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Suggestions::handlePressForChatPreview(
|
void Suggestions::handlePressForChatPreview(
|
||||||
PeerId id,
|
PeerId id,
|
||||||
Fn<void(bool)> callback) {
|
Fn<void(bool)> callback) {
|
||||||
callback = crl::guard(this, callback);
|
callback = crl::guard(this, callback);
|
||||||
const auto row = RowDescriptor(
|
const auto row = RowDescriptor(
|
||||||
_controller->session().data().history(id),
|
_controller->session().data().history(id),
|
||||||
@ -1812,7 +1820,10 @@ bool Suggestions::consumeSearchQuery(const QString &query) {
|
|||||||
const auto key = _key.current();
|
const auto key = _key.current();
|
||||||
const auto tab = key.tab;
|
const auto tab = key.tab;
|
||||||
const auto type = (key.tab == Tab::Media) ? key.mediaType : Type::kCount;
|
const auto type = (key.tab == Tab::Media) ? key.mediaType : Type::kCount;
|
||||||
if (tab != Tab::Downloads
|
if (tab == Tab::Posts) {
|
||||||
|
setPostsSearchQuery(query);
|
||||||
|
return !query.isEmpty();
|
||||||
|
} else if (tab != Tab::Downloads
|
||||||
&& type != Type::File
|
&& type != Type::File
|
||||||
&& type != Type::Link
|
&& type != Type::Link
|
||||||
&& type != Type::MusicFile) {
|
&& type != Type::MusicFile) {
|
||||||
@ -1831,6 +1842,111 @@ bool Suggestions::consumeSearchQuery(const QString &query) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Suggestions::setupPostsSearch() {
|
||||||
|
_postsSearch = std::make_unique<PostsSearch>(&_controller->session());
|
||||||
|
|
||||||
|
_postsSearch->stateUpdates(
|
||||||
|
) | rpl::start_with_next([=](const PostsSearchState &state) {
|
||||||
|
if (state.intro) {
|
||||||
|
if (!_postsSearchIntro) {
|
||||||
|
setupPostsIntro(*state.intro);
|
||||||
|
} else {
|
||||||
|
_postsSearchIntro->update(*state.intro);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (!_postsContent) {
|
||||||
|
setupPostsResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
_postsContent->applySearchState(SearchState{
|
||||||
|
.tab = ChatSearchTab::PublicPosts,
|
||||||
|
.query = _searchQuery,
|
||||||
|
});
|
||||||
|
if (state.loading) {
|
||||||
|
_postsContent->searchRequested(true);
|
||||||
|
} else {
|
||||||
|
_postsContent->searchReceived(
|
||||||
|
state.first,
|
||||||
|
nullptr,
|
||||||
|
{ .posts = true, .start = true },
|
||||||
|
state.totalCount);
|
||||||
|
}
|
||||||
|
const auto top = _postsScroll->scrollTop();
|
||||||
|
const auto height = _postsScroll->height();
|
||||||
|
_postsContent->setVisibleTopBottom(top, top + height);
|
||||||
|
}, _postsWrap->lifetime());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Suggestions::setPostsSearchQuery(const QString &query) {
|
||||||
|
if (!_postsSearch) {
|
||||||
|
setupPostsSearch();
|
||||||
|
}
|
||||||
|
_searchQueryTimer.cancel();
|
||||||
|
_postsSearch->setQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Suggestions::setupPostsResults() {
|
||||||
|
Expects(!_postsContent);
|
||||||
|
|
||||||
|
delete base::take(_postsSearchIntro);
|
||||||
|
_postsContent = Ui::CreateChild<InnerWidget>(
|
||||||
|
_postsWrap.get(),
|
||||||
|
_controller,
|
||||||
|
rpl::single(InnerWidget::ChildListShown()));
|
||||||
|
|
||||||
|
_postsContent->applySearchState(SearchState{
|
||||||
|
.tab = ChatSearchTab::PublicPosts,
|
||||||
|
.query = _searchQuery,
|
||||||
|
});
|
||||||
|
_postsContent->searchRequested(true);
|
||||||
|
|
||||||
|
_postsContent->chosenRow(
|
||||||
|
) | rpl::start_with_next([=](const ChosenRow &row) {
|
||||||
|
const auto history = row.key.history();
|
||||||
|
if (!history) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_persist = true;
|
||||||
|
const auto showAtMsgId = row.message.fullId.msg;
|
||||||
|
auto params = Window::SectionShow(
|
||||||
|
Window::SectionShow::Way::ClearStack);
|
||||||
|
params.highlight = Window::SearchHighlightId(_searchQuery);
|
||||||
|
if (row.newWindow) {
|
||||||
|
_controller->showInNewWindow(history->peer, showAtMsgId);
|
||||||
|
} else {
|
||||||
|
_controller->showThread(history, showAtMsgId, params);
|
||||||
|
}
|
||||||
|
}, _postsContent->lifetime());
|
||||||
|
|
||||||
|
_postsContent->heightValue() | rpl::start_with_next([=](int height) {
|
||||||
|
_postsWrap->resize(_postsWrap->width(), height);
|
||||||
|
}, _postsContent->lifetime());
|
||||||
|
_postsContent->setNarrowRatio(0.);
|
||||||
|
_postsContent->show();
|
||||||
|
updateControlsGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Suggestions::setupPostsIntro(const PostsSearchIntroState &intro) {
|
||||||
|
Expects(!_postsSearchIntro);
|
||||||
|
|
||||||
|
delete base::take(_postsContent);
|
||||||
|
_postsSearchIntro = Ui::CreateChild<PostsSearchIntro>(_postsWrap, intro);
|
||||||
|
|
||||||
|
_postsSearchIntro->searchWithStars(
|
||||||
|
) | rpl::start_with_next([=](int stars) {
|
||||||
|
if (!_controller->session().premium()) {
|
||||||
|
Settings::ShowPremium(
|
||||||
|
_controller,
|
||||||
|
u"posts_search"_q);
|
||||||
|
} else {
|
||||||
|
_postsSearch->setAllowedStars(stars);
|
||||||
|
}
|
||||||
|
}, _postsSearchIntro->lifetime());
|
||||||
|
|
||||||
|
_postsSearchIntro->show();
|
||||||
|
updateControlsGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
void Suggestions::applySearchQuery() {
|
void Suggestions::applySearchQuery() {
|
||||||
const auto key = _key.current();
|
const auto key = _key.current();
|
||||||
const auto controller = _mediaLists[key].wrap->controller();
|
const auto controller = _mediaLists[key].wrap->controller();
|
||||||
@ -1926,7 +2042,10 @@ void Suggestions::switchTab(Key key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Suggestions::ensureContent(Key key) {
|
void Suggestions::ensureContent(Key key) {
|
||||||
if (key.tab != Tab::Downloads && key.tab != Tab::Media) {
|
if (key.tab == Tab::Posts) {
|
||||||
|
setPostsSearchQuery(QString());
|
||||||
|
return;
|
||||||
|
} else if (key.tab != Tab::Downloads && key.tab != Tab::Media) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto &list = _mediaLists[key];
|
auto &list = _mediaLists[key];
|
||||||
@ -1958,6 +2077,7 @@ void Suggestions::startSlideAnimation(Key was, Key now) {
|
|||||||
case Tab::Chats: return _chatsScroll.get();
|
case Tab::Chats: return _chatsScroll.get();
|
||||||
case Tab::Channels: return _channelsScroll.get();
|
case Tab::Channels: return _channelsScroll.get();
|
||||||
case Tab::Apps: return _appsScroll.get();
|
case Tab::Apps: return _appsScroll.get();
|
||||||
|
case Tab::Posts: return _postsScroll.get();
|
||||||
}
|
}
|
||||||
return _mediaLists[key].wrap;
|
return _mediaLists[key].wrap;
|
||||||
};
|
};
|
||||||
@ -2009,6 +2129,7 @@ void Suggestions::startShownAnimation(bool shown, Fn<void()> finish) {
|
|||||||
_chatsScroll->hide();
|
_chatsScroll->hide();
|
||||||
_channelsScroll->hide();
|
_channelsScroll->hide();
|
||||||
_appsScroll->hide();
|
_appsScroll->hide();
|
||||||
|
_postsScroll->hide();
|
||||||
for (const auto &[key, list] : _mediaLists) {
|
for (const auto &[key, list] : _mediaLists) {
|
||||||
list.wrap->hide();
|
list.wrap->hide();
|
||||||
}
|
}
|
||||||
@ -2028,6 +2149,7 @@ void Suggestions::finishShow() {
|
|||||||
_chatsScroll->setVisible(key == Key{ Tab::Chats });
|
_chatsScroll->setVisible(key == Key{ Tab::Chats });
|
||||||
_channelsScroll->setVisible(key == Key{ Tab::Channels });
|
_channelsScroll->setVisible(key == Key{ Tab::Channels });
|
||||||
_appsScroll->setVisible(key == Key{ Tab::Apps });
|
_appsScroll->setVisible(key == Key{ Tab::Apps });
|
||||||
|
_postsScroll->setVisible(key == Key{ Tab::Posts });
|
||||||
for (const auto &[mediaKey, list] : _mediaLists) {
|
for (const auto &[mediaKey, list] : _mediaLists) {
|
||||||
list.wrap->setVisible(key == mediaKey);
|
list.wrap->setVisible(key == mediaKey);
|
||||||
if (key == mediaKey) {
|
if (key == mediaKey) {
|
||||||
@ -2042,6 +2164,8 @@ void Suggestions::finishShow() {
|
|||||||
reinstallSwipe(_channelsScroll.get());
|
reinstallSwipe(_channelsScroll.get());
|
||||||
} else if (key == Key{ Tab::Apps }) {
|
} else if (key == Key{ Tab::Apps }) {
|
||||||
reinstallSwipe(_appsScroll.get());
|
reinstallSwipe(_appsScroll.get());
|
||||||
|
} else if (key == Key{ Tab::Posts }) {
|
||||||
|
reinstallSwipe(_postsScroll.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2055,6 +2179,7 @@ std::vector<Suggestions::Key> Suggestions::TabKeysFor(
|
|||||||
{ Tab::Chats },
|
{ Tab::Chats },
|
||||||
{ Tab::Channels },
|
{ Tab::Channels },
|
||||||
{ Tab::Apps },
|
{ Tab::Apps },
|
||||||
|
{ Tab::Posts },
|
||||||
{ Tab::Media, MediaType::Photo },
|
{ Tab::Media, MediaType::Photo },
|
||||||
{ Tab::Media, MediaType::Video },
|
{ Tab::Media, MediaType::Video },
|
||||||
{ Tab::Downloads },
|
{ Tab::Downloads },
|
||||||
@ -2119,6 +2244,16 @@ void Suggestions::updateControlsGeometry() {
|
|||||||
_appsScroll->setGeometry(content);
|
_appsScroll->setGeometry(content);
|
||||||
_appsContent->resizeToWidth(w);
|
_appsContent->resizeToWidth(w);
|
||||||
|
|
||||||
|
_postsScroll->setGeometry(content);
|
||||||
|
_postsWrap->resizeToWidth(w);
|
||||||
|
if (_postsSearchIntro) {
|
||||||
|
_postsSearchIntro->setGeometry(0, 0, w, height() - tabs);
|
||||||
|
} else if (_postsContent) {
|
||||||
|
_postsContent->resizeToWidth(w);
|
||||||
|
_postsContent->setMinimumHeight(height() - tabs);
|
||||||
|
_postsContent->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
const auto expanding = false;
|
const auto expanding = false;
|
||||||
for (const auto &[key, list] : _mediaLists) {
|
for (const auto &[key, list] : _mediaLists) {
|
||||||
const auto full = !list.wrap->scrollBottomSkip();
|
const auto full = !list.wrap->scrollBottomSkip();
|
||||||
|
@ -32,10 +32,11 @@ namespace Storage {
|
|||||||
enum class SharedMediaType : signed char;
|
enum class SharedMediaType : signed char;
|
||||||
} // namespace Storage
|
} // namespace Storage
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui::Controls {
|
||||||
namespace Controls {
|
|
||||||
struct SwipeHandlerArgs;
|
struct SwipeHandlerArgs;
|
||||||
} // namespace Controls
|
} // namespace Ui::Controls
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
class BoxContent;
|
class BoxContent;
|
||||||
class ScrollArea;
|
class ScrollArea;
|
||||||
class ElasticScroll;
|
class ElasticScroll;
|
||||||
@ -51,6 +52,10 @@ class SessionController;
|
|||||||
|
|
||||||
namespace Dialogs {
|
namespace Dialogs {
|
||||||
|
|
||||||
|
class InnerWidget;
|
||||||
|
class PostsSearch;
|
||||||
|
class PostsSearchIntro;
|
||||||
|
struct PostsSearchIntroState;
|
||||||
enum class SearchEmptyIcon;
|
enum class SearchEmptyIcon;
|
||||||
|
|
||||||
struct RecentPeersList {
|
struct RecentPeersList {
|
||||||
@ -118,6 +123,7 @@ private:
|
|||||||
Chats,
|
Chats,
|
||||||
Channels,
|
Channels,
|
||||||
Apps,
|
Apps,
|
||||||
|
Posts,
|
||||||
Media,
|
Media,
|
||||||
Downloads,
|
Downloads,
|
||||||
};
|
};
|
||||||
@ -209,6 +215,11 @@ private:
|
|||||||
void updateControlsGeometry();
|
void updateControlsGeometry();
|
||||||
void applySearchQuery();
|
void applySearchQuery();
|
||||||
|
|
||||||
|
void setupPostsSearch();
|
||||||
|
void setPostsSearchQuery(const QString &query);
|
||||||
|
void setupPostsResults();
|
||||||
|
void setupPostsIntro(const PostsSearchIntroState &intro);
|
||||||
|
|
||||||
const not_null<Window::SessionController*> _controller;
|
const not_null<Window::SessionController*> _controller;
|
||||||
|
|
||||||
const std::unique_ptr<Ui::ScrollArea> _tabsScroll;
|
const std::unique_ptr<Ui::ScrollArea> _tabsScroll;
|
||||||
@ -240,6 +251,12 @@ private:
|
|||||||
const std::unique_ptr<Ui::ElasticScroll> _appsScroll;
|
const std::unique_ptr<Ui::ElasticScroll> _appsScroll;
|
||||||
const not_null<Ui::VerticalLayout*> _appsContent;
|
const not_null<Ui::VerticalLayout*> _appsContent;
|
||||||
|
|
||||||
|
std::unique_ptr<PostsSearch> _postsSearch;
|
||||||
|
const std::unique_ptr<Ui::ElasticScroll> _postsScroll;
|
||||||
|
const not_null<Ui::RpWidget*> _postsWrap;
|
||||||
|
PostsSearchIntro *_postsSearchIntro = nullptr;
|
||||||
|
InnerWidget *_postsContent = nullptr;
|
||||||
|
|
||||||
rpl::producer<> _recentAppsRefreshed;
|
rpl::producer<> _recentAppsRefreshed;
|
||||||
Fn<bool(not_null<PeerData*>)> _recentAppsShows;
|
Fn<bool(not_null<PeerData*>)> _recentAppsShows;
|
||||||
const std::unique_ptr<ObjectList> _recentApps;
|
const std::unique_ptr<ObjectList> _recentApps;
|
||||||
|
174
Telegram/SourceFiles/dialogs/ui/posts_search_intro.cpp
Normal file
174
Telegram/SourceFiles/dialogs/ui/posts_search_intro.cpp
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
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 "dialogs/ui/posts_search_intro.h"
|
||||||
|
|
||||||
|
#include "base/timer_rpl.h"
|
||||||
|
#include "base/unixtime.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
|
#include "ui/controls/button_two_labels.h"
|
||||||
|
#include "ui/text/text_utilities.h"
|
||||||
|
#include "ui/widgets/buttons.h"
|
||||||
|
#include "ui/widgets/labels.h"
|
||||||
|
#include "ui/wrap/vertical_layout.h"
|
||||||
|
#include "styles/style_credits.h"
|
||||||
|
#include "styles/style_dialogs.h"
|
||||||
|
|
||||||
|
namespace Dialogs {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::producer<QString> FormatCountdownTill(TimeId when) {
|
||||||
|
return rpl::single(rpl::empty) | rpl::then(
|
||||||
|
base::timer_each(1000)
|
||||||
|
) | rpl::map([=] {
|
||||||
|
const auto now = base::unixtime::now();
|
||||||
|
const auto delta = std::max(when - now, 0);
|
||||||
|
const auto hours = delta / 3600;
|
||||||
|
const auto minutes = (delta % 3600) / 60;
|
||||||
|
const auto seconds = delta % 60;
|
||||||
|
constexpr auto kZero = QChar('0');
|
||||||
|
return (hours > 0)
|
||||||
|
? u"%1:%2:%3"_q
|
||||||
|
.arg(hours)
|
||||||
|
.arg(minutes, 2, 10, kZero)
|
||||||
|
.arg(seconds, 2, 10, kZero)
|
||||||
|
: u"%1:%2"_q
|
||||||
|
.arg(minutes)
|
||||||
|
.arg(seconds, 2, 10, kZero);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
PostsSearchIntro::PostsSearchIntro(
|
||||||
|
not_null<Ui::RpWidget*> parent,
|
||||||
|
PostsSearchIntroState state)
|
||||||
|
: RpWidget(parent)
|
||||||
|
, _state(std::move(state))
|
||||||
|
, _content(std::make_unique<Ui::VerticalLayout>(this)) {
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
PostsSearchIntro::~PostsSearchIntro() = default;
|
||||||
|
|
||||||
|
void PostsSearchIntro::update(PostsSearchIntroState state) {
|
||||||
|
_state = std::move(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<int> PostsSearchIntro::searchWithStars() const {
|
||||||
|
return _button->clicks() | rpl::map([=] {
|
||||||
|
const auto &now = _state.current();
|
||||||
|
return (now.needsPremium || now.freeSearchesLeft)
|
||||||
|
? 0
|
||||||
|
: int(now.starsPerPaidSearch);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearchIntro::setup() {
|
||||||
|
auto title = _state.value(
|
||||||
|
) | rpl::map([](const PostsSearchIntroState &state) {
|
||||||
|
return (state.needsPremium || state.freeSearchesLeft > 0)
|
||||||
|
? tr::lng_posts_title()
|
||||||
|
: tr::lng_posts_limit_reached();
|
||||||
|
}) | rpl::flatten_latest();
|
||||||
|
|
||||||
|
auto subtitle = _state.value(
|
||||||
|
) | rpl::map([](const PostsSearchIntroState &state) {
|
||||||
|
return (state.needsPremium || state.freeSearchesLeft > 0)
|
||||||
|
? tr::lng_posts_start()
|
||||||
|
: tr::lng_posts_limit_about(
|
||||||
|
lt_count,
|
||||||
|
rpl::single(state.freeSearchesPerDay * 1.));
|
||||||
|
}) | rpl::flatten_latest();
|
||||||
|
|
||||||
|
auto footer = _state.value(
|
||||||
|
) | rpl::map([](const PostsSearchIntroState &state) {
|
||||||
|
if (state.needsPremium) {
|
||||||
|
return tr::lng_posts_need_subscribe();
|
||||||
|
} else if (state.freeSearchesLeft > 0) {
|
||||||
|
return tr::lng_posts_remaining(
|
||||||
|
lt_count,
|
||||||
|
rpl::single(state.freeSearchesLeft * 1.));
|
||||||
|
} else {
|
||||||
|
return rpl::single(QString());
|
||||||
|
}
|
||||||
|
}) | rpl::flatten_latest();
|
||||||
|
|
||||||
|
_title = _content->add(
|
||||||
|
object_ptr<Ui::FlatLabel>(
|
||||||
|
_content.get(),
|
||||||
|
std::move(title),
|
||||||
|
st::postsSearchIntroTitle),
|
||||||
|
st::postsSearchIntroTitleMargin);
|
||||||
|
_title->setTryMakeSimilarLines(true);
|
||||||
|
_subtitle = _content->add(
|
||||||
|
object_ptr<Ui::FlatLabel>(
|
||||||
|
_content.get(),
|
||||||
|
std::move(subtitle),
|
||||||
|
st::postsSearchIntroSubtitle),
|
||||||
|
st::postsSearchIntroSubtitleMargin);
|
||||||
|
_subtitle->setTryMakeSimilarLines(true);
|
||||||
|
_button = _content->add(
|
||||||
|
object_ptr<Ui::CenterWrap<Ui::RoundButton>>(
|
||||||
|
_content.get(),
|
||||||
|
object_ptr<Ui::RoundButton>(
|
||||||
|
_content.get(),
|
||||||
|
rpl::single(QString()),
|
||||||
|
st::postsSearchIntroButton))
|
||||||
|
)->entity();
|
||||||
|
_button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||||
|
_footer = _content->add(
|
||||||
|
object_ptr<Ui::FlatLabel>(
|
||||||
|
_content.get(),
|
||||||
|
std::move(footer),
|
||||||
|
st::postsSearchIntroFooter),
|
||||||
|
st::postsSearchIntroFooterMargin);
|
||||||
|
_footer->setTryMakeSimilarLines(true);
|
||||||
|
|
||||||
|
_state.value(
|
||||||
|
) | rpl::start_with_next([=](const PostsSearchIntroState &state) {
|
||||||
|
auto copy = _button->children();
|
||||||
|
for (const auto child : copy) {
|
||||||
|
delete child;
|
||||||
|
}
|
||||||
|
if (state.needsPremium) {
|
||||||
|
_button->setText(tr::lng_posts_subscribe());
|
||||||
|
} else if (state.freeSearchesLeft > 0) {
|
||||||
|
_button->setText(tr::lng_posts_search_button(
|
||||||
|
lt_query,
|
||||||
|
rpl::single(state.query)));
|
||||||
|
} else {
|
||||||
|
_button->setText(rpl::single(QString()));
|
||||||
|
|
||||||
|
Ui::SetButtonTwoLabels(
|
||||||
|
_button,
|
||||||
|
tr::lng_posts_limit_search_paid(
|
||||||
|
lt_cost,
|
||||||
|
rpl::single(Ui::Text::IconEmoji(
|
||||||
|
&st::starIconEmoji
|
||||||
|
).append(
|
||||||
|
Lang::FormatCountDecimal(state.starsPerPaidSearch))),
|
||||||
|
Ui::Text::WithEntities),
|
||||||
|
tr::lng_posts_limit_unlocks(
|
||||||
|
lt_duration,
|
||||||
|
FormatCountdownTill(
|
||||||
|
state.nextFreeSearchTime
|
||||||
|
) | Ui::Text::ToWithEntities(),
|
||||||
|
Ui::Text::WithEntities),
|
||||||
|
st::resaleButtonTitle,
|
||||||
|
st::resaleButtonSubtitle);
|
||||||
|
}
|
||||||
|
}, _button->lifetime());
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostsSearchIntro::resizeEvent(QResizeEvent *e) {
|
||||||
|
_content->resizeToWidth(width());
|
||||||
|
const auto top = std::max(0, (height() - _content->height()) / 3);
|
||||||
|
_content->move(0, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Dialogs
|
59
Telegram/SourceFiles/dialogs/ui/posts_search_intro.h
Normal file
59
Telegram/SourceFiles/dialogs/ui/posts_search_intro.h
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
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 "ui/rp_widget.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class FlatLabel;
|
||||||
|
class RoundButton;
|
||||||
|
class VerticalLayout;
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
|
namespace Dialogs {
|
||||||
|
|
||||||
|
struct PostsSearchIntroState {
|
||||||
|
QString query;
|
||||||
|
int freeSearchesPerDay = 0;
|
||||||
|
int freeSearchesLeft = 0;
|
||||||
|
TimeId nextFreeSearchTime = 0;
|
||||||
|
uint32 starsPerPaidSearch : 31 = 0;
|
||||||
|
uint32 needsPremium : 1 = 0;
|
||||||
|
|
||||||
|
friend inline bool operator==(
|
||||||
|
PostsSearchIntroState,
|
||||||
|
PostsSearchIntroState) = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PostsSearchIntro final : public Ui::RpWidget {
|
||||||
|
public:
|
||||||
|
PostsSearchIntro(
|
||||||
|
not_null<Ui::RpWidget*> parent,
|
||||||
|
PostsSearchIntroState state);
|
||||||
|
~PostsSearchIntro();
|
||||||
|
|
||||||
|
void update(PostsSearchIntroState state);
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::producer<int> searchWithStars() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void resizeEvent(QResizeEvent *e) override;
|
||||||
|
|
||||||
|
void setup();
|
||||||
|
|
||||||
|
rpl::variable<PostsSearchIntroState> _state;
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::VerticalLayout> _content;
|
||||||
|
Ui::FlatLabel *_title = nullptr;
|
||||||
|
Ui::FlatLabel *_subtitle = nullptr;
|
||||||
|
Ui::RoundButton *_button = nullptr;
|
||||||
|
Ui::FlatLabel *_footer = nullptr;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Dialogs
|
@ -17,7 +17,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "base/unixtime.h"
|
#include "base/unixtime.h"
|
||||||
#include "boxes/gift_premium_box.h"
|
#include "boxes/gift_premium_box.h"
|
||||||
#include "boxes/share_box.h"
|
#include "boxes/share_box.h"
|
||||||
#include "boxes/send_credits_box.h" // SetButtonTwoLabels
|
|
||||||
#include "boxes/star_gift_box.h"
|
#include "boxes/star_gift_box.h"
|
||||||
#include "boxes/transfer_gift_box.h"
|
#include "boxes/transfer_gift_box.h"
|
||||||
#include "chat_helpers/stickers_gift_box_pack.h"
|
#include "chat_helpers/stickers_gift_box_pack.h"
|
||||||
@ -64,6 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "settings/settings_credits.h"
|
#include "settings/settings_credits.h"
|
||||||
#include "statistics/widgets/chart_header_widget.h"
|
#include "statistics/widgets/chart_header_widget.h"
|
||||||
#include "ui/boxes/confirm_box.h"
|
#include "ui/boxes/confirm_box.h"
|
||||||
|
#include "ui/controls/button_two_labels.h"
|
||||||
#include "ui/controls/ton_common.h"
|
#include "ui/controls/ton_common.h"
|
||||||
#include "ui/controls/userpic_button.h"
|
#include "ui/controls/userpic_button.h"
|
||||||
#include "ui/dynamic_image.h"
|
#include "ui/dynamic_image.h"
|
||||||
|
57
Telegram/SourceFiles/ui/controls/button_two_labels.cpp
Normal file
57
Telegram/SourceFiles/ui/controls/button_two_labels.cpp
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
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/button_two_labels.h"
|
||||||
|
|
||||||
|
#include "ui/widgets/labels.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
|
||||||
|
void SetButtonTwoLabels(
|
||||||
|
not_null<Ui::RpWidget*> button,
|
||||||
|
rpl::producer<TextWithEntities> title,
|
||||||
|
rpl::producer<TextWithEntities> subtitle,
|
||||||
|
const style::FlatLabel &st,
|
||||||
|
const style::FlatLabel &subst,
|
||||||
|
const style::color *textFg) {
|
||||||
|
const auto buttonTitle = Ui::CreateChild<Ui::FlatLabel>(
|
||||||
|
button,
|
||||||
|
std::move(title),
|
||||||
|
st);
|
||||||
|
const auto buttonSubtitle = Ui::CreateChild<Ui::FlatLabel>(
|
||||||
|
button,
|
||||||
|
std::move(subtitle),
|
||||||
|
subst);
|
||||||
|
buttonSubtitle->setOpacity(0.6);
|
||||||
|
if (textFg) {
|
||||||
|
buttonTitle->setTextColorOverride((*textFg)->c);
|
||||||
|
buttonSubtitle->setTextColorOverride((*textFg)->c);
|
||||||
|
style::PaletteChanged() | rpl::start_with_next([=] {
|
||||||
|
buttonTitle->setTextColorOverride((*textFg)->c);
|
||||||
|
buttonSubtitle->setTextColorOverride((*textFg)->c);
|
||||||
|
}, buttonTitle->lifetime());
|
||||||
|
}
|
||||||
|
rpl::combine(
|
||||||
|
button->sizeValue(),
|
||||||
|
buttonTitle->sizeValue(),
|
||||||
|
buttonSubtitle->sizeValue()
|
||||||
|
) | rpl::start_with_next([=](QSize outer, QSize title, QSize subtitle) {
|
||||||
|
const auto two = title.height() + subtitle.height();
|
||||||
|
const auto titleTop = (outer.height() - two) / 2;
|
||||||
|
const auto subtitleTop = titleTop + title.height();
|
||||||
|
buttonTitle->moveToLeft(
|
||||||
|
(outer.width() - title.width()) / 2,
|
||||||
|
titleTop);
|
||||||
|
buttonSubtitle->moveToLeft(
|
||||||
|
(outer.width() - subtitle.width()) / 2,
|
||||||
|
subtitleTop);
|
||||||
|
}, buttonTitle->lifetime());
|
||||||
|
buttonTitle->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
buttonSubtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Ui
|
26
Telegram/SourceFiles/ui/controls/button_two_labels.h
Normal file
26
Telegram/SourceFiles/ui/controls/button_two_labels.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
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
|
||||||
|
|
||||||
|
namespace style {
|
||||||
|
struct FlatLabel;
|
||||||
|
} // namespace style
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
|
||||||
|
class RpWidget;
|
||||||
|
|
||||||
|
void SetButtonTwoLabels(
|
||||||
|
not_null<Ui::RpWidget*> button,
|
||||||
|
rpl::producer<TextWithEntities> title,
|
||||||
|
rpl::producer<TextWithEntities> subtitle,
|
||||||
|
const style::FlatLabel &st,
|
||||||
|
const style::FlatLabel &subst,
|
||||||
|
const style::color *textFg = nullptr);
|
||||||
|
|
||||||
|
} // namespace Ui
|
@ -109,6 +109,8 @@ PRIVATE
|
|||||||
dialogs/ui/dialogs_stories_list.h
|
dialogs/ui/dialogs_stories_list.h
|
||||||
dialogs/ui/dialogs_top_bar_suggestion_content.cpp
|
dialogs/ui/dialogs_top_bar_suggestion_content.cpp
|
||||||
dialogs/ui/dialogs_top_bar_suggestion_content.h
|
dialogs/ui/dialogs_top_bar_suggestion_content.h
|
||||||
|
dialogs/ui/posts_search_intro.cpp
|
||||||
|
dialogs/ui/posts_search_intro.h
|
||||||
dialogs/ui/top_peers_strip.cpp
|
dialogs/ui/top_peers_strip.cpp
|
||||||
dialogs/ui/top_peers_strip.h
|
dialogs/ui/top_peers_strip.h
|
||||||
|
|
||||||
@ -361,6 +363,8 @@ PRIVATE
|
|||||||
ui/chat/pinned_bar.h
|
ui/chat/pinned_bar.h
|
||||||
ui/chat/requests_bar.cpp
|
ui/chat/requests_bar.cpp
|
||||||
ui/chat/requests_bar.h
|
ui/chat/requests_bar.h
|
||||||
|
ui/controls/button_two_labels.cpp
|
||||||
|
ui/controls/button_two_labels.h
|
||||||
ui/controls/call_mute_button.cpp
|
ui/controls/call_mute_button.cpp
|
||||||
ui/controls/call_mute_button.h
|
ui/controls/call_mute_button.h
|
||||||
ui/controls/chat_service_checkbox.cpp
|
ui/controls/chat_service_checkbox.cpp
|
||||||
|
Loading…
x
Reference in New Issue
Block a user