edit_forum_topic_box.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  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 "boxes/peers/edit_forum_topic_box.h"
  8. #include "ui/widgets/fields/input_field.h"
  9. #include "ui/widgets/shadow.h"
  10. #include "ui/effects/emoji_fly_animation.h"
  11. #include "ui/abstract_button.h"
  12. #include "ui/vertical_list.h"
  13. #include "data/data_channel.h"
  14. #include "data/data_document.h"
  15. #include "data/data_forum.h"
  16. #include "data/data_forum_icons.h"
  17. #include "data/data_forum_topic.h"
  18. #include "data/data_session.h"
  19. #include "data/stickers/data_custom_emoji.h"
  20. #include "base/event_filter.h"
  21. #include "base/random.h"
  22. #include "base/qt_signal_producer.h"
  23. #include "chat_helpers/emoji_list_widget.h"
  24. #include "chat_helpers/stickers_list_footer.h"
  25. #include "boxes/premium_preview_box.h"
  26. #include "main/main_session.h"
  27. #include "history/history.h"
  28. #include "history/view/history_view_replies_section.h"
  29. #include "history/view/history_view_sticker_toast.h"
  30. #include "lang/lang_keys.h"
  31. #include "info/profile/info_profile_emoji_status_panel.h"
  32. #include "window/window_session_controller.h"
  33. #include "window/window_controller.h"
  34. #include "apiwrap.h"
  35. #include "mainwindow.h"
  36. #include "styles/style_layers.h"
  37. #include "styles/style_dialogs.h"
  38. #include "styles/style_chat_helpers.h"
  39. namespace {
  40. constexpr auto kDefaultIconId = DocumentId(0x7FFF'FFFF'FFFF'FFFFULL);
  41. using DefaultIcon = Data::TopicIconDescriptor;
  42. class DefaultIconEmoji final : public Ui::Text::CustomEmoji {
  43. public:
  44. DefaultIconEmoji(
  45. rpl::producer<DefaultIcon> value,
  46. Fn<void()> repaint,
  47. Data::CustomEmojiSizeTag tag);
  48. int width() override;
  49. QString entityData() override;
  50. void paint(QPainter &p, const Context &context) override;
  51. void unload() override;
  52. bool ready() override;
  53. bool readyInDefaultState() override;
  54. private:
  55. DefaultIcon _icon = {};
  56. QImage _image;
  57. Data::CustomEmojiSizeTag _tag = {};
  58. rpl::lifetime _lifetime;
  59. };
  60. DefaultIconEmoji::DefaultIconEmoji(
  61. rpl::producer<DefaultIcon> value,
  62. Fn<void()> repaint,
  63. Data::CustomEmojiSizeTag tag)
  64. : _tag(tag) {
  65. std::move(value) | rpl::start_with_next([=](DefaultIcon value) {
  66. _icon = value;
  67. _image = QImage();
  68. repaint();
  69. }, _lifetime);
  70. }
  71. int DefaultIconEmoji::width() {
  72. return st::emojiSize + 2 * st::emojiPadding;
  73. }
  74. QString DefaultIconEmoji::entityData() {
  75. return u"topic_icon:%1"_q.arg(_icon.colorId);
  76. }
  77. void DefaultIconEmoji::paint(QPainter &p, const Context &context) {
  78. const auto &st = (_tag == Data::CustomEmojiSizeTag::Normal)
  79. ? st::normalForumTopicIcon
  80. : st::defaultForumTopicIcon;
  81. if (_image.isNull()) {
  82. _image = Data::IsForumGeneralIconTitle(_icon.title)
  83. ? Data::ForumTopicGeneralIconFrame(
  84. st.size,
  85. Data::ParseForumGeneralIconColor(_icon.colorId))
  86. : Data::ForumTopicIconFrame(_icon.colorId, _icon.title, st);
  87. }
  88. const auto full = (_tag == Data::CustomEmojiSizeTag::Normal)
  89. ? Ui::Emoji::GetSizeNormal()
  90. : Ui::Emoji::GetSizeLarge();
  91. const auto esize = full / style::DevicePixelRatio();
  92. const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
  93. const auto skip = (customSize - st.size) / 2;
  94. p.drawImage(context.position + QPoint(skip, skip), _image);
  95. }
  96. void DefaultIconEmoji::unload() {
  97. _image = QImage();
  98. }
  99. bool DefaultIconEmoji::ready() {
  100. return true;
  101. }
  102. bool DefaultIconEmoji::readyInDefaultState() {
  103. return true;
  104. }
  105. [[nodiscard]] int EditIconSize() {
  106. const auto tag = Data::CustomEmojiManager::SizeTag::Large;
  107. return Data::FrameSizeFromTag(tag) / style::DevicePixelRatio();
  108. }
  109. [[nodiscard]] int32 ChooseNextColorId(
  110. int32 currentId,
  111. std::vector<int32> &otherIds) {
  112. if (otherIds.size() == 1 && otherIds.front() == currentId) {
  113. otherIds = Data::ForumTopicColorIds();
  114. }
  115. const auto i = ranges::find(otherIds, currentId);
  116. if (i != end(otherIds)) {
  117. otherIds.erase(i);
  118. }
  119. return otherIds.empty()
  120. ? currentId
  121. : otherIds[base::RandomIndex(otherIds.size())];
  122. }
  123. [[nodiscard]] not_null<Ui::AbstractButton*> EditIconButton(
  124. not_null<QWidget*> parent,
  125. not_null<Window::SessionController*> controller,
  126. rpl::producer<DefaultIcon> defaultIcon,
  127. rpl::producer<DocumentId> iconId,
  128. Fn<bool(not_null<Ui::RpWidget*>)> paintIconFrame) {
  129. using namespace Info::Profile;
  130. struct State {
  131. std::unique_ptr<Ui::Text::CustomEmoji> icon;
  132. QImage defaultIcon;
  133. };
  134. const auto tag = Data::CustomEmojiManager::SizeTag::Large;
  135. const auto size = EditIconSize();
  136. const auto result = Ui::CreateChild<Ui::AbstractButton>(parent.get());
  137. result->show();
  138. const auto state = result->lifetime().make_state<State>();
  139. std::move(
  140. iconId
  141. ) | rpl::start_with_next([=](DocumentId id) {
  142. const auto owner = &controller->session().data();
  143. state->icon = id
  144. ? owner->customEmojiManager().create(
  145. id,
  146. [=] { result->update(); },
  147. tag)
  148. : nullptr;
  149. result->update();
  150. }, result->lifetime());
  151. std::move(
  152. defaultIcon
  153. ) | rpl::start_with_next([=](DefaultIcon icon) {
  154. state->defaultIcon = Data::ForumTopicIconFrame(
  155. icon.colorId,
  156. icon.title,
  157. st::largeForumTopicIcon);
  158. result->update();
  159. }, result->lifetime());
  160. result->resize(size, size);
  161. result->paintRequest(
  162. ) | rpl::filter([=] {
  163. return !paintIconFrame(result);
  164. }) | rpl::start_with_next([=](QRect clip) {
  165. auto args = Ui::Text::CustomEmoji::Context{
  166. .textColor = st::windowFg->c,
  167. .now = crl::now(),
  168. .paused = controller->isGifPausedAtLeastFor(
  169. Window::GifPauseReason::Layer),
  170. };
  171. auto p = QPainter(result);
  172. if (state->icon) {
  173. state->icon->paint(p, args);
  174. } else {
  175. const auto skip = (size - st::largeForumTopicIcon.size) / 2;
  176. p.drawImage(skip, skip, state->defaultIcon);
  177. }
  178. }, result->lifetime());
  179. return result;
  180. }
  181. [[nodiscard]] not_null<Ui::AbstractButton*> GeneralIconPreview(
  182. not_null<QWidget*> parent) {
  183. using namespace Info::Profile;
  184. struct State {
  185. QImage frame;
  186. };
  187. const auto size = EditIconSize();
  188. const auto result = Ui::CreateChild<Ui::AbstractButton>(parent.get());
  189. result->show();
  190. result->setAttribute(Qt::WA_TransparentForMouseEvents);
  191. const auto state = result->lifetime().make_state<State>();
  192. rpl::single(rpl::empty) | rpl::then(
  193. style::PaletteChanged()
  194. ) | rpl::start_with_next([=] {
  195. state->frame = Data::ForumTopicGeneralIconFrame(
  196. st::largeForumTopicIcon.size,
  197. st::windowSubTextFg->c);
  198. result->update();
  199. }, result->lifetime());
  200. result->resize(size, size);
  201. result->paintRequest(
  202. ) | rpl::start_with_next([=](QRect clip) {
  203. auto p = QPainter(result);
  204. const auto skip = (size - st::largeForumTopicIcon.size) / 2;
  205. p.drawImage(skip, skip, state->frame);
  206. }, result->lifetime());
  207. return result;
  208. }
  209. struct IconSelector {
  210. Fn<bool(not_null<Ui::RpWidget*>)> paintIconFrame;
  211. rpl::producer<DocumentId> iconIdValue;
  212. };
  213. [[nodiscard]] IconSelector AddIconSelector(
  214. not_null<Ui::GenericBox*> box,
  215. not_null<Ui::RpWidget*> button,
  216. not_null<Window::SessionController*> controller,
  217. rpl::producer<DefaultIcon> defaultIcon,
  218. rpl::producer<int> coverHeight,
  219. DocumentId iconId,
  220. Fn<void(object_ptr<Ui::RpWidget>)> placeFooter) {
  221. using namespace ChatHelpers;
  222. struct State {
  223. std::unique_ptr<Ui::EmojiFlyAnimation> animation;
  224. std::unique_ptr<HistoryView::StickerToast> toast;
  225. rpl::variable<DocumentId> iconId;
  226. QPointer<QWidget> button;
  227. };
  228. const auto state = box->lifetime().make_state<State>(State{
  229. .iconId = iconId,
  230. .button = button.get(),
  231. });
  232. const auto manager = &controller->session().data().customEmojiManager();
  233. auto factory = [=](DocumentId id, Fn<void()> repaint)
  234. -> std::unique_ptr<Ui::Text::CustomEmoji> {
  235. const auto tag = Data::CustomEmojiManager::SizeTag::Large;
  236. if (id == kDefaultIconId) {
  237. return std::make_unique<DefaultIconEmoji>(
  238. rpl::duplicate(defaultIcon),
  239. std::move(repaint),
  240. tag);
  241. }
  242. return manager->create(id, std::move(repaint), tag);
  243. };
  244. const auto icons = &controller->session().data().forumIcons();
  245. const auto body = box->verticalLayout();
  246. const auto recent = [=] {
  247. auto list = icons->list();
  248. list.insert(begin(list), kDefaultIconId);
  249. return list;
  250. };
  251. const auto selector = body->add(
  252. object_ptr<EmojiListWidget>(body, EmojiListDescriptor{
  253. .show = controller->uiShow(),
  254. .mode = EmojiListWidget::Mode::TopicIcon,
  255. .paused = Window::PausedIn(controller, PauseReason::Layer),
  256. .customRecentList = DocumentListToRecent(recent()),
  257. .customRecentFactory = std::move(factory),
  258. .st = &st::reactPanelEmojiPan,
  259. }),
  260. st::reactPanelEmojiPan.padding);
  261. icons->requestDefaultIfUnknown();
  262. icons->defaultUpdates(
  263. ) | rpl::start_with_next([=] {
  264. selector->provideRecent(DocumentListToRecent(recent()));
  265. }, selector->lifetime());
  266. placeFooter(selector->createFooter());
  267. const auto shadow = Ui::CreateChild<Ui::PlainShadow>(box.get());
  268. shadow->show();
  269. rpl::combine(
  270. rpl::duplicate(coverHeight),
  271. selector->widthValue()
  272. ) | rpl::start_with_next([=](int top, int width) {
  273. shadow->setGeometry(0, top, width, st::lineWidth);
  274. }, shadow->lifetime());
  275. selector->refreshEmoji();
  276. selector->scrollToRequests(
  277. ) | rpl::start_with_next([=](int y) {
  278. box->scrollToY(y);
  279. shadow->update();
  280. }, selector->lifetime());
  281. rpl::combine(
  282. box->heightValue(),
  283. std::move(coverHeight),
  284. rpl::mappers::_1 - rpl::mappers::_2
  285. ) | rpl::start_with_next([=](int height) {
  286. selector->setMinimalHeight(selector->width(), height);
  287. }, body->lifetime());
  288. const auto showToast = [=](not_null<DocumentData*> document) {
  289. if (!state->toast) {
  290. state->toast = std::make_unique<HistoryView::StickerToast>(
  291. controller,
  292. controller->widget()->bodyWidget(),
  293. [=] { state->toast = nullptr; });
  294. }
  295. state->toast->showFor(
  296. document,
  297. HistoryView::StickerToast::Section::TopicIcon);
  298. };
  299. selector->customChosen(
  300. ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
  301. const auto owner = &controller->session().data();
  302. const auto document = data.document;
  303. const auto id = document->id;
  304. const auto custom = (id != kDefaultIconId);
  305. const auto premium = custom
  306. && !ranges::contains(document->owner().forumIcons().list(), id);
  307. if (premium && !controller->session().premium()) {
  308. showToast(document);
  309. return;
  310. }
  311. const auto body = controller->window().widget()->bodyWidget();
  312. if (state->button && custom) {
  313. const auto &from = data.messageSendingFrom;
  314. auto args = Ui::ReactionFlyAnimationArgs{
  315. .id = { { id } },
  316. .flyIcon = from.frame,
  317. .flyFrom = body->mapFromGlobal(from.globalStartGeometry),
  318. };
  319. state->animation = std::make_unique<Ui::EmojiFlyAnimation>(
  320. body,
  321. &owner->reactions(),
  322. std::move(args),
  323. [=] { state->animation->repaint(); },
  324. [] { return st::windowFg->c; },
  325. Data::CustomEmojiSizeTag::Large);
  326. }
  327. state->iconId = id;
  328. }, selector->lifetime());
  329. auto paintIconFrame = [=](not_null<Ui::RpWidget*> button) {
  330. if (!state->animation) {
  331. return false;
  332. } else if (state->animation->paintBadgeFrame(button)) {
  333. return true;
  334. }
  335. InvokeQueued(state->animation->layer(), [=] {
  336. state->animation = nullptr;
  337. });
  338. return false;
  339. };
  340. return {
  341. .paintIconFrame = std::move(paintIconFrame),
  342. .iconIdValue = state->iconId.value(),
  343. };
  344. }
  345. } // namespace
  346. void NewForumTopicBox(
  347. not_null<Ui::GenericBox*> box,
  348. not_null<Window::SessionController*> controller,
  349. not_null<History*> forum) {
  350. EditForumTopicBox(box, controller, forum, MsgId(0));
  351. }
  352. void EditForumTopicBox(
  353. not_null<Ui::GenericBox*> box,
  354. not_null<Window::SessionController*> controller,
  355. not_null<History*> forum,
  356. MsgId rootId) {
  357. const auto creating = !rootId;
  358. const auto topic = (!creating && forum->peer->forum())
  359. ? forum->peer->forum()->topicFor(rootId)
  360. : nullptr;
  361. const auto created = topic && !topic->creating();
  362. box->setTitle(creating
  363. ? tr::lng_forum_topic_new()
  364. : tr::lng_forum_topic_edit());
  365. box->setMaxHeight(st::editTopicMaxHeight);
  366. struct State {
  367. rpl::variable<DefaultIcon> defaultIcon;
  368. rpl::variable<DocumentId> iconId = 0;
  369. std::vector<int32> otherColorIds;
  370. mtpRequestId requestId = 0;
  371. Fn<bool(not_null<Ui::RpWidget*>)> paintIconFrame;
  372. };
  373. const auto state = box->lifetime().make_state<State>();
  374. const auto &colors = Data::ForumTopicColorIds();
  375. state->iconId = topic ? topic->iconId() : 0;
  376. state->otherColorIds = colors;
  377. state->defaultIcon = DefaultIcon{
  378. topic ? topic->title() : QString(),
  379. topic ? topic->colorId() : ChooseNextColorId(0, state->otherColorIds)
  380. };
  381. const auto top = box->setPinnedToTopContent(
  382. object_ptr<Ui::VerticalLayout>(box));
  383. const auto title = top->add(
  384. object_ptr<Ui::InputField>(
  385. box,
  386. st::defaultInputField,
  387. tr::lng_forum_topic_title(),
  388. topic ? topic->title() : QString()),
  389. st::editTopicTitleMargin);
  390. box->setFocusCallback([=] {
  391. title->setFocusFast();
  392. });
  393. const auto paintIconFrame = [=](not_null<Ui::RpWidget*> widget) {
  394. return state->paintIconFrame && state->paintIconFrame(widget);
  395. };
  396. const auto icon = (topic && topic->isGeneral())
  397. ? GeneralIconPreview(title->parentWidget())
  398. : EditIconButton(
  399. title->parentWidget(),
  400. controller,
  401. state->defaultIcon.value(),
  402. state->iconId.value(),
  403. paintIconFrame);
  404. title->geometryValue(
  405. ) | rpl::start_with_next([=](QRect geometry) {
  406. icon->move(
  407. st::editTopicIconPosition.x(),
  408. st::editTopicIconPosition.y());
  409. }, icon->lifetime());
  410. state->iconId.value(
  411. ) | rpl::start_with_next([=](DocumentId iconId) {
  412. icon->setAttribute(
  413. Qt::WA_TransparentForMouseEvents,
  414. created || (iconId != 0));
  415. }, box->lifetime());
  416. icon->setClickedCallback([=] {
  417. const auto current = state->defaultIcon.current();
  418. state->defaultIcon = DefaultIcon{
  419. current.title,
  420. ChooseNextColorId(current.colorId, state->otherColorIds),
  421. };
  422. });
  423. title->changes(
  424. ) | rpl::start_with_next([=] {
  425. state->defaultIcon = DefaultIcon{
  426. title->getLastText().trimmed(),
  427. state->defaultIcon.current().colorId,
  428. };
  429. }, title->lifetime());
  430. title->submits() | rpl::start_with_next([box] {
  431. box->triggerButton(0);
  432. }, title->lifetime());
  433. if (!topic || !topic->isGeneral()) {
  434. Ui::AddDividerText(top, tr::lng_forum_choose_title_and_icon());
  435. box->setScrollStyle(st::reactPanelScroll);
  436. auto selector = AddIconSelector(
  437. box,
  438. icon,
  439. controller,
  440. state->defaultIcon.value(),
  441. top->heightValue(),
  442. state->iconId.current(),
  443. [&](object_ptr<Ui::RpWidget> footer) {
  444. top->add(std::move(footer)); });
  445. state->paintIconFrame = std::move(selector.paintIconFrame);
  446. std::move(
  447. selector.iconIdValue
  448. ) | rpl::start_with_next([=](DocumentId iconId) {
  449. state->iconId = (iconId != kDefaultIconId) ? iconId : 0;
  450. }, box->lifetime());
  451. }
  452. const auto create = [=] {
  453. const auto channel = forum->peer->asChannel();
  454. if (!channel || !channel->isForum()) {
  455. box->closeBox();
  456. return;
  457. } else if (title->getLastText().trimmed().isEmpty()) {
  458. title->showError();
  459. return;
  460. }
  461. controller->showSection(
  462. std::make_shared<HistoryView::RepliesMemento>(
  463. forum,
  464. channel->forum()->reserveCreatingId(
  465. title->getLastText().trimmed(),
  466. state->defaultIcon.current().colorId,
  467. state->iconId.current())),
  468. Window::SectionShow::Way::ClearStack);
  469. };
  470. const auto save = [=] {
  471. const auto parent = forum->peer->forum();
  472. const auto topic = parent
  473. ? parent->topicFor(rootId)
  474. : nullptr;
  475. if (!topic) {
  476. box->closeBox();
  477. return;
  478. } else if (state->requestId > 0) {
  479. return;
  480. } else if (title->getLastText().trimmed().isEmpty()) {
  481. title->showError();
  482. return;
  483. } else if (parent->creating(rootId)) {
  484. topic->applyTitle(title->getLastText().trimmed());
  485. topic->applyColorId(state->defaultIcon.current().colorId);
  486. topic->applyIconId(state->iconId.current());
  487. box->closeBox();
  488. } else {
  489. using Flag = MTPchannels_EditForumTopic::Flag;
  490. const auto api = &forum->session().api();
  491. const auto weak = Ui::MakeWeak(box.get());
  492. state->requestId = api->request(MTPchannels_EditForumTopic(
  493. MTP_flags(Flag::f_title
  494. | (topic->isGeneral() ? Flag() : Flag::f_icon_emoji_id)),
  495. topic->channel()->inputChannel,
  496. MTP_int(rootId),
  497. MTP_string(title->getLastText().trimmed()),
  498. MTP_long(state->iconId.current()),
  499. MTPBool(), // closed
  500. MTPBool() // hidden
  501. )).done([=](const MTPUpdates &result) {
  502. api->applyUpdates(result);
  503. if (const auto strong = weak.data()) {
  504. strong->closeBox();
  505. }
  506. }).fail([=](const MTP::Error &error) {
  507. if (const auto strong = weak.data()) {
  508. if (error.type() == u"TOPIC_NOT_MODIFIED") {
  509. strong->closeBox();
  510. } else {
  511. state->requestId = -1;
  512. }
  513. }
  514. }).send();
  515. }
  516. };
  517. if (creating) {
  518. box->addButton(tr::lng_create_group_create(), create);
  519. } else {
  520. box->addButton(tr::lng_settings_save(), save);
  521. }
  522. box->addButton(tr::lng_cancel(), [=] {
  523. box->closeBox();
  524. });
  525. }
  526. std::unique_ptr<Ui::Text::CustomEmoji> MakeTopicIconEmoji(
  527. Data::TopicIconDescriptor descriptor,
  528. Fn<void()> repaint,
  529. Data::CustomEmojiSizeTag tag) {
  530. return std::make_unique<DefaultIconEmoji>(
  531. rpl::single(descriptor),
  532. std::move(repaint),
  533. tag);
  534. }