edit_invite_link.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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 "ui/boxes/edit_invite_link.h"
  8. #include "base/unixtime.h"
  9. #include "lang/lang_keys.h"
  10. #include "ui/boxes/choose_date_time.h"
  11. #include "ui/layers/generic_box.h"
  12. #include "ui/vertical_list.h"
  13. #include "ui/text/format_values.h"
  14. #include "ui/widgets/checkbox.h"
  15. #include "ui/widgets/fields/input_field.h"
  16. #include "ui/widgets/fields/number_input.h"
  17. #include "ui/effects/credits_graphics.h"
  18. #include "ui/widgets/labels.h"
  19. #include "ui/wrap/slide_wrap.h"
  20. #include "styles/style_settings.h"
  21. #include "styles/style_layers.h"
  22. #include "styles/style_info.h"
  23. namespace Ui {
  24. namespace {
  25. constexpr auto kMaxLimit = std::numeric_limits<int>::max();
  26. constexpr auto kHour = 3600;
  27. constexpr auto kDay = 86400;
  28. constexpr auto kMaxLabelLength = 32;
  29. [[nodiscard]] QString FormatExpireDate(TimeId date) {
  30. if (date > 0) {
  31. return langDateTime(base::unixtime::parse(date));
  32. } else if (-date < kDay) {
  33. return tr::lng_hours(tr::now, lt_count, (-date / kHour));
  34. } else if (-date < 7 * kDay) {
  35. return tr::lng_days(tr::now, lt_count, (-date / kDay));
  36. } else {
  37. return tr::lng_weeks(tr::now, lt_count, (-date / (7 * kDay)));
  38. }
  39. }
  40. } // namespace
  41. void EditInviteLinkBox(
  42. not_null<GenericBox*> box,
  43. Fn<InviteLinkSubscriptionToggle()> fillSubscription,
  44. const InviteLinkFields &data,
  45. Fn<void(InviteLinkFields)> done) {
  46. using namespace rpl::mappers;
  47. const auto link = data.link;
  48. const auto isGroup = data.isGroup;
  49. const auto isPublic = data.isPublic;
  50. const auto subscriptionLocked = data.subscriptionCredits > 0;
  51. box->setTitle(link.isEmpty()
  52. ? tr::lng_group_invite_new_title()
  53. : tr::lng_group_invite_edit_title());
  54. const auto container = box->verticalLayout();
  55. const auto addTitle = [&](
  56. not_null<VerticalLayout*> container,
  57. rpl::producer<QString> text) {
  58. container->add(
  59. object_ptr<FlatLabel>(
  60. container,
  61. std::move(text),
  62. st::defaultSubsectionTitle),
  63. (st::defaultSubsectionTitlePadding
  64. + style::margins(0, st::defaultVerticalListSkip, 0, 0)));
  65. };
  66. const auto addDivider = [&](
  67. not_null<VerticalLayout*> container,
  68. rpl::producer<QString> text,
  69. style::margins margins = style::margins()) {
  70. container->add(
  71. object_ptr<DividerLabel>(
  72. container,
  73. object_ptr<FlatLabel>(
  74. container,
  75. std::move(text),
  76. st::boxDividerLabel),
  77. st::defaultBoxDividerLabelPadding),
  78. margins);
  79. };
  80. const auto now = base::unixtime::now();
  81. const auto expire = data.expireDate ? data.expireDate : kMaxLimit;
  82. const auto expireGroup = std::make_shared<RadiobuttonGroup>(expire);
  83. const auto usage = data.usageLimit ? data.usageLimit : kMaxLimit;
  84. const auto usageGroup = std::make_shared<RadiobuttonGroup>(usage);
  85. using Buttons = base::flat_map<int, base::unique_qptr<Radiobutton>>;
  86. struct State {
  87. Buttons expireButtons;
  88. Buttons usageButtons;
  89. int expireValue = 0;
  90. int usageValue = 0;
  91. rpl::variable<bool> requestApproval = false;
  92. rpl::variable<bool> subscription = false;
  93. };
  94. const auto state = box->lifetime().make_state<State>(State{
  95. .expireValue = expire,
  96. .usageValue = usage,
  97. .requestApproval = (data.requestApproval && !isPublic),
  98. .subscription = false,
  99. });
  100. const auto requestApproval = (isPublic || subscriptionLocked)
  101. ? nullptr
  102. : container->add(
  103. object_ptr<SettingsButton>(
  104. container,
  105. tr::lng_group_invite_request_approve(),
  106. st::settingsButtonNoIcon),
  107. style::margins{ 0, 0, 0, st::defaultVerticalListSkip });
  108. if (requestApproval) {
  109. requestApproval->toggleOn(state->requestApproval.value(), true);
  110. requestApproval->setClickedCallback([=] {
  111. state->requestApproval.force_assign(!requestApproval->toggled());
  112. state->subscription.force_assign(false);
  113. });
  114. addDivider(container, rpl::conditional(
  115. state->requestApproval.value(),
  116. (isGroup
  117. ? tr::lng_group_invite_about_approve()
  118. : tr::lng_group_invite_about_approve_channel()),
  119. (isGroup
  120. ? tr::lng_group_invite_about_no_approve()
  121. : tr::lng_group_invite_about_no_approve_channel())));
  122. }
  123. auto credits = (Ui::NumberInput*)(nullptr);
  124. if (!isPublic && fillSubscription) {
  125. Ui::AddSkip(container);
  126. const auto &[subscription, input] = fillSubscription();
  127. credits = input.get();
  128. subscription->toggleOn(state->subscription.value(), true);
  129. if (subscriptionLocked) {
  130. input->setText(QString::number(data.subscriptionCredits));
  131. input->setReadOnly(true);
  132. state->subscription.force_assign(true);
  133. state->requestApproval.force_assign(false);
  134. subscription->setToggleLocked(true);
  135. subscription->finishAnimating();
  136. }
  137. subscription->setClickedCallback([=, show = box->uiShow()] {
  138. if (subscriptionLocked) {
  139. show->showToast(
  140. tr::lng_group_invite_subscription_toast(tr::now));
  141. return;
  142. }
  143. state->subscription.force_assign(!subscription->toggled());
  144. state->requestApproval.force_assign(false);
  145. });
  146. }
  147. const auto labelField = container->add(
  148. object_ptr<Ui::InputField>(
  149. container,
  150. st::defaultInputField,
  151. tr::lng_group_invite_label_header(),
  152. data.label),
  153. style::margins(
  154. st::defaultSubsectionTitlePadding.left(),
  155. st::defaultVerticalListSkip,
  156. st::defaultSubsectionTitlePadding.right(),
  157. st::defaultVerticalListSkip * 2));
  158. labelField->setMaxLength(kMaxLabelLength);
  159. addDivider(container, tr::lng_group_invite_label_about());
  160. const auto &saveLabel = link.isEmpty()
  161. ? tr::lng_formatting_link_create
  162. : tr::lng_settings_save;
  163. box->addButton(saveLabel(), [=] {
  164. const auto label = labelField->getLastText();
  165. const auto expireDate = (state->expireValue == kMaxLimit)
  166. ? 0
  167. : (state->expireValue < 0)
  168. ? (base::unixtime::now() - state->expireValue)
  169. : state->expireValue;
  170. const auto usageLimit = (state->usageValue == kMaxLimit)
  171. ? 0
  172. : state->usageValue;
  173. done(InviteLinkFields{
  174. .link = link,
  175. .label = label,
  176. .expireDate = expireDate,
  177. .usageLimit = usageLimit,
  178. .subscriptionCredits = credits
  179. ? credits->getLastText().toInt()
  180. : 0,
  181. .requestApproval = state->requestApproval.current(),
  182. .isGroup = isGroup,
  183. .isPublic = isPublic,
  184. });
  185. });
  186. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  187. if (subscriptionLocked) {
  188. return;
  189. }
  190. addTitle(container, tr::lng_group_invite_expire_title());
  191. const auto expiresWrap = container->add(
  192. object_ptr<VerticalLayout>(container),
  193. style::margins(0, 0, 0, st::defaultVerticalListSkip));
  194. addDivider(
  195. container,
  196. tr::lng_group_invite_expire_about());
  197. const auto usagesSlide = container->add(
  198. object_ptr<SlideWrap<VerticalLayout>>(
  199. container,
  200. object_ptr<VerticalLayout>(container)));
  201. const auto usagesInner = usagesSlide->entity();
  202. addTitle(usagesInner, tr::lng_group_invite_usage_title());
  203. const auto usagesWrap = usagesInner->add(
  204. object_ptr<VerticalLayout>(usagesInner),
  205. style::margins(0, 0, 0, st::defaultVerticalListSkip));
  206. addDivider(usagesInner, tr::lng_group_invite_usage_about());
  207. static const auto addButton = [](
  208. not_null<VerticalLayout*> container,
  209. const std::shared_ptr<RadiobuttonGroup> &group,
  210. int value,
  211. const QString &text) {
  212. return container->add(
  213. object_ptr<Radiobutton>(
  214. container,
  215. group,
  216. value,
  217. text),
  218. st::inviteLinkLimitMargin);
  219. };
  220. const auto regenerate = [=] {
  221. expireGroup->setValue(state->expireValue);
  222. usageGroup->setValue(state->usageValue);
  223. auto expires = std::vector{ kMaxLimit, -kHour, -kDay, -kDay * 7, 0 };
  224. auto usages = std::vector{ kMaxLimit, 1, 10, 100, 0 };
  225. auto defaults = State();
  226. for (auto i = begin(expires); i != end(expires); ++i) {
  227. if (*i == state->expireValue) {
  228. break;
  229. } else if (*i == kMaxLimit) {
  230. continue;
  231. } else if (!*i || (now - *i >= state->expireValue)) {
  232. expires.insert(i, state->expireValue);
  233. break;
  234. }
  235. }
  236. for (auto i = begin(usages); i != end(usages); ++i) {
  237. if (*i == state->usageValue) {
  238. break;
  239. } else if (*i == kMaxLimit) {
  240. continue;
  241. } else if (!*i || *i > state->usageValue) {
  242. usages.insert(i, state->usageValue);
  243. break;
  244. }
  245. }
  246. state->expireButtons.clear();
  247. state->usageButtons.clear();
  248. for (const auto limit : expires) {
  249. const auto text = (limit == kMaxLimit)
  250. ? tr::lng_group_invite_expire_never(tr::now)
  251. : !limit
  252. ? tr::lng_group_invite_expire_custom(tr::now)
  253. : FormatExpireDate(limit);
  254. state->expireButtons.emplace(
  255. limit,
  256. addButton(expiresWrap, expireGroup, limit, text));
  257. }
  258. for (const auto limit : usages) {
  259. const auto text = (limit == kMaxLimit)
  260. ? tr::lng_group_invite_usage_any(tr::now)
  261. : !limit
  262. ? tr::lng_group_invite_usage_custom(tr::now)
  263. : Lang::FormatCountDecimal(limit);
  264. state->usageButtons.emplace(
  265. limit,
  266. addButton(usagesWrap, usageGroup, limit, text));
  267. }
  268. };
  269. const auto guard = MakeWeak(box);
  270. expireGroup->setChangedCallback([=](int value) {
  271. if (value) {
  272. state->expireValue = value;
  273. return;
  274. }
  275. expireGroup->setValue(state->expireValue);
  276. box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
  277. const auto save = [=](TimeId result) {
  278. if (!result) {
  279. return;
  280. }
  281. if (guard) {
  282. state->expireValue = result;
  283. regenerate();
  284. }
  285. box->closeBox();
  286. };
  287. const auto now = base::unixtime::now();
  288. const auto time = (state->expireValue == kMaxLimit)
  289. ? (now + kDay)
  290. : (state->expireValue > now)
  291. ? state->expireValue
  292. : (state->expireValue < 0)
  293. ? (now - state->expireValue)
  294. : (now + kDay);
  295. ChooseDateTimeBox(box, {
  296. .title = tr::lng_group_invite_expire_after(),
  297. .submit = tr::lng_settings_save(),
  298. .done = save,
  299. .time = time,
  300. });
  301. }));
  302. });
  303. usageGroup->setChangedCallback([=](int value) {
  304. if (value) {
  305. state->usageValue = value;
  306. return;
  307. }
  308. usageGroup->setValue(state->usageValue);
  309. box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
  310. const auto height = st::boxPadding.bottom()
  311. + st::defaultInputField.heightMin
  312. + st::boxPadding.bottom();
  313. box->setTitle(tr::lng_group_invite_expire_after());
  314. const auto wrap = box->addRow(object_ptr<FixedHeightWidget>(
  315. box,
  316. height));
  317. const auto input = CreateChild<NumberInput>(
  318. wrap,
  319. st::defaultInputField,
  320. tr::lng_group_invite_custom_limit(),
  321. (state->usageValue == kMaxLimit
  322. ? QString()
  323. : QString::number(state->usageValue)),
  324. 200'000);
  325. wrap->widthValue(
  326. ) | rpl::start_with_next([=](int width) {
  327. input->resize(width, input->height());
  328. input->moveToLeft(0, st::boxPadding.bottom());
  329. }, input->lifetime());
  330. box->setFocusCallback([=] {
  331. input->setFocusFast();
  332. });
  333. const auto save = [=] {
  334. const auto value = input->getLastText().toInt();
  335. if (value <= 0) {
  336. input->showError();
  337. return;
  338. }
  339. if (guard) {
  340. state->usageValue = value;
  341. regenerate();
  342. }
  343. box->closeBox();
  344. };
  345. QObject::connect(input, &NumberInput::submitted, save);
  346. box->addButton(tr::lng_settings_save(), save);
  347. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  348. }));
  349. });
  350. regenerate();
  351. usagesSlide->toggleOn(state->requestApproval.value() | rpl::map(!_1));
  352. usagesSlide->finishAnimating();
  353. }
  354. void CreateInviteLinkBox(
  355. not_null<GenericBox*> box,
  356. Fn<InviteLinkSubscriptionToggle()> fillSubscription,
  357. bool isGroup,
  358. bool isPublic,
  359. Fn<void(InviteLinkFields)> done) {
  360. EditInviteLinkBox(
  361. box,
  362. std::move(fillSubscription),
  363. InviteLinkFields{ .isGroup = isGroup, .isPublic = isPublic },
  364. std::move(done));
  365. }
  366. } // namespace Ui