settings_chat_intro.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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 "settings/business/settings_chat_intro.h"
  8. #include "api/api_premium.h"
  9. #include "boxes/peers/edit_peer_color_box.h" // ButtonStyleWithRightEmoji
  10. #include "chat_helpers/stickers_lottie.h"
  11. #include "chat_helpers/tabbed_panel.h"
  12. #include "chat_helpers/tabbed_selector.h"
  13. #include "core/application.h"
  14. #include "data/business/data_business_info.h"
  15. #include "data/data_document.h"
  16. #include "data/data_document_media.h"
  17. #include "data/data_session.h"
  18. #include "data/data_user.h"
  19. #include "history/view/media/history_view_media_common.h"
  20. #include "history/view/media/history_view_sticker_player.h"
  21. #include "history/view/history_view_about_view.h"
  22. #include "history/view/history_view_element.h"
  23. #include "history/history.h"
  24. #include "lang/lang_keys.h"
  25. #include "main/main_app_config.h"
  26. #include "main/main_session.h"
  27. #include "settings/business/settings_recipients_helper.h"
  28. #include "ui/chat/chat_style.h"
  29. #include "ui/chat/chat_theme.h"
  30. #include "ui/effects/path_shift_gradient.h"
  31. #include "ui/text/text_utilities.h"
  32. #include "ui/widgets/fields/input_field.h"
  33. #include "ui/widgets/buttons.h"
  34. #include "ui/wrap/vertical_layout.h"
  35. #include "ui/wrap/slide_wrap.h"
  36. #include "ui/painter.h"
  37. #include "ui/vertical_list.h"
  38. #include "window/themes/window_theme.h"
  39. #include "window/section_widget.h"
  40. #include "window/window_controller.h"
  41. #include "window/window_session_controller.h"
  42. #include "styles/style_chat.h"
  43. #include "styles/style_chat_helpers.h"
  44. #include "styles/style_layers.h"
  45. #include "styles/style_settings.h"
  46. namespace Settings {
  47. namespace {
  48. using namespace HistoryView;
  49. class PreviewDelegate final : public DefaultElementDelegate {
  50. public:
  51. PreviewDelegate(
  52. not_null<QWidget*> parent,
  53. not_null<Ui::ChatStyle*> st,
  54. Fn<void()> update);
  55. bool elementAnimationsPaused() override;
  56. not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;
  57. Context elementContext() override;
  58. private:
  59. const not_null<QWidget*> _parent;
  60. const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
  61. };
  62. class PreviewWrap final : public Ui::RpWidget {
  63. public:
  64. PreviewWrap(
  65. not_null<QWidget*> parent,
  66. not_null<Main::Session*> session,
  67. rpl::producer<Data::ChatIntro> value);
  68. ~PreviewWrap();
  69. private:
  70. void paintEvent(QPaintEvent *e) override;
  71. void resizeTo(int width);
  72. void prepare(rpl::producer<Data::ChatIntro> value);
  73. const not_null<History*> _history;
  74. const std::unique_ptr<Ui::ChatTheme> _theme;
  75. const std::unique_ptr<Ui::ChatStyle> _style;
  76. const std::unique_ptr<PreviewDelegate> _delegate;
  77. std::unique_ptr<AboutView> _view;
  78. QPoint _position;
  79. };
  80. class StickerPanel final {
  81. public:
  82. StickerPanel();
  83. ~StickerPanel();
  84. struct Descriptor {
  85. not_null<Window::SessionController*> controller;
  86. not_null<QWidget*> button;
  87. };
  88. void show(Descriptor &&descriptor);
  89. struct CustomChosen {
  90. not_null<DocumentData*> sticker;
  91. };
  92. [[nodiscard]] rpl::producer<CustomChosen> someCustomChosen() const {
  93. return _someCustomChosen.events();
  94. }
  95. private:
  96. void create(const Descriptor &descriptor);
  97. base::unique_qptr<ChatHelpers::TabbedPanel> _panel;
  98. QPointer<QWidget> _panelButton;
  99. rpl::event_stream<CustomChosen> _someCustomChosen;
  100. };
  101. class ChatIntro final : public BusinessSection<ChatIntro> {
  102. public:
  103. ChatIntro(
  104. QWidget *parent,
  105. not_null<Window::SessionController*> controller);
  106. ~ChatIntro();
  107. [[nodiscard]] bool closeByOutsideClick() const override;
  108. [[nodiscard]] rpl::producer<QString> title() override;
  109. void setInnerFocus() override {
  110. _setFocus();
  111. }
  112. private:
  113. void setupContent(not_null<Window::SessionController*> controller);
  114. void save();
  115. Fn<void()> _setFocus;
  116. rpl::variable<Data::ChatIntro> _intro;
  117. };
  118. [[nodiscard]] int PartLimit(
  119. not_null<Main::Session*> session,
  120. const QString &key,
  121. int defaultValue) {
  122. return session->appConfig().get<int>(key, defaultValue);
  123. }
  124. [[nodiscard]] not_null<Ui::InputField*> AddPartInput(
  125. not_null<Ui::VerticalLayout*> container,
  126. rpl::producer<QString> placeholder,
  127. QString current,
  128. int limit) {
  129. const auto field = container->add(
  130. object_ptr<Ui::InputField>(
  131. container,
  132. st::settingsChatIntroField,
  133. std::move(placeholder),
  134. current),
  135. st::settingsChatIntroFieldMargins);
  136. field->setMaxLength(limit);
  137. Ui::AddLengthLimitLabel(field, limit);
  138. return field;
  139. }
  140. rpl::producer<std::shared_ptr<StickerPlayer>> IconPlayerValue(
  141. not_null<DocumentData*> sticker,
  142. Fn<void()> update) {
  143. const auto media = sticker->createMediaView();
  144. media->checkStickerLarge();
  145. media->goodThumbnailWanted();
  146. return rpl::single() | rpl::then(
  147. sticker->owner().session().downloaderTaskFinished()
  148. ) | rpl::filter([=] {
  149. return media->loaded();
  150. }) | rpl::take(1) | rpl::map([=] {
  151. auto result = std::shared_ptr<StickerPlayer>();
  152. const auto info = sticker->sticker();
  153. const auto box = QSize(st::emojiSize, st::emojiSize);
  154. if (info->isLottie()) {
  155. result = std::make_shared<LottiePlayer>(
  156. ChatHelpers::LottiePlayerFromDocument(
  157. media.get(),
  158. ChatHelpers::StickerLottieSize::StickerEmojiSize,
  159. box,
  160. Lottie::Quality::High));
  161. } else if (info->isWebm()) {
  162. result = std::make_shared<WebmPlayer>(
  163. media->owner()->location(),
  164. media->bytes(),
  165. box);
  166. } else {
  167. result = std::make_shared<StaticStickerPlayer>(
  168. media->owner()->location(),
  169. media->bytes(),
  170. box);
  171. }
  172. result->setRepaintCallback(update);
  173. return result;
  174. });
  175. }
  176. [[nodiscard]] object_ptr<Ui::SettingsButton> CreateIntroStickerButton(
  177. not_null<Ui::RpWidget*> parent,
  178. std::shared_ptr<ChatHelpers::Show> show,
  179. rpl::producer<DocumentData*> stickerValue,
  180. Fn<void(DocumentData*)> stickerChosen) {
  181. const auto button = ButtonStyleWithRightEmoji(
  182. parent,
  183. tr::lng_chat_intro_random_sticker(tr::now),
  184. st::settingsButtonNoIcon);
  185. auto result = Settings::CreateButtonWithIcon(
  186. parent,
  187. tr::lng_chat_intro_choose_sticker(),
  188. *button.st);
  189. const auto raw = result.data();
  190. const auto right = Ui::CreateChild<Ui::RpWidget>(raw);
  191. right->show();
  192. struct State {
  193. StickerPanel panel;
  194. DocumentData *sticker = nullptr;
  195. std::shared_ptr<StickerPlayer> player;
  196. rpl::lifetime playerLifetime;
  197. };
  198. const auto state = right->lifetime().make_state<State>();
  199. state->panel.someCustomChosen(
  200. ) | rpl::start_with_next([=](StickerPanel::CustomChosen chosen) {
  201. stickerChosen(chosen.sticker);
  202. }, raw->lifetime());
  203. std::move(
  204. stickerValue
  205. ) | rpl::start_with_next([=](DocumentData *sticker) {
  206. state->sticker = sticker;
  207. if (sticker) {
  208. right->resize(button.emojiWidth + button.added, right->height());
  209. IconPlayerValue(
  210. sticker,
  211. [=] { right->update(); }
  212. ) | rpl::start_with_next([=](
  213. std::shared_ptr<StickerPlayer> player) {
  214. state->player = std::move(player);
  215. right->update();
  216. }, state->playerLifetime);
  217. } else {
  218. state->playerLifetime.destroy();
  219. state->player = nullptr;
  220. right->resize(button.noneWidth + button.added, right->height());
  221. right->update();
  222. }
  223. }, right->lifetime());
  224. rpl::combine(
  225. raw->sizeValue(),
  226. right->widthValue()
  227. ) | rpl::start_with_next([=](QSize outer, int width) {
  228. right->resize(width, outer.height());
  229. const auto skip = st::settingsButton.padding.right();
  230. right->moveToRight(skip - button.added, 0, outer.width());
  231. }, right->lifetime());
  232. right->paintRequest(
  233. ) | rpl::start_with_next([=] {
  234. auto p = QPainter(right);
  235. const auto height = right->height();
  236. if (state->player) {
  237. if (state->player->ready()) {
  238. const auto frame = state->player->frame(
  239. QSize(st::emojiSize, st::emojiSize),
  240. QColor(0, 0, 0, 0),
  241. false,
  242. crl::now(),
  243. !right->window()->isActiveWindow()).image;
  244. const auto target = DownscaledSize(
  245. frame.size(),
  246. QSize(st::emojiSize, st::emojiSize));
  247. p.drawImage(
  248. QRect(
  249. button.added + (st::emojiSize - target.width()) / 2,
  250. (height - target.height()) / 2,
  251. target.width(),
  252. target.height()),
  253. frame);
  254. state->player->markFrameShown();
  255. }
  256. } else {
  257. const auto &font = st::normalFont;
  258. p.setFont(font);
  259. p.setPen(st::windowActiveTextFg);
  260. p.drawText(
  261. QPoint(
  262. button.added,
  263. (height - font->height) / 2 + font->ascent),
  264. tr::lng_chat_intro_random_sticker(tr::now));
  265. }
  266. }, right->lifetime());
  267. raw->setClickedCallback([=] {
  268. if (const auto controller = show->resolveWindow()) {
  269. state->panel.show({
  270. .controller = controller,
  271. .button = right,
  272. });
  273. }
  274. });
  275. return result;
  276. }
  277. PreviewDelegate::PreviewDelegate(
  278. not_null<QWidget*> parent,
  279. not_null<Ui::ChatStyle*> st,
  280. Fn<void()> update)
  281. : _parent(parent)
  282. , _pathGradient(MakePathShiftGradient(st, update)) {
  283. }
  284. bool PreviewDelegate::elementAnimationsPaused() {
  285. return _parent->window()->isActiveWindow();
  286. }
  287. auto PreviewDelegate::elementPathShiftGradient()
  288. -> not_null<Ui::PathShiftGradient*> {
  289. return _pathGradient.get();
  290. }
  291. Context PreviewDelegate::elementContext() {
  292. return Context::History;
  293. }
  294. PreviewWrap::PreviewWrap(
  295. not_null<QWidget*> parent,
  296. not_null<Main::Session*> session,
  297. rpl::producer<Data::ChatIntro> value)
  298. : RpWidget(parent)
  299. , _history(session->data().history(session->userPeerId()))
  300. , _theme(Window::Theme::DefaultChatThemeOn(lifetime()))
  301. , _style(std::make_unique<Ui::ChatStyle>(
  302. _history->session().colorIndicesValue()))
  303. , _delegate(std::make_unique<PreviewDelegate>(
  304. parent,
  305. _style.get(),
  306. [=] { update(); }))
  307. , _position(0, st::msgMargin.bottom()) {
  308. _style->apply(_theme.get());
  309. session->data().viewRepaintRequest(
  310. ) | rpl::start_with_next([=](not_null<const Element*> view) {
  311. if (view == _view->view()) {
  312. update();
  313. }
  314. }, lifetime());
  315. session->downloaderTaskFinished() | rpl::start_with_next([=] {
  316. update();
  317. }, lifetime());
  318. prepare(std::move(value));
  319. }
  320. PreviewWrap::~PreviewWrap() {
  321. _view = nullptr;
  322. }
  323. void PreviewWrap::prepare(rpl::producer<Data::ChatIntro> value) {
  324. _view = std::make_unique<AboutView>(
  325. _history.get(),
  326. _delegate.get());
  327. std::move(value) | rpl::start_with_next([=](Data::ChatIntro intro) {
  328. _view->make(std::move(intro), true);
  329. if (width() >= st::msgMinWidth) {
  330. resizeTo(width());
  331. }
  332. update();
  333. }, lifetime());
  334. widthValue(
  335. ) | rpl::filter([=](int width) {
  336. return width >= st::msgMinWidth;
  337. }) | rpl::start_with_next([=](int width) {
  338. resizeTo(width);
  339. }, lifetime());
  340. }
  341. void PreviewWrap::resizeTo(int width) {
  342. const auto height = _position.y()
  343. + _view->view()->resizeGetHeight(width)
  344. + _position.y()
  345. + st::msgServiceMargin.top()
  346. + st::msgServiceGiftBoxTopSkip
  347. - st::msgServiceMargin.bottom();
  348. resize(width, height);
  349. }
  350. void PreviewWrap::paintEvent(QPaintEvent *e) {
  351. auto p = Painter(this);
  352. const auto clip = e->rect();
  353. if (!clip.isEmpty()) {
  354. p.setClipRect(clip);
  355. Window::SectionWidget::PaintBackground(
  356. p,
  357. _theme.get(),
  358. QSize(width(), window()->height()),
  359. clip);
  360. }
  361. auto context = _theme->preparePaintContext(
  362. _style.get(),
  363. rect(),
  364. e->rect(),
  365. !window()->isActiveWindow());
  366. p.translate(_position);
  367. _view->view()->draw(p, context);
  368. }
  369. StickerPanel::StickerPanel() = default;
  370. StickerPanel::~StickerPanel() = default;
  371. void StickerPanel::show(Descriptor &&descriptor) {
  372. if (!_panel) {
  373. create(descriptor);
  374. _panel->shownValue(
  375. ) | rpl::filter([=] {
  376. return (_panelButton != nullptr);
  377. }) | rpl::start_with_next([=](bool shown) {
  378. if (shown) {
  379. _panelButton->installEventFilter(_panel.get());
  380. } else {
  381. _panelButton->removeEventFilter(_panel.get());
  382. }
  383. }, _panel->lifetime());
  384. }
  385. const auto button = descriptor.button;
  386. if (const auto previous = _panelButton.data()) {
  387. if (previous != button) {
  388. previous->removeEventFilter(_panel.get());
  389. }
  390. }
  391. _panelButton = button;
  392. const auto parent = _panel->parentWidget();
  393. const auto global = button->mapToGlobal(QPoint());
  394. const auto local = parent->mapFromGlobal(global);
  395. _panel->moveBottomRight(
  396. local.y() + (st::normalFont->height / 2),
  397. local.x() + button->width() * 3);
  398. _panel->toggleAnimated();
  399. }
  400. void StickerPanel::create(const Descriptor &descriptor) {
  401. using Selector = ChatHelpers::TabbedSelector;
  402. using Descriptor = ChatHelpers::TabbedSelectorDescriptor;
  403. using Mode = ChatHelpers::TabbedSelector::Mode;
  404. const auto controller = descriptor.controller;
  405. const auto body = controller->window().widget()->bodyWidget();
  406. _panel = base::make_unique_q<ChatHelpers::TabbedPanel>(
  407. body,
  408. controller,
  409. object_ptr<Selector>(
  410. nullptr,
  411. Descriptor{
  412. .show = controller->uiShow(),
  413. .st = st::backgroundEmojiPan,
  414. .level = Window::GifPauseReason::Layer,
  415. .mode = Mode::ChatIntro,
  416. .features = {
  417. .megagroupSet = false,
  418. .stickersSettings = false,
  419. .openStickerSets = false,
  420. },
  421. }));
  422. _panel->setDropDown(false);
  423. _panel->setDesiredHeightValues(
  424. 1.,
  425. st::emojiPanMinHeight / 2,
  426. st::emojiPanMinHeight);
  427. _panel->hide();
  428. _panel->selector()->fileChosen(
  429. ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
  430. _someCustomChosen.fire({ data.document });
  431. _panel->hideAnimated();
  432. }, _panel->lifetime());
  433. }
  434. ChatIntro::ChatIntro(
  435. QWidget *parent,
  436. not_null<Window::SessionController*> controller)
  437. : BusinessSection(parent, controller) {
  438. setupContent(controller);
  439. }
  440. ChatIntro::~ChatIntro() {
  441. if (!Core::Quitting()) {
  442. save();
  443. }
  444. }
  445. bool ChatIntro::closeByOutsideClick() const {
  446. return false;
  447. }
  448. rpl::producer<QString> ChatIntro::title() {
  449. return tr::lng_chat_intro_title();
  450. }
  451. [[nodiscard]] rpl::producer<Data::ChatIntro> IntroWithRandomSticker(
  452. not_null<Main::Session*> session,
  453. rpl::producer<Data::ChatIntro> intro) {
  454. auto random = rpl::single(
  455. Api::RandomHelloStickerValue(session)
  456. ) | rpl::then(rpl::duplicate(
  457. intro
  458. ) | rpl::map([=](const Data::ChatIntro &intro) {
  459. return intro.sticker;
  460. }) | rpl::distinct_until_changed(
  461. ) | rpl::filter([](DocumentData *sticker) {
  462. return !sticker;
  463. }) | rpl::map([=] {
  464. return Api::RandomHelloStickerValue(session);
  465. })) | rpl::flatten_latest();
  466. return rpl::combine(
  467. std::move(intro),
  468. std::move(random)
  469. ) | rpl::map([=](Data::ChatIntro intro, DocumentData *hello) {
  470. if (!intro.sticker) {
  471. intro.sticker = hello;
  472. }
  473. return intro;
  474. });
  475. }
  476. void ChatIntro::setupContent(
  477. not_null<Window::SessionController*> controller) {
  478. using namespace rpl::mappers;
  479. const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
  480. const auto session = &controller->session();
  481. _intro = controller->session().user()->businessDetails().intro;
  482. const auto change = [=](Fn<void(Data::ChatIntro &)> modify) {
  483. auto intro = _intro.current();
  484. modify(intro);
  485. _intro = intro;
  486. };
  487. content->add(
  488. object_ptr<PreviewWrap>(
  489. content,
  490. session,
  491. IntroWithRandomSticker(session, _intro.value())),
  492. {});
  493. const auto title = AddPartInput(
  494. content,
  495. tr::lng_chat_intro_enter_title(),
  496. _intro.current().title,
  497. PartLimit(session, u"intro_title_length_limit"_q, 32));
  498. const auto description = AddPartInput(
  499. content,
  500. tr::lng_chat_intro_enter_message(),
  501. _intro.current().description,
  502. PartLimit(session, u"intro_description_length_limit"_q, 70));
  503. content->add(CreateIntroStickerButton(
  504. content,
  505. controller->uiShow(),
  506. _intro.value() | rpl::map([](const Data::ChatIntro &intro) {
  507. return intro.sticker;
  508. }) | rpl::distinct_until_changed(),
  509. [=](DocumentData *sticker) {
  510. change([&](Data::ChatIntro &intro) {
  511. intro.sticker = sticker;
  512. });
  513. }));
  514. Ui::AddSkip(content);
  515. title->changes() | rpl::start_with_next([=] {
  516. change([&](Data::ChatIntro &intro) {
  517. intro.title = title->getLastText();
  518. });
  519. }, title->lifetime());
  520. description->changes() | rpl::start_with_next([=] {
  521. change([&](Data::ChatIntro &intro) {
  522. intro.description = description->getLastText();
  523. });
  524. }, description->lifetime());
  525. _setFocus = [=] {
  526. title->setFocusFast();
  527. };
  528. Ui::AddDividerText(
  529. content,
  530. tr::lng_chat_intro_about(),
  531. st::peerAppearanceDividerTextMargin);
  532. Ui::AddSkip(content);
  533. const auto resetWrap = content->add(
  534. object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
  535. content,
  536. object_ptr<Ui::SettingsButton>(
  537. content,
  538. tr::lng_chat_intro_reset(),
  539. st::settingsAttentionButton
  540. )));
  541. resetWrap->toggleOn(
  542. _intro.value() | rpl::map([](const Data::ChatIntro &intro) {
  543. return !!intro;
  544. }));
  545. resetWrap->entity()->setClickedCallback([=] {
  546. _intro = Data::ChatIntro();
  547. title->clear();
  548. description->clear();
  549. title->setFocus();
  550. });
  551. Ui::ResizeFitChild(this, content);
  552. }
  553. void ChatIntro::save() {
  554. const auto fail = [=](QString error) {
  555. };
  556. controller()->session().data().businessInfo().saveChatIntro(
  557. _intro.current(),
  558. fail);
  559. }
  560. } // namespace
  561. Type ChatIntroId() {
  562. return ChatIntro::Id();
  563. }
  564. } // namespace Settings