info_profile_actions.cpp 81 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844
  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 "info/profile/info_profile_actions.h"
  8. #include "api/api_blocked_peers.h"
  9. #include "api/api_chat_participants.h"
  10. #include "api/api_credits.h"
  11. #include "api/api_statistics.h"
  12. #include "apiwrap.h"
  13. #include "base/options.h"
  14. #include "base/timer_rpl.h"
  15. #include "base/unixtime.h"
  16. #include "boxes/peers/add_bot_to_chat_box.h"
  17. #include "boxes/peers/edit_contact_box.h"
  18. #include "boxes/peers/edit_participants_box.h"
  19. #include "boxes/peers/edit_peer_info_box.h"
  20. #include "boxes/peers/verify_peers_box.h"
  21. #include "boxes/report_messages_box.h"
  22. #include "boxes/share_box.h"
  23. #include "boxes/star_gift_box.h"
  24. #include "boxes/translate_box.h"
  25. #include "core/application.h"
  26. #include "core/click_handler_types.h"
  27. #include "core/ui_integration.h"
  28. #include "data/business/data_business_common.h"
  29. #include "data/business/data_business_info.h"
  30. #include "data/components/credits.h"
  31. #include "data/data_changes.h"
  32. #include "data/data_channel.h"
  33. #include "data/data_chat.h"
  34. #include "data/data_folder.h"
  35. #include "data/data_forum_topic.h"
  36. #include "data/data_peer_values.h"
  37. #include "data/data_session.h"
  38. #include "data/data_user.h"
  39. #include "data/notify/data_notify_settings.h"
  40. #include "data/stickers/data_custom_emoji.h"
  41. #include "dialogs/ui/dialogs_layout.h"
  42. #include "dialogs/ui/dialogs_message_view.h"
  43. #include "history/history.h"
  44. #include "history/history_item.h"
  45. #include "history/history_item_components.h"
  46. #include "history/history_item_helpers.h"
  47. #include "history/view/history_view_item_preview.h"
  48. #include "info/bot/earn/info_bot_earn_widget.h"
  49. #include "info/bot/starref/info_bot_starref_common.h"
  50. #include "info/channel_statistics/earn/earn_format.h"
  51. #include "info/channel_statistics/earn/earn_icons.h"
  52. #include "info/channel_statistics/earn/info_channel_earn_list.h"
  53. #include "info/profile/info_profile_cover.h"
  54. #include "info/profile/info_profile_icon.h"
  55. #include "info/profile/info_profile_phone_menu.h"
  56. #include "info/profile/info_profile_text.h"
  57. #include "info/profile/info_profile_values.h"
  58. #include "info/profile/info_profile_widget.h"
  59. #include "info/info_controller.h"
  60. #include "info/info_memento.h"
  61. #include "inline_bots/bot_attach_web_view.h"
  62. #include "iv/iv_instance.h"
  63. #include "lang/lang_keys.h"
  64. #include "main/main_session.h"
  65. #include "menu/menu_mute.h"
  66. #include "settings/settings_common.h"
  67. #include "support/support_helper.h"
  68. #include "ui/boxes/peer_qr_box.h"
  69. #include "ui/boxes/report_box_graphics.h"
  70. #include "ui/controls/userpic_button.h"
  71. #include "ui/effects/toggle_arrow.h"
  72. #include "ui/painter.h"
  73. #include "ui/rect.h"
  74. #include "ui/ui_utility.h"
  75. #include "ui/text/format_values.h"
  76. #include "ui/text/text_utilities.h" // Ui::Text::ToUpper
  77. #include "ui/text/text_variant.h"
  78. #include "ui/toast/toast.h"
  79. #include "ui/vertical_list.h"
  80. #include "ui/widgets/buttons.h"
  81. #include "ui/widgets/checkbox.h"
  82. #include "ui/widgets/labels.h"
  83. #include "ui/widgets/popup_menu.h"
  84. #include "ui/widgets/shadow.h"
  85. #include "ui/wrap/padding_wrap.h"
  86. #include "ui/wrap/slide_wrap.h"
  87. #include "ui/wrap/vertical_layout.h"
  88. #include "window/window_controller.h" // Window::Controller::show.
  89. #include "window/window_peer_menu.h"
  90. #include "window/window_session_controller.h"
  91. #include "styles/style_channel_earn.h" // st::channelEarnCurrencyCommonMargins
  92. #include "styles/style_info.h"
  93. #include "styles/style_layers.h"
  94. #include "styles/style_menu_icons.h"
  95. #include "styles/style_settings.h" // settingsButtonRightSkip.
  96. #include "styles/style_window.h" // mainMenuToggleFourStrokes.
  97. #include <QtGui/QGuiApplication>
  98. #include <QtGui/QClipboard>
  99. namespace Info {
  100. namespace Profile {
  101. namespace {
  102. constexpr auto kDay = Data::WorkingInterval::kDay;
  103. base::options::toggle ShowPeerIdBelowAbout({
  104. .id = kOptionShowPeerIdBelowAbout,
  105. .name = "Show Peer IDs in Profile",
  106. .description = "Show peer IDs from API below their Bio / Description."
  107. " Add contact IDs to exported data.",
  108. });
  109. [[nodiscard]] rpl::producer<TextWithEntities> UsernamesSubtext(
  110. not_null<PeerData*> peer,
  111. rpl::producer<QString> fallback) {
  112. return rpl::combine(
  113. UsernamesValue(peer),
  114. std::move(fallback)
  115. ) | rpl::map([](std::vector<TextWithEntities> usernames, QString text) {
  116. if (usernames.size() < 2) {
  117. return TextWithEntities{ .text = text };
  118. } else {
  119. auto result = TextWithEntities();
  120. result.append(tr::lng_info_usernames_label(tr::now));
  121. result.append(' ');
  122. auto &&subrange = ranges::make_subrange(
  123. begin(usernames) + 1,
  124. end(usernames));
  125. for (auto &username : std::move(subrange)) {
  126. const auto isLast = (usernames.back() == username);
  127. result.append(Ui::Text::Link(
  128. '@' + base::take(username.text),
  129. username.entities.front().data()));
  130. if (!isLast) {
  131. result.append(u", "_q);
  132. }
  133. }
  134. return result;
  135. }
  136. });
  137. }
  138. [[nodiscard]] Fn<void(QString)> UsernamesLinkCallback(
  139. not_null<PeerData*> peer,
  140. not_null<Window::SessionController*> controller,
  141. const QString &addToLink) {
  142. const auto weak = base::make_weak(controller);
  143. return [=](QString link) {
  144. if (link.startsWith(u"internal:"_q)) {
  145. Core::App().openInternalUrl(link,
  146. QVariant::fromValue(ClickHandlerContext{
  147. .sessionWindow = weak,
  148. }));
  149. return;
  150. } else if (!link.startsWith(u"https://"_q)) {
  151. link = peer->session().createInternalLinkFull(peer->username())
  152. + addToLink;
  153. }
  154. if (!link.isEmpty()) {
  155. TextUtilities::SetClipboardText({ link });
  156. if (const auto strong = weak.get()) {
  157. strong->showToast(
  158. tr::lng_channel_public_link_copied(tr::now));
  159. }
  160. }
  161. };
  162. }
  163. [[nodiscard]] object_ptr<Ui::RpWidget> CreateSkipWidget(
  164. not_null<Ui::RpWidget*> parent) {
  165. return Ui::CreateSkipWidget(parent, st::infoProfileSkip);
  166. }
  167. [[nodiscard]] object_ptr<Ui::SlideWrap<>> CreateSlideSkipWidget(
  168. not_null<Ui::RpWidget*> parent) {
  169. auto result = Ui::CreateSlideSkipWidget(
  170. parent,
  171. st::infoProfileSkip);
  172. result->setDuration(st::infoSlideDuration);
  173. return result;
  174. }
  175. [[nodiscard]] rpl::producer<TextWithEntities> AboutWithIdValue(
  176. not_null<PeerData*> peer) {
  177. return AboutValue(
  178. peer
  179. ) | rpl::map([=](TextWithEntities &&value) {
  180. if (!ShowPeerIdBelowAbout.value()) {
  181. return std::move(value);
  182. }
  183. using namespace Ui::Text;
  184. if (!value.empty()) {
  185. value.append("\n\n");
  186. }
  187. value.append(Italic(u"id: "_q));
  188. const auto raw = peer->id.value & PeerId::kChatTypeMask;
  189. value.append(Link(
  190. Italic(Lang::FormatCountDecimal(raw)),
  191. "internal:~peer_id~:copy:" + QString::number(raw)));
  192. return std::move(value);
  193. });
  194. }
  195. [[nodiscard]] bool AreNonTrivialHours(const Data::WorkingHours &hours) {
  196. if (!hours) {
  197. return false;
  198. }
  199. const auto &intervals = hours.intervals.list;
  200. for (auto i = 0; i != 7; ++i) {
  201. const auto day = Data::WorkingInterval{ i * kDay, (i + 1) * kDay };
  202. for (const auto &interval : intervals) {
  203. const auto intersection = interval.intersected(day);
  204. if (intersection && intersection != day) {
  205. return true;
  206. }
  207. }
  208. }
  209. return false;
  210. }
  211. [[nodiscard]] TimeId OpensIn(
  212. const Data::WorkingIntervals &intervals,
  213. TimeId now) {
  214. using namespace Data;
  215. while (now < 0) {
  216. now += WorkingInterval::kWeek;
  217. }
  218. while (now > WorkingInterval::kWeek) {
  219. now -= WorkingInterval::kWeek;
  220. }
  221. auto closest = WorkingInterval::kWeek;
  222. for (const auto &interval : intervals.list) {
  223. if (interval.start <= now && interval.end > now) {
  224. return TimeId(0);
  225. } else if (interval.start > now && interval.start - now < closest) {
  226. closest = interval.start - now;
  227. } else if (interval.start < now) {
  228. const auto next = interval.start + WorkingInterval::kWeek - now;
  229. if (next < closest) {
  230. closest = next;
  231. }
  232. }
  233. }
  234. return closest;
  235. }
  236. [[nodiscard]] rpl::producer<QString> OpensInText(
  237. rpl::producer<TimeId> in,
  238. rpl::producer<bool> hoursExpanded,
  239. rpl::producer<QString> fallback) {
  240. return rpl::combine(
  241. std::move(in),
  242. std::move(hoursExpanded),
  243. std::move(fallback)
  244. ) | rpl::map([](TimeId in, bool hoursExpanded, QString fallback) {
  245. return (!in || hoursExpanded)
  246. ? std::move(fallback)
  247. : (in >= 86400)
  248. ? tr::lng_info_hours_opens_in_days(tr::now, lt_count, in / 86400)
  249. : (in >= 3600)
  250. ? tr::lng_info_hours_opens_in_hours(tr::now, lt_count, in / 3600)
  251. : tr::lng_info_hours_opens_in_minutes(
  252. tr::now,
  253. lt_count,
  254. std::max(in / 60, 1));
  255. });
  256. }
  257. [[nodiscard]] QString FormatDayTime(TimeId time) {
  258. const auto wrap = [](TimeId value) {
  259. const auto hours = value / 3600;
  260. const auto minutes = (value % 3600) / 60;
  261. return QString::number(hours).rightJustified(2, u'0')
  262. + ':'
  263. + QString::number(minutes).rightJustified(2, u'0');
  264. };
  265. return (time > kDay)
  266. ? tr::lng_info_hours_next_day(tr::now, lt_time, wrap(time - kDay))
  267. : wrap(time == kDay ? 0 : time);
  268. }
  269. [[nodiscard]] QString JoinIntervals(const Data::WorkingIntervals &data) {
  270. auto result = QStringList();
  271. result.reserve(data.list.size());
  272. for (const auto &interval : data.list) {
  273. const auto start = FormatDayTime(interval.start);
  274. const auto end = FormatDayTime(interval.end);
  275. result.push_back(start + u" - "_q + end);
  276. }
  277. return result.join('\n');
  278. }
  279. [[nodiscard]] QString FormatDayHours(
  280. const Data::WorkingHours &hours,
  281. const Data::WorkingIntervals &mine,
  282. bool my,
  283. int day) {
  284. using namespace Data;
  285. const auto local = ExtractDayIntervals(hours.intervals, day);
  286. if (IsFullOpen(local)) {
  287. return tr::lng_info_hours_open_full(tr::now);
  288. }
  289. const auto use = my ? ExtractDayIntervals(mine, day) : local;
  290. if (!use) {
  291. return tr::lng_info_hours_closed(tr::now);
  292. }
  293. return JoinIntervals(use);
  294. }
  295. [[nodiscard]] Data::WorkingIntervals ShiftedIntervals(
  296. Data::WorkingIntervals intervals,
  297. int delta) {
  298. auto &list = intervals.list;
  299. if (!delta || list.empty()) {
  300. return { std::move(list) };
  301. }
  302. for (auto &interval : list) {
  303. interval.start += delta;
  304. interval.end += delta;
  305. }
  306. while (list.front().start < 0) {
  307. constexpr auto kWeek = Data::WorkingInterval::kWeek;
  308. const auto first = list.front();
  309. if (first.end > 0) {
  310. list.push_back({ first.start + kWeek, kWeek });
  311. list.front().start = 0;
  312. } else {
  313. list.push_back(first.shifted(kWeek));
  314. list.erase(list.begin());
  315. }
  316. }
  317. return intervals.normalized();
  318. }
  319. [[nodiscard]] object_ptr<Ui::SlideWrap<>> CreateWorkingHours(
  320. not_null<QWidget*> parent,
  321. not_null<UserData*> user) {
  322. using namespace Data;
  323. auto result = object_ptr<Ui::SlideWrap<Ui::RoundButton>>(
  324. parent,
  325. object_ptr<Ui::RoundButton>(
  326. parent,
  327. rpl::single(QString()),
  328. st::infoHoursOuter),
  329. st::infoProfileLabeledPadding - st::infoHoursOuterMargin);
  330. const auto button = result->entity();
  331. const auto inner = Ui::CreateChild<Ui::VerticalLayout>(button);
  332. button->widthValue() | rpl::start_with_next([=](int width) {
  333. const auto margin = st::infoHoursOuterMargin;
  334. inner->resizeToWidth(width - margin.left() - margin.right());
  335. inner->move(margin.left(), margin.top());
  336. }, inner->lifetime());
  337. inner->heightValue() | rpl::start_with_next([=](int height) {
  338. const auto margin = st::infoHoursOuterMargin;
  339. height += margin.top() + margin.bottom();
  340. button->resize(button->width(), height);
  341. }, inner->lifetime());
  342. const auto info = &user->owner().businessInfo();
  343. struct State {
  344. rpl::variable<WorkingHours> hours;
  345. rpl::variable<TimeId> time;
  346. rpl::variable<int> day;
  347. rpl::variable<int> timezoneDelta;
  348. rpl::variable<WorkingIntervals> mine;
  349. rpl::variable<WorkingIntervals> mineByDays;
  350. rpl::variable<TimeId> opensIn;
  351. rpl::variable<bool> opened;
  352. rpl::variable<bool> expanded;
  353. rpl::variable<bool> nonTrivial;
  354. rpl::variable<bool> myTimezone;
  355. rpl::event_stream<> recounts;
  356. };
  357. const auto state = inner->lifetime().make_state<State>();
  358. auto recounts = state->recounts.events_starting_with_copy(rpl::empty);
  359. const auto recount = [=] {
  360. state->recounts.fire({});
  361. };
  362. state->hours = user->session().changes().peerFlagsValue(
  363. user,
  364. PeerUpdate::Flag::BusinessDetails
  365. ) | rpl::map([=] {
  366. return user->businessDetails().hours;
  367. });
  368. state->nonTrivial = state->hours.value() | rpl::map(AreNonTrivialHours);
  369. const auto seconds = QTime::currentTime().msecsSinceStartOfDay() / 1000;
  370. const auto inMinute = seconds % 60;
  371. const auto firstTick = inMinute ? (61 - inMinute) : 1;
  372. state->time = rpl::single(rpl::empty) | rpl::then(
  373. base::timer_once(firstTick * crl::time(1000))
  374. ) | rpl::then(
  375. base::timer_each(60 * crl::time(1000))
  376. ) | rpl::map([] {
  377. const auto local = QDateTime::currentDateTime();
  378. const auto day = local.date().dayOfWeek() - 1;
  379. const auto seconds = local.time().msecsSinceStartOfDay() / 1000;
  380. return day * kDay + seconds;
  381. });
  382. state->day = state->time.value() | rpl::map([](TimeId time) {
  383. return time / kDay;
  384. });
  385. state->timezoneDelta = rpl::combine(
  386. state->hours.value(),
  387. info->timezonesValue()
  388. ) | rpl::filter([](
  389. const WorkingHours &hours,
  390. const Timezones &timezones) {
  391. return ranges::contains(
  392. timezones.list,
  393. hours.timezoneId,
  394. &Timezone::id);
  395. }) | rpl::map([](WorkingHours &&hours, const Timezones &timezones) {
  396. const auto &list = timezones.list;
  397. const auto closest = FindClosestTimezoneId(list);
  398. const auto i = ranges::find(list, closest, &Timezone::id);
  399. const auto j = ranges::find(list, hours.timezoneId, &Timezone::id);
  400. Assert(i != end(list));
  401. Assert(j != end(list));
  402. return i->utcOffset - j->utcOffset;
  403. });
  404. state->mine = rpl::combine(
  405. state->hours.value(),
  406. state->timezoneDelta.value()
  407. ) | rpl::map([](WorkingHours &&hours, int delta) {
  408. return ShiftedIntervals(hours.intervals, delta);
  409. });
  410. state->opensIn = rpl::combine(
  411. state->mine.value(),
  412. state->time.value()
  413. ) | rpl::map([](const WorkingIntervals &mine, TimeId time) {
  414. return OpensIn(mine, time);
  415. });
  416. state->opened = state->opensIn.value() | rpl::map(rpl::mappers::_1 == 0);
  417. state->mineByDays = rpl::combine(
  418. state->hours.value(),
  419. state->timezoneDelta.value()
  420. ) | rpl::map([](WorkingHours &&hours, int delta) {
  421. auto full = std::array<bool, 7>();
  422. auto withoutFullDays = hours.intervals;
  423. for (auto i = 0; i != 7; ++i) {
  424. if (IsFullOpen(ExtractDayIntervals(hours.intervals, i))) {
  425. full[i] = true;
  426. withoutFullDays = ReplaceDayIntervals(
  427. withoutFullDays,
  428. i,
  429. Data::WorkingIntervals());
  430. }
  431. }
  432. auto result = ShiftedIntervals(withoutFullDays, delta);
  433. for (auto i = 0; i != 7; ++i) {
  434. if (full[i]) {
  435. result = ReplaceDayIntervals(
  436. result,
  437. i,
  438. Data::WorkingIntervals{ { { 0, kDay } } });
  439. }
  440. }
  441. return result;
  442. });
  443. const auto dayHoursText = [=](int day) {
  444. return rpl::combine(
  445. state->hours.value(),
  446. state->mineByDays.value(),
  447. state->myTimezone.value()
  448. ) | rpl::map([=](
  449. const WorkingHours &hours,
  450. const WorkingIntervals &mine,
  451. bool my) {
  452. return FormatDayHours(hours, mine, my, day);
  453. });
  454. };
  455. const auto dayHoursTextValue = [=](rpl::producer<int> day) {
  456. return std::move(day)
  457. | rpl::map(dayHoursText)
  458. | rpl::flatten_latest();
  459. };
  460. const auto openedWrap = inner->add(object_ptr<Ui::RpWidget>(inner));
  461. const auto opened = Ui::CreateChild<Ui::FlatLabel>(
  462. openedWrap,
  463. rpl::conditional(
  464. state->opened.value(),
  465. tr::lng_info_work_open(),
  466. tr::lng_info_work_closed()
  467. ) | rpl::after_next(recount),
  468. st::infoHoursState);
  469. opened->setAttribute(Qt::WA_TransparentForMouseEvents);
  470. const auto timing = Ui::CreateChild<Ui::FlatLabel>(
  471. openedWrap,
  472. OpensInText(
  473. state->opensIn.value(),
  474. state->expanded.value(),
  475. dayHoursTextValue(state->day.value())
  476. ) | rpl::after_next(recount),
  477. st::infoHoursValue);
  478. const auto timingArrow = Ui::CreateChild<Ui::RpWidget>(openedWrap);
  479. timingArrow->resize(Size(timing->st().style.font->height));
  480. timing->setAttribute(Qt::WA_TransparentForMouseEvents);
  481. state->opened.value() | rpl::start_with_next([=](bool value) {
  482. opened->setTextColorOverride(value
  483. ? st::boxTextFgGood->c
  484. : st::boxTextFgError->c);
  485. }, opened->lifetime());
  486. rpl::combine(
  487. openedWrap->widthValue(),
  488. opened->heightValue(),
  489. timing->sizeValue()
  490. ) | rpl::start_with_next([=](int width, int h1, QSize size) {
  491. opened->moveToLeft(0, 0, width);
  492. timingArrow->moveToRight(0, 0, width);
  493. timing->moveToRight(timingArrow->width(), 0, width);
  494. const auto margins = opened->getMargins();
  495. const auto added = margins.top() + margins.bottom();
  496. openedWrap->resize(width, std::max(h1, size.height()) - added);
  497. }, openedWrap->lifetime());
  498. const auto labelWrap = inner->add(object_ptr<Ui::RpWidget>(inner));
  499. const auto label = Ui::CreateChild<Ui::FlatLabel>(
  500. labelWrap,
  501. tr::lng_info_hours_label(),
  502. st::infoLabel);
  503. label->setAttribute(Qt::WA_TransparentForMouseEvents);
  504. auto linkText = rpl::combine(
  505. state->nonTrivial.value(),
  506. state->hours.value(),
  507. state->mine.value(),
  508. state->myTimezone.value()
  509. ) | rpl::map([=](
  510. bool complex,
  511. const WorkingHours &hours,
  512. const WorkingIntervals &mine,
  513. bool my) {
  514. return (!complex || hours.intervals == mine)
  515. ? rpl::single(QString())
  516. : my
  517. ? tr::lng_info_hours_my_time()
  518. : tr::lng_info_hours_local_time();
  519. }) | rpl::flatten_latest();
  520. const auto link = Ui::CreateChild<Ui::RoundButton>(
  521. labelWrap,
  522. std::move(linkText),
  523. st::defaultTableSmallButton);
  524. link->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
  525. link->setClickedCallback([=] {
  526. state->myTimezone = !state->myTimezone.current();
  527. state->expanded = true;
  528. });
  529. rpl::combine(
  530. labelWrap->widthValue(),
  531. label->heightValue(),
  532. link->sizeValue()
  533. ) | rpl::start_with_next([=](int width, int h1, QSize size) {
  534. label->moveToLeft(0, 0, width);
  535. link->moveToRight(0, 0, width);
  536. const auto margins = label->getMargins();
  537. const auto added = margins.top() + margins.bottom();
  538. labelWrap->resize(width, std::max(h1, size.height()) - added);
  539. }, labelWrap->lifetime());
  540. const auto other = inner->add(
  541. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  542. inner,
  543. object_ptr<Ui::VerticalLayout>(inner)));
  544. other->toggleOn(state->expanded.value(), anim::type::normal);
  545. constexpr auto kSlideDuration = float64(st::slideWrapDuration);
  546. other->setDuration(kSlideDuration);
  547. {
  548. const auto arrowAnimation
  549. = other->lifetime().make_state<Ui::Animations::Basic>();
  550. arrowAnimation->init([=] {
  551. timingArrow->update();
  552. if (!other->animating()) {
  553. arrowAnimation->stop();
  554. }
  555. });
  556. timingArrow->paintRequest() | rpl::start_with_next([=] {
  557. auto p = QPainter(timingArrow);
  558. const auto progress = other->animating()
  559. ? (crl::now() - arrowAnimation->started()) / kSlideDuration
  560. : 1.;
  561. const auto path = Ui::ToggleUpDownArrowPath(
  562. timingArrow->width() / 2,
  563. timingArrow->height() / 2,
  564. st::infoHoursArrowSize,
  565. st::mainMenuToggleFourStrokes,
  566. other->toggled() ? progress : 1 - progress);
  567. auto hq = PainterHighQualityEnabler(p);
  568. p.fillPath(path, timing->st().textFg);
  569. }, timingArrow->lifetime());
  570. state->expanded.value() | rpl::start_with_next([=] {
  571. arrowAnimation->start();
  572. }, other->lifetime());
  573. }
  574. other->finishAnimating();
  575. const auto days = other->entity();
  576. for (auto i = 1; i != 7; ++i) {
  577. const auto dayWrap = days->add(
  578. object_ptr<Ui::RpWidget>(other),
  579. QMargins(0, st::infoHoursDaySkip, 0, 0));
  580. auto label = state->day.value() | rpl::map([=](int day) {
  581. switch ((day + i) % 7) {
  582. case 0: return tr::lng_hours_monday();
  583. case 1: return tr::lng_hours_tuesday();
  584. case 2: return tr::lng_hours_wednesday();
  585. case 3: return tr::lng_hours_thursday();
  586. case 4: return tr::lng_hours_friday();
  587. case 5: return tr::lng_hours_saturday();
  588. case 6: return tr::lng_hours_sunday();
  589. }
  590. Unexpected("Index in working hours.");
  591. }) | rpl::flatten_latest();
  592. const auto dayLabel = Ui::CreateChild<Ui::FlatLabel>(
  593. dayWrap,
  594. std::move(label),
  595. st::infoHoursDayLabel);
  596. dayLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
  597. const auto dayHours = Ui::CreateChild<Ui::FlatLabel>(
  598. dayWrap,
  599. dayHoursTextValue(state->day.value()
  600. | rpl::map((rpl::mappers::_1 + i) % 7)),
  601. st::infoHoursValue);
  602. dayHours->setAttribute(Qt::WA_TransparentForMouseEvents);
  603. rpl::combine(
  604. dayWrap->widthValue(),
  605. dayLabel->heightValue(),
  606. dayHours->sizeValue()
  607. ) | rpl::start_with_next([=](int width, int h1, QSize size) {
  608. dayLabel->moveToLeft(0, 0, width);
  609. dayHours->moveToRight(0, 0, width);
  610. const auto margins = dayLabel->getMargins();
  611. const auto added = margins.top() + margins.bottom();
  612. dayWrap->resize(width, std::max(h1, size.height()) - added);
  613. }, dayWrap->lifetime());
  614. }
  615. button->setClickedCallback([=] {
  616. state->expanded = !state->expanded.current();
  617. });
  618. result->toggleOn(state->hours.value(
  619. ) | rpl::map([](const WorkingHours &data) {
  620. return bool(data);
  621. }));
  622. return result;
  623. }
  624. [[nodiscard]] object_ptr<Ui::SlideWrap<>> CreateBirthday(
  625. not_null<QWidget*> parent,
  626. not_null<Window::SessionController*> controller,
  627. not_null<UserData*> user) {
  628. using namespace Data;
  629. auto result = object_ptr<Ui::SlideWrap<Ui::RoundButton>>(
  630. parent,
  631. object_ptr<Ui::RoundButton>(
  632. parent,
  633. rpl::single(QString()),
  634. st::infoHoursOuter),
  635. st::infoProfileLabeledPadding - st::infoHoursOuterMargin);
  636. result->setDuration(st::infoSlideDuration);
  637. const auto button = result->entity();
  638. auto outer = Ui::CreateChild<Ui::SlideWrap<Ui::VerticalLayout>>(
  639. button,
  640. object_ptr<Ui::VerticalLayout>(button),
  641. st::infoHoursOuterMargin);
  642. const auto layout = outer->entity();
  643. layout->setAttribute(Qt::WA_TransparentForMouseEvents);
  644. auto birthday = BirthdayValue(
  645. user
  646. ) | rpl::start_spawning(result->lifetime());
  647. auto label = BirthdayLabelText(rpl::duplicate(birthday));
  648. auto text = BirthdayValueText(
  649. rpl::duplicate(birthday)
  650. ) | Ui::Text::ToWithEntities();
  651. const auto giftIcon = Ui::CreateChild<Ui::RpWidget>(layout);
  652. giftIcon->resize(st::birthdayTodayIcon.size());
  653. layout->sizeValue() | rpl::start_with_next([=](QSize size) {
  654. giftIcon->moveToRight(
  655. 0,
  656. (size.height() - giftIcon->height()) / 2,
  657. size.width());
  658. }, giftIcon->lifetime());
  659. giftIcon->paintRequest() | rpl::start_with_next([=] {
  660. auto p = QPainter(giftIcon);
  661. st::birthdayTodayIcon.paint(p, 0, 0, giftIcon->width());
  662. }, giftIcon->lifetime());
  663. rpl::duplicate(
  664. birthday
  665. ) | rpl::map([](Data::Birthday value) {
  666. return Data::IsBirthdayTodayValue(value);
  667. }) | rpl::flatten_latest(
  668. ) | rpl::distinct_until_changed(
  669. ) | rpl::start_with_next([=](bool today) {
  670. const auto disable = !today && user->session().premiumCanBuy();
  671. button->setDisabled(disable);
  672. button->setAttribute(Qt::WA_TransparentForMouseEvents, disable);
  673. button->clearState();
  674. giftIcon->setVisible(!disable);
  675. }, result->lifetime());
  676. auto nonEmptyText = std::move(
  677. text
  678. ) | rpl::before_next([slide = result.data()](
  679. const TextWithEntities &value) {
  680. if (value.text.isEmpty()) {
  681. slide->hide(anim::type::normal);
  682. }
  683. }) | rpl::filter([](const TextWithEntities &value) {
  684. return !value.text.isEmpty();
  685. }) | rpl::after_next([slide = result.data()](
  686. const TextWithEntities &value) {
  687. slide->show(anim::type::normal);
  688. });
  689. layout->add(object_ptr<Ui::FlatLabel>(
  690. layout,
  691. std::move(nonEmptyText),
  692. st::birthdayLabeled));
  693. layout->add(Ui::CreateSkipWidget(layout, st::infoLabelSkip));
  694. layout->add(object_ptr<Ui::FlatLabel>(
  695. layout,
  696. std::move(
  697. label
  698. ) | rpl::after_next([=] {
  699. layout->resizeToWidth(layout->widthNoMargins());
  700. }),
  701. st::birthdayLabel));
  702. result->finishAnimating();
  703. Ui::ResizeFitChild(button, outer);
  704. button->setClickedCallback([=] {
  705. if (!button->isDisabled()) {
  706. Ui::ShowStarGiftBox(controller, user);
  707. }
  708. });
  709. return result;
  710. }
  711. template <typename Text, typename ToggleOn, typename Callback>
  712. auto AddActionButton(
  713. not_null<Ui::VerticalLayout*> parent,
  714. Text &&text,
  715. ToggleOn &&toggleOn,
  716. Callback &&callback,
  717. const style::icon *icon,
  718. const style::SettingsButton &st = st::infoSharedMediaButton) {
  719. auto result = parent->add(object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
  720. parent,
  721. object_ptr<Ui::SettingsButton>(
  722. parent,
  723. std::move(text),
  724. st))
  725. );
  726. result->setDuration(
  727. st::infoSlideDuration
  728. )->toggleOn(
  729. std::move(toggleOn)
  730. )->entity()->addClickHandler(std::move(callback));
  731. result->finishAnimating();
  732. if (icon) {
  733. object_ptr<Profile::FloatingIcon>(
  734. result,
  735. *icon,
  736. st::infoSharedMediaButtonIconPosition);
  737. }
  738. return result;
  739. };
  740. template <typename Text, typename ToggleOn, typename Callback>
  741. [[nodiscard]] auto AddMainButton(
  742. not_null<Ui::VerticalLayout*> parent,
  743. Text &&text,
  744. ToggleOn &&toggleOn,
  745. Callback &&callback,
  746. Ui::MultiSlideTracker &tracker,
  747. const style::SettingsButton &st = st::infoMainButton) {
  748. tracker.track(AddActionButton(
  749. parent,
  750. std::move(text) | Ui::Text::ToUpper(),
  751. std::move(toggleOn),
  752. std::move(callback),
  753. nullptr,
  754. st));
  755. }
  756. rpl::producer<uint64> AddCurrencyAction(
  757. not_null<UserData*> user,
  758. not_null<Ui::VerticalLayout*> wrap,
  759. not_null<Controller*> controller) {
  760. struct State final {
  761. rpl::variable<uint64> balance;
  762. };
  763. const auto state = wrap->lifetime().make_state<State>();
  764. const auto parentController = controller->parentController();
  765. const auto wrapButton = AddActionButton(
  766. wrap,
  767. tr::lng_manage_peer_bot_balance_currency(),
  768. state->balance.value() | rpl::map(rpl::mappers::_1 > 0),
  769. [=] { parentController->showSection(Info::ChannelEarn::Make(user)); },
  770. nullptr);
  771. {
  772. const auto button = wrapButton->entity();
  773. const auto icon = Ui::CreateChild<Ui::RpWidget>(button);
  774. icon->resize(st::infoIconReport.size());
  775. const auto image = Ui::Earn::MenuIconCurrency(icon->size());
  776. icon->paintRequest() | rpl::start_with_next([=] {
  777. auto p = QPainter(icon);
  778. p.drawImage(0, 0, image);
  779. }, icon->lifetime());
  780. button->sizeValue(
  781. ) | rpl::start_with_next([=](const QSize &size) {
  782. icon->move(st::infoEarnCurrencyIconPosition);
  783. }, icon->lifetime());
  784. }
  785. const auto balance = user->session().credits().balanceCurrency(user->id);
  786. if (balance) {
  787. state->balance = balance;
  788. }
  789. {
  790. const auto weak = Ui::MakeWeak(wrap);
  791. const auto currencyLoadLifetime
  792. = std::make_shared<rpl::lifetime>();
  793. const auto currencyLoad
  794. = currencyLoadLifetime->make_state<Api::EarnStatistics>(user);
  795. currencyLoad->request(
  796. ) | rpl::start_with_error_done([=](const QString &error) {
  797. currencyLoadLifetime->destroy();
  798. }, [=] {
  799. if (const auto strong = weak.data()) {
  800. state->balance = currencyLoad->data().currentBalance;
  801. currencyLoadLifetime->destroy();
  802. }
  803. }, *currencyLoadLifetime);
  804. }
  805. const auto &st = st::infoSharedMediaButton;
  806. const auto button = wrapButton->entity();
  807. const auto name = Ui::CreateChild<Ui::FlatLabel>(button, st.rightLabel);
  808. const auto icon = Ui::Text::SingleCustomEmoji(
  809. user->owner().customEmojiManager().registerInternalEmoji(
  810. Ui::Earn::IconCurrencyColored(
  811. st.rightLabel.style.font,
  812. st.rightLabel.textFg->c),
  813. st::channelEarnCurrencyCommonMargins,
  814. false));
  815. name->show();
  816. rpl::combine(
  817. button->widthValue(),
  818. tr::lng_manage_peer_bot_balance_currency(),
  819. state->balance.value()
  820. ) | rpl::start_with_next([=, &st](
  821. int width,
  822. const QString &button,
  823. uint64 balance) {
  824. const auto available = width
  825. - rect::m::sum::h(st.padding)
  826. - st.style.font->width(button)
  827. - st::settingsButtonRightSkip;
  828. name->setMarkedText(
  829. base::duplicate(icon)
  830. .append(QChar(' '))
  831. .append(Info::ChannelEarn::MajorPart(balance))
  832. .append(Info::ChannelEarn::MinorPart(balance)),
  833. Core::TextContext({
  834. .session = &user->session(),
  835. .repaint = [=] { name->update(); },
  836. }));
  837. name->resizeToNaturalWidth(available);
  838. name->moveToRight(st::settingsButtonRightSkip, st.padding.top());
  839. }, name->lifetime());
  840. name->setAttribute(Qt::WA_TransparentForMouseEvents);
  841. wrapButton->finishAnimating();
  842. return state->balance.value();
  843. }
  844. rpl::producer<StarsAmount> AddCreditsAction(
  845. not_null<UserData*> user,
  846. not_null<Ui::VerticalLayout*> wrap,
  847. not_null<Controller*> controller) {
  848. struct State final {
  849. rpl::variable<StarsAmount> balance;
  850. };
  851. const auto state = wrap->lifetime().make_state<State>();
  852. const auto parentController = controller->parentController();
  853. const auto wrapButton = AddActionButton(
  854. wrap,
  855. tr::lng_manage_peer_bot_balance_credits(),
  856. state->balance.value() | rpl::map(rpl::mappers::_1 > StarsAmount(0)),
  857. [=] { parentController->showSection(Info::BotEarn::Make(user)); },
  858. nullptr);
  859. {
  860. const auto button = wrapButton->entity();
  861. const auto icon = Ui::CreateChild<Ui::RpWidget>(button);
  862. const auto image = Ui::Earn::MenuIconCredits();
  863. icon->resize(image.size() / style::DevicePixelRatio());
  864. icon->paintRequest() | rpl::start_with_next([=] {
  865. auto p = QPainter(icon);
  866. p.drawImage(0, 0, image);
  867. }, icon->lifetime());
  868. button->sizeValue(
  869. ) | rpl::start_with_next([=](const QSize &size) {
  870. icon->move(st::infoEarnCreditsIconPosition);
  871. }, icon->lifetime());
  872. }
  873. if (const auto balance = user->session().credits().balance(user->id)) {
  874. state->balance = balance;
  875. }
  876. {
  877. const auto api = wrap->lifetime().make_state<Api::CreditsStatus>(
  878. user);
  879. api->request({}, [=](Data::CreditsStatusSlice data) {
  880. state->balance = data.balance;
  881. });
  882. }
  883. const auto &st = st::infoSharedMediaButton;
  884. const auto button = wrapButton->entity();
  885. const auto name = Ui::CreateChild<Ui::FlatLabel>(button, st.rightLabel);
  886. const auto icon = user->owner().customEmojiManager().creditsEmoji();
  887. name->show();
  888. rpl::combine(
  889. button->widthValue(),
  890. tr::lng_manage_peer_bot_balance_credits(),
  891. state->balance.value()
  892. ) | rpl::start_with_next([=, &st](
  893. int width,
  894. const QString &button,
  895. StarsAmount balance) {
  896. const auto available = width
  897. - rect::m::sum::h(st.padding)
  898. - st.style.font->width(button)
  899. - st::settingsButtonRightSkip;
  900. name->setMarkedText(
  901. base::duplicate(icon)
  902. .append(QChar(' '))
  903. .append(Lang::FormatStarsAmountDecimal(balance)),
  904. Core::TextContext({
  905. .session = &user->session(),
  906. .repaint = [=] { name->update(); },
  907. }));
  908. name->resizeToNaturalWidth(available);
  909. name->moveToRight(st::settingsButtonRightSkip, st.padding.top());
  910. }, name->lifetime());
  911. name->setAttribute(Qt::WA_TransparentForMouseEvents);
  912. wrapButton->finishAnimating();
  913. return state->balance.value();
  914. }
  915. class DetailsFiller {
  916. public:
  917. DetailsFiller(
  918. not_null<Controller*> controller,
  919. not_null<Ui::RpWidget*> parent,
  920. not_null<PeerData*> peer,
  921. Origin origin);
  922. DetailsFiller(
  923. not_null<Controller*> controller,
  924. not_null<Ui::RpWidget*> parent,
  925. not_null<Data::ForumTopic*> topic);
  926. object_ptr<Ui::RpWidget> fill();
  927. private:
  928. object_ptr<Ui::RpWidget> setupPersonalChannel(not_null<UserData*> user);
  929. object_ptr<Ui::RpWidget> setupInfo();
  930. object_ptr<Ui::RpWidget> setupMuteToggle();
  931. void setupAboutVerification();
  932. void setupMainApp();
  933. void setupBotPermissions();
  934. void setupMainButtons();
  935. Ui::MultiSlideTracker fillTopicButtons();
  936. Ui::MultiSlideTracker fillUserButtons(
  937. not_null<UserData*> user);
  938. Ui::MultiSlideTracker fillChannelButtons(
  939. not_null<ChannelData*> channel);
  940. Ui::MultiSlideTracker fillDiscussionButtons(
  941. not_null<ChannelData*> channel);
  942. void addReportReaction(Ui::MultiSlideTracker &tracker);
  943. void addReportReaction(
  944. GroupReactionOrigin data,
  945. bool ban,
  946. Ui::MultiSlideTracker &tracker);
  947. template <
  948. typename Widget,
  949. typename = std::enable_if_t<
  950. std::is_base_of_v<Ui::RpWidget, Widget>>>
  951. Widget *add(
  952. object_ptr<Widget> &&child,
  953. const style::margins &margin = style::margins()) {
  954. return _wrap->add(
  955. std::move(child),
  956. margin);
  957. }
  958. not_null<Controller*> _controller;
  959. not_null<Ui::RpWidget*> _parent;
  960. not_null<PeerData*> _peer;
  961. Data::ForumTopic *_topic = nullptr;
  962. Origin _origin;
  963. object_ptr<Ui::VerticalLayout> _wrap;
  964. };
  965. class ActionsFiller {
  966. public:
  967. ActionsFiller(
  968. not_null<Controller*> controller,
  969. not_null<Ui::RpWidget*> parent,
  970. not_null<PeerData*> peer);
  971. object_ptr<Ui::RpWidget> fill();
  972. private:
  973. void addAffiliateProgram(not_null<UserData*> user);
  974. void addBalanceActions(not_null<UserData*> user);
  975. void addInviteToGroupAction(not_null<UserData*> user);
  976. void addShareContactAction(not_null<UserData*> user);
  977. void addEditContactAction(not_null<UserData*> user);
  978. void addDeleteContactAction(not_null<UserData*> user);
  979. void addBotCommandActions(not_null<UserData*> user);
  980. void addFastButtonsMode(not_null<UserData*> user);
  981. void addReportAction();
  982. void addBlockAction(not_null<UserData*> user);
  983. void addLeaveChannelAction(not_null<ChannelData*> channel);
  984. void addJoinChannelAction(not_null<ChannelData*> channel);
  985. void fillUserActions(not_null<UserData*> user);
  986. void fillChannelActions(not_null<ChannelData*> channel);
  987. not_null<Controller*> _controller;
  988. not_null<Ui::RpWidget*> _parent;
  989. not_null<PeerData*> _peer;
  990. object_ptr<Ui::VerticalLayout> _wrap = { nullptr };
  991. };
  992. void ReportReactionBox(
  993. not_null<Ui::GenericBox*> box,
  994. not_null<Window::SessionController*> controller,
  995. not_null<PeerData*> participant,
  996. GroupReactionOrigin data,
  997. bool ban,
  998. Fn<void()> sent) {
  999. box->setTitle(tr::lng_report_reaction_title());
  1000. box->addRow(object_ptr<Ui::FlatLabel>(
  1001. box,
  1002. tr::lng_report_reaction_about(),
  1003. st::boxLabel));
  1004. const auto check = ban
  1005. ? box->addRow(
  1006. object_ptr<Ui::Checkbox>(
  1007. box,
  1008. tr::lng_report_and_ban_button(tr::now),
  1009. true),
  1010. st::boxRowPadding + QMargins{ 0, st::boxLittleSkip, 0, 0 })
  1011. : nullptr;
  1012. box->addButton(tr::lng_report_button(), [=] {
  1013. const auto chat = data.group->asChat();
  1014. const auto channel = data.group->asMegagroup();
  1015. if (check && check->checked()) {
  1016. if (chat) {
  1017. chat->session().api().chatParticipants().kick(
  1018. chat,
  1019. participant);
  1020. } else if (channel) {
  1021. channel->session().api().chatParticipants().kick(
  1022. channel,
  1023. participant,
  1024. ChatRestrictionsInfo());
  1025. }
  1026. }
  1027. data.group->session().api().request(MTPmessages_ReportReaction(
  1028. data.group->input,
  1029. MTP_int(data.messageId.bare),
  1030. participant->input
  1031. )).done(crl::guard(controller, [=] {
  1032. controller->showToast(tr::lng_report_thanks(tr::now));
  1033. })).send();
  1034. sent();
  1035. box->closeBox();
  1036. }, st::attentionBoxButton);
  1037. box->addButton(tr::lng_cancel(), [=] {
  1038. box->closeBox();
  1039. });
  1040. }
  1041. DetailsFiller::DetailsFiller(
  1042. not_null<Controller*> controller,
  1043. not_null<Ui::RpWidget*> parent,
  1044. not_null<PeerData*> peer,
  1045. Origin origin)
  1046. : _controller(controller)
  1047. , _parent(parent)
  1048. , _peer(peer)
  1049. , _origin(origin)
  1050. , _wrap(_parent) {
  1051. }
  1052. DetailsFiller::DetailsFiller(
  1053. not_null<Controller*> controller,
  1054. not_null<Ui::RpWidget*> parent,
  1055. not_null<Data::ForumTopic*> topic)
  1056. : _controller(controller)
  1057. , _parent(parent)
  1058. , _peer(topic->peer())
  1059. , _topic(topic)
  1060. , _wrap(_parent) {
  1061. }
  1062. template <typename T>
  1063. bool SetClickContext(
  1064. const ClickHandlerPtr &handler,
  1065. const ClickContext &context) {
  1066. if (const auto casted = std::dynamic_pointer_cast<T>(handler)) {
  1067. casted->T::onClick(context);
  1068. return true;
  1069. }
  1070. return false;
  1071. }
  1072. object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
  1073. auto result = object_ptr<Ui::VerticalLayout>(_wrap);
  1074. auto tracker = Ui::MultiSlideTracker();
  1075. // Fill context for a mention / hashtag / bot command link.
  1076. const auto infoClickFilter = [=,
  1077. peer = _peer.get(),
  1078. window = _controller->parentController()](
  1079. const ClickHandlerPtr &handler,
  1080. Qt::MouseButton button) {
  1081. const auto context = ClickContext{
  1082. button,
  1083. QVariant::fromValue(ClickHandlerContext{
  1084. .sessionWindow = base::make_weak(window),
  1085. .peer = peer,
  1086. })
  1087. };
  1088. if (SetClickContext<BotCommandClickHandler>(handler, context)) {
  1089. return false;
  1090. } else if (SetClickContext<MentionClickHandler>(handler, context)) {
  1091. return false;
  1092. } else if (SetClickContext<HashtagClickHandler>(handler, context)) {
  1093. return false;
  1094. } else if (SetClickContext<CashtagClickHandler>(handler, context)) {
  1095. return false;
  1096. } else if (SetClickContext<UrlClickHandler>(handler, context)) {
  1097. return false;
  1098. }
  1099. return true;
  1100. };
  1101. const auto addTranslateToMenu = [&,
  1102. peer = _peer.get(),
  1103. controller = _controller->parentController()](
  1104. not_null<Ui::FlatLabel*> label,
  1105. rpl::producer<TextWithEntities> &&text) {
  1106. struct State {
  1107. rpl::variable<TextWithEntities> labelText;
  1108. };
  1109. const auto state = label->lifetime().make_state<State>();
  1110. state->labelText = std::move(text);
  1111. label->setContextMenuHook([=](
  1112. Ui::FlatLabel::ContextMenuRequest request) {
  1113. if (request.link) {
  1114. const auto &url = request.link->url();
  1115. if (url.startsWith(u"internal:~peer_id~:"_q)) {
  1116. const auto weak = base::make_weak(controller);
  1117. request.menu->addAction(u"Copy ID"_q, [=] {
  1118. Core::App().openInternalUrl(
  1119. url,
  1120. QVariant::fromValue(ClickHandlerContext{
  1121. .sessionWindow = weak,
  1122. }));
  1123. });
  1124. return;
  1125. }
  1126. }
  1127. label->fillContextMenu(request);
  1128. if (Ui::SkipTranslate(state->labelText.current())) {
  1129. return;
  1130. }
  1131. auto item = (request.selection.empty()
  1132. ? tr::lng_context_translate
  1133. : tr::lng_context_translate_selected)(tr::now);
  1134. request.menu->addAction(std::move(item), [=] {
  1135. controller->window().show(Box(
  1136. Ui::TranslateBox,
  1137. peer,
  1138. MsgId(),
  1139. request.selection.empty()
  1140. ? state->labelText.current()
  1141. : Ui::Text::Mid(
  1142. state->labelText.current(),
  1143. request.selection.from,
  1144. request.selection.to - request.selection.from),
  1145. false));
  1146. });
  1147. });
  1148. };
  1149. const auto addInfoLineGeneric = [&](
  1150. v::text::data &&label,
  1151. rpl::producer<TextWithEntities> &&text,
  1152. const style::FlatLabel &textSt = st::infoLabeled,
  1153. const style::margins &padding = st::infoProfileLabeledPadding) {
  1154. auto line = CreateTextWithLabel(
  1155. result,
  1156. v::text::take_marked(std::move(label)),
  1157. std::move(text),
  1158. st::infoLabel,
  1159. textSt,
  1160. padding);
  1161. tracker.track(result->add(std::move(line.wrap)));
  1162. line.text->setClickHandlerFilter(infoClickFilter);
  1163. return line;
  1164. };
  1165. const auto addInfoLine = [&](
  1166. v::text::data &&label,
  1167. rpl::producer<TextWithEntities> &&text,
  1168. const style::FlatLabel &textSt = st::infoLabeled,
  1169. const style::margins &padding = st::infoProfileLabeledPadding) {
  1170. return addInfoLineGeneric(
  1171. std::move(label),
  1172. std::move(text),
  1173. textSt,
  1174. padding);
  1175. };
  1176. const auto addInfoOneLine = [&](
  1177. v::text::data &&label,
  1178. rpl::producer<TextWithEntities> &&text,
  1179. const QString &contextCopyText,
  1180. const style::margins &padding = st::infoProfileLabeledPadding) {
  1181. auto result = addInfoLine(
  1182. std::move(label),
  1183. std::move(text),
  1184. st::infoLabeledOneLine,
  1185. padding);
  1186. result.text->setDoubleClickSelectsParagraph(true);
  1187. result.text->setContextCopyText(contextCopyText);
  1188. return result;
  1189. };
  1190. const auto fitLabelToButton = [&](
  1191. not_null<Ui::RpWidget*> button,
  1192. not_null<Ui::FlatLabel*> label,
  1193. int rightSkip) {
  1194. const auto parent = label->parentWidget();
  1195. const auto container = result.data();
  1196. rpl::combine(
  1197. container->widthValue(),
  1198. label->geometryValue(),
  1199. button->sizeValue()
  1200. ) | rpl::start_with_next([=](int width, QRect, QSize buttonSize) {
  1201. button->moveToRight(
  1202. rightSkip,
  1203. (parent->height() - buttonSize.height()) / 2);
  1204. const auto x = Ui::MapFrom(container, label, QPoint(0, 0)).x();
  1205. const auto s = Ui::MapFrom(container, button, QPoint(0, 0)).x();
  1206. label->resizeToWidth(s - x);
  1207. }, button->lifetime());
  1208. };
  1209. const auto controller = _controller->parentController();
  1210. const auto weak = base::make_weak(controller);
  1211. const auto peerIdRaw = QString::number(_peer->id.value);
  1212. const auto lnkHook = [=](Ui::FlatLabel::ContextMenuRequest request) {
  1213. const auto strong = weak.get();
  1214. if (!strong || !request.link) {
  1215. return;
  1216. }
  1217. const auto url = request.link->url();
  1218. if (url.startsWith(u"https://")) {
  1219. request.menu->addAction(
  1220. tr::lng_context_copy_link(tr::now),
  1221. [=] {
  1222. TextUtilities::SetClipboardText({ url });
  1223. if (const auto strong = weak.get()) {
  1224. strong->showToast(
  1225. tr::lng_channel_public_link_copied(tr::now));
  1226. }
  1227. });
  1228. request.menu->addAction(
  1229. tr::lng_group_invite_share(tr::now),
  1230. [=] {
  1231. if (const auto strong = weak.get()) {
  1232. FastShareLink(strong, url);
  1233. }
  1234. });
  1235. return;
  1236. }
  1237. static const auto kPrefix = QRegularExpression(u"^internal:"
  1238. "(collectible_username|username_link|username_regular)/"
  1239. "([a-zA-Z0-9\\-\\_\\.]+)@"_q);
  1240. const auto match = kPrefix.match(url);
  1241. if (!match.hasMatch()) {
  1242. return;
  1243. }
  1244. const auto username = match.captured(2);
  1245. const auto fullname = username + '@' + peerIdRaw;
  1246. const auto mentionLink = "internal:username_regular/" + fullname;
  1247. const auto linkLink = "internal:username_link/" + fullname;
  1248. const auto context = QVariant::fromValue(ClickHandlerContext{
  1249. .sessionWindow = weak,
  1250. });
  1251. const auto session = &strong->session();
  1252. const auto link = session->createInternalLinkFull(username);
  1253. request.menu->addAction(
  1254. tr::lng_context_copy_mention(tr::now),
  1255. [=] { Core::App().openInternalUrl(mentionLink, context); });
  1256. request.menu->addAction(
  1257. tr::lng_context_copy_link(tr::now),
  1258. [=] { Core::App().openInternalUrl(linkLink, context); });
  1259. request.menu->addAction(
  1260. tr::lng_group_invite_share(tr::now),
  1261. [=] {
  1262. if (const auto strong = weak.get()) {
  1263. FastShareLink(strong, link);
  1264. }
  1265. });
  1266. };
  1267. if (const auto user = _peer->asUser()) {
  1268. if (user->session().supportMode()) {
  1269. addInfoLineGeneric(
  1270. user->session().supportHelper().infoLabelValue(user),
  1271. user->session().supportHelper().infoTextValue(user));
  1272. }
  1273. {
  1274. const auto phoneLabel = addInfoOneLine(
  1275. tr::lng_info_mobile_label(),
  1276. PhoneOrHiddenValue(user),
  1277. tr::lng_profile_copy_phone(tr::now)).text;
  1278. const auto hook = [=](Ui::FlatLabel::ContextMenuRequest request) {
  1279. if (request.selection.empty()) {
  1280. const auto callback = [=] {
  1281. auto phone = rpl::variable<TextWithEntities>(
  1282. PhoneOrHiddenValue(user)).current().text;
  1283. phone.replace(' ', QString()).replace('-', QString());
  1284. TextUtilities::SetClipboardText({ phone });
  1285. };
  1286. request.menu->addAction(
  1287. tr::lng_profile_copy_phone(tr::now),
  1288. callback);
  1289. } else {
  1290. phoneLabel->fillContextMenu(request);
  1291. }
  1292. AddPhoneMenu(request.menu, user);
  1293. };
  1294. phoneLabel->setContextMenuHook(hook);
  1295. }
  1296. auto label = user->isBot()
  1297. ? tr::lng_info_about_label()
  1298. : tr::lng_info_bio_label();
  1299. addTranslateToMenu(
  1300. addInfoLine(std::move(label), AboutWithIdValue(user)).text,
  1301. AboutWithIdValue(user));
  1302. const auto usernameLine = addInfoOneLine(
  1303. UsernamesSubtext(_peer, tr::lng_info_username_label()),
  1304. UsernameValue(user, true) | rpl::map([=](TextWithEntities u) {
  1305. return u.text.isEmpty()
  1306. ? TextWithEntities()
  1307. : Ui::Text::Link(u, UsernameUrl(user, u.text.mid(1)));
  1308. }),
  1309. QString(),
  1310. st::infoProfileLabeledUsernamePadding);
  1311. const auto callback = UsernamesLinkCallback(
  1312. _peer,
  1313. controller,
  1314. QString());
  1315. usernameLine.text->overrideLinkClickHandler(callback);
  1316. usernameLine.subtext->overrideLinkClickHandler(callback);
  1317. usernameLine.text->setContextMenuHook(lnkHook);
  1318. usernameLine.subtext->setContextMenuHook(lnkHook);
  1319. const auto qrButton = Ui::CreateChild<Ui::IconButton>(
  1320. usernameLine.text->parentWidget(),
  1321. st::infoProfileLabeledButtonQr);
  1322. const auto rightSkip = 0;// st::infoProfileLabeledButtonQrRightSkip;
  1323. fitLabelToButton(qrButton, usernameLine.text, rightSkip);
  1324. fitLabelToButton(qrButton, usernameLine.subtext, rightSkip);
  1325. qrButton->setClickedCallback([=] {
  1326. controller->show(
  1327. Box(Ui::FillPeerQrBox, user, std::nullopt, nullptr));
  1328. return false;
  1329. });
  1330. if (!user->isBot()) {
  1331. tracker.track(result->add(
  1332. CreateBirthday(result, controller, user)));
  1333. tracker.track(result->add(CreateWorkingHours(result, user)));
  1334. auto locationText = user->session().changes().peerFlagsValue(
  1335. user,
  1336. Data::PeerUpdate::Flag::BusinessDetails
  1337. ) | rpl::map([=] {
  1338. const auto &details = user->businessDetails();
  1339. if (!details.location) {
  1340. return TextWithEntities();
  1341. } else if (!details.location.point) {
  1342. return TextWithEntities{ details.location.address };
  1343. }
  1344. return Ui::Text::Link(
  1345. TextUtilities::SingleLine(details.location.address),
  1346. LocationClickHandler::Url(*details.location.point));
  1347. });
  1348. addInfoOneLine(
  1349. tr::lng_info_location_label(),
  1350. std::move(locationText),
  1351. QString()
  1352. ).text->setLinksTrusted();
  1353. }
  1354. AddMainButton(
  1355. result,
  1356. tr::lng_info_add_as_contact(),
  1357. CanAddContactValue(user),
  1358. [=] { controller->window().show(Box(EditContactBox, controller, user)); },
  1359. tracker);
  1360. } else {
  1361. const auto topicRootId = _topic ? _topic->rootId() : 0;
  1362. const auto addToLink = topicRootId
  1363. ? ('/' + QString::number(topicRootId.bare))
  1364. : QString();
  1365. auto linkText = LinkValue(
  1366. _peer,
  1367. true
  1368. ) | rpl::map([=](const LinkWithUrl &link) {
  1369. const auto text = link.text;
  1370. return text.isEmpty()
  1371. ? TextWithEntities()
  1372. : Ui::Text::Link(
  1373. (text.startsWith(u"https://"_q)
  1374. ? text.mid(u"https://"_q.size())
  1375. : text) + addToLink,
  1376. (addToLink.isEmpty() ? link.url : (text + addToLink)));
  1377. });
  1378. const auto linkLine = addInfoOneLine(
  1379. (topicRootId
  1380. ? tr::lng_info_link_label(Ui::Text::WithEntities)
  1381. : UsernamesSubtext(_peer, tr::lng_info_link_label())),
  1382. std::move(linkText),
  1383. QString());
  1384. const auto controller = _controller->parentController();
  1385. const auto linkCallback = UsernamesLinkCallback(
  1386. _peer,
  1387. controller,
  1388. addToLink);
  1389. linkLine.text->overrideLinkClickHandler(linkCallback);
  1390. linkLine.subtext->overrideLinkClickHandler(linkCallback);
  1391. linkLine.text->setContextMenuHook(lnkHook);
  1392. linkLine.subtext->setContextMenuHook(lnkHook);
  1393. {
  1394. const auto qr = Ui::CreateChild<Ui::IconButton>(
  1395. linkLine.text->parentWidget(),
  1396. st::infoProfileLabeledButtonQr);
  1397. const auto rightSkip = 0;// st::infoProfileLabeledButtonQrRightSkip;
  1398. fitLabelToButton(qr, linkLine.text, rightSkip);
  1399. fitLabelToButton(qr, linkLine.subtext, rightSkip);
  1400. qr->setClickedCallback([=, peer = _peer] {
  1401. controller->show(
  1402. Box(Ui::FillPeerQrBox, peer, std::nullopt, nullptr));
  1403. return false;
  1404. });
  1405. }
  1406. if (const auto channel = _topic ? nullptr : _peer->asChannel()) {
  1407. auto locationText = LocationValue(
  1408. channel
  1409. ) | rpl::map([](const ChannelLocation *location) {
  1410. return location
  1411. ? Ui::Text::Link(
  1412. TextUtilities::SingleLine(location->address),
  1413. LocationClickHandler::Url(location->point))
  1414. : TextWithEntities();
  1415. });
  1416. addInfoOneLine(
  1417. tr::lng_info_location_label(),
  1418. std::move(locationText),
  1419. QString()
  1420. ).text->setLinksTrusted();
  1421. }
  1422. const auto about = addInfoLine(tr::lng_info_about_label(), _topic
  1423. ? rpl::single(TextWithEntities())
  1424. : AboutWithIdValue(_peer));
  1425. if (!_topic) {
  1426. addTranslateToMenu(about.text, AboutWithIdValue(_peer));
  1427. }
  1428. }
  1429. if (!_peer->isSelf()) {
  1430. // No notifications toggle for Self => no separator.
  1431. const auto user = _peer->asUser();
  1432. const auto app = user && user->botInfo && user->botInfo->hasMainApp;
  1433. const auto padding = app
  1434. ? QMargins(
  1435. st::infoOpenAppMargin.left(),
  1436. st::infoProfileSeparatorPadding.top(),
  1437. st::infoOpenAppMargin.right(),
  1438. 0)
  1439. : st::infoProfileSeparatorPadding;
  1440. result->add(object_ptr<Ui::SlideWrap<>>(
  1441. result,
  1442. object_ptr<Ui::PlainShadow>(result),
  1443. padding)
  1444. )->setDuration(
  1445. st::infoSlideDuration
  1446. )->toggleOn(
  1447. std::move(tracker).atLeastOneShownValue()
  1448. );
  1449. }
  1450. object_ptr<FloatingIcon>(
  1451. result,
  1452. st::infoIconInformation,
  1453. st::infoInformationIconPosition);
  1454. return result;
  1455. }
  1456. object_ptr<Ui::RpWidget> DetailsFiller::setupPersonalChannel(
  1457. not_null<UserData*> user) {
  1458. auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  1459. _wrap,
  1460. object_ptr<Ui::VerticalLayout>(_wrap));
  1461. const auto container = result->entity();
  1462. const auto window = _controller->parentController();
  1463. result->toggleOn(PersonalChannelValue(
  1464. user
  1465. ) | rpl::map(rpl::mappers::_1 != nullptr));
  1466. result->finishAnimating();
  1467. auto channel = PersonalChannelValue(
  1468. user
  1469. ) | rpl::start_spawning(result->lifetime());
  1470. const auto channelLabelFactory = [=](rpl::producer<ChannelData*> c) {
  1471. return rpl::combine(
  1472. tr::lng_info_personal_channel_label(Ui::Text::WithEntities),
  1473. std::move(c)
  1474. ) | rpl::map([](TextWithEntities &&text, ChannelData *channel) {
  1475. const auto count = channel ? channel->membersCount() : 0;
  1476. if (count > 1) {
  1477. text.append(
  1478. QString::fromUtf8(" \xE2\x80\xA2 ")
  1479. ).append(tr::lng_chat_status_subscribers(
  1480. tr::now,
  1481. lt_count_decimal,
  1482. count));
  1483. }
  1484. return text;
  1485. });
  1486. };
  1487. {
  1488. const auto onlyChannelWrap = container->add(
  1489. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  1490. container,
  1491. object_ptr<Ui::VerticalLayout>(container)));
  1492. onlyChannelWrap->toggleOn(PersonalChannelValue(user) | rpl::map([=] {
  1493. return user->personalChannelId()
  1494. && !user->personalChannelMessageId();
  1495. }));
  1496. onlyChannelWrap->finishAnimating();
  1497. auto text = rpl::duplicate(
  1498. channel
  1499. ) | rpl::map([=](ChannelData *channel) {
  1500. return channel ? NameValue(channel) : rpl::single(QString());
  1501. }) | rpl::flatten_latest() | rpl::map([](const QString &name) {
  1502. return name.isEmpty() ? TextWithEntities() : Ui::Text::Link(name);
  1503. });
  1504. auto line = CreateTextWithLabel(
  1505. result,
  1506. channelLabelFactory(rpl::duplicate(channel)),
  1507. std::move(text),
  1508. st::infoLabel,
  1509. st::infoLabeled,
  1510. st::infoProfileLabeledPadding);
  1511. onlyChannelWrap->entity()->add(std::move(line.wrap));
  1512. line.text->setClickHandlerFilter([=](
  1513. const ClickHandlerPtr &handler,
  1514. Qt::MouseButton button) {
  1515. if (const auto channelId = user->personalChannelId()) {
  1516. window->showPeerInfo(peerFromChannel(channelId));
  1517. }
  1518. return false;
  1519. });
  1520. object_ptr<FloatingIcon>(
  1521. onlyChannelWrap,
  1522. st::infoIconMediaChannel,
  1523. st::infoPersonalChannelIconPosition);
  1524. Ui::AddDivider(onlyChannelWrap->entity());
  1525. }
  1526. {
  1527. const auto messageChannelWrap = container->add(
  1528. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  1529. container,
  1530. object_ptr<Ui::VerticalLayout>(container)));
  1531. messageChannelWrap->toggleOn(PersonalChannelValue(
  1532. user
  1533. ) | rpl::map([=] {
  1534. return user->personalChannelId()
  1535. && user->personalChannelMessageId();
  1536. }));
  1537. messageChannelWrap->finishAnimating();
  1538. messageChannelWrap->toggledValue(
  1539. ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] {
  1540. messageChannelWrap->resizeToWidth(messageChannelWrap->width());
  1541. }, messageChannelWrap->lifetime());
  1542. const auto clear = [=] {
  1543. while (messageChannelWrap->entity()->count()) {
  1544. delete messageChannelWrap->entity()->widgetAt(0);
  1545. }
  1546. };
  1547. const auto rebuild = [=](
  1548. not_null<HistoryItem*> item,
  1549. anim::type animated) {
  1550. const auto &stUserpic = st::infoPersonalChannelUserpic;
  1551. const auto &stLabeled = st::infoProfileLabeledPadding;
  1552. messageChannelWrap->toggle(false, anim::type::instant);
  1553. clear();
  1554. Ui::AddSkip(messageChannelWrap->entity());
  1555. const auto inner = messageChannelWrap->entity()->add(
  1556. object_ptr<Ui::VerticalLayout>(messageChannelWrap->entity()));
  1557. const auto line = inner->add(
  1558. object_ptr<Ui::FixedHeightWidget>(
  1559. inner,
  1560. stUserpic.photoSize + rect::m::sum::v(stLabeled)));
  1561. const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
  1562. line,
  1563. item->history()->peer,
  1564. st::infoPersonalChannelUserpic);
  1565. userpic->moveToLeft(
  1566. -st::infoPersonalChannelUserpicSkip
  1567. + (stLabeled.left() - stUserpic.photoSize) / 2,
  1568. stLabeled.top());
  1569. userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
  1570. const auto date = Ui::CreateChild<Ui::FlatLabel>(
  1571. line,
  1572. Ui::FormatDialogsDate(ItemDateTime(item)),
  1573. st::infoPersonalChannelDateLabel);
  1574. const auto name = Ui::CreateChild<Ui::FlatLabel>(
  1575. line,
  1576. NameValue(item->history()->peer),
  1577. st::infoPersonalChannelNameLabel);
  1578. const auto preview = Ui::CreateChild<Ui::RpWidget>(line);
  1579. auto &lifetime = preview->lifetime();
  1580. using namespace Dialogs::Ui;
  1581. struct State {
  1582. MessageView view;
  1583. HistoryItem *item = nullptr;
  1584. rpl::lifetime lifetime;
  1585. };
  1586. const auto state = lifetime.make_state<State>();
  1587. state->item = item;
  1588. item->history()->session().changes().realtimeMessageUpdates(
  1589. Data::MessageUpdate::Flag::Destroyed
  1590. ) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
  1591. if (update.item == state->item) {
  1592. state->lifetime.destroy();
  1593. state->item = nullptr;
  1594. preview->update();
  1595. }
  1596. }, state->lifetime);
  1597. preview->resize(0, st::infoLabeled.style.font->height);
  1598. preview->paintRequest(
  1599. ) | rpl::start_with_next([=] {
  1600. auto p = Painter(preview);
  1601. const auto item = state->item;
  1602. if (!item) {
  1603. p.setPen(st::infoPersonalChannelDateLabel.textFg);
  1604. p.setBrush(Qt::NoBrush);
  1605. p.setFont(st::infoPersonalChannelDateLabel.style.font);
  1606. p.drawText(
  1607. preview->rect(),
  1608. tr::lng_deleted_message(tr::now),
  1609. style::al_left);
  1610. return;
  1611. }
  1612. if (!state->view.prepared(item, nullptr)) {
  1613. const auto repaint = [=] { preview->update(); };
  1614. state->view.prepare(item, nullptr, repaint, {});
  1615. }
  1616. state->view.paint(p, preview->rect(), {
  1617. .st = &st::defaultDialogRow,
  1618. .currentBg = st::boxBg->b,
  1619. });
  1620. }, preview->lifetime());
  1621. line->sizeValue() | rpl::filter_size(
  1622. ) | rpl::start_with_next([=](const QSize &size) {
  1623. const auto left = stLabeled.left();
  1624. const auto right = st::infoPersonalChannelDateSkip;
  1625. const auto top = stLabeled.top();
  1626. date->moveToRight(right, top, size.width());
  1627. name->resizeToWidth(size.width()
  1628. - left
  1629. - date->width()
  1630. - st::defaultVerticalListSkip
  1631. - right);
  1632. name->moveToLeft(left, top);
  1633. preview->resize(
  1634. size.width() - left - right,
  1635. st::infoLabeled.style.font->height);
  1636. preview->moveToLeft(
  1637. left,
  1638. size.height() - stLabeled.bottom() - preview->height());
  1639. }, preview->lifetime());
  1640. {
  1641. inner->add(
  1642. object_ptr<Ui::FlatLabel>(
  1643. inner,
  1644. channelLabelFactory(
  1645. rpl::single(item->history()->peer->asChannel())),
  1646. st::infoLabel),
  1647. QMargins(
  1648. st::infoProfileLabeledPadding.left(),
  1649. 0,
  1650. st::infoProfileLabeledPadding.right(),
  1651. st::infoProfileLabeledPadding.bottom()));
  1652. }
  1653. {
  1654. const auto button = Ui::CreateSimpleRectButton(
  1655. messageChannelWrap->entity(),
  1656. st::defaultRippleAnimation);
  1657. inner->geometryValue(
  1658. ) | rpl::start_with_next([=](const QRect &rect) {
  1659. button->setGeometry(rect);
  1660. }, button->lifetime());
  1661. button->setClickedCallback([=, msg = item->fullId().msg] {
  1662. window->showPeerHistory(
  1663. item->history()->peer,
  1664. Window::SectionShow::Way::Forward,
  1665. msg);
  1666. });
  1667. button->lower();
  1668. inner->lifetime().make_state<base::unique_qptr<Ui::RpWidget>>(
  1669. button);
  1670. }
  1671. inner->setAttribute(Qt::WA_TransparentForMouseEvents);
  1672. Ui::AddSkip(messageChannelWrap->entity());
  1673. Ui::AddDivider(messageChannelWrap->entity());
  1674. Ui::ToggleChildrenVisibility(messageChannelWrap->entity(), true);
  1675. Ui::ToggleChildrenVisibility(line, true);
  1676. messageChannelWrap->toggle(true, animated);
  1677. };
  1678. rpl::duplicate(
  1679. channel
  1680. ) | rpl::start_with_next([=](ChannelData *channel) {
  1681. clear();
  1682. if (!channel) {
  1683. return;
  1684. }
  1685. const auto id = FullMsgId(
  1686. channel->id,
  1687. user->personalChannelMessageId());
  1688. if (const auto item = user->session().data().message(id)) {
  1689. return rebuild(item, anim::type::instant);
  1690. }
  1691. user->session().api().requestMessageData(
  1692. channel,
  1693. user->personalChannelMessageId(),
  1694. crl::guard(container, [=] {
  1695. if (const auto i = user->session().data().message(id)) {
  1696. rebuild(i, anim::type::normal);
  1697. }
  1698. }));
  1699. }, messageChannelWrap->lifetime());
  1700. }
  1701. return result;
  1702. }
  1703. object_ptr<Ui::RpWidget> DetailsFiller::setupMuteToggle() {
  1704. const auto peer = _peer;
  1705. const auto topicRootId = _topic ? _topic->rootId() : MsgId();
  1706. const auto makeThread = [=] {
  1707. return topicRootId
  1708. ? static_cast<Data::Thread*>(peer->forumTopicFor(topicRootId))
  1709. : peer->owner().history(peer).get();
  1710. };
  1711. auto result = object_ptr<Ui::SettingsButton>(
  1712. _wrap,
  1713. tr::lng_profile_enable_notifications(),
  1714. st::infoNotificationsButton);
  1715. result->toggleOn(_topic
  1716. ? NotificationsEnabledValue(_topic)
  1717. : NotificationsEnabledValue(peer), true);
  1718. result->setAcceptBoth();
  1719. const auto notifySettings = &peer->owner().notifySettings();
  1720. MuteMenu::SetupMuteMenu(
  1721. result.data(),
  1722. result->clicks(
  1723. ) | rpl::filter([=](Qt::MouseButton button) {
  1724. if (button == Qt::RightButton) {
  1725. return true;
  1726. }
  1727. const auto topic = topicRootId
  1728. ? peer->forumTopicFor(topicRootId)
  1729. : nullptr;
  1730. Assert(!topicRootId || topic != nullptr);
  1731. const auto is = topic
  1732. ? notifySettings->isMuted(topic)
  1733. : notifySettings->isMuted(peer);
  1734. if (is) {
  1735. if (topic) {
  1736. notifySettings->update(topic, { .unmute = true });
  1737. } else {
  1738. notifySettings->update(peer, { .unmute = true });
  1739. }
  1740. return false;
  1741. } else {
  1742. return true;
  1743. }
  1744. }) | rpl::to_empty,
  1745. makeThread,
  1746. _controller->uiShow());
  1747. object_ptr<FloatingIcon>(
  1748. result,
  1749. st::infoIconNotifications,
  1750. st::infoNotificationsIconPosition);
  1751. return result;
  1752. }
  1753. void DetailsFiller::setupAboutVerification() {
  1754. const auto peer = _peer;
  1755. const auto inner = _wrap->add(object_ptr<Ui::VerticalLayout>(_wrap));
  1756. peer->session().changes().peerFlagsValue(
  1757. peer,
  1758. Data::PeerUpdate::Flag::VerifyInfo
  1759. ) | rpl::start_with_next([=] {
  1760. const auto info = peer->botVerifyDetails();
  1761. while (inner->count()) {
  1762. delete inner->widgetAt(0);
  1763. }
  1764. if (!info) {
  1765. Ui::AddDivider(inner);
  1766. } else if (!info->description.empty()) {
  1767. Ui::AddDividerText(inner, rpl::single(info->description));
  1768. }
  1769. inner->resizeToWidth(inner->width());
  1770. }, inner->lifetime());
  1771. }
  1772. void DetailsFiller::setupMainApp() {
  1773. const auto button = _wrap->add(
  1774. object_ptr<Ui::RoundButton>(
  1775. _wrap,
  1776. tr::lng_profile_open_app(),
  1777. st::infoOpenApp),
  1778. st::infoOpenAppMargin);
  1779. button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
  1780. const auto user = _peer->asUser();
  1781. const auto controller = _controller->parentController();
  1782. button->setClickedCallback([=] {
  1783. user->session().attachWebView().open({
  1784. .bot = user,
  1785. .context = {
  1786. .controller = controller,
  1787. .maySkipConfirmation = true,
  1788. },
  1789. .source = InlineBots::WebViewSourceBotProfile(),
  1790. });
  1791. });
  1792. const auto url = tr::lng_mini_apps_tos_url(tr::now);
  1793. Ui::AddDividerText(
  1794. _wrap,
  1795. tr::lng_profile_open_app_about(
  1796. lt_terms,
  1797. tr::lng_profile_open_app_terms() | Ui::Text::ToLink(url),
  1798. Ui::Text::WithEntities)
  1799. )->setClickHandlerFilter([=](const auto &...) {
  1800. UrlClickHandler::Open(url);
  1801. return false;
  1802. });
  1803. Ui::AddSkip(_wrap);
  1804. }
  1805. void DetailsFiller::setupBotPermissions() {
  1806. AddSkip(_wrap);
  1807. AddSubsectionTitle(_wrap, tr::lng_profile_bot_permissions_title());
  1808. const auto emoji = _wrap->add(
  1809. object_ptr<Ui::SettingsButton>(
  1810. _wrap,
  1811. tr::lng_profile_bot_emoji_status_access(),
  1812. st::infoSharedMediaButton));
  1813. object_ptr<Profile::FloatingIcon>(
  1814. emoji,
  1815. st::infoIconEmojiStatusAccess,
  1816. st::infoSharedMediaButtonIconPosition);
  1817. const auto user = _peer->asUser();
  1818. emoji->toggleOn(
  1819. rpl::single(bool(user->botInfo->canManageEmojiStatus))
  1820. )->toggledValue() | rpl::filter([=](bool allowed) {
  1821. return allowed != user->botInfo->canManageEmojiStatus;
  1822. }) | rpl::start_with_next([=](bool allowed) {
  1823. user->botInfo->canManageEmojiStatus = allowed;
  1824. const auto session = &user->session();
  1825. session->api().request(MTPbots_ToggleUserEmojiStatusPermission(
  1826. user->inputUser,
  1827. MTP_bool(allowed)
  1828. )).send();
  1829. }, emoji->lifetime());
  1830. AddSkip(_wrap);
  1831. AddDivider(_wrap);
  1832. AddSkip(_wrap);
  1833. }
  1834. void DetailsFiller::setupMainButtons() {
  1835. auto wrapButtons = [=](auto &&callback) {
  1836. auto topSkip = _wrap->add(CreateSlideSkipWidget(_wrap));
  1837. auto tracker = callback();
  1838. topSkip->toggleOn(std::move(tracker).atLeastOneShownValue());
  1839. };
  1840. if (_topic) {
  1841. wrapButtons([=] {
  1842. return fillTopicButtons();
  1843. });
  1844. } else if (const auto user = _peer->asUser()) {
  1845. wrapButtons([=] {
  1846. return fillUserButtons(user);
  1847. });
  1848. } else if (const auto channel = _peer->asChannel()) {
  1849. if (channel->isMegagroup()) {
  1850. wrapButtons([=] {
  1851. return fillDiscussionButtons(channel);
  1852. });
  1853. } else {
  1854. wrapButtons([=] {
  1855. return fillChannelButtons(channel);
  1856. });
  1857. }
  1858. }
  1859. }
  1860. void DetailsFiller::addReportReaction(Ui::MultiSlideTracker &tracker) {
  1861. v::match(_origin.data, [&](GroupReactionOrigin data) {
  1862. const auto user = _peer->asUser();
  1863. if (_peer->isSelf()) {
  1864. return;
  1865. #if 0 // Only public groups allow reaction reports for now.
  1866. } else if (const auto chat = data.group->asChat()) {
  1867. const auto ban = chat->canBanMembers()
  1868. && (!user || !chat->admins.contains(_peer))
  1869. && (!user || chat->creator != user->id);
  1870. addReportReaction(data, ban, tracker);
  1871. #endif
  1872. } else if (const auto channel = data.group->asMegagroup()) {
  1873. if (channel->isPublic()) {
  1874. const auto ban = channel->canBanMembers()
  1875. && (!user || !channel->mgInfo->admins.contains(user->id))
  1876. && (!user || channel->mgInfo->creator != user);
  1877. addReportReaction(data, ban, tracker);
  1878. }
  1879. }
  1880. }, [](const auto &) {});
  1881. }
  1882. void DetailsFiller::addReportReaction(
  1883. GroupReactionOrigin data,
  1884. bool ban,
  1885. Ui::MultiSlideTracker &tracker) {
  1886. const auto peer = _peer;
  1887. if (!peer) {
  1888. return;
  1889. }
  1890. const auto controller = _controller->parentController();
  1891. const auto forceHidden = std::make_shared<rpl::variable<bool>>(false);
  1892. const auto user = peer->asUser();
  1893. auto shown = user
  1894. ? rpl::combine(
  1895. Info::Profile::IsContactValue(user),
  1896. forceHidden->value(),
  1897. !rpl::mappers::_1 && !rpl::mappers::_2
  1898. ) | rpl::type_erased()
  1899. : (forceHidden->value() | rpl::map(!rpl::mappers::_1));
  1900. const auto sent = [=] {
  1901. *forceHidden = true;
  1902. };
  1903. AddMainButton(
  1904. _wrap,
  1905. (ban
  1906. ? tr::lng_report_and_ban()
  1907. : tr::lng_report_reaction()),
  1908. std::move(shown),
  1909. [=] { controller->show(
  1910. Box(ReportReactionBox, controller, peer, data, ban, sent)); },
  1911. tracker,
  1912. st::infoMainButtonAttention);
  1913. }
  1914. Ui::MultiSlideTracker DetailsFiller::fillTopicButtons() {
  1915. using namespace rpl::mappers;
  1916. Ui::MultiSlideTracker tracker;
  1917. const auto window = _controller->parentController();
  1918. const auto forum = _topic->forum();
  1919. auto showTopicsVisible = rpl::combine(
  1920. window->adaptive().oneColumnValue(),
  1921. window->shownForum().value(),
  1922. _1 || (_2 != forum));
  1923. AddMainButton(
  1924. _wrap,
  1925. tr::lng_forum_show_topics_list(),
  1926. std::move(showTopicsVisible),
  1927. [=] { window->showForum(forum); },
  1928. tracker);
  1929. return tracker;
  1930. }
  1931. Ui::MultiSlideTracker DetailsFiller::fillUserButtons(
  1932. not_null<UserData*> user) {
  1933. using namespace rpl::mappers;
  1934. Ui::MultiSlideTracker tracker;
  1935. if (user->isSelf()) {
  1936. return tracker;
  1937. }
  1938. auto window = _controller->parentController();
  1939. auto addSendMessageButton = [&] {
  1940. auto activePeerValue = window->activeChatValue(
  1941. ) | rpl::map([](Dialogs::Key key) {
  1942. return key.peer();
  1943. });
  1944. auto sendMessageVisible = rpl::combine(
  1945. _controller->wrapValue(),
  1946. std::move(activePeerValue),
  1947. (_1 != Wrap::Side) || (_2 != user));
  1948. auto sendMessage = [window, user] {
  1949. window->showPeerHistory(
  1950. user,
  1951. Window::SectionShow::Way::Forward);
  1952. };
  1953. AddMainButton(
  1954. _wrap,
  1955. tr::lng_profile_send_message(),
  1956. std::move(sendMessageVisible),
  1957. std::move(sendMessage),
  1958. tracker);
  1959. };
  1960. addSendMessageButton();
  1961. addReportReaction(tracker);
  1962. return tracker;
  1963. }
  1964. Ui::MultiSlideTracker DetailsFiller::fillChannelButtons(
  1965. not_null<ChannelData*> channel) {
  1966. using namespace rpl::mappers;
  1967. Ui::MultiSlideTracker tracker;
  1968. auto window = _controller->parentController();
  1969. auto activePeerValue = window->activeChatValue(
  1970. ) | rpl::map([](Dialogs::Key key) {
  1971. return key.peer();
  1972. });
  1973. auto viewChannelVisible = rpl::combine(
  1974. _controller->wrapValue(),
  1975. std::move(activePeerValue),
  1976. (_1 != Wrap::Side) || (_2 != channel));
  1977. auto viewChannel = [=] {
  1978. window->showPeerHistory(
  1979. channel,
  1980. Window::SectionShow::Way::Forward);
  1981. };
  1982. AddMainButton(
  1983. _wrap,
  1984. tr::lng_profile_view_channel(),
  1985. std::move(viewChannelVisible),
  1986. std::move(viewChannel),
  1987. tracker);
  1988. return tracker;
  1989. }
  1990. Ui::MultiSlideTracker DetailsFiller::fillDiscussionButtons(
  1991. not_null<ChannelData*> channel) {
  1992. using namespace rpl::mappers;
  1993. Ui::MultiSlideTracker tracker;
  1994. auto window = _controller->parentController();
  1995. auto viewDiscussionVisible = window->dialogsEntryStateValue(
  1996. ) | rpl::map([=](const Dialogs::EntryState &state) {
  1997. const auto history = state.key.history();
  1998. return (state.section == Dialogs::EntryState::Section::Replies)
  1999. && history
  2000. && (history->peer == channel);
  2001. });
  2002. auto viewDiscussion = [=] {
  2003. window->showPeerHistory(
  2004. channel,
  2005. Window::SectionShow::Way::Forward);
  2006. };
  2007. AddMainButton(
  2008. _wrap,
  2009. tr::lng_profile_view_discussion(),
  2010. std::move(viewDiscussionVisible),
  2011. std::move(viewDiscussion),
  2012. tracker);
  2013. return tracker;
  2014. }
  2015. object_ptr<Ui::RpWidget> DetailsFiller::fill() {
  2016. Expects(!_topic || !_topic->creating());
  2017. if (!_topic) {
  2018. setupAboutVerification();
  2019. } else {
  2020. add(object_ptr<Ui::BoxContentDivider>(_wrap));
  2021. }
  2022. if (const auto user = _peer->asUser()) {
  2023. add(setupPersonalChannel(user));
  2024. }
  2025. add(CreateSkipWidget(_wrap));
  2026. add(setupInfo());
  2027. if (const auto user = _peer->asUser()) {
  2028. if (const auto info = user->botInfo.get()) {
  2029. if (info->hasMainApp) {
  2030. setupMainApp();
  2031. }
  2032. if (info->canManageEmojiStatus) {
  2033. setupBotPermissions();
  2034. }
  2035. }
  2036. }
  2037. if (!_peer->isSelf()) {
  2038. add(setupMuteToggle());
  2039. }
  2040. setupMainButtons();
  2041. add(CreateSkipWidget(_wrap));
  2042. return std::move(_wrap);
  2043. }
  2044. ActionsFiller::ActionsFiller(
  2045. not_null<Controller*> controller,
  2046. not_null<Ui::RpWidget*> parent,
  2047. not_null<PeerData*> peer)
  2048. : _controller(controller)
  2049. , _parent(parent)
  2050. , _peer(peer) {
  2051. }
  2052. void ActionsFiller::addAffiliateProgram(not_null<UserData*> user) {
  2053. if (!user->isBot()) {
  2054. return;
  2055. }
  2056. const auto wrap = _wrap->add(
  2057. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  2058. _wrap.data(),
  2059. object_ptr<Ui::VerticalLayout>(_wrap.data())));
  2060. const auto inner = wrap->entity();
  2061. auto program = user->session().changes().peerFlagsValue(
  2062. user,
  2063. Data::PeerUpdate::Flag::StarRefProgram
  2064. ) | rpl::map([=] {
  2065. return user->botInfo->starRefProgram;
  2066. }) | rpl::start_spawning(inner->lifetime());
  2067. auto commission = rpl::duplicate(
  2068. program
  2069. ) | rpl::filter([=](StarRefProgram program) {
  2070. return program.commission > 0;
  2071. }) | rpl::map([=](StarRefProgram program) {
  2072. return Info::BotStarRef::FormatCommission(program.commission);
  2073. });
  2074. const auto show = _controller->uiShow();
  2075. struct StarRefRecipients {
  2076. std::vector<not_null<PeerData*>> list;
  2077. bool requested = false;
  2078. Fn<void()> open;
  2079. };
  2080. const auto recipients = std::make_shared<StarRefRecipients>();
  2081. recipients->open = [=] {
  2082. if (!recipients->list.empty()) {
  2083. const auto program = user->botInfo->starRefProgram;
  2084. show->show(Info::BotStarRef::JoinStarRefBox(
  2085. { user, { program } },
  2086. user->session().user(),
  2087. recipients->list));
  2088. } else if (!recipients->requested) {
  2089. recipients->requested = true;
  2090. const auto done = [=](std::vector<not_null<PeerData*>> list) {
  2091. recipients->list = std::move(list);
  2092. recipients->open();
  2093. };
  2094. Info::BotStarRef::ResolveRecipients(&user->session(), done);
  2095. }
  2096. };
  2097. inner->add(EditPeerInfoBox::CreateButton(
  2098. inner,
  2099. tr::lng_manage_peer_bot_star_ref(),
  2100. rpl::duplicate(commission),
  2101. recipients->open,
  2102. st::infoSharedMediaCountButton,
  2103. { .icon = &st::menuIconSharing, .newBadge = true }));
  2104. Ui::AddSkip(inner);
  2105. Ui::AddDividerText(
  2106. inner,
  2107. tr::lng_manage_peer_bot_star_ref_about(
  2108. lt_bot,
  2109. rpl::single(TextWithEntities{ user->name() }),
  2110. lt_amount,
  2111. rpl::duplicate(commission) | Ui::Text::ToWithEntities(),
  2112. Ui::Text::RichLangValue));
  2113. Ui::AddSkip(inner);
  2114. wrap->toggleOn(std::move(
  2115. program
  2116. ) | rpl::map([](StarRefProgram program) {
  2117. return program.commission > 0;
  2118. }));
  2119. wrap->finishAnimating();
  2120. }
  2121. void ActionsFiller::addBalanceActions(not_null<UserData*> user) {
  2122. const auto wrap = _wrap->add(
  2123. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  2124. _wrap.data(),
  2125. object_ptr<Ui::VerticalLayout>(_wrap.data())));
  2126. const auto inner = wrap->entity();
  2127. Ui::AddSubsectionTitle(inner, tr::lng_manage_peer_bot_balance());
  2128. auto currencyBalance = AddCurrencyAction(user, inner, _controller);
  2129. auto creditsBalance = AddCreditsAction(user, inner, _controller);
  2130. Ui::AddSkip(inner);
  2131. Ui::AddDivider(inner);
  2132. Ui::AddSkip(inner);
  2133. wrap->toggleOn(
  2134. rpl::combine(
  2135. std::move(currencyBalance),
  2136. std::move(creditsBalance)
  2137. ) | rpl::map((rpl::mappers::_1 > 0)
  2138. || (rpl::mappers::_2 > StarsAmount(0))));
  2139. }
  2140. void ActionsFiller::addInviteToGroupAction(not_null<UserData*> user) {
  2141. const auto notEmpty = [](const QString &value) {
  2142. return !value.isEmpty();
  2143. };
  2144. const auto controller = _controller->parentController();
  2145. AddActionButton(
  2146. _wrap,
  2147. InviteToChatButton(user) | rpl::filter(notEmpty),
  2148. InviteToChatButton(user) | rpl::map(notEmpty),
  2149. [=] { AddBotToGroupBoxController::Start(controller, user); },
  2150. &st::infoIconAddMember);
  2151. const auto about = _wrap->add(
  2152. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  2153. _wrap.data(),
  2154. object_ptr<Ui::VerticalLayout>(_wrap.data())));
  2155. about->toggleOn(InviteToChatAbout(user) | rpl::map(notEmpty));
  2156. Ui::AddSkip(about->entity());
  2157. Ui::AddDividerText(
  2158. about->entity(),
  2159. InviteToChatAbout(user) | rpl::filter(notEmpty));
  2160. Ui::AddSkip(about->entity());
  2161. about->finishAnimating();
  2162. }
  2163. void ActionsFiller::addShareContactAction(not_null<UserData*> user) {
  2164. const auto controller = _controller->parentController();
  2165. AddActionButton(
  2166. _wrap,
  2167. tr::lng_info_share_contact(),
  2168. CanShareContactValue(user),
  2169. [=] { Window::PeerMenuShareContactBox(controller, user); },
  2170. &st::infoIconShare);
  2171. }
  2172. void ActionsFiller::addEditContactAction(not_null<UserData*> user) {
  2173. const auto controller = _controller->parentController();
  2174. AddActionButton(
  2175. _wrap,
  2176. tr::lng_info_edit_contact(),
  2177. IsContactValue(user),
  2178. [=] { controller->window().show(Box(EditContactBox, controller, user)); },
  2179. &st::infoIconEdit);
  2180. }
  2181. void ActionsFiller::addDeleteContactAction(not_null<UserData*> user) {
  2182. const auto controller = _controller->parentController();
  2183. AddActionButton(
  2184. _wrap,
  2185. tr::lng_info_delete_contact(),
  2186. IsContactValue(user),
  2187. [=] { Window::PeerMenuDeleteContact(controller, user); },
  2188. &st::infoIconDelete);
  2189. }
  2190. void ActionsFiller::addFastButtonsMode(not_null<UserData*> user) {
  2191. Expects(user->isBot());
  2192. const auto bots = &user->session().fastButtonsBots();
  2193. const auto button = _wrap->add(object_ptr<Ui::SettingsButton>(
  2194. _wrap,
  2195. rpl::single(u"Fast buttons mode"_q),
  2196. st::infoSharedMediaButton));
  2197. object_ptr<Info::Profile::FloatingIcon>(
  2198. button,
  2199. st::infoIconMediaBot,
  2200. st::infoSharedMediaButtonIconPosition);
  2201. AddSkip(_wrap);
  2202. AddDivider(_wrap);
  2203. AddSkip(_wrap);
  2204. button->toggleOn(bots->enabledValue(user));
  2205. button->toggledValue(
  2206. ) | rpl::filter([=](bool value) {
  2207. return value != bots->enabled(user);
  2208. }) | rpl::start_with_next([=](bool value) {
  2209. bots->setEnabled(user, value);
  2210. }, button->lifetime());
  2211. }
  2212. void ActionsFiller::addBotCommandActions(not_null<UserData*> user) {
  2213. if (FastButtonsMode()) {
  2214. addFastButtonsMode(user);
  2215. }
  2216. const auto window = _controller->parentController();
  2217. const auto findBotCommand = [user](const QString &command) {
  2218. if (!user->isBot()) {
  2219. return QString();
  2220. }
  2221. for (const auto &data : user->botInfo->commands) {
  2222. const auto isSame = !data.command.compare(
  2223. command,
  2224. Qt::CaseInsensitive);
  2225. if (isSame) {
  2226. return data.command;
  2227. }
  2228. }
  2229. return QString();
  2230. };
  2231. const auto hasBotCommandValue = [=](const QString &command) {
  2232. return user->session().changes().peerFlagsValue(
  2233. user,
  2234. Data::PeerUpdate::Flag::BotCommands
  2235. ) | rpl::map([=] {
  2236. return !findBotCommand(command).isEmpty();
  2237. });
  2238. };
  2239. const auto makeOtherContext = [=] {
  2240. return QVariant::fromValue(ClickHandlerContext{
  2241. .sessionWindow = base::make_weak(window),
  2242. .peer = user,
  2243. });
  2244. };
  2245. const auto sendBotCommand = [=](const QString &command) {
  2246. const auto original = findBotCommand(command);
  2247. if (original.isEmpty()) {
  2248. return false;
  2249. }
  2250. BotCommandClickHandler('/' + original).onClick(ClickContext{
  2251. Qt::LeftButton,
  2252. makeOtherContext()
  2253. });
  2254. return true;
  2255. };
  2256. const auto addBotCommand = [=](
  2257. rpl::producer<QString> text,
  2258. const QString &command,
  2259. const style::icon *icon = nullptr) {
  2260. AddActionButton(
  2261. _wrap,
  2262. std::move(text),
  2263. hasBotCommandValue(command),
  2264. [=] { sendBotCommand(command); },
  2265. icon);
  2266. };
  2267. addBotCommand(
  2268. tr::lng_profile_bot_help(),
  2269. u"help"_q,
  2270. &st::infoIconInformation);
  2271. addBotCommand(tr::lng_profile_bot_settings(), u"settings"_q);
  2272. //addBotCommand(tr::lng_profile_bot_privacy(), u"privacy"_q);
  2273. const auto openUrl = [=](const QString &url) {
  2274. Core::App().iv().openWithIvPreferred(
  2275. &user->session(),
  2276. url,
  2277. makeOtherContext());
  2278. };
  2279. const auto openPrivacyPolicy = [=] {
  2280. if (const auto info = user->botInfo.get()) {
  2281. if (!info->privacyPolicyUrl.isEmpty()) {
  2282. openUrl(info->privacyPolicyUrl);
  2283. return;
  2284. }
  2285. }
  2286. if (!sendBotCommand(u"privacy"_q)) {
  2287. openUrl(tr::lng_profile_bot_privacy_url(tr::now));
  2288. }
  2289. };
  2290. AddActionButton(
  2291. _wrap,
  2292. tr::lng_profile_bot_privacy(),
  2293. rpl::single(true),
  2294. openPrivacyPolicy,
  2295. nullptr);
  2296. }
  2297. void ActionsFiller::addReportAction() {
  2298. const auto peer = _peer;
  2299. const auto controller = _controller->parentController();
  2300. const auto report = [=] {
  2301. ShowReportMessageBox(controller->uiShow(), peer, {}, {});
  2302. };
  2303. AddActionButton(
  2304. _wrap,
  2305. tr::lng_profile_report(),
  2306. rpl::single(true),
  2307. report,
  2308. &st::infoIconReport,
  2309. st::infoBlockButton);
  2310. }
  2311. void ActionsFiller::addBlockAction(not_null<UserData*> user) {
  2312. const auto controller = _controller->parentController();
  2313. const auto window = &controller->window();
  2314. auto text = user->session().changes().peerFlagsValue(
  2315. user,
  2316. Data::PeerUpdate::Flag::IsBlocked
  2317. ) | rpl::map([=] {
  2318. switch (user->blockStatus()) {
  2319. case UserData::BlockStatus::Blocked:
  2320. return ((user->isBot() && !user->isSupport())
  2321. ? tr::lng_profile_restart_bot
  2322. : tr::lng_profile_unblock_user)();
  2323. case UserData::BlockStatus::NotBlocked:
  2324. default:
  2325. return ((user->isBot() && !user->isSupport())
  2326. ? tr::lng_profile_block_bot
  2327. : tr::lng_profile_block_user)();
  2328. }
  2329. }) | rpl::flatten_latest(
  2330. ) | rpl::start_spawning(_wrap->lifetime());
  2331. auto toggleOn = rpl::duplicate(
  2332. text
  2333. ) | rpl::map([](const QString &text) {
  2334. return !text.isEmpty();
  2335. });
  2336. auto callback = [=] {
  2337. if (user->isBlocked()) {
  2338. const auto show = controller->uiShow();
  2339. Window::PeerMenuUnblockUserWithBotRestart(show, user);
  2340. if (user->isBot()) {
  2341. controller->showPeerHistory(user);
  2342. }
  2343. } else if (user->isBot()) {
  2344. user->session().api().blockedPeers().block(user);
  2345. } else {
  2346. window->show(Box(
  2347. Window::PeerMenuBlockUserBox,
  2348. window,
  2349. user,
  2350. v::null,
  2351. v::null));
  2352. }
  2353. };
  2354. AddActionButton(
  2355. _wrap,
  2356. rpl::duplicate(text),
  2357. std::move(toggleOn),
  2358. std::move(callback),
  2359. &st::infoIconBlock,
  2360. st::infoBlockButton);
  2361. }
  2362. void ActionsFiller::addLeaveChannelAction(not_null<ChannelData*> channel) {
  2363. Expects(_controller->parentController());
  2364. AddActionButton(
  2365. _wrap,
  2366. tr::lng_profile_leave_channel(),
  2367. AmInChannelValue(channel),
  2368. Window::DeleteAndLeaveHandler(
  2369. _controller->parentController(),
  2370. channel),
  2371. &st::infoIconLeave);
  2372. }
  2373. void ActionsFiller::addJoinChannelAction(
  2374. not_null<ChannelData*> channel) {
  2375. using namespace rpl::mappers;
  2376. auto joinVisible = AmInChannelValue(channel)
  2377. | rpl::map(!_1)
  2378. | rpl::start_spawning(_wrap->lifetime());
  2379. AddActionButton(
  2380. _wrap,
  2381. tr::lng_profile_join_channel(),
  2382. rpl::duplicate(joinVisible),
  2383. [=] { channel->session().api().joinChannel(channel); },
  2384. &st::infoIconAddMember);
  2385. _wrap->add(object_ptr<Ui::SlideWrap<Ui::FixedHeightWidget>>(
  2386. _wrap,
  2387. CreateSkipWidget(
  2388. _wrap,
  2389. st::infoBlockButtonSkip))
  2390. )->setDuration(
  2391. st::infoSlideDuration
  2392. )->toggleOn(
  2393. rpl::duplicate(joinVisible)
  2394. );
  2395. }
  2396. void ActionsFiller::fillUserActions(not_null<UserData*> user) {
  2397. if (user->isBot()) {
  2398. addAffiliateProgram(user);
  2399. addBalanceActions(user);
  2400. addInviteToGroupAction(user);
  2401. }
  2402. addShareContactAction(user);
  2403. if (!user->isSelf()) {
  2404. addEditContactAction(user);
  2405. addDeleteContactAction(user);
  2406. }
  2407. if (!user->isSelf() && !user->isSupport() && !user->isVerifyCodes()) {
  2408. if (user->isBot()) {
  2409. addBotCommandActions(user);
  2410. }
  2411. _wrap->add(CreateSkipWidget(
  2412. _wrap,
  2413. st::infoBlockButtonSkip));
  2414. if (user->isBot()) {
  2415. addReportAction();
  2416. }
  2417. addBlockAction(user);
  2418. }
  2419. }
  2420. void ActionsFiller::fillChannelActions(
  2421. not_null<ChannelData*> channel) {
  2422. using namespace rpl::mappers;
  2423. addJoinChannelAction(channel);
  2424. addLeaveChannelAction(channel);
  2425. if (!channel->amCreator()) {
  2426. addReportAction();
  2427. }
  2428. }
  2429. object_ptr<Ui::RpWidget> ActionsFiller::fill() {
  2430. auto wrapResult = [=](auto &&callback) {
  2431. _wrap = object_ptr<Ui::VerticalLayout>(_parent);
  2432. _wrap->add(CreateSkipWidget(_wrap));
  2433. callback();
  2434. _wrap->add(CreateSkipWidget(_wrap));
  2435. return std::move(_wrap);
  2436. };
  2437. if (auto user = _peer->asUser()) {
  2438. return wrapResult([=] {
  2439. fillUserActions(user);
  2440. });
  2441. } else if (auto channel = _peer->asChannel()) {
  2442. if (channel->isMegagroup()) {
  2443. return { nullptr };
  2444. }
  2445. return wrapResult([=] {
  2446. fillChannelActions(channel);
  2447. });
  2448. }
  2449. return { nullptr };
  2450. }
  2451. } // namespace
  2452. const char kOptionShowPeerIdBelowAbout[] = "show-peer-id-below-about";
  2453. object_ptr<Ui::RpWidget> SetupDetails(
  2454. not_null<Controller*> controller,
  2455. not_null<Ui::RpWidget*> parent,
  2456. not_null<PeerData*> peer,
  2457. Origin origin) {
  2458. DetailsFiller filler(controller, parent, peer, origin);
  2459. return filler.fill();
  2460. }
  2461. object_ptr<Ui::RpWidget> SetupDetails(
  2462. not_null<Controller*> controller,
  2463. not_null<Ui::RpWidget*> parent,
  2464. not_null<Data::ForumTopic*> topic) {
  2465. DetailsFiller filler(controller, parent, topic);
  2466. return filler.fill();
  2467. }
  2468. object_ptr<Ui::RpWidget> SetupActions(
  2469. not_null<Controller*> controller,
  2470. not_null<Ui::RpWidget*> parent,
  2471. not_null<PeerData*> peer) {
  2472. ActionsFiller filler(controller, parent, peer);
  2473. return filler.fill();
  2474. }
  2475. void SetupAddChannelMember(
  2476. not_null<Window::SessionNavigation*> navigation,
  2477. not_null<Ui::RpWidget*> parent,
  2478. not_null<ChannelData*> channel) {
  2479. auto add = Ui::CreateChild<Ui::IconButton>(
  2480. parent.get(),
  2481. st::infoMembersAddMember);
  2482. add->showOn(CanAddMemberValue(channel));
  2483. add->addClickHandler([=] {
  2484. Window::PeerMenuAddChannelMembers(navigation, channel);
  2485. });
  2486. parent->widthValue(
  2487. ) | rpl::start_with_next([add](int newWidth) {
  2488. auto availableWidth = newWidth
  2489. - st::infoMembersButtonPosition.x();
  2490. add->moveToLeft(
  2491. availableWidth - add->width(),
  2492. st::infoMembersButtonPosition.y(),
  2493. newWidth);
  2494. }, add->lifetime());
  2495. }
  2496. object_ptr<Ui::RpWidget> SetupChannelMembersAndManage(
  2497. not_null<Controller*> controller,
  2498. not_null<Ui::RpWidget*> parent,
  2499. not_null<PeerData*> peer) {
  2500. using namespace rpl::mappers;
  2501. auto channel = peer->asChannel();
  2502. if (!channel || channel->isMegagroup()) {
  2503. return { nullptr };
  2504. }
  2505. auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  2506. parent,
  2507. object_ptr<Ui::VerticalLayout>(parent));
  2508. result->entity()->add(object_ptr<Ui::BoxContentDivider>(result));
  2509. result->entity()->add(CreateSkipWidget(result));
  2510. auto membersShown = rpl::combine(
  2511. MembersCountValue(channel),
  2512. Data::PeerFlagValue(
  2513. channel,
  2514. ChannelDataFlag::CanViewParticipants),
  2515. (_1 > 0) && _2);
  2516. auto membersText = tr::lng_chat_status_subscribers(
  2517. lt_count_decimal,
  2518. MembersCountValue(channel) | tr::to_count());
  2519. auto membersCallback = [=] {
  2520. controller->showSection(std::make_shared<Info::Memento>(
  2521. channel,
  2522. Section::Type::Members));
  2523. };
  2524. const auto membersWrap = result->entity()->add(
  2525. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  2526. result->entity(),
  2527. object_ptr<Ui::VerticalLayout>(result->entity())));
  2528. membersWrap->setDuration(
  2529. st::infoSlideDuration
  2530. )->toggleOn(rpl::duplicate(membersShown));
  2531. const auto members = membersWrap->entity();
  2532. {
  2533. auto button = AddActionButton(
  2534. members,
  2535. std::move(membersText),
  2536. rpl::single(true),
  2537. std::move(membersCallback),
  2538. nullptr)->entity();
  2539. SetupAddChannelMember(controller, button, channel);
  2540. }
  2541. object_ptr<FloatingIcon>(
  2542. members,
  2543. st::infoIconMembers,
  2544. st::infoChannelMembersIconPosition);
  2545. auto adminsShown = peer->session().changes().peerFlagsValue(
  2546. channel,
  2547. Data::PeerUpdate::Flag::Rights
  2548. ) | rpl::map([=] { return channel->canViewAdmins(); });
  2549. auto adminsText = tr::lng_profile_administrators(
  2550. lt_count_decimal,
  2551. Info::Profile::MigratedOrMeValue(
  2552. channel
  2553. ) | rpl::map(
  2554. Info::Profile::AdminsCountValue
  2555. ) | rpl::flatten_latest() | tr::to_count());
  2556. auto adminsCallback = [=] {
  2557. ParticipantsBoxController::Start(
  2558. controller,
  2559. channel,
  2560. ParticipantsBoxController::Role::Admins);
  2561. };
  2562. const auto adminsWrap = result->entity()->add(
  2563. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  2564. result->entity(),
  2565. object_ptr<Ui::VerticalLayout>(result->entity())));
  2566. adminsWrap->setDuration(
  2567. st::infoSlideDuration
  2568. )->toggleOn(rpl::duplicate(adminsShown));
  2569. const auto admins = adminsWrap->entity();
  2570. AddActionButton(
  2571. admins,
  2572. std::move(adminsText),
  2573. rpl::single(true),
  2574. std::move(adminsCallback),
  2575. nullptr);
  2576. object_ptr<FloatingIcon>(
  2577. admins,
  2578. st::menuIconAdmin,
  2579. st::infoChannelAdminsIconPosition);
  2580. if (EditPeerInfoBox::Available(channel)) {
  2581. const auto sessionController = controller->parentController();
  2582. const auto button = AddActionButton(
  2583. result->entity(),
  2584. tr::lng_profile_manage(),
  2585. rpl::single(true),
  2586. [=] { sessionController->showEditPeerBox(channel); },
  2587. nullptr);
  2588. object_ptr<FloatingIcon>(
  2589. button,
  2590. st::menuIconManage,
  2591. st::infoChannelAdminsIconPosition);
  2592. }
  2593. result->setDuration(st::infoSlideDuration)->toggleOn(
  2594. rpl::combine(
  2595. std::move(membersShown),
  2596. std::move(adminsShown)
  2597. ) | rpl::map(rpl::mappers::_1 || rpl::mappers::_2));
  2598. result->entity()->add(CreateSkipWidget(result));
  2599. return result;
  2600. }
  2601. Cover *AddCover(
  2602. not_null<Ui::VerticalLayout*> container,
  2603. not_null<Controller*> controller,
  2604. not_null<PeerData*> peer,
  2605. Data::ForumTopic *topic) {
  2606. const auto result = topic
  2607. ? container->add(object_ptr<Cover>(
  2608. container,
  2609. controller->parentController(),
  2610. topic))
  2611. : container->add(object_ptr<Cover>(
  2612. container,
  2613. controller->parentController(),
  2614. peer,
  2615. [=] { return controller->wrapWidget(); }));
  2616. result->showSection(
  2617. ) | rpl::start_with_next([=](Section section) {
  2618. controller->showSection(topic
  2619. ? std::make_shared<Info::Memento>(topic, section)
  2620. : std::make_shared<Info::Memento>(peer, section));
  2621. }, result->lifetime());
  2622. result->setOnlineCount(rpl::single(0));
  2623. return result;
  2624. }
  2625. void AddDetails(
  2626. not_null<Ui::VerticalLayout*> container,
  2627. not_null<Controller*> controller,
  2628. not_null<PeerData*> peer,
  2629. Data::ForumTopic *topic,
  2630. Origin origin) {
  2631. if (topic) {
  2632. container->add(SetupDetails(controller, container, topic));
  2633. } else {
  2634. container->add(SetupDetails(controller, container, peer, origin));
  2635. }
  2636. }
  2637. } // namespace Profile
  2638. } // namespace Info