dialogs_suggestions.cpp 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "dialogs/ui/dialogs_suggestions.h"
  8. #include "api/api_chat_participants.h"
  9. #include "apiwrap.h"
  10. #include "base/unixtime.h"
  11. #include "base/qt/qt_key_modifiers.h"
  12. #include "boxes/peer_list_box.h"
  13. #include "core/application.h"
  14. #include "data/components/recent_peers.h"
  15. #include "data/components/top_peers.h"
  16. #include "data/data_changes.h"
  17. #include "data/data_channel.h"
  18. #include "data/data_chat.h"
  19. #include "data/data_download_manager.h"
  20. #include "data/data_folder.h"
  21. #include "data/data_peer_values.h"
  22. #include "data/data_session.h"
  23. #include "data/data_user.h"
  24. #include "dialogs/ui/chat_search_empty.h"
  25. #include "history/history.h"
  26. #include "info/downloads/info_downloads_widget.h"
  27. #include "info/media/info_media_widget.h"
  28. #include "info/info_controller.h"
  29. #include "info/info_memento.h"
  30. #include "info/info_wrap_widget.h"
  31. #include "inline_bots/bot_attach_web_view.h"
  32. #include "lang/lang_keys.h"
  33. #include "main/main_session.h"
  34. #include "settings/settings_common.h"
  35. #include "storage/storage_shared_media.h"
  36. #include "ui/boxes/confirm_box.h"
  37. #include "ui/effects/ripple_animation.h"
  38. #include "ui/text/text_utilities.h"
  39. #include "ui/widgets/menu/menu_add_action_callback_factory.h"
  40. #include "ui/widgets/buttons.h"
  41. #include "ui/widgets/discrete_sliders.h"
  42. #include "ui/widgets/elastic_scroll.h"
  43. #include "ui/widgets/labels.h"
  44. #include "ui/widgets/popup_menu.h"
  45. #include "ui/widgets/shadow.h"
  46. #include "ui/wrap/vertical_layout.h"
  47. #include "ui/wrap/slide_wrap.h"
  48. #include "ui/delayed_activation.h"
  49. #include "ui/dynamic_thumbnails.h"
  50. #include "ui/painter.h"
  51. #include "ui/search_field_controller.h"
  52. #include "ui/unread_badge_paint.h"
  53. #include "ui/ui_utility.h"
  54. #include "window/window_separate_id.h"
  55. #include "window/window_session_controller.h"
  56. #include "window/window_peer_menu.h"
  57. #include "styles/style_chat.h"
  58. #include "styles/style_dialogs.h"
  59. #include "styles/style_layers.h"
  60. #include "styles/style_menu_icons.h"
  61. #include "styles/style_window.h"
  62. namespace Dialogs {
  63. namespace {
  64. constexpr auto kCollapsedChannelsCount = 5;
  65. constexpr auto kProbablyMaxChannels = 1000;
  66. constexpr auto kCollapsedAppsCount = 5;
  67. constexpr auto kProbablyMaxApps = 100;
  68. constexpr auto kSearchQueryDelay = crl::time(900);
  69. class RecentRow final : public PeerListRow {
  70. public:
  71. explicit RecentRow(not_null<PeerData*> peer);
  72. bool refreshBadge();
  73. QSize rightActionSize() const override;
  74. QMargins rightActionMargins() const override;
  75. void rightActionPaint(
  76. Painter &p,
  77. int x,
  78. int y,
  79. int outerWidth,
  80. bool selected,
  81. bool actionSelected) override;
  82. bool rightActionDisabled() const override;
  83. void rightActionAddRipple(
  84. QPoint point,
  85. Fn<void()> updateCallback) override;
  86. void rightActionStopLastRipple() override;
  87. const style::PeerListItem &computeSt(
  88. const style::PeerListItem &st) const override;
  89. private:
  90. const not_null<History*> _history;
  91. std::unique_ptr<Ui::Text::String> _mainAppText;
  92. std::unique_ptr<Ui::RippleAnimation> _actionRipple;
  93. QString _badgeString;
  94. QSize _badgeSize;
  95. uint32 _counter : 30 = 0;
  96. uint32 _unread : 1 = 0;
  97. uint32 _muted : 1 = 0;
  98. };
  99. class ChannelRow final : public PeerListRow {
  100. public:
  101. using PeerListRow::PeerListRow;
  102. void setActive(bool active);
  103. const style::PeerListItem &computeSt(
  104. const style::PeerListItem &st) const override;
  105. private:
  106. bool _active = false;
  107. };
  108. struct EntryMenuDescriptor {
  109. not_null<Window::SessionController*> controller;
  110. not_null<PeerData*> peer;
  111. QString removeOneText;
  112. Fn<void()> removeOne;
  113. QString removeAllText;
  114. QString removeAllConfirm;
  115. Fn<void()> removeAll;
  116. };
  117. [[nodiscard]] Fn<void()> RemoveAllConfirm(
  118. not_null<Window::SessionController*> controller,
  119. QString removeAllConfirm,
  120. Fn<void()> removeAll) {
  121. return [=] {
  122. controller->show(Ui::MakeConfirmBox({
  123. .text = removeAllConfirm,
  124. .confirmed = [=](Fn<void()> close) { removeAll(); close(); }
  125. }));
  126. };
  127. }
  128. void FillEntryMenu(
  129. const Ui::Menu::MenuCallback &add,
  130. EntryMenuDescriptor &&descriptor) {
  131. const auto peer = descriptor.peer;
  132. const auto controller = descriptor.controller;
  133. const auto group = peer->isMegagroup();
  134. const auto channel = peer->isChannel();
  135. add(tr::lng_context_new_window(tr::now), [=] {
  136. Ui::PreventDelayedActivation();
  137. controller->showInNewWindow(peer);
  138. }, &st::menuIconNewWindow);
  139. Window::AddSeparatorAndShiftUp(add);
  140. const auto showHistoryText = group
  141. ? tr::lng_context_open_group(tr::now)
  142. : channel
  143. ? tr::lng_context_open_channel(tr::now)
  144. : tr::lng_profile_send_message(tr::now);
  145. add(showHistoryText, [=] {
  146. controller->showPeerHistory(peer);
  147. }, channel ? &st::menuIconChannel : &st::menuIconChatBubble);
  148. const auto viewProfileText = group
  149. ? tr::lng_context_view_group(tr::now)
  150. : channel
  151. ? tr::lng_context_view_channel(tr::now)
  152. : tr::lng_context_view_profile(tr::now);
  153. add(viewProfileText, [=] {
  154. controller->showPeerInfo(peer);
  155. }, channel ? &st::menuIconInfo : &st::menuIconProfile);
  156. add({ .separatorSt = &st::expandedMenuSeparator });
  157. add({
  158. .text = descriptor.removeOneText,
  159. .handler = descriptor.removeOne,
  160. .icon = &st::menuIconDeleteAttention,
  161. .isAttention = true,
  162. });
  163. if (!descriptor.removeAllText.isEmpty()) {
  164. add({
  165. .text = descriptor.removeAllText,
  166. .handler = RemoveAllConfirm(
  167. descriptor.controller,
  168. descriptor.removeAllConfirm,
  169. descriptor.removeAll),
  170. .icon = &st::menuIconCancelAttention,
  171. .isAttention = true,
  172. });
  173. }
  174. }
  175. RecentRow::RecentRow(not_null<PeerData*> peer)
  176. : PeerListRow(peer)
  177. , _history(peer->owner().history(peer))
  178. , _mainAppText([&]() -> std::unique_ptr<Ui::Text::String> {
  179. if (const auto user = peer->asUser()) {
  180. if (user->botInfo && user->botInfo->hasMainApp) {
  181. return std::make_unique<Ui::Text::String>(
  182. st::dialogRowOpenBotTextStyle,
  183. tr::lng_profile_open_app_short(tr::now));
  184. }
  185. }
  186. return nullptr;
  187. }()) {
  188. if (peer->isSelf() || peer->isRepliesChat() || peer->isVerifyCodes()) {
  189. setCustomStatus(u" "_q);
  190. } else if (const auto chat = peer->asChat()) {
  191. if (chat->count > 0) {
  192. setCustomStatus(
  193. tr::lng_chat_status_members(
  194. tr::now,
  195. lt_count_decimal,
  196. chat->count));
  197. }
  198. } else if (const auto channel = peer->asChannel()) {
  199. if (channel->membersCountKnown()) {
  200. setCustomStatus((channel->isBroadcast()
  201. ? tr::lng_chat_status_subscribers
  202. : tr::lng_chat_status_members)(
  203. tr::now,
  204. lt_count_decimal,
  205. channel->membersCount()));
  206. }
  207. }
  208. refreshBadge();
  209. }
  210. bool RecentRow::refreshBadge() {
  211. if (_history->peer->isSelf()) {
  212. return false;
  213. }
  214. auto result = false;
  215. const auto muted = _history->muted() ? 1 : 0;
  216. if (_muted != muted) {
  217. _muted = muted;
  218. if (_counter || _unread) {
  219. result = true;
  220. }
  221. }
  222. const auto badges = _history->chatListBadgesState();
  223. const auto unread = badges.unread ? 1 : 0;
  224. if (_counter != badges.unreadCounter || _unread != unread) {
  225. _counter = badges.unreadCounter;
  226. _unread = unread;
  227. result = true;
  228. _badgeString = !_counter
  229. ? (_unread ? u" "_q : QString())
  230. : (_counter < 1000)
  231. ? QString::number(_counter)
  232. : (QString::number(_counter / 1000) + 'K');
  233. if (_badgeString.isEmpty()) {
  234. _badgeSize = QSize();
  235. } else {
  236. auto st = Ui::UnreadBadgeStyle();
  237. const auto unreadRectHeight = st.size;
  238. const auto unreadWidth = st.font->width(_badgeString);
  239. _badgeSize = QSize(
  240. std::max(unreadWidth + 2 * st.padding, unreadRectHeight),
  241. unreadRectHeight);
  242. }
  243. }
  244. return result;
  245. }
  246. QSize RecentRow::rightActionSize() const {
  247. if (_mainAppText && _badgeSize.isEmpty()) {
  248. return QSize(
  249. _mainAppText->maxWidth() + _mainAppText->minHeight(),
  250. st::dialogRowOpenBotHeight);
  251. }
  252. return _badgeSize;
  253. }
  254. QMargins RecentRow::rightActionMargins() const {
  255. if (_mainAppText && _badgeSize.isEmpty()) {
  256. return QMargins(
  257. 0,
  258. st::dialogRowOpenBotRecentTop,
  259. st::dialogRowOpenBotRight,
  260. 0);
  261. }
  262. if (_badgeSize.isEmpty()) {
  263. return {};
  264. }
  265. const auto x = st::recentPeersItem.photoPosition.x();
  266. const auto y = (st::recentPeersItem.height - _badgeSize.height()) / 2;
  267. return QMargins(x, y, x, y);
  268. }
  269. void RecentRow::rightActionPaint(
  270. Painter &p,
  271. int x,
  272. int y,
  273. int outerWidth,
  274. bool selected,
  275. bool actionSelected) {
  276. if (_mainAppText && _badgeSize.isEmpty()) {
  277. const auto size = RecentRow::rightActionSize();
  278. p.setPen(Qt::NoPen);
  279. p.setBrush(actionSelected
  280. ? st::activeButtonBgOver
  281. : st::activeButtonBg);
  282. const auto radius = size.height() / 2;
  283. auto hq = PainterHighQualityEnabler(p);
  284. p.drawRoundedRect(QRect(QPoint(x, y), size), radius, radius);
  285. if (_actionRipple) {
  286. _actionRipple->paint(p, x, y, outerWidth);
  287. if (_actionRipple->empty()) {
  288. _actionRipple.reset();
  289. }
  290. }
  291. p.setPen(actionSelected
  292. ? st::activeButtonFgOver
  293. : st::activeButtonFg);
  294. const auto top = 0
  295. + (st::dialogRowOpenBotHeight - _mainAppText->minHeight()) / 2;
  296. _mainAppText->draw(p, {
  297. .position = QPoint(x + size.height() / 2, y + top),
  298. .outerWidth = outerWidth,
  299. .availableWidth = outerWidth,
  300. .elisionLines = 1,
  301. });
  302. }
  303. if (!_counter && !_unread) {
  304. return;
  305. } else if (_badgeString.isEmpty()) {
  306. _badgeString = !_counter
  307. ? u" "_q
  308. : (_counter < 1000)
  309. ? QString::number(_counter)
  310. : (QString::number(_counter / 1000) + 'K');
  311. }
  312. auto st = Ui::UnreadBadgeStyle();
  313. st.selected = selected;
  314. st.muted = _muted;
  315. const auto &counter = _badgeString;
  316. PaintUnreadBadge(p, counter, x + _badgeSize.width(), y, st);
  317. }
  318. bool RecentRow::rightActionDisabled() const {
  319. return !_mainAppText || !_badgeSize.isEmpty();
  320. }
  321. void RecentRow::rightActionAddRipple(
  322. QPoint point,
  323. Fn<void()> updateCallback) {
  324. if (!_mainAppText || !_badgeSize.isEmpty()) {
  325. return;
  326. }
  327. if (!_actionRipple) {
  328. const auto size = rightActionSize();
  329. const auto radius = size.height() / 2;
  330. auto mask = Ui::RippleAnimation::RoundRectMask(size, radius);
  331. _actionRipple = std::make_unique<Ui::RippleAnimation>(
  332. st::defaultActiveButton.ripple,
  333. std::move(mask),
  334. std::move(updateCallback));
  335. }
  336. _actionRipple->add(point);
  337. }
  338. void RecentRow::rightActionStopLastRipple() {
  339. if (_actionRipple) {
  340. _actionRipple->lastStop();
  341. }
  342. }
  343. const style::PeerListItem &RecentRow::computeSt(
  344. const style::PeerListItem &st) const {
  345. return (peer()->isSelf()
  346. || peer()->isRepliesChat()
  347. || peer()->isVerifyCodes())
  348. ? st::recentPeersSpecialName
  349. : st;
  350. }
  351. void ChannelRow::setActive(bool active) {
  352. _active = active;
  353. }
  354. const style::PeerListItem &ChannelRow::computeSt(
  355. const style::PeerListItem &st) const {
  356. return _active ? st::recentPeersItemActive : st::recentPeersItem;
  357. }
  358. } // namespace
  359. class Suggestions::ObjectListController
  360. : public PeerListController
  361. , public base::has_weak_ptr {
  362. public:
  363. explicit ObjectListController(
  364. not_null<Window::SessionController*> window);
  365. [[nodiscard]] not_null<Window::SessionController*> window() const {
  366. return _window;
  367. }
  368. [[nodiscard]] rpl::producer<int> count() const {
  369. return _count.value();
  370. }
  371. [[nodiscard]] rpl::producer<not_null<PeerData*>> chosen() const {
  372. return _chosen.events();
  373. }
  374. Main::Session &session() const override {
  375. return _window->session();
  376. }
  377. void rowClicked(not_null<PeerListRow*> row) override;
  378. void rowMiddleClicked(not_null<PeerListRow*> row) override;
  379. bool rowTrackPress(not_null<PeerListRow*> row) override;
  380. void rowTrackPressCancel() override;
  381. bool rowTrackPressSkipMouseSelection() override;
  382. bool processTouchEvent(not_null<QTouchEvent*> e);
  383. void setupTouchChatPreview(not_null<Ui::ElasticScroll*> scroll);
  384. protected:
  385. [[nodiscard]] int countCurrent() const;
  386. void setCount(int count);
  387. [[nodiscard]] bool expandedCurrent() const;
  388. [[nodiscard]] rpl::producer<bool> expanded() const;
  389. void setupPlainDivider(rpl::producer<QString> title);
  390. void setupExpandDivider(rpl::producer<QString> title);
  391. private:
  392. const not_null<Window::SessionController*> _window;
  393. std::optional<QPoint> _chatPreviewTouchGlobal;
  394. rpl::event_stream<> _touchCancelRequests;
  395. rpl::event_stream<not_null<PeerData*>> _chosen;
  396. rpl::variable<int> _count;
  397. rpl::variable<Ui::RpWidget*> _toggleExpanded = nullptr;
  398. rpl::variable<bool> _expanded = false;
  399. };
  400. class RecentsController final : public Suggestions::ObjectListController {
  401. public:
  402. using RightActionCallback = Fn<void(not_null<PeerData*>)>;
  403. RecentsController(
  404. not_null<Window::SessionController*> window,
  405. RecentPeersList list,
  406. RightActionCallback rightActionCallback);
  407. void prepare() override;
  408. base::unique_qptr<Ui::PopupMenu> rowContextMenu(
  409. QWidget *parent,
  410. not_null<PeerListRow*> row) override;
  411. void rowRightActionClicked(not_null<PeerListRow*> row) override;
  412. QString savedMessagesChatStatus() const override;
  413. private:
  414. void setupDivider();
  415. void subscribeToEvents();
  416. [[nodiscard]] Fn<void()> removeAllCallback();
  417. RecentPeersList _recent;
  418. RightActionCallback _rightActionCallback;
  419. rpl::lifetime _lifetime;
  420. };
  421. class MyChannelsController final
  422. : public Suggestions::ObjectListController {
  423. public:
  424. explicit MyChannelsController(
  425. not_null<Window::SessionController*> window);
  426. void prepare() override;
  427. base::unique_qptr<Ui::PopupMenu> rowContextMenu(
  428. QWidget *parent,
  429. not_null<PeerListRow*> row) override;
  430. private:
  431. void appendRow(not_null<ChannelData*> channel);
  432. void fill(bool force = false);
  433. std::vector<not_null<History*>> _channels;
  434. rpl::lifetime _lifetime;
  435. };
  436. class RecommendationsController final
  437. : public Suggestions::ObjectListController {
  438. public:
  439. explicit RecommendationsController(
  440. not_null<Window::SessionController*> window);
  441. void prepare() override;
  442. void load();
  443. private:
  444. void fill();
  445. void appendRow(not_null<ChannelData*> channel);
  446. History *_activeHistory = nullptr;
  447. bool _requested = false;
  448. rpl::lifetime _lifetime;
  449. };
  450. class RecentAppsController final
  451. : public Suggestions::ObjectListController {
  452. public:
  453. explicit RecentAppsController(
  454. not_null<Window::SessionController*> window);
  455. void prepare() override;
  456. base::unique_qptr<Ui::PopupMenu> rowContextMenu(
  457. QWidget *parent,
  458. not_null<PeerListRow*> row) override;
  459. void load();
  460. [[nodiscard]] rpl::producer<> refreshed() const;
  461. [[nodiscard]] bool shown(not_null<PeerData*> peer) const;
  462. private:
  463. void appendRow(not_null<UserData*> bot);
  464. void fill();
  465. std::vector<not_null<UserData*>> _bots;
  466. rpl::event_stream<> _refreshed;
  467. rpl::lifetime _lifetime;
  468. };
  469. class PopularAppsController final
  470. : public Suggestions::ObjectListController {
  471. public:
  472. PopularAppsController(
  473. not_null<Window::SessionController*> window,
  474. Fn<bool(not_null<PeerData*>)> filterOut,
  475. rpl::producer<> filterOutRefreshes);
  476. void prepare() override;
  477. void load();
  478. private:
  479. void fill();
  480. void appendRow(not_null<UserData*> bot);
  481. Fn<bool(not_null<PeerData*>)> _filterOut;
  482. rpl::producer<> _filterOutRefreshes;
  483. History *_activeHistory = nullptr;
  484. bool _requested = false;
  485. rpl::lifetime _lifetime;
  486. };
  487. Suggestions::ObjectListController::ObjectListController(
  488. not_null<Window::SessionController*> window)
  489. : _window(window) {
  490. }
  491. bool Suggestions::ObjectListController::rowTrackPress(
  492. not_null<PeerListRow*> row) {
  493. const auto peer = row->peer();
  494. const auto history = peer->owner().history(peer);
  495. const auto callback = crl::guard(this, [=](bool shown) {
  496. delegate()->peerListPressLeftToContextMenu(shown);
  497. });
  498. if (base::IsAltPressed()) {
  499. _window->showChatPreview(
  500. { history, FullMsgId() },
  501. callback,
  502. nullptr,
  503. _chatPreviewTouchGlobal);
  504. return false;
  505. }
  506. const auto point = delegate()->peerListLastRowMousePosition();
  507. const auto &st = computeListSt().item;
  508. if (point && point->x() < st.photoPosition.x() + st.photoSize) {
  509. _window->scheduleChatPreview(
  510. { history, FullMsgId() },
  511. callback,
  512. nullptr,
  513. _chatPreviewTouchGlobal);
  514. return true;
  515. }
  516. return false;
  517. }
  518. void Suggestions::ObjectListController::rowTrackPressCancel() {
  519. _chatPreviewTouchGlobal = {};
  520. _window->cancelScheduledPreview();
  521. }
  522. bool Suggestions::ObjectListController::rowTrackPressSkipMouseSelection() {
  523. return _chatPreviewTouchGlobal.has_value();
  524. }
  525. bool Suggestions::ObjectListController::processTouchEvent(
  526. not_null<QTouchEvent*> e) {
  527. const auto point = e->touchPoints().empty()
  528. ? std::optional<QPoint>()
  529. : e->touchPoints().front().screenPos().toPoint();
  530. switch (e->type()) {
  531. case QEvent::TouchBegin: {
  532. if (!point) {
  533. return false;
  534. }
  535. _chatPreviewTouchGlobal = point;
  536. if (!delegate()->peerListTrackRowPressFromGlobal(*point)) {
  537. _chatPreviewTouchGlobal = {};
  538. }
  539. } break;
  540. case QEvent::TouchUpdate: {
  541. if (!point) {
  542. return false;
  543. }
  544. if (_chatPreviewTouchGlobal) {
  545. const auto delta = (*_chatPreviewTouchGlobal - *point);
  546. if (delta.manhattanLength() > computeListSt().item.photoSize) {
  547. rowTrackPressCancel();
  548. }
  549. }
  550. } break;
  551. case QEvent::TouchEnd:
  552. case QEvent::TouchCancel: {
  553. if (_chatPreviewTouchGlobal) {
  554. rowTrackPressCancel();
  555. }
  556. } break;
  557. }
  558. return false;
  559. }
  560. void Suggestions::ObjectListController::setupTouchChatPreview(
  561. not_null<Ui::ElasticScroll*> scroll) {
  562. _touchCancelRequests.events() | rpl::start_with_next([=] {
  563. QTouchEvent ev(QEvent::TouchCancel);
  564. ev.setTimestamp(crl::now());
  565. QGuiApplication::sendEvent(scroll, &ev);
  566. }, lifetime());
  567. }
  568. int Suggestions::ObjectListController::countCurrent() const {
  569. return _count.current();
  570. }
  571. void Suggestions::ObjectListController::setCount(int count) {
  572. _count = count;
  573. }
  574. bool Suggestions::ObjectListController::expandedCurrent() const {
  575. return _expanded.current();
  576. }
  577. rpl::producer<bool> Suggestions::ObjectListController::expanded() const {
  578. return _expanded.value();
  579. }
  580. void Suggestions::ObjectListController::rowClicked(
  581. not_null<PeerListRow*> row) {
  582. _chosen.fire(row->peer());
  583. }
  584. void Suggestions::ObjectListController::rowMiddleClicked(
  585. not_null<PeerListRow*> row) {
  586. window()->showInNewWindow(row->peer());
  587. }
  588. void Suggestions::ObjectListController::setupPlainDivider(
  589. rpl::producer<QString> title) {
  590. auto result = object_ptr<Ui::FixedHeightWidget>(
  591. (QWidget*)nullptr,
  592. st::searchedBarHeight);
  593. const auto raw = result.data();
  594. const auto label = Ui::CreateChild<Ui::FlatLabel>(
  595. raw,
  596. std::move(title),
  597. st::searchedBarLabel);
  598. raw->sizeValue(
  599. ) | rpl::start_with_next([=](QSize size) {
  600. const auto x = st::searchedBarPosition.x();
  601. const auto y = st::searchedBarPosition.y();
  602. label->resizeToWidth(size.width() - x * 2);
  603. label->moveToLeft(x, y, size.width());
  604. }, raw->lifetime());
  605. raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
  606. QPainter(raw).fillRect(clip, st::searchedBarBg);
  607. }, raw->lifetime());
  608. delegate()->peerListSetAboveWidget(std::move(result));
  609. }
  610. void Suggestions::ObjectListController::setupExpandDivider(
  611. rpl::producer<QString> title) {
  612. auto result = object_ptr<Ui::FixedHeightWidget>(
  613. (QWidget*)nullptr,
  614. st::searchedBarHeight);
  615. const auto raw = result.data();
  616. const auto label = Ui::CreateChild<Ui::FlatLabel>(
  617. raw,
  618. std::move(title),
  619. st::searchedBarLabel);
  620. count(
  621. ) | rpl::map(
  622. rpl::mappers::_1 > kCollapsedChannelsCount
  623. ) | rpl::distinct_until_changed() | rpl::start_with_next([=](bool more) {
  624. _expanded = false;
  625. if (!more) {
  626. const auto toggle = _toggleExpanded.current();
  627. _toggleExpanded = nullptr;
  628. delete toggle;
  629. return;
  630. } else if (_toggleExpanded.current()) {
  631. return;
  632. }
  633. const auto toggle = Ui::CreateChild<Ui::LinkButton>(
  634. raw,
  635. tr::lng_channels_your_more(tr::now),
  636. st::searchedBarLink);
  637. toggle->show();
  638. toggle->setClickedCallback([=] {
  639. const auto expand = !_expanded.current();
  640. toggle->setText(expand
  641. ? tr::lng_channels_your_less(tr::now)
  642. : tr::lng_channels_your_more(tr::now));
  643. _expanded = expand;
  644. });
  645. rpl::combine(
  646. raw->sizeValue(),
  647. toggle->widthValue()
  648. ) | rpl::start_with_next([=](QSize size, int width) {
  649. const auto x = st::searchedBarPosition.x();
  650. const auto y = st::searchedBarPosition.y();
  651. toggle->moveToRight(0, 0, size.width());
  652. label->resizeToWidth(size.width() - x - width);
  653. label->moveToLeft(x, y, size.width());
  654. }, toggle->lifetime());
  655. _toggleExpanded = toggle;
  656. }, raw->lifetime());
  657. rpl::combine(
  658. raw->sizeValue(),
  659. _toggleExpanded.value()
  660. ) | rpl::filter(
  661. rpl::mappers::_2 == nullptr
  662. ) | rpl::start_with_next([=](QSize size, const auto) {
  663. const auto x = st::searchedBarPosition.x();
  664. const auto y = st::searchedBarPosition.y();
  665. label->resizeToWidth(size.width() - x * 2);
  666. label->moveToLeft(x, y, size.width());
  667. }, raw->lifetime());
  668. raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
  669. QPainter(raw).fillRect(clip, st::searchedBarBg);
  670. }, raw->lifetime());
  671. delegate()->peerListSetAboveWidget(std::move(result));
  672. }
  673. RecentsController::RecentsController(
  674. not_null<Window::SessionController*> window,
  675. RecentPeersList list,
  676. RightActionCallback rightActionCallback)
  677. : ObjectListController(window)
  678. , _recent(std::move(list))
  679. , _rightActionCallback(std::move(rightActionCallback)) {
  680. }
  681. void RecentsController::prepare() {
  682. setupDivider();
  683. for (const auto &peer : _recent.list) {
  684. delegate()->peerListAppendRow(std::make_unique<RecentRow>(peer));
  685. }
  686. delegate()->peerListRefreshRows();
  687. setCount(_recent.list.size());
  688. subscribeToEvents();
  689. }
  690. Fn<void()> RecentsController::removeAllCallback() {
  691. const auto weak = base::make_weak(this);
  692. const auto session = &this->session();
  693. return crl::guard(session, [=] {
  694. if (weak) {
  695. setCount(0);
  696. while (delegate()->peerListFullRowsCount() > 0) {
  697. delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
  698. }
  699. delegate()->peerListRefreshRows();
  700. }
  701. session->recentPeers().clear();
  702. });
  703. }
  704. base::unique_qptr<Ui::PopupMenu> RecentsController::rowContextMenu(
  705. QWidget *parent,
  706. not_null<PeerListRow*> row) {
  707. auto result = base::make_unique_q<Ui::PopupMenu>(
  708. parent,
  709. st::popupMenuWithIcons);
  710. const auto peer = row->peer();
  711. const auto weak = base::make_weak(this);
  712. const auto session = &this->session();
  713. const auto removeOne = crl::guard(session, [=] {
  714. if (weak) {
  715. const auto rowId = peer->id.value;
  716. if (const auto row = delegate()->peerListFindRow(rowId)) {
  717. setCount(std::max(0, countCurrent() - 1));
  718. delegate()->peerListRemoveRow(row);
  719. delegate()->peerListRefreshRows();
  720. }
  721. }
  722. session->recentPeers().remove(peer);
  723. });
  724. FillEntryMenu(Ui::Menu::CreateAddActionCallback(result), {
  725. .controller = window(),
  726. .peer = peer,
  727. .removeOneText = tr::lng_recent_remove(tr::now),
  728. .removeOne = removeOne,
  729. .removeAllText = tr::lng_recent_clear_all(tr::now),
  730. .removeAllConfirm = tr::lng_recent_clear_sure(tr::now),
  731. .removeAll = removeAllCallback(),
  732. });
  733. return result;
  734. }
  735. void RecentsController::rowRightActionClicked(not_null<PeerListRow*> row) {
  736. if (_rightActionCallback) {
  737. if (const auto peer = row->peer()) {
  738. _rightActionCallback(peer);
  739. }
  740. }
  741. }
  742. QString RecentsController::savedMessagesChatStatus() const {
  743. return tr::lng_saved_forward_here(tr::now);
  744. }
  745. void RecentsController::setupDivider() {
  746. auto result = object_ptr<Ui::FixedHeightWidget>(
  747. (QWidget*)nullptr,
  748. st::searchedBarHeight);
  749. const auto raw = result.data();
  750. const auto label = Ui::CreateChild<Ui::FlatLabel>(
  751. raw,
  752. tr::lng_recent_title(),
  753. st::searchedBarLabel);
  754. const auto clear = Ui::CreateChild<Ui::LinkButton>(
  755. raw,
  756. tr::lng_recent_clear(tr::now),
  757. st::searchedBarLink);
  758. clear->setClickedCallback(RemoveAllConfirm(
  759. window(),
  760. tr::lng_recent_clear_sure(tr::now),
  761. removeAllCallback()));
  762. rpl::combine(
  763. raw->sizeValue(),
  764. clear->widthValue()
  765. ) | rpl::start_with_next([=](QSize size, int width) {
  766. const auto x = st::searchedBarPosition.x();
  767. const auto y = st::searchedBarPosition.y();
  768. clear->moveToRight(0, 0, size.width());
  769. label->resizeToWidth(size.width() - x - width);
  770. label->moveToLeft(x, y, size.width());
  771. }, raw->lifetime());
  772. raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
  773. QPainter(raw).fillRect(clip, st::searchedBarBg);
  774. }, raw->lifetime());
  775. delegate()->peerListSetAboveWidget(std::move(result));
  776. }
  777. void RecentsController::subscribeToEvents() {
  778. using Flag = Data::PeerUpdate::Flag;
  779. session().changes().peerUpdates(
  780. Flag::Notifications
  781. | Flag::OnlineStatus
  782. ) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
  783. const auto peer = update.peer;
  784. if (peer->isSelf()) {
  785. return;
  786. }
  787. auto refreshed = false;
  788. const auto row = delegate()->peerListFindRow(update.peer->id.value);
  789. if (!row) {
  790. return;
  791. } else if (update.flags & Flag::Notifications) {
  792. refreshed = static_cast<RecentRow*>(row)->refreshBadge();
  793. }
  794. if (!peer->isRepliesChat()
  795. && !peer->isVerifyCodes()
  796. && (update.flags & Flag::OnlineStatus)) {
  797. row->clearCustomStatus();
  798. refreshed = true;
  799. }
  800. if (refreshed) {
  801. delegate()->peerListUpdateRow(row);
  802. }
  803. }, _lifetime);
  804. session().data().unreadBadgeChanges(
  805. ) | rpl::start_with_next([=] {
  806. for (auto i = 0; i != countCurrent(); ++i) {
  807. const auto row = delegate()->peerListRowAt(i);
  808. if (static_cast<RecentRow*>(row.get())->refreshBadge()) {
  809. delegate()->peerListUpdateRow(row);
  810. }
  811. }
  812. }, _lifetime);
  813. }
  814. MyChannelsController::MyChannelsController(
  815. not_null<Window::SessionController*> window)
  816. : ObjectListController(window) {
  817. }
  818. void MyChannelsController::prepare() {
  819. setupExpandDivider(tr::lng_channels_your_title());
  820. session().changes().peerUpdates(
  821. Data::PeerUpdate::Flag::ChannelAmIn
  822. ) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
  823. const auto channel = update.peer->asBroadcast();
  824. if (!channel || channel->amIn()) {
  825. return;
  826. }
  827. const auto history = channel->owner().history(channel);
  828. const auto i = ranges::remove(_channels, history);
  829. if (i == end(_channels)) {
  830. return;
  831. }
  832. _channels.erase(i, end(_channels));
  833. const auto row = delegate()->peerListFindRow(channel->id.value);
  834. if (row) {
  835. delegate()->peerListRemoveRow(row);
  836. }
  837. setCount(_channels.size());
  838. fill(true);
  839. }, _lifetime);
  840. _channels.reserve(kProbablyMaxChannels);
  841. const auto owner = &session().data();
  842. const auto add = [&](not_null<Dialogs::MainList*> list) {
  843. for (const auto &row : list->indexed()->all()) {
  844. if (const auto history = row->history()) {
  845. if (const auto channel = history->peer->asBroadcast()) {
  846. _channels.push_back(history);
  847. }
  848. }
  849. }
  850. };
  851. add(owner->chatsList());
  852. if (const auto folder = owner->folderLoaded(Data::Folder::kId)) {
  853. add(owner->chatsList(folder));
  854. }
  855. ranges::sort(_channels, ranges::greater(), &History::chatListTimeId);
  856. setCount(_channels.size());
  857. expanded() | rpl::start_with_next([=] {
  858. fill();
  859. }, _lifetime);
  860. auto loading = owner->chatsListChanges(
  861. ) | rpl::take_while([=](Data::Folder *folder) {
  862. return !owner->chatsListLoaded(folder);
  863. });
  864. rpl::merge(
  865. std::move(loading),
  866. owner->chatsListLoadedEvents()
  867. ) | rpl::start_with_next([=](Data::Folder *folder) {
  868. const auto list = owner->chatsList(folder);
  869. for (const auto &row : list->indexed()->all()) {
  870. if (const auto history = row->history()) {
  871. if (const auto channel = history->peer->asBroadcast()) {
  872. if (ranges::contains(_channels, not_null(history))) {
  873. _channels.push_back(history);
  874. }
  875. }
  876. }
  877. }
  878. const auto was = countCurrent();
  879. const auto now = int(_channels.size());
  880. if (was != now) {
  881. setCount(now);
  882. fill();
  883. }
  884. }, _lifetime);
  885. }
  886. void MyChannelsController::fill(bool force) {
  887. const auto count = countCurrent();
  888. const auto limit = expandedCurrent()
  889. ? count
  890. : std::min(count, kCollapsedChannelsCount);
  891. const auto already = delegate()->peerListFullRowsCount();
  892. const auto delta = limit - already;
  893. if (!delta && !force) {
  894. return;
  895. } else if (delta > 0) {
  896. for (auto i = already; i != limit; ++i) {
  897. appendRow(_channels[i]->peer->asBroadcast());
  898. }
  899. } else if (delta < 0) {
  900. for (auto i = already; i != limit;) {
  901. delegate()->peerListRemoveRow(delegate()->peerListRowAt(--i));
  902. }
  903. }
  904. delegate()->peerListRefreshRows();
  905. }
  906. void MyChannelsController::appendRow(not_null<ChannelData*> channel) {
  907. auto row = std::make_unique<PeerListRow>(channel);
  908. if (channel->membersCountKnown()) {
  909. row->setCustomStatus((channel->isBroadcast()
  910. ? tr::lng_chat_status_subscribers
  911. : tr::lng_chat_status_members)(
  912. tr::now,
  913. lt_count_decimal,
  914. channel->membersCount()));
  915. }
  916. delegate()->peerListAppendRow(std::move(row));
  917. }
  918. base::unique_qptr<Ui::PopupMenu> MyChannelsController::rowContextMenu(
  919. QWidget *parent,
  920. not_null<PeerListRow*> row) {
  921. auto result = base::make_unique_q<Ui::PopupMenu>(
  922. parent,
  923. st::popupMenuWithIcons);
  924. const auto peer = row->peer();
  925. const auto addAction = Ui::Menu::CreateAddActionCallback(result);
  926. Window::FillDialogsEntryMenu(
  927. window(),
  928. Dialogs::EntryState{
  929. .key = peer->owner().history(peer),
  930. .section = Dialogs::EntryState::Section::ContextMenu,
  931. },
  932. addAction);
  933. return result;
  934. }
  935. RecommendationsController::RecommendationsController(
  936. not_null<Window::SessionController*> window)
  937. : ObjectListController(window) {
  938. }
  939. void RecommendationsController::prepare() {
  940. setupPlainDivider(tr::lng_channels_recommended());
  941. fill();
  942. }
  943. void RecommendationsController::load() {
  944. if (_requested || countCurrent()) {
  945. return;
  946. }
  947. _requested = true;
  948. const auto participants = &session().api().chatParticipants();
  949. participants->loadRecommendations();
  950. participants->recommendationsLoaded(
  951. ) | rpl::take(1) | rpl::start_with_next([=] {
  952. fill();
  953. }, _lifetime);
  954. }
  955. void RecommendationsController::fill() {
  956. const auto participants = &session().api().chatParticipants();
  957. const auto &list = participants->recommendations().list;
  958. if (list.empty()) {
  959. return;
  960. }
  961. for (const auto &peer : list) {
  962. if (const auto channel = peer->asBroadcast()) {
  963. appendRow(channel);
  964. }
  965. }
  966. delegate()->peerListRefreshRows();
  967. setCount(delegate()->peerListFullRowsCount());
  968. window()->activeChatValue() | rpl::start_with_next([=](const Key &key) {
  969. const auto history = key.history();
  970. if (_activeHistory == history) {
  971. return;
  972. } else if (_activeHistory) {
  973. const auto id = _activeHistory->peer->id.value;
  974. if (const auto row = delegate()->peerListFindRow(id)) {
  975. static_cast<ChannelRow*>(row)->setActive(false);
  976. delegate()->peerListUpdateRow(row);
  977. }
  978. }
  979. _activeHistory = history;
  980. if (_activeHistory) {
  981. const auto id = _activeHistory->peer->id.value;
  982. if (const auto row = delegate()->peerListFindRow(id)) {
  983. static_cast<ChannelRow*>(row)->setActive(true);
  984. delegate()->peerListUpdateRow(row);
  985. }
  986. }
  987. }, _lifetime);
  988. }
  989. void RecommendationsController::appendRow(not_null<ChannelData*> channel) {
  990. auto row = std::make_unique<ChannelRow>(channel);
  991. if (channel->membersCountKnown()) {
  992. row->setCustomStatus((channel->isBroadcast()
  993. ? tr::lng_chat_status_subscribers
  994. : tr::lng_chat_status_members)(
  995. tr::now,
  996. lt_count_decimal,
  997. channel->membersCount()));
  998. }
  999. delegate()->peerListAppendRow(std::move(row));
  1000. }
  1001. RecentAppsController::RecentAppsController(
  1002. not_null<Window::SessionController*> window)
  1003. : ObjectListController(window) {
  1004. }
  1005. void RecentAppsController::prepare() {
  1006. setupExpandDivider(tr::lng_bot_apps_your());
  1007. _bots.reserve(kProbablyMaxApps);
  1008. rpl::single() | rpl::then(
  1009. session().topBotApps().updates()
  1010. ) | rpl::start_with_next([=] {
  1011. _bots.clear();
  1012. for (const auto &peer : session().topBotApps().list()) {
  1013. if (const auto bot = peer->asUser()) {
  1014. if (bot->isBot() && !bot->isInaccessible()) {
  1015. _bots.push_back(bot);
  1016. }
  1017. }
  1018. }
  1019. setCount(_bots.size());
  1020. while (delegate()->peerListFullRowsCount()) {
  1021. delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
  1022. }
  1023. fill();
  1024. }, _lifetime);
  1025. expanded() | rpl::skip(1) | rpl::start_with_next([=] {
  1026. fill();
  1027. }, _lifetime);
  1028. }
  1029. base::unique_qptr<Ui::PopupMenu> RecentAppsController::rowContextMenu(
  1030. QWidget *parent,
  1031. not_null<PeerListRow*> row) {
  1032. auto result = base::make_unique_q<Ui::PopupMenu>(
  1033. parent,
  1034. st::popupMenuWithIcons);
  1035. const auto peer = row->peer();
  1036. const auto weak = base::make_weak(this);
  1037. const auto session = &this->session();
  1038. const auto removeOne = crl::guard(session, [=] {
  1039. if (weak) {
  1040. const auto rowId = peer->id.value;
  1041. if (const auto row = delegate()->peerListFindRow(rowId)) {
  1042. setCount(std::max(0, countCurrent() - 1));
  1043. delegate()->peerListRemoveRow(row);
  1044. delegate()->peerListRefreshRows();
  1045. }
  1046. }
  1047. session->topBotApps().remove(peer);
  1048. });
  1049. FillEntryMenu(Ui::Menu::CreateAddActionCallback(result), {
  1050. .controller = window(),
  1051. .peer = peer,
  1052. .removeOneText = tr::lng_recent_remove(tr::now),
  1053. .removeOne = removeOne,
  1054. });
  1055. return result;
  1056. }
  1057. void RecentAppsController::load() {
  1058. session().topBotApps().reload();
  1059. }
  1060. rpl::producer<> RecentAppsController::refreshed() const {
  1061. return _refreshed.events();
  1062. }
  1063. bool RecentAppsController::shown(not_null<PeerData*> peer) const {
  1064. return delegate()->peerListFindRow(peer->id.value) != nullptr;
  1065. }
  1066. void RecentAppsController::fill() {
  1067. const auto count = countCurrent();
  1068. const auto limit = expandedCurrent()
  1069. ? count
  1070. : std::min(count, kCollapsedAppsCount);
  1071. const auto already = delegate()->peerListFullRowsCount();
  1072. const auto delta = limit - already;
  1073. if (!delta) {
  1074. return;
  1075. } else if (delta > 0) {
  1076. for (auto i = already; i != limit; ++i) {
  1077. appendRow(_bots[i]);
  1078. }
  1079. } else if (delta < 0) {
  1080. for (auto i = already; i != limit;) {
  1081. delegate()->peerListRemoveRow(delegate()->peerListRowAt(--i));
  1082. }
  1083. }
  1084. delegate()->peerListRefreshRows();
  1085. _refreshed.fire({});
  1086. }
  1087. void RecentAppsController::appendRow(not_null<UserData*> bot) {
  1088. auto row = std::make_unique<PeerListRow>(bot);
  1089. if (const auto count = bot->botInfo->activeUsers) {
  1090. row->setCustomStatus(
  1091. tr::lng_bot_status_users(tr::now, lt_count_decimal, count));
  1092. }
  1093. delegate()->peerListAppendRow(std::move(row));
  1094. }
  1095. PopularAppsController::PopularAppsController(
  1096. not_null<Window::SessionController*> window,
  1097. Fn<bool(not_null<PeerData*>)> filterOut,
  1098. rpl::producer<> filterOutRefreshes)
  1099. : ObjectListController(window)
  1100. , _filterOut(std::move(filterOut))
  1101. , _filterOutRefreshes(std::move(filterOutRefreshes)) {
  1102. }
  1103. void PopularAppsController::prepare() {
  1104. if (_filterOut) {
  1105. setupPlainDivider(tr::lng_bot_apps_popular());
  1106. }
  1107. rpl::single() | rpl::then(
  1108. std::move(_filterOutRefreshes)
  1109. ) | rpl::start_with_next([=] {
  1110. fill();
  1111. }, _lifetime);
  1112. }
  1113. void PopularAppsController::load() {
  1114. if (_requested || countCurrent()) {
  1115. return;
  1116. }
  1117. _requested = true;
  1118. const auto attachWebView = &session().attachWebView();
  1119. attachWebView->loadPopularAppBots();
  1120. attachWebView->popularAppBotsLoaded(
  1121. ) | rpl::take(1) | rpl::start_with_next([=] {
  1122. fill();
  1123. }, _lifetime);
  1124. }
  1125. void PopularAppsController::fill() {
  1126. while (delegate()->peerListFullRowsCount()) {
  1127. delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
  1128. }
  1129. for (const auto &bot : session().attachWebView().popularAppBots()) {
  1130. if (!_filterOut || !_filterOut(bot)) {
  1131. appendRow(bot);
  1132. }
  1133. }
  1134. const auto count = delegate()->peerListFullRowsCount();
  1135. setCount(count);
  1136. if (count > 0) {
  1137. delegate()->peerListSetBelowWidget(object_ptr<Ui::DividerLabel>(
  1138. (QWidget*)nullptr,
  1139. object_ptr<Ui::FlatLabel>(
  1140. (QWidget*)nullptr,
  1141. tr::lng_bot_apps_which(
  1142. lt_link,
  1143. tr::lng_bot_apps_which_link(
  1144. ) | Ui::Text::ToLink(u"internal:about_popular_apps"_q),
  1145. Ui::Text::WithEntities),
  1146. st::dialogsPopularAppsAbout),
  1147. st::dialogsPopularAppsPadding));
  1148. }
  1149. delegate()->peerListRefreshRows();
  1150. }
  1151. void PopularAppsController::appendRow(not_null<UserData*> bot) {
  1152. auto row = std::make_unique<PeerListRow>(bot);
  1153. if (bot->isBot()) {
  1154. if (!bot->botInfo->activeUsers && !bot->username().isEmpty()) {
  1155. row->setCustomStatus('@' + bot->username());
  1156. }
  1157. }
  1158. delegate()->peerListAppendRow(std::move(row));
  1159. }
  1160. Suggestions::Suggestions(
  1161. not_null<QWidget*> parent,
  1162. not_null<Window::SessionController*> controller,
  1163. rpl::producer<TopPeersList> topPeers,
  1164. RecentPeersList recentPeers)
  1165. : RpWidget(parent)
  1166. , _controller(controller)
  1167. , _tabsScroll(
  1168. std::make_unique<Ui::ScrollArea>(this, st::dialogsTabsScroll, true))
  1169. , _tabs(
  1170. _tabsScroll->setOwnedWidget(
  1171. object_ptr<Ui::SettingsSlider>(this, st::dialogsSearchTabs)))
  1172. , _tabKeys(TabKeysFor(controller))
  1173. , _chatsScroll(std::make_unique<Ui::ElasticScroll>(this))
  1174. , _chatsContent(
  1175. _chatsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
  1176. , _topPeersWrap(
  1177. _chatsContent->add(object_ptr<Ui::SlideWrap<TopPeersStrip>>(
  1178. this,
  1179. object_ptr<TopPeersStrip>(this, std::move(topPeers)))))
  1180. , _topPeers(_topPeersWrap->entity())
  1181. , _recent(setupRecentPeers(std::move(recentPeers)))
  1182. , _emptyRecent(_chatsContent->add(setupEmptyRecent()))
  1183. , _channelsScroll(std::make_unique<Ui::ElasticScroll>(this))
  1184. , _channelsContent(
  1185. _channelsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
  1186. , _myChannels(setupMyChannels())
  1187. , _recommendations(setupRecommendations())
  1188. , _emptyChannels(_channelsContent->add(setupEmptyChannels()))
  1189. , _appsScroll(std::make_unique<Ui::ElasticScroll>(this))
  1190. , _appsContent(
  1191. _appsScroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
  1192. , _recentApps(setupRecentApps())
  1193. , _popularApps(setupPopularApps())
  1194. , _searchQueryTimer([=] { applySearchQuery(); }) {
  1195. setupTabs();
  1196. setupChats();
  1197. setupChannels();
  1198. setupApps();
  1199. }
  1200. Suggestions::~Suggestions() = default;
  1201. void Suggestions::setupTabs() {
  1202. _tabsScroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {
  1203. const auto pixelDelta = e->pixelDelta();
  1204. const auto angleDelta = e->angleDelta();
  1205. if (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) {
  1206. return false;
  1207. }
  1208. const auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y();
  1209. _tabsScroll->scrollToX(_tabsScroll->scrollLeft() - y);
  1210. return true;
  1211. });
  1212. const auto scrollToIndex = [=](int index, anim::type type) {
  1213. const auto to = index
  1214. ? (_tabs->centerOfSection(index) - _tabsScroll->width() / 2)
  1215. : 0;
  1216. _tabsScrollAnimation.stop();
  1217. if (type == anim::type::instant) {
  1218. _tabsScroll->scrollToX(to);
  1219. } else {
  1220. _tabsScrollAnimation.start(
  1221. [=](float64 v) { _tabsScroll->scrollToX(v); },
  1222. _tabsScroll->scrollLeft(),
  1223. std::min(to, _tabsScroll->scrollLeftMax()),
  1224. st::defaultTabsSlider.duration);
  1225. }
  1226. };
  1227. rpl::single(-1) | rpl::then(
  1228. _tabs->sectionActivated()
  1229. ) | rpl::combine_previous(
  1230. ) | rpl::start_with_next([=](int was, int index) {
  1231. if (was != index) {
  1232. scrollToIndex(index, anim::type::normal);
  1233. }
  1234. }, _tabs->lifetime());
  1235. const auto shadow = Ui::CreateChild<Ui::PlainShadow>(this);
  1236. shadow->lower();
  1237. _tabsScroll->move(0, 0);
  1238. _tabs->move(0, 0);
  1239. rpl::combine(
  1240. widthValue(),
  1241. _tabs->heightValue()
  1242. ) | rpl::start_with_next([=](int width, int height) {
  1243. const auto line = st::lineWidth;
  1244. shadow->setGeometry(0, height - line, width, line);
  1245. }, shadow->lifetime());
  1246. shadow->showOn(_tabsScroll->shownValue());
  1247. const auto labels = base::flat_map<Key, QString>{
  1248. { Key{ Tab::Chats }, tr::lng_recent_chats(tr::now) },
  1249. { Key{ Tab::Channels }, tr::lng_recent_channels(tr::now) },
  1250. { Key{ Tab::Apps }, tr::lng_recent_apps(tr::now) },
  1251. { Key{ Tab::Media, MediaType::Photo }, tr::lng_all_photos(tr::now) },
  1252. { Key{ Tab::Media, MediaType::Video }, tr::lng_all_videos(tr::now) },
  1253. { Key{ Tab::Downloads }, tr::lng_all_downloads(tr::now) },
  1254. { Key{ Tab::Media, MediaType::Link }, tr::lng_all_links(tr::now) },
  1255. { Key{ Tab::Media, MediaType::File }, tr::lng_all_files(tr::now) },
  1256. {
  1257. Key{ Tab::Media, MediaType::MusicFile },
  1258. tr::lng_all_music(tr::now),
  1259. },
  1260. {
  1261. Key{ Tab::Media, MediaType::RoundVoiceFile },
  1262. tr::lng_all_voice(tr::now),
  1263. },
  1264. };
  1265. auto sections = std::vector<QString>();
  1266. for (const auto key : _tabKeys) {
  1267. const auto i = labels.find(key);
  1268. Assert(i != end(labels));
  1269. sections.push_back(i->second);
  1270. }
  1271. _tabs->setSections(sections);
  1272. _tabs->sectionActivated(
  1273. ) | rpl::start_with_next([=](int section) {
  1274. Assert(section >= 0 && section < _tabKeys.size());
  1275. switchTab(_tabKeys[section]);
  1276. }, _tabs->lifetime());
  1277. }
  1278. void Suggestions::setupChats() {
  1279. _recent->count.value() | rpl::start_with_next([=](int count) {
  1280. _recent->wrap->toggle(count > 0, anim::type::instant);
  1281. _emptyRecent->toggle(count == 0, anim::type::instant);
  1282. }, _recent->wrap->lifetime());
  1283. _topPeers->emptyValue() | rpl::start_with_next([=](bool empty) {
  1284. _topPeersWrap->toggle(!empty, anim::type::instant);
  1285. }, _topPeers->lifetime());
  1286. _topPeers->clicks() | rpl::start_with_next([=](uint64 peerIdRaw) {
  1287. const auto peerId = PeerId(peerIdRaw);
  1288. _topPeerChosen.fire(_controller->session().data().peer(peerId));
  1289. }, _topPeers->lifetime());
  1290. _topPeers->pressed() | rpl::start_with_next([=](uint64 peerIdRaw) {
  1291. handlePressForChatPreview(PeerId(peerIdRaw), [=](bool shown) {
  1292. _topPeers->pressLeftToContextMenu(shown);
  1293. });
  1294. }, _topPeers->lifetime());
  1295. _topPeers->pressCancelled() | rpl::start_with_next([=] {
  1296. _controller->cancelScheduledPreview();
  1297. }, _topPeers->lifetime());
  1298. _topPeers->showMenuRequests(
  1299. ) | rpl::start_with_next([=](const ShowTopPeerMenuRequest &request) {
  1300. const auto weak = Ui::MakeWeak(this);
  1301. const auto owner = &_controller->session().data();
  1302. const auto peer = owner->peer(PeerId(request.id));
  1303. const auto removeOne = [=] {
  1304. peer->session().topPeers().remove(peer);
  1305. if (weak) {
  1306. _topPeers->removeLocally(peer->id.value);
  1307. }
  1308. };
  1309. const auto session = &_controller->session();
  1310. const auto removeAll = crl::guard(session, [=] {
  1311. session->topPeers().toggleDisabled(true);
  1312. if (weak) {
  1313. _topPeers->removeLocally();
  1314. }
  1315. });
  1316. FillEntryMenu(request.callback, {
  1317. .controller = _controller,
  1318. .peer = peer,
  1319. .removeOneText = tr::lng_recent_remove(tr::now),
  1320. .removeOne = removeOne,
  1321. .removeAllText = tr::lng_recent_hide_top(
  1322. tr::now,
  1323. Ui::Text::FixAmpersandInAction),
  1324. .removeAllConfirm = tr::lng_recent_hide_sure(tr::now),
  1325. .removeAll = removeAll,
  1326. });
  1327. }, _topPeers->lifetime());
  1328. _topPeers->scrollToRequests(
  1329. ) | rpl::start_with_next([this](Ui::ScrollToRequest request) {
  1330. _chatsScroll->scrollToY(request.ymin, request.ymax);
  1331. }, _topPeers->lifetime());
  1332. _topPeers->verticalScrollEvents(
  1333. ) | rpl::start_with_next([=](not_null<QWheelEvent*> e) {
  1334. _chatsScroll->viewportEvent(e);
  1335. }, _topPeers->lifetime());
  1336. _chatsScroll->setVisible(_key.current().tab == Tab::Chats);
  1337. _chatsScroll->setCustomTouchProcess(_recent->processTouch);
  1338. }
  1339. void Suggestions::handlePressForChatPreview(
  1340. PeerId id,
  1341. Fn<void(bool)> callback) {
  1342. callback = crl::guard(this, callback);
  1343. const auto row = RowDescriptor(
  1344. _controller->session().data().history(id),
  1345. FullMsgId());
  1346. if (base::IsAltPressed()) {
  1347. _controller->showChatPreview(row, callback);
  1348. } else {
  1349. _controller->scheduleChatPreview(row, callback);
  1350. }
  1351. }
  1352. void Suggestions::setupChannels() {
  1353. _myChannels->count.value() | rpl::start_with_next([=](int count) {
  1354. _myChannels->wrap->toggle(count > 0, anim::type::instant);
  1355. }, _myChannels->wrap->lifetime());
  1356. _recommendations->count.value() | rpl::start_with_next([=](int count) {
  1357. _recommendations->wrap->toggle(count > 0, anim::type::instant);
  1358. }, _recommendations->wrap->lifetime());
  1359. _emptyChannels->toggleOn(
  1360. rpl::combine(
  1361. _myChannels->count.value(),
  1362. _recommendations->count.value(),
  1363. rpl::mappers::_1 + rpl::mappers::_2 == 0),
  1364. anim::type::instant);
  1365. _channelsScroll->setVisible(_key.current().tab == Tab::Channels);
  1366. _channelsScroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {
  1367. const auto myChannels = _myChannels->processTouch(e);
  1368. const auto recommendations = _recommendations->processTouch(e);
  1369. return myChannels || recommendations;
  1370. });
  1371. }
  1372. void Suggestions::setupApps() {
  1373. _recentApps->count.value() | rpl::start_with_next([=](int count) {
  1374. _recentApps->wrap->toggle(count > 0, anim::type::instant);
  1375. }, _recentApps->wrap->lifetime());
  1376. _popularApps->count.value() | rpl::start_with_next([=](int count) {
  1377. _popularApps->wrap->toggle(count > 0, anim::type::instant);
  1378. }, _popularApps->wrap->lifetime());
  1379. _appsScroll->setVisible(_key.current().tab == Tab::Apps);
  1380. _appsScroll->setCustomTouchProcess([=](not_null<QTouchEvent*> e) {
  1381. const auto recentApps = _recentApps->processTouch(e);
  1382. const auto popularApps = _popularApps->processTouch(e);
  1383. return recentApps || popularApps;
  1384. });
  1385. }
  1386. void Suggestions::selectJump(Qt::Key direction, int pageSize) {
  1387. switch (_key.current().tab) {
  1388. case Tab::Chats: selectJumpChats(direction, pageSize); return;
  1389. case Tab::Channels: selectJumpChannels(direction, pageSize); return;
  1390. case Tab::Apps: selectJumpApps(direction, pageSize); return;
  1391. }
  1392. }
  1393. void Suggestions::selectJumpChats(Qt::Key direction, int pageSize) {
  1394. const auto recentHasSelection = [=] {
  1395. return _recent->selectJump({}, 0) == JumpResult::Applied;
  1396. };
  1397. if (pageSize) {
  1398. if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
  1399. _topPeers->deselectByKeyboard();
  1400. if (!recentHasSelection()) {
  1401. if (direction == Qt::Key_Down) {
  1402. _recent->selectJump(direction, 0);
  1403. } else {
  1404. return;
  1405. }
  1406. }
  1407. if (_recent->selectJump(direction, pageSize)
  1408. == JumpResult::AppliedAndOut) {
  1409. if (direction == Qt::Key_Up) {
  1410. _chatsScroll->scrollTo(0);
  1411. }
  1412. }
  1413. }
  1414. } else if (direction == Qt::Key_Up) {
  1415. if (_recent->selectJump(direction, pageSize)
  1416. == JumpResult::AppliedAndOut) {
  1417. _topPeers->selectByKeyboard(direction);
  1418. } else if (_topPeers->selectedByKeyboard()) {
  1419. _topPeers->selectByKeyboard(direction);
  1420. }
  1421. } else if (direction == Qt::Key_Down) {
  1422. if (!_topPeersWrap->toggled() || recentHasSelection()) {
  1423. _recent->selectJump(direction, pageSize);
  1424. } else if (_topPeers->selectedByKeyboard()) {
  1425. if (!_topPeers->selectByKeyboard(direction)
  1426. && _recent->count.current() > 0) {
  1427. _topPeers->deselectByKeyboard();
  1428. _recent->selectJump(direction, pageSize);
  1429. }
  1430. } else {
  1431. _topPeers->selectByKeyboard({});
  1432. _chatsScroll->scrollTo(0);
  1433. }
  1434. } else if (direction == Qt::Key_Left || direction == Qt::Key_Right) {
  1435. if (!recentHasSelection()) {
  1436. _topPeers->selectByKeyboard(direction);
  1437. }
  1438. }
  1439. }
  1440. void Suggestions::selectJumpChannels(Qt::Key direction, int pageSize) {
  1441. const auto myChannelsHasSelection = [=] {
  1442. return _myChannels->selectJump({}, 0) == JumpResult::Applied;
  1443. };
  1444. const auto recommendationsHasSelection = [=] {
  1445. return _recommendations->selectJump({}, 0) == JumpResult::Applied;
  1446. };
  1447. if (pageSize) {
  1448. if (direction == Qt::Key_Down) {
  1449. if (recommendationsHasSelection()) {
  1450. _recommendations->selectJump(direction, pageSize);
  1451. } else if (myChannelsHasSelection()) {
  1452. if (_myChannels->selectJump(direction, pageSize)
  1453. == JumpResult::AppliedAndOut) {
  1454. _recommendations->selectJump(direction, 0);
  1455. }
  1456. } else if (_myChannels->count.current()) {
  1457. _myChannels->selectJump(direction, 0);
  1458. _myChannels->selectJump(direction, pageSize);
  1459. } else if (_recommendations->count.current()) {
  1460. _recommendations->selectJump(direction, 0);
  1461. _recommendations->selectJump(direction, pageSize);
  1462. }
  1463. } else if (direction == Qt::Key_Up) {
  1464. if (myChannelsHasSelection()) {
  1465. if (_myChannels->selectJump(direction, pageSize)
  1466. == JumpResult::AppliedAndOut) {
  1467. _channelsScroll->scrollTo(0);
  1468. }
  1469. } else if (recommendationsHasSelection()) {
  1470. if (_recommendations->selectJump(direction, pageSize)
  1471. == JumpResult::AppliedAndOut) {
  1472. _myChannels->selectJump(direction, -1);
  1473. }
  1474. }
  1475. }
  1476. } else if (direction == Qt::Key_Up) {
  1477. if (myChannelsHasSelection()) {
  1478. _myChannels->selectJump(direction, 0);
  1479. } else if (_recommendations->selectJump(direction, 0)
  1480. == JumpResult::AppliedAndOut) {
  1481. _myChannels->selectJump(direction, -1);
  1482. } else if (!recommendationsHasSelection()) {
  1483. if (_myChannels->selectJump(direction, 0)
  1484. == JumpResult::AppliedAndOut) {
  1485. _channelsScroll->scrollTo(0);
  1486. }
  1487. }
  1488. } else if (direction == Qt::Key_Down) {
  1489. if (recommendationsHasSelection()) {
  1490. _recommendations->selectJump(direction, 0);
  1491. } else if (_myChannels->selectJump(direction, 0)
  1492. == JumpResult::AppliedAndOut) {
  1493. _recommendations->selectJump(direction, 0);
  1494. } else if (!myChannelsHasSelection()) {
  1495. if (_recommendations->selectJump(direction, 0)
  1496. == JumpResult::AppliedAndOut) {
  1497. _myChannels->selectJump(direction, 0);
  1498. }
  1499. }
  1500. }
  1501. }
  1502. void Suggestions::selectJumpApps(Qt::Key direction, int pageSize) {
  1503. const auto recentAppsHasSelection = [=] {
  1504. return _recentApps->selectJump({}, 0) == JumpResult::Applied;
  1505. };
  1506. const auto popularAppsHasSelection = [=] {
  1507. return _popularApps->selectJump({}, 0) == JumpResult::Applied;
  1508. };
  1509. if (pageSize) {
  1510. if (direction == Qt::Key_Down) {
  1511. if (popularAppsHasSelection()) {
  1512. _popularApps->selectJump(direction, pageSize);
  1513. } else if (recentAppsHasSelection()) {
  1514. if (_recentApps->selectJump(direction, pageSize)
  1515. == JumpResult::AppliedAndOut) {
  1516. _popularApps->selectJump(direction, 0);
  1517. }
  1518. } else if (_recentApps->count.current()) {
  1519. _recentApps->selectJump(direction, 0);
  1520. _recentApps->selectJump(direction, pageSize);
  1521. } else if (_popularApps->count.current()) {
  1522. _popularApps->selectJump(direction, 0);
  1523. _popularApps->selectJump(direction, pageSize);
  1524. }
  1525. } else if (direction == Qt::Key_Up) {
  1526. if (recentAppsHasSelection()) {
  1527. if (_recentApps->selectJump(direction, pageSize)
  1528. == JumpResult::AppliedAndOut) {
  1529. _channelsScroll->scrollTo(0);
  1530. }
  1531. } else if (popularAppsHasSelection()) {
  1532. if (_popularApps->selectJump(direction, pageSize)
  1533. == JumpResult::AppliedAndOut) {
  1534. _recentApps->selectJump(direction, -1);
  1535. }
  1536. }
  1537. }
  1538. } else if (direction == Qt::Key_Up) {
  1539. if (recentAppsHasSelection()) {
  1540. _recentApps->selectJump(direction, 0);
  1541. } else if (_popularApps->selectJump(direction, 0)
  1542. == JumpResult::AppliedAndOut) {
  1543. _recentApps->selectJump(direction, -1);
  1544. } else if (!popularAppsHasSelection()) {
  1545. if (_recentApps->selectJump(direction, 0)
  1546. == JumpResult::AppliedAndOut) {
  1547. _channelsScroll->scrollTo(0);
  1548. }
  1549. }
  1550. } else if (direction == Qt::Key_Down) {
  1551. if (popularAppsHasSelection()) {
  1552. _popularApps->selectJump(direction, 0);
  1553. } else if (_recentApps->selectJump(direction, 0)
  1554. == JumpResult::AppliedAndOut) {
  1555. _popularApps->selectJump(direction, 0);
  1556. } else if (!recentAppsHasSelection()) {
  1557. if (_popularApps->selectJump(direction, 0)
  1558. == JumpResult::AppliedAndOut) {
  1559. _recentApps->selectJump(direction, 0);
  1560. }
  1561. }
  1562. }
  1563. }
  1564. void Suggestions::chooseRow() {
  1565. switch (_key.current().tab) {
  1566. case Tab::Chats:
  1567. if (!_topPeers->chooseRow()) {
  1568. _recent->choose();
  1569. }
  1570. break;
  1571. case Tab::Channels:
  1572. if (!_myChannels->choose()) {
  1573. _recommendations->choose();
  1574. }
  1575. break;
  1576. case Tab::Apps:
  1577. if (!_recentApps->choose()) {
  1578. _popularApps->choose();
  1579. }
  1580. break;
  1581. }
  1582. }
  1583. bool Suggestions::consumeSearchQuery(const QString &query) {
  1584. using Type = MediaType;
  1585. const auto key = _key.current();
  1586. const auto tab = key.tab;
  1587. const auto type = (key.tab == Tab::Media) ? key.mediaType : Type::kCount;
  1588. if (tab != Tab::Downloads
  1589. && type != Type::File
  1590. && type != Type::Link
  1591. && type != Type::MusicFile) {
  1592. return false;
  1593. } else if (_searchQuery == query) {
  1594. return false;
  1595. }
  1596. _searchQuery = query;
  1597. _persist = !_searchQuery.isEmpty();
  1598. if (query.isEmpty() || tab == Tab::Downloads) {
  1599. _searchQueryTimer.cancel();
  1600. applySearchQuery();
  1601. } else {
  1602. _searchQueryTimer.callOnce(kSearchQueryDelay);
  1603. }
  1604. return true;
  1605. }
  1606. void Suggestions::applySearchQuery() {
  1607. const auto key = _key.current();
  1608. const auto controller = _mediaLists[key].wrap->controller();
  1609. const auto search = controller->searchFieldController();
  1610. if (search->query() != _searchQuery) {
  1611. search->setQuery(_searchQuery);
  1612. }
  1613. }
  1614. rpl::producer<> Suggestions::clearSearchQueryRequests() const {
  1615. return _clearSearchQueryRequests.events();
  1616. }
  1617. Data::Thread *Suggestions::updateFromParentDrag(QPoint globalPosition) {
  1618. switch (_key.current().tab) {
  1619. case Tab::Chats: return updateFromChatsDrag(globalPosition);
  1620. case Tab::Channels: return updateFromChannelsDrag(globalPosition);
  1621. }
  1622. return nullptr;
  1623. }
  1624. Data::Thread *Suggestions::updateFromChatsDrag(QPoint globalPosition) {
  1625. if (const auto top = _topPeers->updateFromParentDrag(globalPosition)) {
  1626. return _controller->session().data().history(PeerId(top));
  1627. }
  1628. return fromListId(_recent->updateFromParentDrag(globalPosition));
  1629. }
  1630. Data::Thread *Suggestions::updateFromChannelsDrag(QPoint globalPosition) {
  1631. if (const auto id = _myChannels->updateFromParentDrag(globalPosition)) {
  1632. return fromListId(id);
  1633. }
  1634. return fromListId(_recommendations->updateFromParentDrag(globalPosition));
  1635. }
  1636. Data::Thread *Suggestions::updateFromAppsDrag(QPoint globalPosition) {
  1637. if (const auto id = _recentApps->updateFromParentDrag(globalPosition)) {
  1638. return fromListId(id);
  1639. }
  1640. return fromListId(_popularApps->updateFromParentDrag(globalPosition));
  1641. }
  1642. Data::Thread *Suggestions::fromListId(uint64 peerListRowId) {
  1643. return peerListRowId
  1644. ? _controller->session().data().history(PeerId(peerListRowId)).get()
  1645. : nullptr;
  1646. }
  1647. void Suggestions::dragLeft() {
  1648. _topPeers->dragLeft();
  1649. _recent->dragLeft();
  1650. _myChannels->dragLeft();
  1651. _recommendations->dragLeft();
  1652. _recentApps->dragLeft();
  1653. _popularApps->dragLeft();
  1654. }
  1655. void Suggestions::show(anim::type animated, Fn<void()> finish) {
  1656. RpWidget::show();
  1657. _hidden = false;
  1658. if (animated == anim::type::instant) {
  1659. finishShow();
  1660. } else {
  1661. startShownAnimation(true, std::move(finish));
  1662. }
  1663. }
  1664. void Suggestions::hide(anim::type animated, Fn<void()> finish) {
  1665. _hidden = true;
  1666. if (isHidden()) {
  1667. return;
  1668. } else if (animated == anim::type::instant) {
  1669. RpWidget::hide();
  1670. } else {
  1671. startShownAnimation(false, std::move(finish));
  1672. }
  1673. }
  1674. void Suggestions::switchTab(Key key) {
  1675. const auto was = _key.current();
  1676. if (was == key) {
  1677. return;
  1678. }
  1679. consumeSearchQuery(QString());
  1680. _key = key;
  1681. _persist = false;
  1682. _clearSearchQueryRequests.fire({});
  1683. if (_tabs->isHidden()) {
  1684. return;
  1685. }
  1686. startSlideAnimation(was, key);
  1687. }
  1688. void Suggestions::ensureContent(Key key) {
  1689. if (key.tab != Tab::Downloads && key.tab != Tab::Media) {
  1690. return;
  1691. }
  1692. auto &list = _mediaLists[key];
  1693. if (list.wrap) {
  1694. return;
  1695. }
  1696. const auto self = _controller->session().user();
  1697. const auto memento = (key.tab == Tab::Downloads)
  1698. ? Info::Downloads::Make(self)
  1699. : std::make_shared<Info::Memento>(
  1700. self,
  1701. Info::Section(key.mediaType, Info::Section::Type::GlobalMedia));
  1702. list.wrap = Ui::CreateChild<Info::WrapWidget>(
  1703. this,
  1704. _controller,
  1705. Info::Wrap::Search,
  1706. memento.get());
  1707. list.wrap->show();
  1708. updateControlsGeometry();
  1709. }
  1710. void Suggestions::startSlideAnimation(Key was, Key now) {
  1711. ensureContent(now);
  1712. const auto wasIndex = ranges::find(_tabKeys, was);
  1713. const auto nowIndex = ranges::find(_tabKeys, now);
  1714. if (!_slideAnimation.animating()) {
  1715. const auto find = [&](Key key) -> not_null<QWidget*> {
  1716. switch (key.tab) {
  1717. case Tab::Chats: return _chatsScroll.get();
  1718. case Tab::Channels: return _channelsScroll.get();
  1719. case Tab::Apps: return _appsScroll.get();
  1720. }
  1721. return _mediaLists[key].wrap;
  1722. };
  1723. auto left = find(was);
  1724. auto right = find(now);
  1725. if (wasIndex > nowIndex) {
  1726. std::swap(left, right);
  1727. }
  1728. _slideLeft = Ui::GrabWidget(left);
  1729. _slideLeftTop = left->y();
  1730. _slideRight = Ui::GrabWidget(right);
  1731. _slideRightTop = right->y();
  1732. left->hide();
  1733. right->hide();
  1734. }
  1735. const auto from = (nowIndex > wasIndex) ? 0. : 1.;
  1736. const auto to = (nowIndex > wasIndex) ? 1. : 0.;
  1737. _slideAnimation.start([=] {
  1738. update();
  1739. if (!_slideAnimation.animating() && !_shownAnimation.animating()) {
  1740. finishShow();
  1741. }
  1742. }, from, to, st::slideDuration, anim::sineInOut);
  1743. }
  1744. void Suggestions::startShownAnimation(bool shown, Fn<void()> finish) {
  1745. const auto from = shown ? 0. : 1.;
  1746. const auto to = shown ? 1. : 0.;
  1747. _shownAnimation.start([=] {
  1748. update();
  1749. if (!_shownAnimation.animating() && finish) {
  1750. finish();
  1751. if (shown) {
  1752. finishShow();
  1753. }
  1754. }
  1755. }, from, to, st::slideDuration, anim::easeOutQuint);
  1756. if (_cache.isNull()) {
  1757. const auto now = width();
  1758. if (now < st::columnMinimalWidthLeft) {
  1759. resize(st::columnMinimalWidthLeft, height());
  1760. }
  1761. _cache = Ui::GrabWidget(this);
  1762. if (now < st::columnMinimalWidthLeft) {
  1763. resize(now, height());
  1764. }
  1765. }
  1766. _tabsScroll->hide();
  1767. _chatsScroll->hide();
  1768. _channelsScroll->hide();
  1769. _appsScroll->hide();
  1770. for (const auto &[key, list] : _mediaLists) {
  1771. list.wrap->hide();
  1772. }
  1773. _slideAnimation.stop();
  1774. }
  1775. void Suggestions::finishShow() {
  1776. _slideAnimation.stop();
  1777. _slideLeft = _slideRight = QPixmap();
  1778. _slideLeftTop = _slideRightTop = 0;
  1779. _shownAnimation.stop();
  1780. _cache = QPixmap();
  1781. _tabsScroll->show();
  1782. const auto key = _key.current();
  1783. _chatsScroll->setVisible(key == Key{ Tab::Chats });
  1784. _channelsScroll->setVisible(key == Key{ Tab::Channels });
  1785. _appsScroll->setVisible(key == Key{ Tab::Apps });
  1786. for (const auto &[mediaKey, list] : _mediaLists) {
  1787. list.wrap->setVisible(key == mediaKey);
  1788. }
  1789. }
  1790. float64 Suggestions::shownOpacity() const {
  1791. return _shownAnimation.value(_hidden ? 0. : 1.);
  1792. }
  1793. std::vector<Suggestions::Key> Suggestions::TabKeysFor(
  1794. not_null<Window::SessionController*> controller) {
  1795. auto result = std::vector<Key>{
  1796. { Tab::Chats },
  1797. { Tab::Channels },
  1798. { Tab::Apps },
  1799. { Tab::Media, MediaType::Photo },
  1800. { Tab::Media, MediaType::Video },
  1801. { Tab::Downloads },
  1802. { Tab::Media, MediaType::Link },
  1803. { Tab::Media, MediaType::File },
  1804. { Tab::Media, MediaType::MusicFile },
  1805. { Tab::Media, MediaType::RoundVoiceFile },
  1806. };
  1807. if (Core::App().downloadManager().empty()) {
  1808. result.erase(ranges::find(result, Key{ Tab::Downloads }));
  1809. }
  1810. return result;
  1811. }
  1812. void Suggestions::paintEvent(QPaintEvent *e) {
  1813. const auto opacity = shownOpacity();
  1814. auto color = st::windowBg->c;
  1815. color.setAlphaF(color.alphaF() * opacity);
  1816. auto p = QPainter(this);
  1817. p.fillRect(e->rect(), color);
  1818. if (!_cache.isNull()) {
  1819. const auto slide = st::topPeers.height + st::searchedBarHeight;
  1820. p.setOpacity(opacity);
  1821. p.drawPixmap(0, (opacity - 1.) * slide, _cache);
  1822. } else if (!_slideLeft.isNull()) {
  1823. const auto slide = st::topPeers.height + st::searchedBarHeight;
  1824. const auto right = (_key.current().tab == Tab::Channels);
  1825. const auto progress = _slideAnimation.value(right ? 1. : 0.);
  1826. p.setOpacity(1. - progress);
  1827. p.drawPixmap(
  1828. anim::interpolate(0, -slide, progress),
  1829. _slideLeftTop,
  1830. _slideLeft);
  1831. p.setOpacity(progress);
  1832. p.drawPixmap(
  1833. anim::interpolate(slide, 0, progress),
  1834. _slideRightTop,
  1835. _slideRight);
  1836. }
  1837. }
  1838. void Suggestions::resizeEvent(QResizeEvent *e) {
  1839. updateControlsGeometry();
  1840. }
  1841. void Suggestions::updateControlsGeometry() {
  1842. const auto w = std::max(width(), st::columnMinimalWidthLeft);
  1843. _tabs->fitWidthToSections();
  1844. const auto tabs = _tabs->height();
  1845. _tabsScroll->setGeometry(0, 0, w, tabs);
  1846. const auto content = QRect(0, tabs, w, height() - tabs);
  1847. _chatsScroll->setGeometry(content);
  1848. _chatsContent->resizeToWidth(w);
  1849. _channelsScroll->setGeometry(content);
  1850. _channelsContent->resizeToWidth(w);
  1851. _appsScroll->setGeometry(content);
  1852. _appsContent->resizeToWidth(w);
  1853. const auto expanding = false;
  1854. for (const auto &[key, list] : _mediaLists) {
  1855. const auto full = !list.wrap->scrollBottomSkip();
  1856. const auto additionalScroll = (full ? st::boxRadius : 0);
  1857. const auto height = content.height() - (full ? 0 : st::boxRadius);
  1858. const auto wrapGeometry = QRect{ 0, tabs, w, height};
  1859. list.wrap->updateGeometry(
  1860. wrapGeometry,
  1861. expanding,
  1862. additionalScroll,
  1863. content.height());
  1864. }
  1865. }
  1866. auto Suggestions::setupRecentPeers(RecentPeersList recentPeers)
  1867. -> std::unique_ptr<ObjectList> {
  1868. const auto controller = lifetime().make_state<RecentsController>(
  1869. _controller,
  1870. std::move(recentPeers),
  1871. [=](not_null<PeerData*> p) { _openBotMainAppRequests.fire_copy(p); });
  1872. const auto addToScroll = [=] {
  1873. return _topPeersWrap->toggled() ? _topPeers->height() : 0;
  1874. };
  1875. auto result = setupObjectList(
  1876. _chatsScroll.get(),
  1877. _chatsContent,
  1878. controller,
  1879. addToScroll);
  1880. const auto raw = result.get();
  1881. const auto list = raw->wrap->entity();
  1882. raw->selectJump = [list](Qt::Key direction, int pageSize) {
  1883. const auto had = list->hasSelection();
  1884. if (direction == Qt::Key()) {
  1885. return had ? JumpResult::Applied : JumpResult::NotApplied;
  1886. } else if (direction == Qt::Key_Up && !had) {
  1887. return JumpResult::NotApplied;
  1888. } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
  1889. const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
  1890. if (pageSize > 0) {
  1891. list->selectSkipPage(pageSize, delta);
  1892. } else {
  1893. list->selectSkip(delta);
  1894. }
  1895. return list->hasSelection()
  1896. ? JumpResult::Applied
  1897. : had
  1898. ? JumpResult::AppliedAndOut
  1899. : JumpResult::NotApplied;
  1900. }
  1901. return JumpResult::NotApplied;
  1902. };
  1903. raw->chosen.events(
  1904. ) | rpl::start_with_next([=](not_null<PeerData*> peer) {
  1905. _controller->session().recentPeers().bump(peer);
  1906. }, list->lifetime());
  1907. return result;
  1908. }
  1909. object_ptr<Ui::SlideWrap<>> Suggestions::setupEmptyRecent() {
  1910. const auto icon = SearchEmptyIcon::Search;
  1911. return setupEmpty(_chatsContent, icon, tr::lng_recent_none());
  1912. }
  1913. auto Suggestions::setupMyChannels() -> std::unique_ptr<ObjectList> {
  1914. const auto controller = lifetime().make_state<MyChannelsController>(
  1915. _controller);
  1916. auto result = setupObjectList(
  1917. _channelsScroll.get(),
  1918. _channelsContent,
  1919. controller);
  1920. const auto raw = result.get();
  1921. const auto list = raw->wrap->entity();
  1922. raw->selectJump = [=](Qt::Key direction, int pageSize) {
  1923. const auto had = list->hasSelection();
  1924. if (direction == Qt::Key()) {
  1925. return had ? JumpResult::Applied : JumpResult::NotApplied;
  1926. } else if (direction == Qt::Key_Up && !had) {
  1927. if (pageSize < 0) {
  1928. list->selectLast();
  1929. return list->hasSelection()
  1930. ? JumpResult::Applied
  1931. : JumpResult::NotApplied;
  1932. }
  1933. return JumpResult::NotApplied;
  1934. } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
  1935. const auto was = list->selectedIndex();
  1936. const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
  1937. if (pageSize > 0) {
  1938. list->selectSkipPage(pageSize, delta);
  1939. } else {
  1940. list->selectSkip(delta);
  1941. }
  1942. if (had
  1943. && delta > 0
  1944. && raw->count.current()
  1945. && list->selectedIndex() == was) {
  1946. list->clearSelection();
  1947. return JumpResult::AppliedAndOut;
  1948. }
  1949. return list->hasSelection()
  1950. ? JumpResult::Applied
  1951. : had
  1952. ? JumpResult::AppliedAndOut
  1953. : JumpResult::NotApplied;
  1954. }
  1955. return JumpResult::NotApplied;
  1956. };
  1957. raw->chosen.events(
  1958. ) | rpl::start_with_next([=] {
  1959. _persist = false;
  1960. }, list->lifetime());
  1961. return result;
  1962. }
  1963. auto Suggestions::setupRecommendations() -> std::unique_ptr<ObjectList> {
  1964. const auto controller = lifetime().make_state<RecommendationsController>(
  1965. _controller);
  1966. const auto addToScroll = [=] {
  1967. const auto wrap = _myChannels->wrap;
  1968. return wrap->toggled() ? wrap->height() : 0;
  1969. };
  1970. auto result = setupObjectList(
  1971. _channelsScroll.get(),
  1972. _channelsContent,
  1973. controller,
  1974. addToScroll);
  1975. const auto raw = result.get();
  1976. const auto list = raw->wrap->entity();
  1977. raw->selectJump = [list](Qt::Key direction, int pageSize) {
  1978. const auto had = list->hasSelection();
  1979. if (direction == Qt::Key()) {
  1980. return had ? JumpResult::Applied : JumpResult::NotApplied;
  1981. } else if (direction == Qt::Key_Up && !had) {
  1982. return JumpResult::NotApplied;
  1983. } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
  1984. const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
  1985. if (pageSize > 0) {
  1986. list->selectSkipPage(pageSize, delta);
  1987. } else {
  1988. list->selectSkip(delta);
  1989. }
  1990. return list->hasSelection()
  1991. ? JumpResult::Applied
  1992. : had
  1993. ? JumpResult::AppliedAndOut
  1994. : JumpResult::NotApplied;
  1995. }
  1996. return JumpResult::NotApplied;
  1997. };
  1998. raw->chosen.events(
  1999. ) | rpl::start_with_next([=] {
  2000. _persist = true;
  2001. }, list->lifetime());
  2002. _key.value() | rpl::filter(
  2003. rpl::mappers::_1 == Key{ Tab::Channels }
  2004. ) | rpl::start_with_next([=] {
  2005. controller->load();
  2006. }, list->lifetime());
  2007. return result;
  2008. }
  2009. auto Suggestions::setupRecentApps() -> std::unique_ptr<ObjectList> {
  2010. const auto controller = lifetime().make_state<RecentAppsController>(
  2011. _controller);
  2012. _recentAppsShows = [=](not_null<PeerData*> peer) {
  2013. return controller->shown(peer);
  2014. };
  2015. _recentAppsRefreshed = controller->refreshed();
  2016. auto result = setupObjectList(
  2017. _appsScroll.get(),
  2018. _appsContent,
  2019. controller);
  2020. const auto raw = result.get();
  2021. const auto list = raw->wrap->entity();
  2022. raw->selectJump = [=](Qt::Key direction, int pageSize) {
  2023. const auto had = list->hasSelection();
  2024. if (direction == Qt::Key()) {
  2025. return had ? JumpResult::Applied : JumpResult::NotApplied;
  2026. } else if (direction == Qt::Key_Up && !had) {
  2027. if (pageSize < 0) {
  2028. list->selectLast();
  2029. return list->hasSelection()
  2030. ? JumpResult::Applied
  2031. : JumpResult::NotApplied;
  2032. }
  2033. return JumpResult::NotApplied;
  2034. } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
  2035. const auto was = list->selectedIndex();
  2036. const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
  2037. if (pageSize > 0) {
  2038. list->selectSkipPage(pageSize, delta);
  2039. } else {
  2040. list->selectSkip(delta);
  2041. }
  2042. if (had
  2043. && delta > 0
  2044. && raw->count.current()
  2045. && list->selectedIndex() == was) {
  2046. list->clearSelection();
  2047. return JumpResult::AppliedAndOut;
  2048. }
  2049. return list->hasSelection()
  2050. ? JumpResult::Applied
  2051. : had
  2052. ? JumpResult::AppliedAndOut
  2053. : JumpResult::NotApplied;
  2054. }
  2055. return JumpResult::NotApplied;
  2056. };
  2057. raw->chosen.events(
  2058. ) | rpl::start_with_next([=] {
  2059. _persist = false;
  2060. }, list->lifetime());
  2061. controller->load();
  2062. return result;
  2063. }
  2064. auto Suggestions::setupPopularApps() -> std::unique_ptr<ObjectList> {
  2065. const auto controller = lifetime().make_state<PopularAppsController>(
  2066. _controller,
  2067. _recentAppsShows,
  2068. rpl::duplicate(_recentAppsRefreshed));
  2069. const auto addToScroll = [=] {
  2070. const auto wrap = _recentApps->wrap;
  2071. return wrap->toggled() ? wrap->height() : 0;
  2072. };
  2073. auto result = setupObjectList(
  2074. _appsScroll.get(),
  2075. _appsContent,
  2076. controller,
  2077. addToScroll);
  2078. const auto raw = result.get();
  2079. const auto list = raw->wrap->entity();
  2080. raw->selectJump = [list](Qt::Key direction, int pageSize) {
  2081. const auto had = list->hasSelection();
  2082. if (direction == Qt::Key()) {
  2083. return had ? JumpResult::Applied : JumpResult::NotApplied;
  2084. } else if (direction == Qt::Key_Up && !had) {
  2085. return JumpResult::NotApplied;
  2086. } else if (direction == Qt::Key_Down || direction == Qt::Key_Up) {
  2087. const auto delta = (direction == Qt::Key_Down) ? 1 : -1;
  2088. if (pageSize > 0) {
  2089. list->selectSkipPage(pageSize, delta);
  2090. } else {
  2091. list->selectSkip(delta);
  2092. }
  2093. return list->hasSelection()
  2094. ? JumpResult::Applied
  2095. : had
  2096. ? JumpResult::AppliedAndOut
  2097. : JumpResult::NotApplied;
  2098. }
  2099. return JumpResult::NotApplied;
  2100. };
  2101. raw->chosen.events(
  2102. ) | rpl::start_with_next([=] {
  2103. _persist = true;
  2104. }, list->lifetime());
  2105. _key.value() | rpl::filter(
  2106. rpl::mappers::_1 == Key{ Tab::Apps }
  2107. ) | rpl::start_with_next([=] {
  2108. controller->load();
  2109. }, list->lifetime());
  2110. return result;
  2111. }
  2112. auto Suggestions::setupObjectList(
  2113. not_null<Ui::ElasticScroll*> scroll,
  2114. not_null<Ui::VerticalLayout*> parent,
  2115. not_null<ObjectListController*> controller,
  2116. Fn<int()> addToScroll)
  2117. -> std::unique_ptr<ObjectList> {
  2118. auto &lifetime = parent->lifetime();
  2119. const auto delegate = lifetime.make_state<
  2120. PeerListContentDelegateSimple
  2121. >();
  2122. controller->setStyleOverrides(&st::recentPeersList);
  2123. auto content = object_ptr<PeerListContent>(parent, controller);
  2124. const auto list = content.data();
  2125. auto result = std::make_unique<ObjectList>(ObjectList{
  2126. .wrap = parent->add(object_ptr<Ui::SlideWrap<PeerListContent>>(
  2127. parent,
  2128. std::move(content))),
  2129. });
  2130. const auto raw = result.get();
  2131. raw->count = controller->count();
  2132. raw->processTouch = [=](not_null<QTouchEvent*> e) {
  2133. return controller->processTouchEvent(e);
  2134. };
  2135. controller->chosen(
  2136. ) | rpl::start_with_next([=](not_null<PeerData*> peer) {
  2137. raw->chosen.fire_copy(peer);
  2138. }, lifetime);
  2139. raw->choose = [=] {
  2140. return list->submitted();
  2141. };
  2142. raw->updateFromParentDrag = [=](QPoint globalPosition) {
  2143. return list->updateFromParentDrag(globalPosition);
  2144. };
  2145. raw->dragLeft = [=] {
  2146. list->dragLeft();
  2147. };
  2148. list->scrollToRequests(
  2149. ) | rpl::start_with_next([=](Ui::ScrollToRequest request) {
  2150. const auto add = addToScroll ? addToScroll() : 0;
  2151. scroll->scrollToY(request.ymin + add, request.ymax + add);
  2152. }, list->lifetime());
  2153. delegate->setContent(list);
  2154. controller->setDelegate(delegate);
  2155. controller->setupTouchChatPreview(scroll);
  2156. return result;
  2157. }
  2158. object_ptr<Ui::SlideWrap<>> Suggestions::setupEmptyChannels() {
  2159. const auto icon = SearchEmptyIcon::NoResults;
  2160. return setupEmpty(_channelsContent, icon, tr::lng_channels_none_about());
  2161. }
  2162. object_ptr<Ui::SlideWrap<>> Suggestions::setupEmpty(
  2163. not_null<QWidget*> parent,
  2164. SearchEmptyIcon icon,
  2165. rpl::producer<QString> text) {
  2166. auto content = object_ptr<SearchEmpty>(
  2167. parent,
  2168. icon,
  2169. std::move(text) | Ui::Text::ToWithEntities());
  2170. const auto raw = content.data();
  2171. rpl::combine(
  2172. _chatsScroll->heightValue(),
  2173. _topPeersWrap->heightValue()
  2174. ) | rpl::start_with_next([=](int height, int top) {
  2175. raw->setMinimalHeight(height - top);
  2176. }, raw->lifetime());
  2177. auto result = object_ptr<Ui::SlideWrap<>>(
  2178. parent,
  2179. std::move(content));
  2180. result->toggle(false, anim::type::instant);
  2181. result->toggledValue() | rpl::filter([=](bool shown) {
  2182. return shown && _controller->session().data().chatsListLoaded();
  2183. }) | rpl::start_with_next([=] {
  2184. raw->animate();
  2185. }, raw->lifetime());
  2186. return result;
  2187. }
  2188. bool Suggestions::persist() const {
  2189. return _persist;
  2190. }
  2191. void Suggestions::clearPersistance() {
  2192. _persist = false;
  2193. }
  2194. rpl::producer<TopPeersList> TopPeersContent(
  2195. not_null<Main::Session*> session) {
  2196. return [=](auto consumer) {
  2197. auto lifetime = rpl::lifetime();
  2198. struct Entry {
  2199. not_null<History*> history;
  2200. int index = 0;
  2201. };
  2202. struct State {
  2203. TopPeersList data;
  2204. base::flat_map<not_null<PeerData*>, Entry> indices;
  2205. base::has_weak_ptr guard;
  2206. bool scheduled = true;
  2207. };
  2208. auto state = lifetime.make_state<State>();
  2209. const auto top = session->topPeers().list();
  2210. auto &entries = state->data.entries;
  2211. auto &indices = state->indices;
  2212. entries.reserve(top.size());
  2213. indices.reserve(top.size());
  2214. const auto now = base::unixtime::now();
  2215. for (const auto &peer : top) {
  2216. const auto user = peer->asUser();
  2217. if (user->isInaccessible()) {
  2218. continue;
  2219. }
  2220. const auto self = user && user->isSelf();
  2221. const auto history = peer->owner().history(peer);
  2222. const auto badges = history->chatListBadgesState();
  2223. entries.push_back({
  2224. .id = peer->id.value,
  2225. .name = (self
  2226. ? tr::lng_saved_messages(tr::now)
  2227. : peer->shortName()),
  2228. .userpic = (self
  2229. ? Ui::MakeSavedMessagesThumbnail()
  2230. : Ui::MakeUserpicThumbnail(peer)),
  2231. .badge = uint32(badges.unreadCounter),
  2232. .unread = badges.unread,
  2233. .muted = !self && history->muted(),
  2234. .online = user && !self && Data::IsUserOnline(user, now),
  2235. });
  2236. if (entries.back().online) {
  2237. user->owner().watchForOffline(user, now);
  2238. }
  2239. indices.emplace(peer, Entry{
  2240. .history = peer->owner().history(peer),
  2241. .index = int(entries.size()) - 1,
  2242. });
  2243. }
  2244. const auto push = [=] {
  2245. if (!state->scheduled) {
  2246. return;
  2247. }
  2248. state->scheduled = false;
  2249. consumer.put_next_copy(state->data);
  2250. };
  2251. const auto schedule = [=] {
  2252. if (state->scheduled) {
  2253. return;
  2254. }
  2255. state->scheduled = true;
  2256. crl::on_main(&state->guard, push);
  2257. };
  2258. using Flag = Data::PeerUpdate::Flag;
  2259. session->changes().peerUpdates(
  2260. Flag::Name
  2261. | Flag::Photo
  2262. | Flag::Notifications
  2263. | Flag::OnlineStatus
  2264. ) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
  2265. const auto peer = update.peer;
  2266. if (peer->isSelf()) {
  2267. return;
  2268. }
  2269. const auto i = state->indices.find(peer);
  2270. if (i == end(state->indices)) {
  2271. return;
  2272. }
  2273. auto changed = false;
  2274. auto &entry = state->data.entries[i->second.index];
  2275. const auto flags = update.flags;
  2276. if (flags & Flag::Name) {
  2277. const auto now = peer->shortName();
  2278. if (entry.name != now) {
  2279. entry.name = now;
  2280. changed = true;
  2281. }
  2282. }
  2283. if (flags & Flag::Photo) {
  2284. entry.userpic = Ui::MakeUserpicThumbnail(peer);
  2285. changed = true;
  2286. }
  2287. if (flags & Flag::Notifications) {
  2288. const auto now = i->second.history->muted();
  2289. if (entry.muted != now) {
  2290. entry.muted = now;
  2291. changed = true;
  2292. }
  2293. }
  2294. if (flags & Flag::OnlineStatus) {
  2295. if (const auto user = peer->asUser()) {
  2296. const auto now = base::unixtime::now();
  2297. const auto value = Data::IsUserOnline(user, now);
  2298. if (entry.online != value) {
  2299. entry.online = value;
  2300. changed = true;
  2301. if (value) {
  2302. user->owner().watchForOffline(user, now);
  2303. }
  2304. }
  2305. }
  2306. }
  2307. if (changed) {
  2308. schedule();
  2309. }
  2310. }, lifetime);
  2311. session->data().unreadBadgeChanges(
  2312. ) | rpl::start_with_next([=] {
  2313. auto changed = false;
  2314. auto &entries = state->data.entries;
  2315. for (const auto &[peer, data] : state->indices) {
  2316. const auto badges = data.history->chatListBadgesState();
  2317. auto &entry = entries[data.index];
  2318. if (entry.badge != badges.unreadCounter
  2319. || entry.unread != badges.unread) {
  2320. entry.badge = badges.unreadCounter;
  2321. entry.unread = badges.unread;
  2322. changed = true;
  2323. }
  2324. }
  2325. if (changed) {
  2326. schedule();
  2327. }
  2328. }, lifetime);
  2329. push();
  2330. return lifetime;
  2331. };
  2332. }
  2333. RecentPeersList RecentPeersContent(not_null<Main::Session*> session) {
  2334. return RecentPeersList{ session->recentPeers().list() };
  2335. }
  2336. object_ptr<Ui::BoxContent> StarsExamplesBox(
  2337. not_null<Window::SessionController*> window) {
  2338. auto controller = std::make_unique<PopularAppsController>(
  2339. window,
  2340. nullptr,
  2341. nullptr);
  2342. const auto raw = controller.get();
  2343. auto initBox = [=](not_null<PeerListBox*> box) {
  2344. box->setTitle(tr::lng_credits_box_history_entry_gift_examples());
  2345. box->addButton(tr::lng_close(), [=] {
  2346. box->closeBox();
  2347. });
  2348. raw->load();
  2349. raw->chosen() | rpl::start_with_next([=](not_null<PeerData*> peer) {
  2350. if (const auto user = peer->asUser()) {
  2351. if (const auto info = user->botInfo.get()) {
  2352. if (info->hasMainApp) {
  2353. window->session().attachWebView().open({
  2354. .bot = user,
  2355. .context = {
  2356. .controller = window,
  2357. .maySkipConfirmation = true,
  2358. },
  2359. .source = InlineBots::WebViewSourceBotProfile(),
  2360. });
  2361. return;
  2362. }
  2363. }
  2364. }
  2365. window->showPeerInfo(peer);
  2366. }, box->lifetime());
  2367. };
  2368. return Box<PeerListBox>(std::move(controller), std::move(initBox));
  2369. }
  2370. object_ptr<Ui::BoxContent> PopularAppsAboutBox(
  2371. not_null<Window::SessionController*> window) {
  2372. return Ui::MakeInformBox({
  2373. .text = tr::lng_popular_apps_info_text(
  2374. lt_bot,
  2375. rpl::single(Ui::Text::Link(
  2376. u"@botfather"_q,
  2377. u"https://t.me/botfather"_q)),
  2378. lt_link,
  2379. tr::lng_popular_apps_info_here(
  2380. ) | Ui::Text::ToLink(tr::lng_popular_apps_info_url(tr::now)),
  2381. Ui::Text::RichLangValue),
  2382. .confirmText = tr::lng_popular_apps_info_confirm(),
  2383. .title = tr::lng_popular_apps_info_title(),
  2384. });
  2385. }
  2386. } // namespace Dialogs