message_field.cpp 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303
  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 "chat_helpers/message_field.h"
  8. #include "history/history_widget.h"
  9. #include "history/history.h" // History::session
  10. #include "history/history_item.h" // HistoryItem::originalText
  11. #include "history/history_item_helpers.h" // DropDisallowedCustomEmoji
  12. #include "base/qthelp_regex.h"
  13. #include "base/qthelp_url.h"
  14. #include "base/event_filter.h"
  15. #include "ui/chat/chat_style.h"
  16. #include "ui/layers/generic_box.h"
  17. #include "ui/rect.h"
  18. #include "core/shortcuts.h"
  19. #include "core/application.h"
  20. #include "core/core_settings.h"
  21. #include "core/ui_integration.h"
  22. #include "ui/text/text_utilities.h"
  23. #include "ui/toast/toast.h"
  24. #include "ui/wrap/vertical_layout.h"
  25. #include "ui/widgets/buttons.h"
  26. #include "ui/widgets/popup_menu.h"
  27. #include "ui/power_saving.h"
  28. #include "ui/ui_utility.h"
  29. #include "data/data_session.h"
  30. #include "data/data_user.h"
  31. #include "data/data_document.h"
  32. #include "data/stickers/data_custom_emoji.h"
  33. #include "chat_helpers/emoji_suggestions_widget.h"
  34. #include "window/window_session_controller.h"
  35. #include "lang/lang_keys.h"
  36. #include "mainwindow.h"
  37. #include "main/main_session.h"
  38. #include "settings/settings_premium.h"
  39. #include "styles/style_layers.h"
  40. #include "styles/style_boxes.h"
  41. #include "styles/style_chat.h"
  42. #include "styles/style_chat_helpers.h"
  43. #include "styles/style_credits.h"
  44. #include "styles/style_settings.h"
  45. #include "base/qt/qt_common_adapters.h"
  46. #include <QtCore/QMimeData>
  47. #include <QtCore/QStack>
  48. #include <QtGui/QGuiApplication>
  49. #include <QtGui/QTextBlock>
  50. #include <QtGui/QClipboard>
  51. #include <QtWidgets/QApplication>
  52. namespace {
  53. using namespace Ui::Text;
  54. using EditLinkAction = Ui::InputField::EditLinkAction;
  55. using EditLinkSelection = Ui::InputField::EditLinkSelection;
  56. constexpr auto kParseLinksTimeout = crl::time(1000);
  57. constexpr auto kTypesDuration = 4 * crl::time(1000);
  58. constexpr auto kCodeLanguageLimit = 32;
  59. constexpr auto kLinkProtocols = {
  60. "http://",
  61. "https://",
  62. "tonsite://"
  63. };
  64. // For mention / custom emoji tags save and validate selfId,
  65. // ignore tags for different users.
  66. [[nodiscard]] Fn<QString(QStringView)> FieldTagMimeProcessor(
  67. not_null<Main::Session*> session,
  68. Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
  69. return [=](QStringView mimeTag) {
  70. const auto id = session->userId().bare;
  71. auto all = TextUtilities::SplitTags(mimeTag);
  72. auto premiumSkipped = (DocumentData*)nullptr;
  73. for (auto i = all.begin(); i != all.end();) {
  74. const auto tag = *i;
  75. if (TextUtilities::IsMentionLink(tag)
  76. && TextUtilities::MentionNameDataToFields(tag).selfId != id) {
  77. i = all.erase(i);
  78. continue;
  79. } else if (Ui::InputField::IsCustomEmojiLink(tag)) {
  80. const auto data = Ui::InputField::CustomEmojiEntityData(tag);
  81. const auto emoji = Data::ParseCustomEmojiData(data);
  82. if (!emoji) {
  83. i = all.erase(i);
  84. continue;
  85. } else if (!session->premium()) {
  86. const auto document = session->data().document(emoji);
  87. if (document->isPremiumEmoji()) {
  88. if (!allowPremiumEmoji
  89. || premiumSkipped
  90. || !session->premiumPossible()
  91. || !allowPremiumEmoji(document)) {
  92. premiumSkipped = document;
  93. i = all.erase(i);
  94. continue;
  95. }
  96. }
  97. }
  98. }
  99. ++i;
  100. }
  101. return TextUtilities::JoinTag(all);
  102. };
  103. }
  104. //bool ValidateUrl(const QString &value) {
  105. // const auto match = qthelp::RegExpDomain().match(value);
  106. // if (!match.hasMatch() || match.capturedStart() != 0) {
  107. // return false;
  108. // }
  109. // const auto protocolMatch = RegExpProtocol().match(value);
  110. // return protocolMatch.hasMatch()
  111. // && IsGoodProtocol(protocolMatch.captured(1));
  112. //}
  113. void EditLinkBox(
  114. not_null<Ui::GenericBox*> box,
  115. std::shared_ptr<Main::SessionShow> show,
  116. const TextWithTags &startText,
  117. const QString &startLink,
  118. Fn<void(TextWithTags, QString)> callback,
  119. const style::InputField *fieldStyle,
  120. Fn<QString(QString)> validate) {
  121. Expects(callback != nullptr);
  122. const auto &fieldSt = fieldStyle ? *fieldStyle : st::defaultInputField;
  123. const auto content = box->verticalLayout();
  124. const auto text = content->add(
  125. object_ptr<Ui::InputField>(
  126. content,
  127. fieldSt,
  128. Ui::InputField::Mode::SingleLine,
  129. tr::lng_formatting_link_text(),
  130. startText),
  131. st::markdownLinkFieldPadding);
  132. text->setInstantReplaces(Ui::InstantReplaces::Default());
  133. text->setInstantReplacesEnabled(
  134. Core::App().settings().replaceEmojiValue());
  135. Ui::Emoji::SuggestionsController::Init(
  136. box->getDelegate()->outerContainer(),
  137. text,
  138. &show->session());
  139. InitSpellchecker(show, text, fieldStyle != nullptr);
  140. const auto placeholder = content->add(
  141. object_ptr<Ui::RpWidget>(content),
  142. st::markdownLinkFieldPadding);
  143. placeholder->setAttribute(Qt::WA_TransparentForMouseEvents);
  144. const auto link = [&] {
  145. if (!startLink.trimmed().isEmpty()) {
  146. return startLink.trimmed();
  147. }
  148. const auto clipboard = QGuiApplication::clipboard()->text().trimmed();
  149. const auto starts = [&](const auto &protocol) {
  150. return clipboard.startsWith(protocol);
  151. };
  152. return std::ranges::any_of(kLinkProtocols, starts) ? clipboard : QString();
  153. }();
  154. const auto url = Ui::AttachParentChild(
  155. content,
  156. object_ptr<Ui::InputField>(
  157. content,
  158. fieldSt,
  159. tr::lng_formatting_link_url(),
  160. link));
  161. url->heightValue(
  162. ) | rpl::start_with_next([placeholder](int height) {
  163. placeholder->resize(placeholder->width(), height);
  164. }, placeholder->lifetime());
  165. placeholder->widthValue(
  166. ) | rpl::start_with_next([=](int width) {
  167. url->resize(width, url->height());
  168. }, placeholder->lifetime());
  169. url->move(placeholder->pos());
  170. const auto submit = [=] {
  171. const auto linkText = text->getTextWithTags();
  172. const auto linkUrl = validate(url->getLastText());
  173. if (linkText.text.isEmpty()) {
  174. text->showError();
  175. return;
  176. } else if (linkUrl.isEmpty()) {
  177. url->showError();
  178. return;
  179. }
  180. const auto weak = Ui::MakeWeak(box);
  181. callback(linkText, linkUrl);
  182. if (weak) {
  183. box->closeBox();
  184. }
  185. };
  186. text->submits(
  187. ) | rpl::start_with_next([=] {
  188. url->setFocusFast();
  189. }, text->lifetime());
  190. url->submits(
  191. ) | rpl::start_with_next([=] {
  192. if (text->getLastText().isEmpty()) {
  193. text->setFocusFast();
  194. } else {
  195. submit();
  196. }
  197. }, url->lifetime());
  198. box->setTitle(url->getLastText().isEmpty()
  199. ? tr::lng_formatting_link_create_title()
  200. : tr::lng_formatting_link_edit_title());
  201. box->addButton(tr::lng_formatting_link_create(), submit);
  202. box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
  203. content->resizeToWidth(st::boxWidth);
  204. content->moveToLeft(0, 0);
  205. box->setWidth(st::boxWidth);
  206. box->setFocusCallback([=] {
  207. if (startText.text.isEmpty()) {
  208. text->setFocusFast();
  209. } else {
  210. if (!url->empty()) {
  211. url->selectAll();
  212. }
  213. url->setFocusFast();
  214. }
  215. });
  216. url->customTab(true);
  217. text->customTab(true);
  218. const auto clearFullSelection = [=](not_null<Ui::InputField*> input) {
  219. if (input->empty()) {
  220. return;
  221. }
  222. auto cursor = input->rawTextEdit()->textCursor();
  223. const auto hasFull = (!cursor.selectionStart()
  224. && (cursor.selectionEnd()
  225. == (input->rawTextEdit()->document()->characterCount() - 1)));
  226. if (hasFull) {
  227. cursor.clearSelection();
  228. input->setTextCursor(cursor);
  229. }
  230. };
  231. url->tabbed(
  232. ) | rpl::start_with_next([=] {
  233. clearFullSelection(url);
  234. text->setFocus();
  235. }, url->lifetime());
  236. text->tabbed(
  237. ) | rpl::start_with_next([=] {
  238. if (!url->empty()) {
  239. url->selectAll();
  240. }
  241. clearFullSelection(text);
  242. url->setFocus();
  243. }, text->lifetime());
  244. }
  245. void EditCodeLanguageBox(
  246. not_null<Ui::GenericBox*> box,
  247. QString now,
  248. Fn<void(QString)> save) {
  249. Expects(save != nullptr);
  250. box->setTitle(tr::lng_formatting_code_title());
  251. box->addRow(object_ptr<Ui::FlatLabel>(
  252. box,
  253. tr::lng_formatting_code_language(),
  254. st::settingsAddReplyLabel));
  255. const auto field = box->addRow(object_ptr<Ui::InputField>(
  256. box,
  257. st::settingsAddReplyField,
  258. tr::lng_formatting_code_auto(),
  259. now.trimmed()));
  260. box->setFocusCallback([=] {
  261. field->setFocusFast();
  262. });
  263. field->selectAll();
  264. field->setMaxLength(kCodeLanguageLimit);
  265. Ui::AddLengthLimitLabel(field, kCodeLanguageLimit);
  266. const auto callback = [=] {
  267. const auto name = field->getLastText().trimmed();
  268. const auto check = QRegularExpression("^[a-zA-Z0-9\\+\\-]*$");
  269. if (check.match(name).hasMatch()) {
  270. auto weak = Ui::MakeWeak(box);
  271. save(name);
  272. if (const auto strong = weak.data()) {
  273. strong->closeBox();
  274. }
  275. } else {
  276. field->showError();
  277. }
  278. };
  279. field->submits(
  280. ) | rpl::start_with_next(callback, field->lifetime());
  281. box->addButton(tr::lng_settings_save(), callback);
  282. box->addButton(tr::lng_cancel(), [=] {
  283. box->closeBox();
  284. });
  285. }
  286. TextWithEntities StripSupportHashtag(TextWithEntities text) {
  287. static const auto expression = QRegularExpression(
  288. u"\\n?#tsf[a-z0-9_-]*[\\s#a-z0-9_-]*$"_q,
  289. QRegularExpression::CaseInsensitiveOption);
  290. const auto match = expression.match(text.text);
  291. if (!match.hasMatch()) {
  292. return text;
  293. }
  294. text.text.chop(match.capturedLength());
  295. const auto length = text.text.size();
  296. if (!length) {
  297. return TextWithEntities();
  298. }
  299. for (auto i = text.entities.begin(); i != text.entities.end();) {
  300. auto &entity = *i;
  301. if (entity.offset() >= length) {
  302. i = text.entities.erase(i);
  303. continue;
  304. } else if (entity.offset() + entity.length() > length) {
  305. entity.shrinkFromRight(length - entity.offset());
  306. }
  307. ++i;
  308. }
  309. return text;
  310. }
  311. } // namespace
  312. QString PrepareMentionTag(not_null<UserData*> user) {
  313. return TextUtilities::kMentionTagStart
  314. + QString::number(user->id.value)
  315. + '.'
  316. + QString::number(user->accessHash())
  317. + ':'
  318. + QString::number(user->session().userId().bare);
  319. }
  320. TextWithTags PrepareEditText(not_null<HistoryItem*> item) {
  321. auto original = item->history()->session().supportMode()
  322. ? StripSupportHashtag(item->originalText())
  323. : item->originalText();
  324. original = DropDisallowedCustomEmoji(
  325. item->history()->peer,
  326. std::move(original));
  327. return TextWithTags{
  328. original.text,
  329. TextUtilities::ConvertEntitiesToTextTags(original.entities)
  330. };
  331. }
  332. bool EditTextChanged(
  333. not_null<HistoryItem*> item,
  334. TextWithTags updated) {
  335. const auto original = PrepareEditText(item);
  336. auto originalWithEntities = TextWithEntities{
  337. std::move(original.text),
  338. TextUtilities::ConvertTextTagsToEntities(original.tags)
  339. };
  340. auto updatedWithEntities = TextWithEntities{
  341. std::move(updated.text),
  342. TextUtilities::ConvertTextTagsToEntities(updated.tags)
  343. };
  344. TextUtilities::PrepareForSending(originalWithEntities, 0);
  345. TextUtilities::PrepareForSending(updatedWithEntities, 0);
  346. // Tags can be different for the same entities, because for
  347. // animated emoji each tag contains a different random number.
  348. // So we compare entities instead of tags.
  349. return originalWithEntities != updatedWithEntities;
  350. }
  351. Fn<bool(
  352. Ui::InputField::EditLinkSelection selection,
  353. TextWithTags text,
  354. QString link,
  355. EditLinkAction action)> DefaultEditLinkCallback(
  356. std::shared_ptr<Main::SessionShow> show,
  357. not_null<Ui::InputField*> field,
  358. const style::InputField *fieldStyle) {
  359. const auto weak = Ui::MakeWeak(field);
  360. return [=](
  361. EditLinkSelection selection,
  362. TextWithTags text,
  363. QString link,
  364. EditLinkAction action) {
  365. if (action == EditLinkAction::Check) {
  366. return Ui::InputField::IsValidMarkdownLink(link)
  367. && !TextUtilities::IsMentionLink(link);
  368. }
  369. auto callback = [=](const TextWithTags &text, const QString &link) {
  370. if (const auto strong = weak.data()) {
  371. strong->commitMarkdownLinkEdit(selection, text, link);
  372. }
  373. };
  374. show->showBox(Box(
  375. EditLinkBox,
  376. show,
  377. text,
  378. link,
  379. std::move(callback),
  380. fieldStyle,
  381. qthelp::validate_url));
  382. return true;
  383. };
  384. }
  385. Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
  386. std::shared_ptr<Ui::Show> show) {
  387. return [=](QString now, Fn<void(QString)> save) {
  388. show->showBox(Box(EditCodeLanguageBox, now, save));
  389. };
  390. }
  391. void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args) {
  392. const auto paused = [passed = args.customEmojiPaused] {
  393. return passed && passed();
  394. };
  395. const auto field = args.field;
  396. const auto session = args.session;
  397. field->setTagMimeProcessor(
  398. FieldTagMimeProcessor(session, args.allowPremiumEmoji));
  399. field->setCustomTextContext(Core::TextContext({
  400. .session = session
  401. }), [paused] {
  402. return On(PowerSaving::kEmojiChat) || paused();
  403. }, [paused] {
  404. return On(PowerSaving::kChatSpoiler) || paused();
  405. });
  406. field->setInstantReplaces(Ui::InstantReplaces::Default());
  407. field->setInstantReplacesEnabled(
  408. Core::App().settings().replaceEmojiValue());
  409. field->setMarkdownReplacesEnabled(rpl::single(Ui::MarkdownEnabledState{
  410. Ui::MarkdownEnabled{ std::move(args.allowMarkdownTags) }
  411. }));
  412. if (const auto &show = args.show) {
  413. field->setEditLinkCallback(
  414. DefaultEditLinkCallback(show, field, args.fieldStyle));
  415. field->setEditLanguageCallback(DefaultEditLanguageCallback(show));
  416. InitSpellchecker(show, field, args.fieldStyle != nullptr);
  417. }
  418. const auto style = field->lifetime().make_state<Ui::ChatStyle>(
  419. session->colorIndicesValue());
  420. field->setPreCache([=] {
  421. return style->messageStyle(false, false).preCache.get();
  422. });
  423. field->setBlockquoteCache([=] {
  424. const auto colorIndex = session->user()->colorIndex();
  425. return style->coloredQuoteCache(false, colorIndex).get();
  426. });
  427. }
  428. [[nodiscard]] bool IsGoodFactcheckUrl(QStringView url) {
  429. return url.startsWith(u"t.me/"_q) || url.startsWith(u"https://t.me/"_q);
  430. }
  431. [[nodiscard]] Fn<bool(
  432. Ui::InputField::EditLinkSelection selection,
  433. TextWithTags text,
  434. QString link,
  435. EditLinkAction action)> FactcheckEditLinkCallback(
  436. std::shared_ptr<Main::SessionShow> show,
  437. not_null<Ui::InputField*> field) {
  438. const auto weak = Ui::MakeWeak(field);
  439. return [=](
  440. EditLinkSelection selection,
  441. TextWithTags text,
  442. QString link,
  443. EditLinkAction action) {
  444. const auto validate = [=](QString url) {
  445. if (IsGoodFactcheckUrl(url)) {
  446. const auto start = u"https://"_q;
  447. return url.startsWith(start) ? url : (start + url);
  448. }
  449. show->showToast(
  450. tr::lng_factcheck_links(tr::now, Ui::Text::RichLangValue));
  451. return QString();
  452. };
  453. if (action == EditLinkAction::Check) {
  454. return IsGoodFactcheckUrl(link);
  455. }
  456. auto callback = [=](const TextWithTags &text, const QString &link) {
  457. if (const auto strong = weak.data()) {
  458. strong->commitMarkdownLinkEdit(selection, text, link);
  459. }
  460. };
  461. show->showBox(Box(
  462. EditLinkBox,
  463. show,
  464. text,
  465. link,
  466. std::move(callback),
  467. nullptr,
  468. validate));
  469. return true;
  470. };
  471. }
  472. Fn<void(not_null<Ui::InputField*>)> FactcheckFieldIniter(
  473. std::shared_ptr<Main::SessionShow> show) {
  474. Expects(show != nullptr);
  475. return [=](not_null<Ui::InputField*> field) {
  476. field->setTagMimeProcessor([](QStringView mimeTag) {
  477. using Field = Ui::InputField;
  478. auto all = TextUtilities::SplitTags(mimeTag);
  479. for (auto i = all.begin(); i != all.end();) {
  480. const auto tag = *i;
  481. if (tag != Field::kTagBold
  482. && tag != Field::kTagItalic
  483. && (!Field::IsValidMarkdownLink(mimeTag)
  484. || TextUtilities::IsMentionLink(mimeTag))) {
  485. i = all.erase(i);
  486. continue;
  487. }
  488. ++i;
  489. }
  490. return TextUtilities::JoinTag(all);
  491. });
  492. field->setInstantReplaces(Ui::InstantReplaces::Default());
  493. field->setInstantReplacesEnabled(
  494. Core::App().settings().replaceEmojiValue());
  495. field->setMarkdownReplacesEnabled(rpl::single(
  496. Ui::MarkdownEnabledState{
  497. Ui::MarkdownEnabled{
  498. { Ui::InputField::kTagBold, Ui::InputField::kTagItalic }
  499. }
  500. }
  501. ));
  502. field->setEditLinkCallback(FactcheckEditLinkCallback(show, field));
  503. InitSpellchecker(show, field);
  504. };
  505. }
  506. void InitMessageFieldHandlers(
  507. not_null<Window::SessionController*> controller,
  508. not_null<Ui::InputField*> field,
  509. ChatHelpers::PauseReason pauseReasonLevel,
  510. Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
  511. InitMessageFieldHandlers({
  512. .session = &controller->session(),
  513. .show = controller->uiShow(),
  514. .field = field,
  515. .customEmojiPaused = [=] {
  516. return controller->isGifPausedAtLeastFor(pauseReasonLevel);
  517. },
  518. .allowPremiumEmoji = std::move(allowPremiumEmoji),
  519. });
  520. }
  521. void InitMessageFieldGeometry(not_null<Ui::InputField*> field) {
  522. field->setMinHeight(
  523. st::historySendSize.height() - 2 * st::historySendPadding);
  524. field->setMaxHeight(st::historyComposeFieldMaxHeight);
  525. field->setDocumentMargin(4.);
  526. field->setAdditionalMargin(style::ConvertScale(4) - 4);
  527. }
  528. void InitMessageField(
  529. std::shared_ptr<ChatHelpers::Show> show,
  530. not_null<Ui::InputField*> field,
  531. Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
  532. InitMessageFieldHandlers({
  533. .session = &show->session(),
  534. .show = show,
  535. .field = field,
  536. .customEmojiPaused = [=] {
  537. return show->paused(ChatHelpers::PauseReason::Any);
  538. },
  539. .allowPremiumEmoji = std::move(allowPremiumEmoji),
  540. });
  541. InitMessageFieldGeometry(field);
  542. }
  543. void InitMessageField(
  544. not_null<Window::SessionController*> controller,
  545. not_null<Ui::InputField*> field,
  546. Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
  547. return InitMessageField(
  548. controller->uiShow(),
  549. field,
  550. std::move(allowPremiumEmoji));
  551. }
  552. void InitSpellchecker(
  553. std::shared_ptr<Main::SessionShow> show,
  554. not_null<Ui::InputField*> field,
  555. bool skipDictionariesManager) {
  556. #ifndef TDESKTOP_DISABLE_SPELLCHECK
  557. using namespace Spellchecker;
  558. const auto session = &show->session();
  559. const auto menuItem = skipDictionariesManager
  560. ? std::nullopt
  561. : std::make_optional(SpellingHighlighter::CustomContextMenuItem{
  562. tr::lng_settings_manage_dictionaries(tr::now),
  563. [=] { show->showBox(Box<Ui::ManageDictionariesBox>(session)); }
  564. });
  565. const auto s = Ui::CreateChild<SpellingHighlighter>(
  566. field.get(),
  567. Core::App().settings().spellcheckerEnabledValue(),
  568. menuItem);
  569. field->setExtendedContextMenu(s->contextMenuCreated());
  570. #endif // TDESKTOP_DISABLE_SPELLCHECK
  571. }
  572. bool HasSendText(not_null<const Ui::InputField*> field) {
  573. const auto &text = field->getTextWithTags().text;
  574. for (const auto &ch : text) {
  575. const auto code = ch.unicode();
  576. if (!IsTrimmed(ch) && !IsReplacedBySpace(code)) {
  577. return true;
  578. }
  579. }
  580. return false;
  581. }
  582. void InitMessageFieldFade(
  583. not_null<Ui::InputField*> field,
  584. const style::color &bg) {
  585. class Fade final : public Ui::RpWidget {
  586. public:
  587. using Ui::RpWidget::RpWidget;
  588. void setFade(QPixmap &&fade) {
  589. _fade = std::move(fade);
  590. }
  591. int resizeGetHeight(int newWidth) override {
  592. return st::historyComposeFieldFadeHeight;
  593. }
  594. private:
  595. void paintEvent(QPaintEvent *event) override {
  596. auto p = QPainter(this);
  597. p.drawTiledPixmap(rect(), _fade);
  598. }
  599. QPixmap _fade;
  600. };
  601. const auto topFade = Ui::CreateChild<Fade>(field.get());
  602. const auto bottomFade = Ui::CreateChild<Fade>(field.get());
  603. const auto generateFade = [=] {
  604. const auto size = QSize(1, st::historyComposeFieldFadeHeight);
  605. auto fade = QPixmap(size * style::DevicePixelRatio());
  606. fade.setDevicePixelRatio(style::DevicePixelRatio());
  607. fade.fill(Qt::transparent);
  608. {
  609. auto p = QPainter(&fade);
  610. auto gradient = QLinearGradient(0, 1, 0, size.height());
  611. gradient.setStops({ { 0., bg->c }, { .9, Qt::transparent } });
  612. p.setPen(Qt::NoPen);
  613. p.setBrush(gradient);
  614. p.drawRect(Rect(size));
  615. }
  616. bottomFade->setFade(fade.transformed(QTransform().scale(1, -1)));
  617. topFade->setFade(std::move(fade));
  618. };
  619. generateFade();
  620. style::PaletteChanged(
  621. ) | rpl::start_with_next([=] {
  622. generateFade();
  623. }, topFade->lifetime());
  624. field->sizeValue(
  625. ) | rpl::start_with_next_done([=](const QSize &size) {
  626. topFade->resizeToWidth(size.width());
  627. bottomFade->resizeToWidth(size.width());
  628. bottomFade->move(
  629. 0,
  630. size.height() - st::historyComposeFieldFadeHeight);
  631. }, [t = Ui::MakeWeak(topFade), b = Ui::MakeWeak(bottomFade)] {
  632. Ui::DestroyChild(t.data());
  633. Ui::DestroyChild(b.data());
  634. }, topFade->lifetime());
  635. const auto descent = field->st().style.font->descent;
  636. rpl::merge(
  637. field->changes(),
  638. field->scrollTop().changes() | rpl::to_empty,
  639. field->sizeValue() | rpl::to_empty
  640. ) | rpl::start_with_next([=] {
  641. // InputField::changes fires before the auto-resize is being applied,
  642. // so for the scroll values to be accurate we enqueue the check.
  643. InvokeQueued(field, [=] {
  644. const auto topHidden = !field->scrollTop().current();
  645. if (topFade->isHidden() != topHidden) {
  646. topFade->setVisible(!topHidden);
  647. }
  648. const auto adjusted = field->scrollTop().current() + descent;
  649. const auto bottomHidden = (adjusted >= field->scrollTopMax());
  650. if (bottomFade->isHidden() != bottomHidden) {
  651. bottomFade->setVisible(!bottomHidden);
  652. }
  653. });
  654. }, topFade->lifetime());
  655. }
  656. InlineBotQuery ParseInlineBotQuery(
  657. not_null<Main::Session*> session,
  658. not_null<const Ui::InputField*> field) {
  659. auto result = InlineBotQuery();
  660. const auto &full = field->getTextWithTags();
  661. const auto &text = full.text;
  662. const auto textLength = text.size();
  663. auto inlineUsernameStart = 1;
  664. auto inlineUsernameLength = 0;
  665. if (textLength > 2 && text[0] == '@' && text[1].isLetter()) {
  666. inlineUsernameLength = 1;
  667. for (auto i = inlineUsernameStart + 1; i != textLength; ++i) {
  668. const auto ch = text[i];
  669. if (ch.isLetterOrNumber() || ch.unicode() == '_') {
  670. ++inlineUsernameLength;
  671. continue;
  672. } else if (!ch.isSpace()) {
  673. inlineUsernameLength = 0;
  674. }
  675. break;
  676. }
  677. auto inlineUsernameEnd = inlineUsernameStart + inlineUsernameLength;
  678. auto inlineUsernameEqualsText = (inlineUsernameEnd == textLength);
  679. auto validInlineUsername = false;
  680. if (inlineUsernameEqualsText) {
  681. validInlineUsername = text.endsWith(u"bot"_q);
  682. } else if (inlineUsernameEnd < textLength && inlineUsernameLength) {
  683. validInlineUsername = text[inlineUsernameEnd].isSpace();
  684. }
  685. if (validInlineUsername) {
  686. if (!full.tags.isEmpty()
  687. && (full.tags.front().offset
  688. < inlineUsernameStart + inlineUsernameLength)) {
  689. return InlineBotQuery();
  690. }
  691. auto username = base::StringViewMid(text, inlineUsernameStart, inlineUsernameLength);
  692. if (username != result.username) {
  693. result.username = username.toString();
  694. if (const auto peer = session->data().peerByUsername(result.username)) {
  695. if (const auto user = peer->asUser()) {
  696. result.bot = peer->asUser();
  697. } else {
  698. result.bot = nullptr;
  699. }
  700. result.lookingUpBot = false;
  701. } else {
  702. result.bot = nullptr;
  703. result.lookingUpBot = true;
  704. }
  705. }
  706. if (result.bot
  707. && (!result.bot->isBot()
  708. || result.bot->botInfo->inlinePlaceholder.isEmpty())) {
  709. result.bot = nullptr;
  710. } else {
  711. result.query = inlineUsernameEqualsText
  712. ? QString()
  713. : text.mid(inlineUsernameEnd + 1);
  714. return result;
  715. }
  716. } else {
  717. inlineUsernameLength = 0;
  718. }
  719. }
  720. if (inlineUsernameLength < 3) {
  721. result.bot = nullptr;
  722. result.username = QString();
  723. }
  724. result.query = QString();
  725. return result;
  726. }
  727. AutocompleteQuery ParseMentionHashtagBotCommandQuery(
  728. not_null<const Ui::InputField*> field,
  729. ChatHelpers::ComposeFeatures features) {
  730. auto result = AutocompleteQuery();
  731. const auto cursor = field->textCursor();
  732. if (cursor.hasSelection()) {
  733. return result;
  734. }
  735. const auto position = cursor.position();
  736. const auto document = field->document();
  737. const auto block = document->findBlock(position);
  738. for (auto item = block.begin(); !item.atEnd(); ++item) {
  739. const auto fragment = item.fragment();
  740. if (!fragment.isValid()) {
  741. continue;
  742. }
  743. const auto fragmentPosition = fragment.position();
  744. const auto fragmentEnd = fragmentPosition + fragment.length();
  745. if (fragmentPosition >= position || fragmentEnd < position) {
  746. continue;
  747. }
  748. const auto format = fragment.charFormat();
  749. if (format.isImageFormat()) {
  750. continue;
  751. }
  752. bool mentionInCommand = false;
  753. const auto text = fragment.text();
  754. for (auto i = position - fragmentPosition; i != 0; --i) {
  755. if (text[i - 1] == '@') {
  756. if (!features.autocompleteMentions) {
  757. return {};
  758. }
  759. if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_'))) {
  760. result.fromStart = (i == 1) && (fragmentPosition == 0);
  761. result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
  762. } else if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && i > 2 && (text[i - 2].isLetterOrNumber() || text[i - 2] == '_') && !mentionInCommand) {
  763. mentionInCommand = true;
  764. --i;
  765. continue;
  766. }
  767. return result;
  768. } else if (text[i - 1] == '#') {
  769. if (!features.autocompleteHashtags) {
  770. return {};
  771. }
  772. if (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_')) {
  773. result.fromStart = (i == 1) && (fragmentPosition == 0);
  774. result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
  775. }
  776. return result;
  777. } else if (text[i - 1] == '/') {
  778. if (!features.autocompleteCommands) {
  779. return {};
  780. }
  781. if (i < 2 && !fragmentPosition) {
  782. result.fromStart = (i == 1) && (fragmentPosition == 0);
  783. result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
  784. }
  785. return result;
  786. }
  787. if (position - fragmentPosition - i > 127 || (!mentionInCommand && (position - fragmentPosition - i > 63))) {
  788. break;
  789. }
  790. if (!text[i - 1].isLetterOrNumber() && text[i - 1] != '_') {
  791. break;
  792. }
  793. }
  794. break;
  795. }
  796. return result;
  797. }
  798. MessageLinksParser::MessageLinksParser(not_null<Ui::InputField*> field)
  799. : _field(field)
  800. , _timer([=] { parse(); }) {
  801. _lifetime = _field->changes(
  802. ) | rpl::start_with_next([=] {
  803. const auto length = _field->getTextWithTags().text.size();
  804. if (!length) {
  805. _lastLength = 0;
  806. _timer.cancel();
  807. parse();
  808. return;
  809. }
  810. const auto timeout = (std::abs(length - _lastLength) > 2)
  811. ? 0
  812. : kParseLinksTimeout;
  813. if (!_timer.isActive() || timeout < _timer.remainingTime()) {
  814. _timer.callOnce(timeout);
  815. }
  816. _lastLength = length;
  817. });
  818. _field->installEventFilter(this);
  819. }
  820. void MessageLinksParser::parseNow() {
  821. _timer.cancel();
  822. parse();
  823. }
  824. void MessageLinksParser::setDisabled(bool disabled) {
  825. _disabled = disabled;
  826. }
  827. bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {
  828. if (object == _field) {
  829. if (event->type() == QEvent::KeyPress) {
  830. const auto text = static_cast<QKeyEvent*>(event)->text();
  831. if (!text.isEmpty() && text.size() < 3) {
  832. const auto ch = text[0];
  833. if (IsSpace(ch)) {
  834. _timer.callOnce(0);
  835. }
  836. }
  837. } else if (event->type() == QEvent::Drop) {
  838. _timer.callOnce(0);
  839. }
  840. }
  841. return QObject::eventFilter(object, event);
  842. }
  843. void MessageLinksParser::parse() {
  844. const auto &textWithTags = _field->getTextWithTags();
  845. const auto &text = textWithTags.text;
  846. const auto &tags = textWithTags.tags;
  847. const auto &markdownTags = _field->getMarkdownTags();
  848. if (_disabled || text.isEmpty()) {
  849. _ranges = {};
  850. _list = QStringList();
  851. return;
  852. }
  853. const auto tagCanIntersectWithLink = [](const QString &tag) {
  854. return (tag == Ui::InputField::kTagBold)
  855. || (tag == Ui::InputField::kTagItalic)
  856. || (tag == Ui::InputField::kTagUnderline)
  857. || (tag == Ui::InputField::kTagStrikeOut)
  858. || (tag == Ui::InputField::kTagSpoiler)
  859. || (tag == Ui::InputField::kTagBlockquote)
  860. || (tag == Ui::InputField::kTagBlockquoteCollapsed);
  861. };
  862. _ranges.clear();
  863. auto tag = tags.begin();
  864. const auto tagsEnd = tags.end();
  865. const auto processTag = [&] {
  866. Expects(tag != tagsEnd);
  867. if (Ui::InputField::IsValidMarkdownLink(tag->id)
  868. && !TextUtilities::IsMentionLink(tag->id)) {
  869. _ranges.push_back({ tag->offset, tag->length, tag->id });
  870. }
  871. ++tag;
  872. };
  873. const auto processTagsBefore = [&](int offset) {
  874. while (tag != tagsEnd
  875. && (tag->offset + tag->length <= offset
  876. || tagCanIntersectWithLink(tag->id))) {
  877. processTag();
  878. }
  879. };
  880. const auto hasTagsIntersection = [&](int till) {
  881. if (tag == tagsEnd || tag->offset >= till) {
  882. return false;
  883. }
  884. while (tag != tagsEnd && tag->offset < till) {
  885. processTag();
  886. }
  887. return true;
  888. };
  889. auto markdownTag = markdownTags.begin();
  890. const auto markdownTagsEnd = markdownTags.end();
  891. const auto markdownTagsAllow = [&](int from, int length) {
  892. while (markdownTag != markdownTagsEnd
  893. && (markdownTag->adjustedStart
  894. + markdownTag->adjustedLength <= from
  895. || !markdownTag->closed
  896. || tagCanIntersectWithLink(markdownTag->tag))) {
  897. ++markdownTag;
  898. }
  899. if (markdownTag == markdownTagsEnd
  900. || markdownTag->adjustedStart >= from + length) {
  901. return true;
  902. }
  903. // Ignore http-links that are completely inside some tags.
  904. // This will allow sending http://test.com/__test__/test correctly.
  905. return (markdownTag->adjustedStart > from)
  906. || (markdownTag->adjustedStart
  907. + markdownTag->adjustedLength < from + length);
  908. };
  909. const auto len = text.size();
  910. const QChar *start = text.unicode(), *end = start + text.size();
  911. for (auto offset = 0, matchOffset = offset; offset < len;) {
  912. auto m = qthelp::RegExpDomain().match(text, matchOffset);
  913. if (!m.hasMatch()) break;
  914. auto domainOffset = m.capturedStart();
  915. auto protocol = m.captured(1).toLower();
  916. auto topDomain = m.captured(3).toLower();
  917. auto isProtocolValid = protocol.isEmpty() || TextUtilities::IsValidProtocol(protocol);
  918. auto isTopDomainValid = !protocol.isEmpty() || TextUtilities::IsValidTopDomain(topDomain);
  919. if (protocol.isEmpty() && domainOffset > offset + 1 && *(start + domainOffset - 1) == QChar('@')) {
  920. auto forMailName = text.mid(offset, domainOffset - offset - 1);
  921. auto mMailName = TextUtilities::RegExpMailNameAtEnd().match(forMailName);
  922. if (mMailName.hasMatch()) {
  923. offset = matchOffset = m.capturedEnd();
  924. continue;
  925. }
  926. }
  927. if (!isProtocolValid || !isTopDomainValid) {
  928. offset = matchOffset = m.capturedEnd();
  929. continue;
  930. }
  931. QStack<const QChar*> parenth;
  932. const QChar *domainEnd = start + m.capturedEnd(), *p = domainEnd;
  933. for (; p < end; ++p) {
  934. QChar ch(*p);
  935. if (IsLinkEnd(ch)) {
  936. break; // link finished
  937. } else if (IsAlmostLinkEnd(ch)) {
  938. const QChar *endTest = p + 1;
  939. while (endTest < end && IsAlmostLinkEnd(*endTest)) {
  940. ++endTest;
  941. }
  942. if (endTest >= end || IsLinkEnd(*endTest)) {
  943. break; // link finished at p
  944. }
  945. p = endTest;
  946. ch = *p;
  947. }
  948. if (ch == '(' || ch == '[' || ch == '{' || ch == '<') {
  949. parenth.push(p);
  950. } else if (ch == ')' || ch == ']' || ch == '}' || ch == '>') {
  951. if (parenth.isEmpty()) break;
  952. const QChar *q = parenth.pop(), open(*q);
  953. if ((ch == ')' && open != '(') || (ch == ']' && open != '[') || (ch == '}' && open != '{') || (ch == '>' && open != '<')) {
  954. p = q;
  955. break;
  956. }
  957. }
  958. }
  959. if (p > domainEnd) { // check, that domain ended
  960. if (domainEnd->unicode() != '/' && domainEnd->unicode() != '?') {
  961. matchOffset = domainEnd - start;
  962. continue;
  963. }
  964. }
  965. const auto range = MessageLinkRange{
  966. int(domainOffset),
  967. static_cast<int>(p - start - domainOffset),
  968. QString()
  969. };
  970. processTagsBefore(domainOffset);
  971. if (!hasTagsIntersection(range.start + range.length)) {
  972. if (markdownTagsAllow(range.start, range.length)) {
  973. _ranges.push_back(range);
  974. }
  975. }
  976. offset = matchOffset = p - start;
  977. }
  978. processTagsBefore(Ui::kQFixedMax);
  979. applyRanges(text);
  980. }
  981. void MessageLinksParser::applyRanges(const QString &text) {
  982. const auto count = int(_ranges.size());
  983. const auto current = _list.current();
  984. const auto computeLink = [&](const MessageLinkRange &range) {
  985. return range.custom.isEmpty()
  986. ? base::StringViewMid(text, range.start, range.length)
  987. : QStringView(range.custom);
  988. };
  989. const auto changed = [&] {
  990. if (current.size() != count) {
  991. return true;
  992. }
  993. for (auto i = 0; i != count; ++i) {
  994. if (computeLink(_ranges[i]) != current[i]) {
  995. return true;
  996. }
  997. }
  998. return false;
  999. }();
  1000. if (!changed) {
  1001. return;
  1002. }
  1003. auto parsed = QStringList();
  1004. parsed.reserve(count);
  1005. for (const auto &range : _ranges) {
  1006. parsed.push_back(computeLink(range).toString());
  1007. }
  1008. _list = std::move(parsed);
  1009. }
  1010. base::unique_qptr<Ui::RpWidget> CreateDisabledFieldView(
  1011. QWidget *parent,
  1012. not_null<PeerData*> peer) {
  1013. auto result = base::make_unique_q<Ui::AbstractButton>(parent);
  1014. const auto raw = result.get();
  1015. const auto label = CreateChild<Ui::FlatLabel>(
  1016. result.get(),
  1017. tr::lng_send_text_no(),
  1018. st::historySendDisabled);
  1019. label->setAttribute(Qt::WA_TransparentForMouseEvents);
  1020. raw->setPointerCursor(false);
  1021. const auto &st = st::historyComposeField;
  1022. const auto metrics = QFontMetricsF(st.style.font->f);
  1023. const auto realAscent = int(base::SafeRound(metrics.ascent()));
  1024. const auto ascentAdd = st.style.font->ascent - realAscent;
  1025. const auto customFontMarginTop = ascentAdd;
  1026. const auto leading = qMax(metrics.leading(), qreal(0.0));
  1027. const auto adjustment = (metrics.ascent() + leading)
  1028. - ((st.style.font->height * 4) / 5);
  1029. const auto placeholderCustomFontSkip = int(base::SafeRound(-adjustment));
  1030. const auto margins = st.textMargins
  1031. + st.placeholderMargins
  1032. + QMargins(0, style::ConvertScale(4)
  1033. + placeholderCustomFontSkip
  1034. + customFontMarginTop, 0, 0);
  1035. raw->widthValue(
  1036. ) | rpl::start_with_next([=](int width) {
  1037. const auto available = width - margins.left() - margins.right();
  1038. const auto skip = st::historySendDisabledIconSkip;
  1039. label->resizeToWidth(available - skip);
  1040. label->moveToLeft(margins.left() + skip, margins.top(), width);
  1041. }, label->lifetime());
  1042. raw->paintRequest(
  1043. ) | rpl::start_with_next([=] {
  1044. auto p = QPainter(raw);
  1045. const auto &icon = st::historySendDisabledIcon;
  1046. icon.paint(
  1047. p,
  1048. margins.left() + st::historySendDisabledPosition.x(),
  1049. margins.top() + st::historySendDisabledPosition.y(),
  1050. raw->width());
  1051. }, raw->lifetime());
  1052. using WeakToast = base::weak_ptr<Ui::Toast::Instance>;
  1053. const auto toast = raw->lifetime().make_state<WeakToast>();
  1054. raw->setClickedCallback([=] {
  1055. if (toast->get()) {
  1056. return;
  1057. }
  1058. using Flag = ChatRestriction;
  1059. const auto map = base::flat_map<Flag, tr::phrase<>>{
  1060. { Flag::SendPhotos, tr::lng_send_text_type_photos },
  1061. { Flag::SendVideos, tr::lng_send_text_type_videos },
  1062. {
  1063. Flag::SendVideoMessages,
  1064. tr::lng_send_text_type_video_messages,
  1065. },
  1066. { Flag::SendMusic, tr::lng_send_text_type_music },
  1067. {
  1068. Flag::SendVoiceMessages,
  1069. tr::lng_send_text_type_voice_messages,
  1070. },
  1071. { Flag::SendFiles, tr::lng_send_text_type_files },
  1072. { Flag::SendStickers, tr::lng_send_text_type_stickers },
  1073. { Flag::SendPolls, tr::lng_send_text_type_polls },
  1074. };
  1075. auto list = QStringList();
  1076. for (const auto &[flag, phrase] : map) {
  1077. if (Data::CanSend(peer, flag, false)) {
  1078. list.append(phrase(tr::now));
  1079. }
  1080. }
  1081. if (list.empty()) {
  1082. return;
  1083. }
  1084. const auto types = (list.size() > 1)
  1085. ? tr::lng_send_text_type_and_last(
  1086. tr::now,
  1087. lt_types,
  1088. list.mid(0, list.size() - 1).join(", "),
  1089. lt_last,
  1090. list.back())
  1091. : list.back();
  1092. *toast = Ui::Toast::Show(parent, {
  1093. .text = { tr::lng_send_text_no_about(tr::now, lt_types, types) },
  1094. .attach = RectPart::Bottom,
  1095. .duration = kTypesDuration,
  1096. });
  1097. });
  1098. return result;
  1099. }
  1100. base::unique_qptr<Ui::RpWidget> TextErrorSendRestriction(
  1101. QWidget *parent,
  1102. const QString &text) {
  1103. auto result = base::make_unique_q<Ui::RpWidget>(parent);
  1104. const auto raw = result.get();
  1105. const auto label = CreateChild<Ui::FlatLabel>(
  1106. result.get(),
  1107. text,
  1108. st::historySendPremiumRequired);
  1109. label->setAttribute(Qt::WA_TransparentForMouseEvents);
  1110. raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
  1111. QPainter(raw).fillRect(clip, st::windowBg);
  1112. }, raw->lifetime());
  1113. raw->sizeValue(
  1114. ) | rpl::start_with_next([=](QSize size) {
  1115. const auto &st = st::historyComposeField;
  1116. const auto width = size.width();
  1117. const auto margins = (st.textMargins + st.placeholderMargins);
  1118. const auto available = width - margins.left() - margins.right();
  1119. label->resizeToWidth(available);
  1120. label->moveToLeft(
  1121. margins.left(),
  1122. (size.height() - label->height()) / 2,
  1123. width);
  1124. }, label->lifetime());
  1125. return result;
  1126. }
  1127. base::unique_qptr<Ui::RpWidget> PremiumRequiredSendRestriction(
  1128. QWidget *parent,
  1129. not_null<UserData*> user,
  1130. not_null<Window::SessionController*> controller) {
  1131. auto result = base::make_unique_q<Ui::RpWidget>(parent);
  1132. const auto raw = result.get();
  1133. const auto label = CreateChild<Ui::FlatLabel>(
  1134. result.get(),
  1135. tr::lng_restricted_send_non_premium(
  1136. tr::now,
  1137. lt_user,
  1138. user->shortName()),
  1139. st::historySendPremiumRequired);
  1140. label->setAttribute(Qt::WA_TransparentForMouseEvents);
  1141. const auto link = CreateChild<Ui::LinkButton>(
  1142. result.get(),
  1143. tr::lng_restricted_send_non_premium_more(tr::now));
  1144. raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
  1145. QPainter(raw).fillRect(clip, st::windowBg);
  1146. }, raw->lifetime());
  1147. raw->widthValue(
  1148. ) | rpl::start_with_next([=](int width) {
  1149. const auto &st = st::historyComposeField;
  1150. const auto margins = (st.textMargins + st.placeholderMargins);
  1151. const auto available = width - margins.left() - margins.right();
  1152. label->resizeToWidth(available);
  1153. const auto height = label->height() + link->height();
  1154. const auto top = (raw->height() - height) / 2;
  1155. label->moveToLeft(margins.left(), top, width);
  1156. link->move(
  1157. (width - link->width()) / 2,
  1158. label->y() + label->height());
  1159. }, label->lifetime());
  1160. link->setClickedCallback([=] {
  1161. Settings::ShowPremium(controller, u"require_premium"_q);
  1162. });
  1163. return result;
  1164. }
  1165. void SelectTextInFieldWithMargins(
  1166. not_null<Ui::InputField*> field,
  1167. const TextSelection &selection) {
  1168. if (selection.empty()) {
  1169. return;
  1170. }
  1171. auto textCursor = field->textCursor();
  1172. // Try to set equal margins for top and bottom sides.
  1173. const auto charsCountInLine = field->width()
  1174. / field->st().style.font->width('W');
  1175. const auto linesCount = field->height() / field->st().style.font->height;
  1176. const auto selectedLines = (selection.to - selection.from)
  1177. / charsCountInLine;
  1178. constexpr auto kMinDiff = ushort(3);
  1179. if ((linesCount - selectedLines) > kMinDiff) {
  1180. textCursor.setPosition(selection.from
  1181. - charsCountInLine * ((linesCount - 1) / 2));
  1182. field->setTextCursor(textCursor);
  1183. }
  1184. textCursor.setPosition(selection.from);
  1185. field->setTextCursor(textCursor);
  1186. textCursor.setPosition(selection.to, QTextCursor::KeepAnchor);
  1187. field->setTextCursor(textCursor);
  1188. }
  1189. TextWithEntities PaidSendButtonText(tr::now_t, int stars) {
  1190. return Ui::Text::IconEmoji(&st::starIconEmoji).append(
  1191. Lang::FormatCountToShort(stars).string);
  1192. }
  1193. rpl::producer<TextWithEntities> PaidSendButtonText(
  1194. rpl::producer<int> stars,
  1195. rpl::producer<QString> fallback) {
  1196. if (fallback) {
  1197. return rpl::combine(
  1198. std::move(fallback),
  1199. std::move(stars)
  1200. ) | rpl::map([=](QString zero, int count) {
  1201. return count
  1202. ? PaidSendButtonText(tr::now, count)
  1203. : TextWithEntities{ zero };
  1204. });
  1205. }
  1206. return std::move(stars) | rpl::map([=](int count) {
  1207. return PaidSendButtonText(tr::now, count);
  1208. });
  1209. }