| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303 |
- /*
- This file is part of Telegram Desktop,
- the official desktop application for the Telegram messaging service.
- For license and copyright information please follow this link:
- https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
- */
- #include "chat_helpers/message_field.h"
- #include "history/history_widget.h"
- #include "history/history.h" // History::session
- #include "history/history_item.h" // HistoryItem::originalText
- #include "history/history_item_helpers.h" // DropDisallowedCustomEmoji
- #include "base/qthelp_regex.h"
- #include "base/qthelp_url.h"
- #include "base/event_filter.h"
- #include "ui/chat/chat_style.h"
- #include "ui/layers/generic_box.h"
- #include "ui/rect.h"
- #include "core/shortcuts.h"
- #include "core/application.h"
- #include "core/core_settings.h"
- #include "core/ui_integration.h"
- #include "ui/text/text_utilities.h"
- #include "ui/toast/toast.h"
- #include "ui/wrap/vertical_layout.h"
- #include "ui/widgets/buttons.h"
- #include "ui/widgets/popup_menu.h"
- #include "ui/power_saving.h"
- #include "ui/ui_utility.h"
- #include "data/data_session.h"
- #include "data/data_user.h"
- #include "data/data_document.h"
- #include "data/stickers/data_custom_emoji.h"
- #include "chat_helpers/emoji_suggestions_widget.h"
- #include "window/window_session_controller.h"
- #include "lang/lang_keys.h"
- #include "mainwindow.h"
- #include "main/main_session.h"
- #include "settings/settings_premium.h"
- #include "styles/style_layers.h"
- #include "styles/style_boxes.h"
- #include "styles/style_chat.h"
- #include "styles/style_chat_helpers.h"
- #include "styles/style_credits.h"
- #include "styles/style_settings.h"
- #include "base/qt/qt_common_adapters.h"
- #include <QtCore/QMimeData>
- #include <QtCore/QStack>
- #include <QtGui/QGuiApplication>
- #include <QtGui/QTextBlock>
- #include <QtGui/QClipboard>
- #include <QtWidgets/QApplication>
- namespace {
- using namespace Ui::Text;
- using EditLinkAction = Ui::InputField::EditLinkAction;
- using EditLinkSelection = Ui::InputField::EditLinkSelection;
- constexpr auto kParseLinksTimeout = crl::time(1000);
- constexpr auto kTypesDuration = 4 * crl::time(1000);
- constexpr auto kCodeLanguageLimit = 32;
- constexpr auto kLinkProtocols = {
- "http://",
- "https://",
- "tonsite://"
- };
- // For mention / custom emoji tags save and validate selfId,
- // ignore tags for different users.
- [[nodiscard]] Fn<QString(QStringView)> FieldTagMimeProcessor(
- not_null<Main::Session*> session,
- Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
- return [=](QStringView mimeTag) {
- const auto id = session->userId().bare;
- auto all = TextUtilities::SplitTags(mimeTag);
- auto premiumSkipped = (DocumentData*)nullptr;
- for (auto i = all.begin(); i != all.end();) {
- const auto tag = *i;
- if (TextUtilities::IsMentionLink(tag)
- && TextUtilities::MentionNameDataToFields(tag).selfId != id) {
- i = all.erase(i);
- continue;
- } else if (Ui::InputField::IsCustomEmojiLink(tag)) {
- const auto data = Ui::InputField::CustomEmojiEntityData(tag);
- const auto emoji = Data::ParseCustomEmojiData(data);
- if (!emoji) {
- i = all.erase(i);
- continue;
- } else if (!session->premium()) {
- const auto document = session->data().document(emoji);
- if (document->isPremiumEmoji()) {
- if (!allowPremiumEmoji
- || premiumSkipped
- || !session->premiumPossible()
- || !allowPremiumEmoji(document)) {
- premiumSkipped = document;
- i = all.erase(i);
- continue;
- }
- }
- }
- }
- ++i;
- }
- return TextUtilities::JoinTag(all);
- };
- }
- //bool ValidateUrl(const QString &value) {
- // const auto match = qthelp::RegExpDomain().match(value);
- // if (!match.hasMatch() || match.capturedStart() != 0) {
- // return false;
- // }
- // const auto protocolMatch = RegExpProtocol().match(value);
- // return protocolMatch.hasMatch()
- // && IsGoodProtocol(protocolMatch.captured(1));
- //}
- void EditLinkBox(
- not_null<Ui::GenericBox*> box,
- std::shared_ptr<Main::SessionShow> show,
- const TextWithTags &startText,
- const QString &startLink,
- Fn<void(TextWithTags, QString)> callback,
- const style::InputField *fieldStyle,
- Fn<QString(QString)> validate) {
- Expects(callback != nullptr);
- const auto &fieldSt = fieldStyle ? *fieldStyle : st::defaultInputField;
- const auto content = box->verticalLayout();
- const auto text = content->add(
- object_ptr<Ui::InputField>(
- content,
- fieldSt,
- Ui::InputField::Mode::SingleLine,
- tr::lng_formatting_link_text(),
- startText),
- st::markdownLinkFieldPadding);
- text->setInstantReplaces(Ui::InstantReplaces::Default());
- text->setInstantReplacesEnabled(
- Core::App().settings().replaceEmojiValue());
- Ui::Emoji::SuggestionsController::Init(
- box->getDelegate()->outerContainer(),
- text,
- &show->session());
- InitSpellchecker(show, text, fieldStyle != nullptr);
- const auto placeholder = content->add(
- object_ptr<Ui::RpWidget>(content),
- st::markdownLinkFieldPadding);
- placeholder->setAttribute(Qt::WA_TransparentForMouseEvents);
- const auto link = [&] {
- if (!startLink.trimmed().isEmpty()) {
- return startLink.trimmed();
- }
- const auto clipboard = QGuiApplication::clipboard()->text().trimmed();
- const auto starts = [&](const auto &protocol) {
- return clipboard.startsWith(protocol);
- };
- return std::ranges::any_of(kLinkProtocols, starts) ? clipboard : QString();
- }();
- const auto url = Ui::AttachParentChild(
- content,
- object_ptr<Ui::InputField>(
- content,
- fieldSt,
- tr::lng_formatting_link_url(),
- link));
- url->heightValue(
- ) | rpl::start_with_next([placeholder](int height) {
- placeholder->resize(placeholder->width(), height);
- }, placeholder->lifetime());
- placeholder->widthValue(
- ) | rpl::start_with_next([=](int width) {
- url->resize(width, url->height());
- }, placeholder->lifetime());
- url->move(placeholder->pos());
- const auto submit = [=] {
- const auto linkText = text->getTextWithTags();
- const auto linkUrl = validate(url->getLastText());
- if (linkText.text.isEmpty()) {
- text->showError();
- return;
- } else if (linkUrl.isEmpty()) {
- url->showError();
- return;
- }
- const auto weak = Ui::MakeWeak(box);
- callback(linkText, linkUrl);
- if (weak) {
- box->closeBox();
- }
- };
- text->submits(
- ) | rpl::start_with_next([=] {
- url->setFocusFast();
- }, text->lifetime());
- url->submits(
- ) | rpl::start_with_next([=] {
- if (text->getLastText().isEmpty()) {
- text->setFocusFast();
- } else {
- submit();
- }
- }, url->lifetime());
- box->setTitle(url->getLastText().isEmpty()
- ? tr::lng_formatting_link_create_title()
- : tr::lng_formatting_link_edit_title());
- box->addButton(tr::lng_formatting_link_create(), submit);
- box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
- content->resizeToWidth(st::boxWidth);
- content->moveToLeft(0, 0);
- box->setWidth(st::boxWidth);
- box->setFocusCallback([=] {
- if (startText.text.isEmpty()) {
- text->setFocusFast();
- } else {
- if (!url->empty()) {
- url->selectAll();
- }
- url->setFocusFast();
- }
- });
- url->customTab(true);
- text->customTab(true);
- const auto clearFullSelection = [=](not_null<Ui::InputField*> input) {
- if (input->empty()) {
- return;
- }
- auto cursor = input->rawTextEdit()->textCursor();
- const auto hasFull = (!cursor.selectionStart()
- && (cursor.selectionEnd()
- == (input->rawTextEdit()->document()->characterCount() - 1)));
- if (hasFull) {
- cursor.clearSelection();
- input->setTextCursor(cursor);
- }
- };
- url->tabbed(
- ) | rpl::start_with_next([=] {
- clearFullSelection(url);
- text->setFocus();
- }, url->lifetime());
- text->tabbed(
- ) | rpl::start_with_next([=] {
- if (!url->empty()) {
- url->selectAll();
- }
- clearFullSelection(text);
- url->setFocus();
- }, text->lifetime());
- }
- void EditCodeLanguageBox(
- not_null<Ui::GenericBox*> box,
- QString now,
- Fn<void(QString)> save) {
- Expects(save != nullptr);
- box->setTitle(tr::lng_formatting_code_title());
- box->addRow(object_ptr<Ui::FlatLabel>(
- box,
- tr::lng_formatting_code_language(),
- st::settingsAddReplyLabel));
- const auto field = box->addRow(object_ptr<Ui::InputField>(
- box,
- st::settingsAddReplyField,
- tr::lng_formatting_code_auto(),
- now.trimmed()));
- box->setFocusCallback([=] {
- field->setFocusFast();
- });
- field->selectAll();
- field->setMaxLength(kCodeLanguageLimit);
- Ui::AddLengthLimitLabel(field, kCodeLanguageLimit);
- const auto callback = [=] {
- const auto name = field->getLastText().trimmed();
- const auto check = QRegularExpression("^[a-zA-Z0-9\\+\\-]*$");
- if (check.match(name).hasMatch()) {
- auto weak = Ui::MakeWeak(box);
- save(name);
- if (const auto strong = weak.data()) {
- strong->closeBox();
- }
- } else {
- field->showError();
- }
- };
- field->submits(
- ) | rpl::start_with_next(callback, field->lifetime());
- box->addButton(tr::lng_settings_save(), callback);
- box->addButton(tr::lng_cancel(), [=] {
- box->closeBox();
- });
- }
- TextWithEntities StripSupportHashtag(TextWithEntities text) {
- static const auto expression = QRegularExpression(
- u"\\n?#tsf[a-z0-9_-]*[\\s#a-z0-9_-]*$"_q,
- QRegularExpression::CaseInsensitiveOption);
- const auto match = expression.match(text.text);
- if (!match.hasMatch()) {
- return text;
- }
- text.text.chop(match.capturedLength());
- const auto length = text.text.size();
- if (!length) {
- return TextWithEntities();
- }
- for (auto i = text.entities.begin(); i != text.entities.end();) {
- auto &entity = *i;
- if (entity.offset() >= length) {
- i = text.entities.erase(i);
- continue;
- } else if (entity.offset() + entity.length() > length) {
- entity.shrinkFromRight(length - entity.offset());
- }
- ++i;
- }
- return text;
- }
- } // namespace
- QString PrepareMentionTag(not_null<UserData*> user) {
- return TextUtilities::kMentionTagStart
- + QString::number(user->id.value)
- + '.'
- + QString::number(user->accessHash())
- + ':'
- + QString::number(user->session().userId().bare);
- }
- TextWithTags PrepareEditText(not_null<HistoryItem*> item) {
- auto original = item->history()->session().supportMode()
- ? StripSupportHashtag(item->originalText())
- : item->originalText();
- original = DropDisallowedCustomEmoji(
- item->history()->peer,
- std::move(original));
- return TextWithTags{
- original.text,
- TextUtilities::ConvertEntitiesToTextTags(original.entities)
- };
- }
- bool EditTextChanged(
- not_null<HistoryItem*> item,
- TextWithTags updated) {
- const auto original = PrepareEditText(item);
- auto originalWithEntities = TextWithEntities{
- std::move(original.text),
- TextUtilities::ConvertTextTagsToEntities(original.tags)
- };
- auto updatedWithEntities = TextWithEntities{
- std::move(updated.text),
- TextUtilities::ConvertTextTagsToEntities(updated.tags)
- };
- TextUtilities::PrepareForSending(originalWithEntities, 0);
- TextUtilities::PrepareForSending(updatedWithEntities, 0);
- // Tags can be different for the same entities, because for
- // animated emoji each tag contains a different random number.
- // So we compare entities instead of tags.
- return originalWithEntities != updatedWithEntities;
- }
- Fn<bool(
- Ui::InputField::EditLinkSelection selection,
- TextWithTags text,
- QString link,
- EditLinkAction action)> DefaultEditLinkCallback(
- std::shared_ptr<Main::SessionShow> show,
- not_null<Ui::InputField*> field,
- const style::InputField *fieldStyle) {
- const auto weak = Ui::MakeWeak(field);
- return [=](
- EditLinkSelection selection,
- TextWithTags text,
- QString link,
- EditLinkAction action) {
- if (action == EditLinkAction::Check) {
- return Ui::InputField::IsValidMarkdownLink(link)
- && !TextUtilities::IsMentionLink(link);
- }
- auto callback = [=](const TextWithTags &text, const QString &link) {
- if (const auto strong = weak.data()) {
- strong->commitMarkdownLinkEdit(selection, text, link);
- }
- };
- show->showBox(Box(
- EditLinkBox,
- show,
- text,
- link,
- std::move(callback),
- fieldStyle,
- qthelp::validate_url));
- return true;
- };
- }
- Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
- std::shared_ptr<Ui::Show> show) {
- return [=](QString now, Fn<void(QString)> save) {
- show->showBox(Box(EditCodeLanguageBox, now, save));
- };
- }
- void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args) {
- const auto paused = [passed = args.customEmojiPaused] {
- return passed && passed();
- };
- const auto field = args.field;
- const auto session = args.session;
- field->setTagMimeProcessor(
- FieldTagMimeProcessor(session, args.allowPremiumEmoji));
- field->setCustomTextContext(Core::TextContext({
- .session = session
- }), [paused] {
- return On(PowerSaving::kEmojiChat) || paused();
- }, [paused] {
- return On(PowerSaving::kChatSpoiler) || paused();
- });
- field->setInstantReplaces(Ui::InstantReplaces::Default());
- field->setInstantReplacesEnabled(
- Core::App().settings().replaceEmojiValue());
- field->setMarkdownReplacesEnabled(rpl::single(Ui::MarkdownEnabledState{
- Ui::MarkdownEnabled{ std::move(args.allowMarkdownTags) }
- }));
- if (const auto &show = args.show) {
- field->setEditLinkCallback(
- DefaultEditLinkCallback(show, field, args.fieldStyle));
- field->setEditLanguageCallback(DefaultEditLanguageCallback(show));
- InitSpellchecker(show, field, args.fieldStyle != nullptr);
- }
- const auto style = field->lifetime().make_state<Ui::ChatStyle>(
- session->colorIndicesValue());
- field->setPreCache([=] {
- return style->messageStyle(false, false).preCache.get();
- });
- field->setBlockquoteCache([=] {
- const auto colorIndex = session->user()->colorIndex();
- return style->coloredQuoteCache(false, colorIndex).get();
- });
- }
- [[nodiscard]] bool IsGoodFactcheckUrl(QStringView url) {
- return url.startsWith(u"t.me/"_q) || url.startsWith(u"https://t.me/"_q);
- }
- [[nodiscard]] Fn<bool(
- Ui::InputField::EditLinkSelection selection,
- TextWithTags text,
- QString link,
- EditLinkAction action)> FactcheckEditLinkCallback(
- std::shared_ptr<Main::SessionShow> show,
- not_null<Ui::InputField*> field) {
- const auto weak = Ui::MakeWeak(field);
- return [=](
- EditLinkSelection selection,
- TextWithTags text,
- QString link,
- EditLinkAction action) {
- const auto validate = [=](QString url) {
- if (IsGoodFactcheckUrl(url)) {
- const auto start = u"https://"_q;
- return url.startsWith(start) ? url : (start + url);
- }
- show->showToast(
- tr::lng_factcheck_links(tr::now, Ui::Text::RichLangValue));
- return QString();
- };
- if (action == EditLinkAction::Check) {
- return IsGoodFactcheckUrl(link);
- }
- auto callback = [=](const TextWithTags &text, const QString &link) {
- if (const auto strong = weak.data()) {
- strong->commitMarkdownLinkEdit(selection, text, link);
- }
- };
- show->showBox(Box(
- EditLinkBox,
- show,
- text,
- link,
- std::move(callback),
- nullptr,
- validate));
- return true;
- };
- }
- Fn<void(not_null<Ui::InputField*>)> FactcheckFieldIniter(
- std::shared_ptr<Main::SessionShow> show) {
- Expects(show != nullptr);
- return [=](not_null<Ui::InputField*> field) {
- field->setTagMimeProcessor([](QStringView mimeTag) {
- using Field = Ui::InputField;
- auto all = TextUtilities::SplitTags(mimeTag);
- for (auto i = all.begin(); i != all.end();) {
- const auto tag = *i;
- if (tag != Field::kTagBold
- && tag != Field::kTagItalic
- && (!Field::IsValidMarkdownLink(mimeTag)
- || TextUtilities::IsMentionLink(mimeTag))) {
- i = all.erase(i);
- continue;
- }
- ++i;
- }
- return TextUtilities::JoinTag(all);
- });
- field->setInstantReplaces(Ui::InstantReplaces::Default());
- field->setInstantReplacesEnabled(
- Core::App().settings().replaceEmojiValue());
- field->setMarkdownReplacesEnabled(rpl::single(
- Ui::MarkdownEnabledState{
- Ui::MarkdownEnabled{
- { Ui::InputField::kTagBold, Ui::InputField::kTagItalic }
- }
- }
- ));
- field->setEditLinkCallback(FactcheckEditLinkCallback(show, field));
- InitSpellchecker(show, field);
- };
- }
- void InitMessageFieldHandlers(
- not_null<Window::SessionController*> controller,
- not_null<Ui::InputField*> field,
- ChatHelpers::PauseReason pauseReasonLevel,
- Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
- InitMessageFieldHandlers({
- .session = &controller->session(),
- .show = controller->uiShow(),
- .field = field,
- .customEmojiPaused = [=] {
- return controller->isGifPausedAtLeastFor(pauseReasonLevel);
- },
- .allowPremiumEmoji = std::move(allowPremiumEmoji),
- });
- }
- void InitMessageFieldGeometry(not_null<Ui::InputField*> field) {
- field->setMinHeight(
- st::historySendSize.height() - 2 * st::historySendPadding);
- field->setMaxHeight(st::historyComposeFieldMaxHeight);
- field->setDocumentMargin(4.);
- field->setAdditionalMargin(style::ConvertScale(4) - 4);
- }
- void InitMessageField(
- std::shared_ptr<ChatHelpers::Show> show,
- not_null<Ui::InputField*> field,
- Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
- InitMessageFieldHandlers({
- .session = &show->session(),
- .show = show,
- .field = field,
- .customEmojiPaused = [=] {
- return show->paused(ChatHelpers::PauseReason::Any);
- },
- .allowPremiumEmoji = std::move(allowPremiumEmoji),
- });
- InitMessageFieldGeometry(field);
- }
- void InitMessageField(
- not_null<Window::SessionController*> controller,
- not_null<Ui::InputField*> field,
- Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
- return InitMessageField(
- controller->uiShow(),
- field,
- std::move(allowPremiumEmoji));
- }
- void InitSpellchecker(
- std::shared_ptr<Main::SessionShow> show,
- not_null<Ui::InputField*> field,
- bool skipDictionariesManager) {
- #ifndef TDESKTOP_DISABLE_SPELLCHECK
- using namespace Spellchecker;
- const auto session = &show->session();
- const auto menuItem = skipDictionariesManager
- ? std::nullopt
- : std::make_optional(SpellingHighlighter::CustomContextMenuItem{
- tr::lng_settings_manage_dictionaries(tr::now),
- [=] { show->showBox(Box<Ui::ManageDictionariesBox>(session)); }
- });
- const auto s = Ui::CreateChild<SpellingHighlighter>(
- field.get(),
- Core::App().settings().spellcheckerEnabledValue(),
- menuItem);
- field->setExtendedContextMenu(s->contextMenuCreated());
- #endif // TDESKTOP_DISABLE_SPELLCHECK
- }
- bool HasSendText(not_null<const Ui::InputField*> field) {
- const auto &text = field->getTextWithTags().text;
- for (const auto &ch : text) {
- const auto code = ch.unicode();
- if (!IsTrimmed(ch) && !IsReplacedBySpace(code)) {
- return true;
- }
- }
- return false;
- }
- void InitMessageFieldFade(
- not_null<Ui::InputField*> field,
- const style::color &bg) {
- class Fade final : public Ui::RpWidget {
- public:
- using Ui::RpWidget::RpWidget;
- void setFade(QPixmap &&fade) {
- _fade = std::move(fade);
- }
- int resizeGetHeight(int newWidth) override {
- return st::historyComposeFieldFadeHeight;
- }
- private:
- void paintEvent(QPaintEvent *event) override {
- auto p = QPainter(this);
- p.drawTiledPixmap(rect(), _fade);
- }
- QPixmap _fade;
- };
- const auto topFade = Ui::CreateChild<Fade>(field.get());
- const auto bottomFade = Ui::CreateChild<Fade>(field.get());
- const auto generateFade = [=] {
- const auto size = QSize(1, st::historyComposeFieldFadeHeight);
- auto fade = QPixmap(size * style::DevicePixelRatio());
- fade.setDevicePixelRatio(style::DevicePixelRatio());
- fade.fill(Qt::transparent);
- {
- auto p = QPainter(&fade);
- auto gradient = QLinearGradient(0, 1, 0, size.height());
- gradient.setStops({ { 0., bg->c }, { .9, Qt::transparent } });
- p.setPen(Qt::NoPen);
- p.setBrush(gradient);
- p.drawRect(Rect(size));
- }
- bottomFade->setFade(fade.transformed(QTransform().scale(1, -1)));
- topFade->setFade(std::move(fade));
- };
- generateFade();
- style::PaletteChanged(
- ) | rpl::start_with_next([=] {
- generateFade();
- }, topFade->lifetime());
- field->sizeValue(
- ) | rpl::start_with_next_done([=](const QSize &size) {
- topFade->resizeToWidth(size.width());
- bottomFade->resizeToWidth(size.width());
- bottomFade->move(
- 0,
- size.height() - st::historyComposeFieldFadeHeight);
- }, [t = Ui::MakeWeak(topFade), b = Ui::MakeWeak(bottomFade)] {
- Ui::DestroyChild(t.data());
- Ui::DestroyChild(b.data());
- }, topFade->lifetime());
- const auto descent = field->st().style.font->descent;
- rpl::merge(
- field->changes(),
- field->scrollTop().changes() | rpl::to_empty,
- field->sizeValue() | rpl::to_empty
- ) | rpl::start_with_next([=] {
- // InputField::changes fires before the auto-resize is being applied,
- // so for the scroll values to be accurate we enqueue the check.
- InvokeQueued(field, [=] {
- const auto topHidden = !field->scrollTop().current();
- if (topFade->isHidden() != topHidden) {
- topFade->setVisible(!topHidden);
- }
- const auto adjusted = field->scrollTop().current() + descent;
- const auto bottomHidden = (adjusted >= field->scrollTopMax());
- if (bottomFade->isHidden() != bottomHidden) {
- bottomFade->setVisible(!bottomHidden);
- }
- });
- }, topFade->lifetime());
- }
- InlineBotQuery ParseInlineBotQuery(
- not_null<Main::Session*> session,
- not_null<const Ui::InputField*> field) {
- auto result = InlineBotQuery();
- const auto &full = field->getTextWithTags();
- const auto &text = full.text;
- const auto textLength = text.size();
- auto inlineUsernameStart = 1;
- auto inlineUsernameLength = 0;
- if (textLength > 2 && text[0] == '@' && text[1].isLetter()) {
- inlineUsernameLength = 1;
- for (auto i = inlineUsernameStart + 1; i != textLength; ++i) {
- const auto ch = text[i];
- if (ch.isLetterOrNumber() || ch.unicode() == '_') {
- ++inlineUsernameLength;
- continue;
- } else if (!ch.isSpace()) {
- inlineUsernameLength = 0;
- }
- break;
- }
- auto inlineUsernameEnd = inlineUsernameStart + inlineUsernameLength;
- auto inlineUsernameEqualsText = (inlineUsernameEnd == textLength);
- auto validInlineUsername = false;
- if (inlineUsernameEqualsText) {
- validInlineUsername = text.endsWith(u"bot"_q);
- } else if (inlineUsernameEnd < textLength && inlineUsernameLength) {
- validInlineUsername = text[inlineUsernameEnd].isSpace();
- }
- if (validInlineUsername) {
- if (!full.tags.isEmpty()
- && (full.tags.front().offset
- < inlineUsernameStart + inlineUsernameLength)) {
- return InlineBotQuery();
- }
- auto username = base::StringViewMid(text, inlineUsernameStart, inlineUsernameLength);
- if (username != result.username) {
- result.username = username.toString();
- if (const auto peer = session->data().peerByUsername(result.username)) {
- if (const auto user = peer->asUser()) {
- result.bot = peer->asUser();
- } else {
- result.bot = nullptr;
- }
- result.lookingUpBot = false;
- } else {
- result.bot = nullptr;
- result.lookingUpBot = true;
- }
- }
- if (result.bot
- && (!result.bot->isBot()
- || result.bot->botInfo->inlinePlaceholder.isEmpty())) {
- result.bot = nullptr;
- } else {
- result.query = inlineUsernameEqualsText
- ? QString()
- : text.mid(inlineUsernameEnd + 1);
- return result;
- }
- } else {
- inlineUsernameLength = 0;
- }
- }
- if (inlineUsernameLength < 3) {
- result.bot = nullptr;
- result.username = QString();
- }
- result.query = QString();
- return result;
- }
- AutocompleteQuery ParseMentionHashtagBotCommandQuery(
- not_null<const Ui::InputField*> field,
- ChatHelpers::ComposeFeatures features) {
- auto result = AutocompleteQuery();
- const auto cursor = field->textCursor();
- if (cursor.hasSelection()) {
- return result;
- }
- const auto position = cursor.position();
- const auto document = field->document();
- const auto block = document->findBlock(position);
- for (auto item = block.begin(); !item.atEnd(); ++item) {
- const auto fragment = item.fragment();
- if (!fragment.isValid()) {
- continue;
- }
- const auto fragmentPosition = fragment.position();
- const auto fragmentEnd = fragmentPosition + fragment.length();
- if (fragmentPosition >= position || fragmentEnd < position) {
- continue;
- }
- const auto format = fragment.charFormat();
- if (format.isImageFormat()) {
- continue;
- }
- bool mentionInCommand = false;
- const auto text = fragment.text();
- for (auto i = position - fragmentPosition; i != 0; --i) {
- if (text[i - 1] == '@') {
- if (!features.autocompleteMentions) {
- return {};
- }
- if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_'))) {
- result.fromStart = (i == 1) && (fragmentPosition == 0);
- result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
- } else if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && i > 2 && (text[i - 2].isLetterOrNumber() || text[i - 2] == '_') && !mentionInCommand) {
- mentionInCommand = true;
- --i;
- continue;
- }
- return result;
- } else if (text[i - 1] == '#') {
- if (!features.autocompleteHashtags) {
- return {};
- }
- if (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_')) {
- result.fromStart = (i == 1) && (fragmentPosition == 0);
- result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
- }
- return result;
- } else if (text[i - 1] == '/') {
- if (!features.autocompleteCommands) {
- return {};
- }
- if (i < 2 && !fragmentPosition) {
- result.fromStart = (i == 1) && (fragmentPosition == 0);
- result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
- }
- return result;
- }
- if (position - fragmentPosition - i > 127 || (!mentionInCommand && (position - fragmentPosition - i > 63))) {
- break;
- }
- if (!text[i - 1].isLetterOrNumber() && text[i - 1] != '_') {
- break;
- }
- }
- break;
- }
- return result;
- }
- MessageLinksParser::MessageLinksParser(not_null<Ui::InputField*> field)
- : _field(field)
- , _timer([=] { parse(); }) {
- _lifetime = _field->changes(
- ) | rpl::start_with_next([=] {
- const auto length = _field->getTextWithTags().text.size();
- if (!length) {
- _lastLength = 0;
- _timer.cancel();
- parse();
- return;
- }
- const auto timeout = (std::abs(length - _lastLength) > 2)
- ? 0
- : kParseLinksTimeout;
- if (!_timer.isActive() || timeout < _timer.remainingTime()) {
- _timer.callOnce(timeout);
- }
- _lastLength = length;
- });
- _field->installEventFilter(this);
- }
- void MessageLinksParser::parseNow() {
- _timer.cancel();
- parse();
- }
- void MessageLinksParser::setDisabled(bool disabled) {
- _disabled = disabled;
- }
- bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {
- if (object == _field) {
- if (event->type() == QEvent::KeyPress) {
- const auto text = static_cast<QKeyEvent*>(event)->text();
- if (!text.isEmpty() && text.size() < 3) {
- const auto ch = text[0];
- if (IsSpace(ch)) {
- _timer.callOnce(0);
- }
- }
- } else if (event->type() == QEvent::Drop) {
- _timer.callOnce(0);
- }
- }
- return QObject::eventFilter(object, event);
- }
- void MessageLinksParser::parse() {
- const auto &textWithTags = _field->getTextWithTags();
- const auto &text = textWithTags.text;
- const auto &tags = textWithTags.tags;
- const auto &markdownTags = _field->getMarkdownTags();
- if (_disabled || text.isEmpty()) {
- _ranges = {};
- _list = QStringList();
- return;
- }
- const auto tagCanIntersectWithLink = [](const QString &tag) {
- return (tag == Ui::InputField::kTagBold)
- || (tag == Ui::InputField::kTagItalic)
- || (tag == Ui::InputField::kTagUnderline)
- || (tag == Ui::InputField::kTagStrikeOut)
- || (tag == Ui::InputField::kTagSpoiler)
- || (tag == Ui::InputField::kTagBlockquote)
- || (tag == Ui::InputField::kTagBlockquoteCollapsed);
- };
- _ranges.clear();
- auto tag = tags.begin();
- const auto tagsEnd = tags.end();
- const auto processTag = [&] {
- Expects(tag != tagsEnd);
- if (Ui::InputField::IsValidMarkdownLink(tag->id)
- && !TextUtilities::IsMentionLink(tag->id)) {
- _ranges.push_back({ tag->offset, tag->length, tag->id });
- }
- ++tag;
- };
- const auto processTagsBefore = [&](int offset) {
- while (tag != tagsEnd
- && (tag->offset + tag->length <= offset
- || tagCanIntersectWithLink(tag->id))) {
- processTag();
- }
- };
- const auto hasTagsIntersection = [&](int till) {
- if (tag == tagsEnd || tag->offset >= till) {
- return false;
- }
- while (tag != tagsEnd && tag->offset < till) {
- processTag();
- }
- return true;
- };
- auto markdownTag = markdownTags.begin();
- const auto markdownTagsEnd = markdownTags.end();
- const auto markdownTagsAllow = [&](int from, int length) {
- while (markdownTag != markdownTagsEnd
- && (markdownTag->adjustedStart
- + markdownTag->adjustedLength <= from
- || !markdownTag->closed
- || tagCanIntersectWithLink(markdownTag->tag))) {
- ++markdownTag;
- }
- if (markdownTag == markdownTagsEnd
- || markdownTag->adjustedStart >= from + length) {
- return true;
- }
- // Ignore http-links that are completely inside some tags.
- // This will allow sending http://test.com/__test__/test correctly.
- return (markdownTag->adjustedStart > from)
- || (markdownTag->adjustedStart
- + markdownTag->adjustedLength < from + length);
- };
- const auto len = text.size();
- const QChar *start = text.unicode(), *end = start + text.size();
- for (auto offset = 0, matchOffset = offset; offset < len;) {
- auto m = qthelp::RegExpDomain().match(text, matchOffset);
- if (!m.hasMatch()) break;
- auto domainOffset = m.capturedStart();
- auto protocol = m.captured(1).toLower();
- auto topDomain = m.captured(3).toLower();
- auto isProtocolValid = protocol.isEmpty() || TextUtilities::IsValidProtocol(protocol);
- auto isTopDomainValid = !protocol.isEmpty() || TextUtilities::IsValidTopDomain(topDomain);
- if (protocol.isEmpty() && domainOffset > offset + 1 && *(start + domainOffset - 1) == QChar('@')) {
- auto forMailName = text.mid(offset, domainOffset - offset - 1);
- auto mMailName = TextUtilities::RegExpMailNameAtEnd().match(forMailName);
- if (mMailName.hasMatch()) {
- offset = matchOffset = m.capturedEnd();
- continue;
- }
- }
- if (!isProtocolValid || !isTopDomainValid) {
- offset = matchOffset = m.capturedEnd();
- continue;
- }
- QStack<const QChar*> parenth;
- const QChar *domainEnd = start + m.capturedEnd(), *p = domainEnd;
- for (; p < end; ++p) {
- QChar ch(*p);
- if (IsLinkEnd(ch)) {
- break; // link finished
- } else if (IsAlmostLinkEnd(ch)) {
- const QChar *endTest = p + 1;
- while (endTest < end && IsAlmostLinkEnd(*endTest)) {
- ++endTest;
- }
- if (endTest >= end || IsLinkEnd(*endTest)) {
- break; // link finished at p
- }
- p = endTest;
- ch = *p;
- }
- if (ch == '(' || ch == '[' || ch == '{' || ch == '<') {
- parenth.push(p);
- } else if (ch == ')' || ch == ']' || ch == '}' || ch == '>') {
- if (parenth.isEmpty()) break;
- const QChar *q = parenth.pop(), open(*q);
- if ((ch == ')' && open != '(') || (ch == ']' && open != '[') || (ch == '}' && open != '{') || (ch == '>' && open != '<')) {
- p = q;
- break;
- }
- }
- }
- if (p > domainEnd) { // check, that domain ended
- if (domainEnd->unicode() != '/' && domainEnd->unicode() != '?') {
- matchOffset = domainEnd - start;
- continue;
- }
- }
- const auto range = MessageLinkRange{
- int(domainOffset),
- static_cast<int>(p - start - domainOffset),
- QString()
- };
- processTagsBefore(domainOffset);
- if (!hasTagsIntersection(range.start + range.length)) {
- if (markdownTagsAllow(range.start, range.length)) {
- _ranges.push_back(range);
- }
- }
- offset = matchOffset = p - start;
- }
- processTagsBefore(Ui::kQFixedMax);
- applyRanges(text);
- }
- void MessageLinksParser::applyRanges(const QString &text) {
- const auto count = int(_ranges.size());
- const auto current = _list.current();
- const auto computeLink = [&](const MessageLinkRange &range) {
- return range.custom.isEmpty()
- ? base::StringViewMid(text, range.start, range.length)
- : QStringView(range.custom);
- };
- const auto changed = [&] {
- if (current.size() != count) {
- return true;
- }
- for (auto i = 0; i != count; ++i) {
- if (computeLink(_ranges[i]) != current[i]) {
- return true;
- }
- }
- return false;
- }();
- if (!changed) {
- return;
- }
- auto parsed = QStringList();
- parsed.reserve(count);
- for (const auto &range : _ranges) {
- parsed.push_back(computeLink(range).toString());
- }
- _list = std::move(parsed);
- }
- base::unique_qptr<Ui::RpWidget> CreateDisabledFieldView(
- QWidget *parent,
- not_null<PeerData*> peer) {
- auto result = base::make_unique_q<Ui::AbstractButton>(parent);
- const auto raw = result.get();
- const auto label = CreateChild<Ui::FlatLabel>(
- result.get(),
- tr::lng_send_text_no(),
- st::historySendDisabled);
- label->setAttribute(Qt::WA_TransparentForMouseEvents);
- raw->setPointerCursor(false);
- const auto &st = st::historyComposeField;
- const auto metrics = QFontMetricsF(st.style.font->f);
- const auto realAscent = int(base::SafeRound(metrics.ascent()));
- const auto ascentAdd = st.style.font->ascent - realAscent;
- const auto customFontMarginTop = ascentAdd;
- const auto leading = qMax(metrics.leading(), qreal(0.0));
- const auto adjustment = (metrics.ascent() + leading)
- - ((st.style.font->height * 4) / 5);
- const auto placeholderCustomFontSkip = int(base::SafeRound(-adjustment));
- const auto margins = st.textMargins
- + st.placeholderMargins
- + QMargins(0, style::ConvertScale(4)
- + placeholderCustomFontSkip
- + customFontMarginTop, 0, 0);
- raw->widthValue(
- ) | rpl::start_with_next([=](int width) {
- const auto available = width - margins.left() - margins.right();
- const auto skip = st::historySendDisabledIconSkip;
- label->resizeToWidth(available - skip);
- label->moveToLeft(margins.left() + skip, margins.top(), width);
- }, label->lifetime());
- raw->paintRequest(
- ) | rpl::start_with_next([=] {
- auto p = QPainter(raw);
- const auto &icon = st::historySendDisabledIcon;
- icon.paint(
- p,
- margins.left() + st::historySendDisabledPosition.x(),
- margins.top() + st::historySendDisabledPosition.y(),
- raw->width());
- }, raw->lifetime());
- using WeakToast = base::weak_ptr<Ui::Toast::Instance>;
- const auto toast = raw->lifetime().make_state<WeakToast>();
- raw->setClickedCallback([=] {
- if (toast->get()) {
- return;
- }
- using Flag = ChatRestriction;
- const auto map = base::flat_map<Flag, tr::phrase<>>{
- { Flag::SendPhotos, tr::lng_send_text_type_photos },
- { Flag::SendVideos, tr::lng_send_text_type_videos },
- {
- Flag::SendVideoMessages,
- tr::lng_send_text_type_video_messages,
- },
- { Flag::SendMusic, tr::lng_send_text_type_music },
- {
- Flag::SendVoiceMessages,
- tr::lng_send_text_type_voice_messages,
- },
- { Flag::SendFiles, tr::lng_send_text_type_files },
- { Flag::SendStickers, tr::lng_send_text_type_stickers },
- { Flag::SendPolls, tr::lng_send_text_type_polls },
- };
- auto list = QStringList();
- for (const auto &[flag, phrase] : map) {
- if (Data::CanSend(peer, flag, false)) {
- list.append(phrase(tr::now));
- }
- }
- if (list.empty()) {
- return;
- }
- const auto types = (list.size() > 1)
- ? tr::lng_send_text_type_and_last(
- tr::now,
- lt_types,
- list.mid(0, list.size() - 1).join(", "),
- lt_last,
- list.back())
- : list.back();
- *toast = Ui::Toast::Show(parent, {
- .text = { tr::lng_send_text_no_about(tr::now, lt_types, types) },
- .attach = RectPart::Bottom,
- .duration = kTypesDuration,
- });
- });
- return result;
- }
- base::unique_qptr<Ui::RpWidget> TextErrorSendRestriction(
- QWidget *parent,
- const QString &text) {
- auto result = base::make_unique_q<Ui::RpWidget>(parent);
- const auto raw = result.get();
- const auto label = CreateChild<Ui::FlatLabel>(
- result.get(),
- text,
- st::historySendPremiumRequired);
- label->setAttribute(Qt::WA_TransparentForMouseEvents);
- raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
- QPainter(raw).fillRect(clip, st::windowBg);
- }, raw->lifetime());
- raw->sizeValue(
- ) | rpl::start_with_next([=](QSize size) {
- const auto &st = st::historyComposeField;
- const auto width = size.width();
- const auto margins = (st.textMargins + st.placeholderMargins);
- const auto available = width - margins.left() - margins.right();
- label->resizeToWidth(available);
- label->moveToLeft(
- margins.left(),
- (size.height() - label->height()) / 2,
- width);
- }, label->lifetime());
- return result;
- }
- base::unique_qptr<Ui::RpWidget> PremiumRequiredSendRestriction(
- QWidget *parent,
- not_null<UserData*> user,
- not_null<Window::SessionController*> controller) {
- auto result = base::make_unique_q<Ui::RpWidget>(parent);
- const auto raw = result.get();
- const auto label = CreateChild<Ui::FlatLabel>(
- result.get(),
- tr::lng_restricted_send_non_premium(
- tr::now,
- lt_user,
- user->shortName()),
- st::historySendPremiumRequired);
- label->setAttribute(Qt::WA_TransparentForMouseEvents);
- const auto link = CreateChild<Ui::LinkButton>(
- result.get(),
- tr::lng_restricted_send_non_premium_more(tr::now));
- raw->paintRequest() | rpl::start_with_next([=](QRect clip) {
- QPainter(raw).fillRect(clip, st::windowBg);
- }, raw->lifetime());
- raw->widthValue(
- ) | rpl::start_with_next([=](int width) {
- const auto &st = st::historyComposeField;
- const auto margins = (st.textMargins + st.placeholderMargins);
- const auto available = width - margins.left() - margins.right();
- label->resizeToWidth(available);
- const auto height = label->height() + link->height();
- const auto top = (raw->height() - height) / 2;
- label->moveToLeft(margins.left(), top, width);
- link->move(
- (width - link->width()) / 2,
- label->y() + label->height());
- }, label->lifetime());
- link->setClickedCallback([=] {
- Settings::ShowPremium(controller, u"require_premium"_q);
- });
- return result;
- }
- void SelectTextInFieldWithMargins(
- not_null<Ui::InputField*> field,
- const TextSelection &selection) {
- if (selection.empty()) {
- return;
- }
- auto textCursor = field->textCursor();
- // Try to set equal margins for top and bottom sides.
- const auto charsCountInLine = field->width()
- / field->st().style.font->width('W');
- const auto linesCount = field->height() / field->st().style.font->height;
- const auto selectedLines = (selection.to - selection.from)
- / charsCountInLine;
- constexpr auto kMinDiff = ushort(3);
- if ((linesCount - selectedLines) > kMinDiff) {
- textCursor.setPosition(selection.from
- - charsCountInLine * ((linesCount - 1) / 2));
- field->setTextCursor(textCursor);
- }
- textCursor.setPosition(selection.from);
- field->setTextCursor(textCursor);
- textCursor.setPosition(selection.to, QTextCursor::KeepAnchor);
- field->setTextCursor(textCursor);
- }
- TextWithEntities PaidSendButtonText(tr::now_t, int stars) {
- return Ui::Text::IconEmoji(&st::starIconEmoji).append(
- Lang::FormatCountToShort(stars).string);
- }
- rpl::producer<TextWithEntities> PaidSendButtonText(
- rpl::producer<int> stars,
- rpl::producer<QString> fallback) {
- if (fallback) {
- return rpl::combine(
- std::move(fallback),
- std::move(stars)
- ) | rpl::map([=](QString zero, int count) {
- return count
- ? PaidSendButtonText(tr::now, count)
- : TextWithEntities{ zero };
- });
- }
- return std::move(stars) | rpl::map([=](int count) {
- return PaidSendButtonText(tr::now, count);
- });
- }
|