| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009 |
- /*
- 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 "boxes/peers/edit_peer_reactions.h"
- #include "apiwrap.h"
- #include "base/event_filter.h"
- #include "chat_helpers/emoji_list_widget.h"
- #include "chat_helpers/tabbed_panel.h"
- #include "chat_helpers/tabbed_selector.h"
- #include "core/ui_integration.h"
- #include "data/data_channel.h"
- #include "data/data_chat.h"
- #include "data/data_document.h"
- #include "data/data_peer_values.h" // UniqueReactionsLimit.
- #include "data/data_session.h"
- #include "data/data_user.h"
- #include "history/view/reactions/history_view_reactions_selector.h"
- #include "lang/lang_keys.h"
- #include "main/main_session.h"
- #include "ui/boxes/boost_box.h"
- #include "ui/layers/generic_box.h"
- #include "ui/text/text_utilities.h"
- #include "ui/vertical_list.h"
- #include "ui/widgets/checkbox.h"
- #include "ui/widgets/continuous_sliders.h"
- #include "ui/widgets/fields/input_field.h"
- #include "ui/wrap/slide_wrap.h"
- #include "ui/ui_utility.h"
- #include "window/window_session_controller.h"
- #include "window/window_session_controller_link_info.h"
- #include "styles/style_chat_helpers.h"
- #include "styles/style_info.h"
- #include "styles/style_layers.h"
- #include "styles/style_settings.h"
- #include <QtWidgets/QTextEdit>
- #include <QtGui/QTextBlock>
- #include <QtGui/QTextDocumentFragment>
- namespace {
- constexpr auto kDisabledEmojiOpacity = 0.4;
- struct UniqueCustomEmojiContext {
- std::vector<DocumentId> ids;
- Fn<bool(DocumentId)> applyHardLimit;
- int hardLimit = 0;
- int hardLimitChecked = 0;
- bool hardLimitHit = false;
- };
- class MaybeDisabledEmoji final : public Ui::Text::CustomEmoji {
- public:
- MaybeDisabledEmoji(
- std::unique_ptr<CustomEmoji> wrapped,
- Fn<bool()> enabled);
- int width() override;
- QString entityData() override;
- void paint(QPainter &p, const Context &context) override;
- void unload() override;
- bool ready() override;
- bool readyInDefaultState() override;
- private:
- const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
- const Fn<bool()> _enabled;
- };
- MaybeDisabledEmoji::MaybeDisabledEmoji(
- std::unique_ptr<CustomEmoji> wrapped,
- Fn<bool()> enabled)
- : _wrapped(std::move(wrapped))
- , _enabled(std::move(enabled)) {
- }
- int MaybeDisabledEmoji::width() {
- return _wrapped->width();
- }
- QString MaybeDisabledEmoji::entityData() {
- return _wrapped->entityData();
- }
- void MaybeDisabledEmoji::paint(QPainter &p, const Context &context) {
- const auto disabled = !_enabled();
- const auto was = disabled ? p.opacity() : 1.;
- if (disabled) {
- p.setOpacity(kDisabledEmojiOpacity);
- }
- _wrapped->paint(p, context);
- if (disabled) {
- p.setOpacity(was);
- }
- }
- void MaybeDisabledEmoji::unload() {
- _wrapped->unload();
- }
- bool MaybeDisabledEmoji::ready() {
- return _wrapped->ready();
- }
- bool MaybeDisabledEmoji::readyInDefaultState() {
- return _wrapped->readyInDefaultState();
- }
- [[nodiscard]] QString AllowOnlyCustomEmojiProcessor(QStringView mimeTag) {
- auto all = TextUtilities::SplitTags(mimeTag);
- for (auto i = all.begin(); i != all.end();) {
- if (Ui::InputField::IsCustomEmojiLink(*i)) {
- ++i;
- } else {
- i = all.erase(i);
- }
- }
- return TextUtilities::JoinTag(all);
- }
- [[nodiscard]] bool AllowOnlyCustomEmojiMimeDataHook(
- not_null<const QMimeData*> data,
- Ui::InputField::MimeAction action) {
- if (action == Ui::InputField::MimeAction::Check) {
- const auto textMime = TextUtilities::TagsTextMimeType();
- const auto tagsMime = TextUtilities::TagsMimeType();
- if (!data->hasFormat(textMime) || !data->hasFormat(tagsMime)) {
- return false;
- }
- auto text = QString::fromUtf8(data->data(textMime));
- auto tags = TextUtilities::DeserializeTags(
- data->data(tagsMime),
- text.size());
- auto checkedTill = 0;
- ranges::sort(tags, ranges::less(), &TextWithTags::Tag::offset);
- for (const auto &tag : tags) {
- if (tag.offset != checkedTill
- || AllowOnlyCustomEmojiProcessor(tag.id) != tag.id) {
- return false;
- }
- checkedTill += tag.length;
- }
- return true;
- } else if (action == Ui::InputField::MimeAction::Insert) {
- return false;
- }
- Unexpected("Action in MimeData hook.");
- }
- [[nodiscard]] std::vector<Data::ReactionId> DefaultSelected() {
- const auto like = QString::fromUtf8("\xf0\x9f\x91\x8d");
- const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e");
- return { Data::ReactionId{ like }, Data::ReactionId{ dislike } };
- }
- [[nodiscard]] bool RemoveNonCustomEmojiFragment(
- not_null<QTextDocument*> document,
- UniqueCustomEmojiContext &context) {
- context.ids.clear();
- context.hardLimitChecked = 0;
- auto removeFrom = 0;
- auto removeTill = 0;
- auto block = document->begin();
- for (auto j = block.begin(); !j.atEnd(); ++j) {
- const auto fragment = j.fragment();
- Assert(fragment.isValid());
- removeTill = removeFrom = fragment.position();
- const auto format = fragment.charFormat();
- if (format.objectType() != Ui::InputField::kCustomEmojiFormat) {
- removeTill += fragment.length();
- break;
- }
- const auto id = format.property(Ui::InputField::kCustomEmojiId);
- const auto documentId = id.toULongLong();
- const auto applyHardLimit = context.applyHardLimit(documentId);
- if (ranges::contains(context.ids, documentId)) {
- removeTill += fragment.length();
- break;
- } else if (applyHardLimit
- && context.hardLimitChecked >= context.hardLimit) {
- context.hardLimitHit = true;
- removeTill += fragment.length();
- break;
- }
- context.ids.push_back(documentId);
- if (applyHardLimit) {
- ++context.hardLimitChecked;
- }
- }
- while (removeTill == removeFrom) {
- block = block.next();
- if (block == document->end()) {
- return false;
- }
- removeTill = block.position();
- }
- Ui::PrepareFormattingOptimization(document);
- auto cursor = QTextCursor(document);
- cursor.setPosition(removeFrom);
- cursor.setPosition(removeTill, QTextCursor::KeepAnchor);
- cursor.removeSelectedText();
- return true;
- }
- bool RemoveNonCustomEmoji(
- not_null<QTextDocument*> document,
- UniqueCustomEmojiContext &context) {
- if (!RemoveNonCustomEmojiFragment(document, context)) {
- return false;
- }
- while (RemoveNonCustomEmojiFragment(document, context)) {
- }
- return true;
- }
- void SetupOnlyCustomEmojiField(
- not_null<Ui::InputField*> field,
- Fn<void(std::vector<DocumentId>, bool)> callback,
- Fn<bool(DocumentId)> applyHardLimit,
- int customHardLimit) {
- field->setTagMimeProcessor(AllowOnlyCustomEmojiProcessor);
- field->setMimeDataHook(AllowOnlyCustomEmojiMimeDataHook);
- struct State {
- bool processing = false;
- bool pending = false;
- };
- const auto state = field->lifetime().make_state<State>();
- field->changes(
- ) | rpl::start_with_next([=] {
- state->pending = true;
- if (state->processing) {
- return;
- }
- auto context = UniqueCustomEmojiContext{
- .applyHardLimit = applyHardLimit,
- .hardLimit = customHardLimit,
- };
- auto changed = false;
- state->processing = true;
- while (state->pending) {
- state->pending = false;
- const auto document = field->rawTextEdit()->document();
- const auto pageSize = document->pageSize();
- QTextCursor(document).joinPreviousEditBlock();
- if (RemoveNonCustomEmoji(document, context)) {
- changed = true;
- }
- state->processing = false;
- QTextCursor(document).endEditBlock();
- if (document->pageSize() != pageSize) {
- document->setPageSize(pageSize);
- }
- }
- callback(context.ids, context.hardLimitHit);
- if (changed) {
- field->forceProcessContentsChanges();
- }
- }, field->lifetime());
- }
- [[nodiscard]] TextWithTags ComposeEmojiList(
- not_null<Data::Reactions*> reactions,
- const std::vector<Data::ReactionId> &list) {
- auto result = TextWithTags();
- const auto size = [&] {
- return int(result.text.size());
- };
- auto added = base::flat_set<Data::ReactionId>();
- const auto &all = reactions->list(Data::Reactions::Type::All);
- const auto add = [&](Data::ReactionId id) {
- if (!added.emplace(id).second) {
- return;
- }
- auto unifiedId = id.custom();
- const auto offset = size();
- if (unifiedId) {
- result.text.append('@');
- } else if (id.paid()) {
- result.text.append(QChar(0x2B50));
- unifiedId = reactions->lookupPaid()->selectAnimation->id;
- } else {
- result.text.append(id.emoji());
- const auto i = ranges::find(all, id, &Data::Reaction::id);
- if (i == end(all)) {
- return;
- }
- unifiedId = i->selectAnimation->id;
- }
- const auto data = Data::SerializeCustomEmojiId(unifiedId);
- const auto tag = Ui::InputField::CustomEmojiLink(data);
- result.tags.append({ offset, size() - offset, tag });
- };
- for (const auto &id : list) {
- add(id);
- }
- return result;
- }
- enum class ReactionsSelectorState {
- Active,
- Disabled,
- Hidden,
- };
- struct ReactionsSelectorArgs {
- not_null<QWidget*> outer;
- not_null<Window::SessionController*> controller;
- rpl::producer<QString> title;
- std::vector<Data::Reaction> list;
- std::vector<Data::ReactionId> selected;
- rpl::producer<bool> paid;
- Fn<void(std::vector<Data::ReactionId>, bool)> callback;
- rpl::producer<ReactionsSelectorState> stateValue;
- int customAllowed = 0;
- int customHardLimit = 0;
- bool all = false;
- };
- object_ptr<Ui::RpWidget> AddReactionsSelector(
- not_null<Ui::RpWidget*> parent,
- ReactionsSelectorArgs &&args) {
- using namespace ChatHelpers;
- using HistoryView::Reactions::UnifiedFactoryOwner;
- auto result = object_ptr<Ui::InputField>(
- parent,
- st::manageGroupReactionsField,
- Ui::InputField::Mode::MultiLine,
- std::move(args.title));
- const auto raw = result.data();
- const auto session = &args.controller->session();
- const auto owner = &session->data();
- const auto reactions = &owner->reactions();
- const auto customAllowed = args.customAllowed;
- struct State {
- std::unique_ptr<Ui::RpWidget> overlay;
- std::unique_ptr<UnifiedFactoryOwner> unifiedFactoryOwner;
- UnifiedFactoryOwner::RecentFactory factory;
- base::flat_set<DocumentId> allowed;
- std::vector<Data::ReactionId> reactions;
- rpl::lifetime focusLifetime;
- };
- const auto paid = reactions->lookupPaid();
- auto normal = reactions->list(Data::Reactions::Type::Active);
- normal.push_back(*paid);
- const auto state = raw->lifetime().make_state<State>();
- state->unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(
- session,
- normal);
- state->factory = state->unifiedFactoryOwner->factory();
- state->reactions = std::move(args.selected);
- const auto customEmojiPaused = [controller = args.controller] {
- return controller->isGifPausedAtLeastFor(PauseReason::Layer);
- };
- auto simpleContext = Core::TextContext({
- .session = session,
- .repaint = [=] { raw->update(); },
- });
- auto context = simpleContext;
- context.customEmojiFactory = [=](
- QStringView data,
- const Ui::Text::MarkedContext &context
- ) -> std::unique_ptr<Ui::Text::CustomEmoji> {
- const auto id = Data::ParseCustomEmojiData(data);
- auto result = Ui::Text::MakeCustomEmoji(data, simpleContext);
- if (state->unifiedFactoryOwner->lookupReactionId(id).custom()) {
- return std::make_unique<MaybeDisabledEmoji>(
- std::move(result),
- [=] { return state->allowed.contains(id); });
- }
- using namespace Ui::Text;
- return std::make_unique<FirstFrameEmoji>(std::move(result));
- };
- raw->setCustomTextContext(
- std::move(context),
- customEmojiPaused,
- customEmojiPaused);
- const auto callback = args.callback;
- const auto isCustom = [=](DocumentId id) {
- return state->unifiedFactoryOwner->lookupReactionId(id).custom();
- };
- SetupOnlyCustomEmojiField(raw, [=](
- std::vector<DocumentId> ids,
- bool hardLimitHit) {
- auto allowed = base::flat_set<DocumentId>();
- auto reactions = std::vector<Data::ReactionId>();
- reactions.reserve(ids.size());
- allowed.reserve(std::min(customAllowed, int(ids.size())));
- const auto owner = state->unifiedFactoryOwner.get();
- for (const auto id : ids) {
- const auto reactionId = owner->lookupReactionId(id);
- if (reactionId.custom() && allowed.size() < customAllowed) {
- allowed.emplace(id);
- }
- reactions.push_back(reactionId);
- }
- if (state->allowed != allowed) {
- state->allowed = std::move(allowed);
- raw->rawTextEdit()->update();
- }
- state->reactions = reactions;
- callback(std::move(reactions), hardLimitHit);
- }, isCustom, args.customHardLimit);
- const auto applyFromState = [=] {
- raw->setTextWithTags(ComposeEmojiList(reactions, state->reactions));
- };
- applyFromState();
- std::move(
- args.paid
- ) | rpl::start_with_next([=](bool paid) {
- const auto id = Data::ReactionId::Paid();
- if (paid && !ranges::contains(state->reactions, id)) {
- state->reactions.insert(begin(state->reactions), id);
- applyFromState();
- } else if (!paid && ranges::contains(state->reactions, id)) {
- state->reactions.erase(
- ranges::remove(state->reactions, id),
- end(state->reactions));
- applyFromState();
- }
- }, raw->lifetime());
- const auto toggle = Ui::CreateChild<Ui::IconButton>(
- parent.get(),
- st::manageGroupReactions);
- using SelectorState = ReactionsSelectorState;
- std::move(
- args.stateValue
- ) | rpl::start_with_next([=](SelectorState value) {
- switch (value) {
- case SelectorState::Active:
- state->overlay = nullptr;
- state->focusLifetime.destroy();
- if (raw->empty()) {
- raw->setTextWithTags(
- ComposeEmojiList(reactions, DefaultSelected()));
- }
- raw->setDisabled(false);
- raw->setFocusFast();
- break;
- case SelectorState::Disabled:
- state->overlay = std::make_unique<Ui::RpWidget>(parent);
- state->overlay->show();
- raw->geometryValue() | rpl::start_with_next([=](QRect rect) {
- state->overlay->setGeometry(rect);
- }, state->overlay->lifetime());
- state->overlay->paintRequest() | rpl::start_with_next([=](QRect clip) {
- auto color = st::boxBg->c;
- color.setAlphaF(0.5);
- QPainter(state->overlay.get()).fillRect(
- clip,
- color);
- }, state->overlay->lifetime());
- [[fallthrough]];
- case SelectorState::Hidden:
- if (Ui::InFocusChain(raw)) {
- raw->parentWidget()->setFocus();
- }
- raw->setDisabled(true);
- raw->focusedChanges(
- ) | rpl::start_with_next([=](bool focused) {
- if (focused) {
- raw->parentWidget()->setFocus();
- }
- }, state->focusLifetime);
- break;
- }
- }, raw->lifetime());
- const auto panel = Ui::CreateChild<TabbedPanel>(
- args.outer.get(),
- args.controller,
- object_ptr<TabbedSelector>(
- nullptr,
- args.controller->uiShow(),
- Window::GifPauseReason::Layer,
- (args.all
- ? TabbedSelector::Mode::FullReactions
- : TabbedSelector::Mode::RecentReactions)));
- auto panelList = state->unifiedFactoryOwner->unifiedIdsList();
- panelList.erase(
- ranges::remove(panelList, paid->selectAnimation->id),
- end(panelList));
- panel->selector()->provideRecentEmoji(
- ChatHelpers::DocumentListToRecent(panelList));
- panel->setDesiredHeightValues(
- 1.,
- st::emojiPanMinHeight / 2,
- st::emojiPanMinHeight);
- panel->hide();
- panel->selector()->customEmojiChosen(
- ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
- Data::InsertCustomEmoji(raw, data.document);
- }, panel->lifetime());
- const auto updateEmojiPanelGeometry = [=] {
- const auto parent = panel->parentWidget();
- const auto global = toggle->mapToGlobal({ 0, 0 });
- const auto local = parent->mapFromGlobal(global);
- panel->moveBottomRight(
- local.y(),
- local.x() + toggle->width() * 3);
- };
- const auto scheduleUpdateEmojiPanelGeometry = [=] {
- // updateEmojiPanelGeometry uses not only container geometry, but
- // also container children geometries that will be updated later.
- crl::on_main(raw, updateEmojiPanelGeometry);
- };
- const auto filterCallback = [=](not_null<QEvent*> event) {
- const auto type = event->type();
- if (type == QEvent::Move || type == QEvent::Resize) {
- scheduleUpdateEmojiPanelGeometry();
- }
- return base::EventFilterResult::Continue;
- };
- for (auto widget = (QWidget*)raw
- ; widget && widget != args.outer
- ; widget = widget->parentWidget()) {
- base::install_event_filter(raw, widget, filterCallback);
- }
- base::install_event_filter(raw, args.outer, filterCallback);
- scheduleUpdateEmojiPanelGeometry();
- toggle->installEventFilter(panel);
- toggle->addClickHandler([=] {
- panel->toggleAnimated();
- });
- raw->geometryValue() | rpl::start_with_next([=](QRect geometry) {
- toggle->move(
- geometry.x() + geometry.width() - toggle->width(),
- geometry.y() + geometry.height() - toggle->height());
- updateEmojiPanelGeometry();
- }, toggle->lifetime());
- return result;
- }
- void AddReactionsText(
- not_null<Ui::VerticalLayout*> container,
- not_null<Window::SessionNavigation*> navigation,
- int allowedCustomReactions,
- rpl::producer<int> customCountValue,
- Fn<void(int required)> askForBoosts) {
- auto ownedInner = object_ptr<Ui::VerticalLayout>(container);
- const auto inner = ownedInner.data();
- const auto count = inner->lifetime().make_state<rpl::variable<int>>(
- std::move(customCountValue));
- container->add(
- object_ptr<Ui::DividerLabel>(
- container,
- std::move(ownedInner),
- st::defaultBoxDividerLabelPadding),
- QMargins(0, st::manageGroupReactionsTextSkip, 0, 0));
- const auto label = inner->add(
- object_ptr<Ui::FlatLabel>(
- inner,
- tr::lng_manage_peer_reactions_own(
- lt_link,
- tr::lng_manage_peer_reactions_own_link(
- ) | Ui::Text::ToLink(),
- Ui::Text::WithEntities),
- st::boxDividerLabel));
- const auto weak = base::make_weak(navigation);
- label->setClickHandlerFilter([=](const auto &...) {
- if (const auto strong = weak.get()) {
- strong->showPeerByLink(Window::PeerByLinkInfo{
- .usernameOrId = u"stickers"_q,
- .resolveType = Window::ResolveType::Mention,
- });
- }
- return false;
- });
- auto countString = count->value() | rpl::map([](int count) {
- return TextWithEntities{ QString::number(count) };
- });
- auto needs = rpl::combine(
- tr::lng_manage_peer_reactions_level(
- lt_count,
- count->value() | tr::to_count(),
- lt_same_count,
- std::move(countString),
- Ui::Text::RichLangValue),
- tr::lng_manage_peer_reactions_boost(
- lt_link,
- tr::lng_manage_peer_reactions_boost_link() | Ui::Text::ToLink(),
- Ui::Text::RichLangValue)
- ) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {
- a.append(' ').append(std::move(b));
- return std::move(a);
- });
- const auto wrap = inner->add(
- object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
- inner,
- object_ptr<Ui::FlatLabel>(
- inner,
- std::move(needs),
- st::boxDividerLabel),
- QMargins{ 0, st::normalFont->height, 0, 0 }));
- wrap->toggleOn(count->value() | rpl::map(
- rpl::mappers::_1 > allowedCustomReactions
- ));
- wrap->finishAnimating();
- wrap->entity()->setClickHandlerFilter([=](const auto &...) {
- askForBoosts(count->current());
- return false;
- });
- }
- } // namespace
- void EditAllowedReactionsBox(
- not_null<Ui::GenericBox*> box,
- EditAllowedReactionsArgs &&args) {
- using namespace Data;
- using namespace rpl::mappers;
- box->setTitle(tr::lng_manage_peer_reactions());
- box->setWidth(st::boxWideWidth);
- enum class Option {
- All,
- Some,
- None,
- };
- using SelectorState = ReactionsSelectorState;
- struct State {
- rpl::variable<Option> option; // For groups.
- rpl::variable<SelectorState> selectorState;
- std::vector<Data::ReactionId> selected;
- rpl::variable<int> customCount;
- rpl::variable<bool> paidEnabled;
- };
- const auto allowed = args.allowed;
- const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
- ? Option::All
- : (allowed.some.empty() && !allowed.paidEnabled)
- ? Option::None
- : Option::Some;
- const auto state = box->lifetime().make_state<State>(State{
- .option = optionInitial,
- .paidEnabled = allowed.paidEnabled,
- });
- const auto container = box->verticalLayout();
- const auto isGroup = args.isGroup;
- const auto enabled = isGroup
- ? nullptr
- : container->add(object_ptr<Ui::SettingsButton>(
- container.get(),
- tr::lng_manage_peer_reactions_enable(),
- st::manageGroupNoIconButton.button));
- if (enabled) {
- enabled->toggleOn(rpl::single(optionInitial != Option::None));
- enabled->toggledValue(
- ) | rpl::start_with_next([=](bool value) {
- state->selectorState = value
- ? SelectorState::Active
- : SelectorState::Disabled;
- }, enabled->lifetime());
- }
- const auto group = std::make_shared<Ui::RadioenumGroup<Option>>(
- state->option.current());
- group->setChangedCallback([=](Option value) {
- state->option = value;
- });
- const auto addOption = [&](Option option, const QString &text) {
- if (!isGroup) {
- return;
- }
- container->add(
- object_ptr<Ui::Radioenum<Option>>(
- container,
- group,
- option,
- text,
- st::settingsSendType),
- st::settingsSendTypePadding);
- };
- addOption(Option::All, tr::lng_manage_peer_reactions_all(tr::now));
- addOption(Option::Some, tr::lng_manage_peer_reactions_some(tr::now));
- addOption(Option::None, tr::lng_manage_peer_reactions_none(tr::now));
- const auto about = [](Option option) {
- switch (option) {
- case Option::All: return tr::lng_manage_peer_reactions_all_about();
- case Option::Some: return tr::lng_manage_peer_reactions_some_about();
- case Option::None: return tr::lng_manage_peer_reactions_none_about();
- }
- Unexpected("Option value in EditAllowedReactionsBox.");
- };
- Ui::AddSkip(container);
- Ui::AddDividerText(
- container,
- (isGroup
- ? (state->option.value()
- | rpl::map(about)
- | rpl::flatten_latest())
- : tr::lng_manage_peer_reactions_about_channel()));
- const auto wrap = enabled ? nullptr : container->add(
- object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
- container,
- object_ptr<Ui::VerticalLayout>(container)));
- if (wrap) {
- wrap->toggleOn(state->option.value(
- ) | rpl::map(_1 == Option::Some) | rpl::before_next([=](bool some) {
- if (!some) {
- state->selectorState = SelectorState::Hidden;
- }
- }) | rpl::after_next([=](bool some) {
- if (some) {
- state->selectorState = SelectorState::Active;
- }
- }));
- wrap->finishAnimating();
- }
- const auto reactions = wrap ? wrap->entity() : container.get();
- Ui::AddSkip(reactions);
- const auto all = args.list;
- auto selected = (allowed.type != AllowedReactionsType::Some)
- ? (all
- | ranges::views::transform(&Data::Reaction::id)
- | ranges::to_vector)
- : allowed.some;
- if (allowed.paidEnabled) {
- selected.insert(begin(selected), Data::ReactionId::Paid());
- }
- const auto changed = [=](
- std::vector<Data::ReactionId> chosen,
- bool hardLimitHit) {
- state->selected = std::move(chosen);
- state->customCount = ranges::count_if(
- state->selected,
- &Data::ReactionId::custom);
- state->paidEnabled = ranges::contains(
- state->selected,
- Data::ReactionId::Paid());
- if (hardLimitHit) {
- box->uiShow()->showToast(
- tr::lng_manage_peer_reactions_limit(tr::now));
- }
- };
- changed(selected.empty() ? DefaultSelected() : std::move(selected), {});
- Ui::AddSubsectionTitle(
- reactions,
- enabled
- ? tr::lng_manage_peer_reactions_available()
- : tr::lng_manage_peer_reactions_some_title(),
- st::manageGroupReactionsFieldPadding);
- reactions->add(AddReactionsSelector(reactions, {
- .outer = box->getDelegate()->outerContainer(),
- .controller = args.navigation->parentController(),
- .title = tr::lng_manage_peer_reactions_available_ph(),
- .list = all,
- .selected = state->selected,
- .paid = state->paidEnabled.value(),
- .callback = changed,
- .stateValue = state->selectorState.value(),
- .customAllowed = args.allowedCustomReactions,
- .customHardLimit = args.customReactionsHardLimit,
- .all = !args.isGroup,
- }), st::boxRowPadding);
- box->setFocusCallback([=] {
- if (state->option.current() == Option::Some) {
- state->selectorState.force_assign(SelectorState::Active);
- }
- });
- const auto reactionsLimit = container->lifetime().make_state<int>(0);
- if (!isGroup) {
- AddReactionsText(
- container,
- args.navigation,
- args.allowedCustomReactions,
- state->customCount.value(),
- args.askForBoosts);
- const auto session = &args.navigation->parentController()->session();
- const auto wrap = container->add(
- object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
- container,
- object_ptr<Ui::VerticalLayout>(container)));
- const auto max = Data::UniqueReactionsLimit(session->user());
- const auto inactiveColor = std::make_optional(st::windowSubTextFg->c);
- const auto activeColor = std::make_optional(
- st::windowActiveTextFg->c);
- const auto inner = wrap->entity();
- Ui::AddSkip(inner);
- Ui::AddSubsectionTitle(
- inner,
- tr::lng_manage_peer_reactions_max_title(),
- st::manageGroupReactionsMaxSubtitlePadding);
- Ui::AddSkip(inner);
- const auto line = inner->add(
- object_ptr<Ui::RpWidget>(inner),
- st::boxRowPadding);
- Ui::AddSkip(inner);
- Ui::AddSkip(inner);
- const auto left = Ui::CreateChild<Ui::FlatLabel>(
- line,
- QString::number(1),
- st::defaultFlatLabel);
- const auto center = Ui::CreateChild<Ui::FlatLabel>(
- line,
- st::defaultFlatLabel);
- const auto right = Ui::CreateChild<Ui::FlatLabel>(
- line,
- QString::number(max),
- st::defaultFlatLabel);
- const auto slider = Ui::CreateChild<Ui::MediaSlider>(
- line,
- st::settingsScale);
- rpl::combine(
- line->sizeValue(),
- left->sizeValue(),
- center->sizeValue(),
- right->sizeValue()
- ) | rpl::start_with_next([=](
- const QSize &s,
- const QSize &leftSize,
- const QSize ¢erSize,
- const QSize &rightSize) {
- const auto sliderHeight = st::settingsScale.seekSize.height();
- line->resize(
- line->width(),
- leftSize.height() + sliderHeight * 2);
- {
- const auto r = line->rect();
- slider->setGeometry(
- 0,
- r.height() - sliderHeight * 1.5,
- r.width(),
- sliderHeight);
- }
- left->moveToLeft(0, 0);
- right->moveToRight(0, 0);
- center->moveToLeft((s.width() - centerSize.width()) / 2, 0);
- }, line->lifetime());
- const auto updateLabels = [=](int limit) {
- left->setTextColorOverride((limit <= 1)
- ? activeColor
- : inactiveColor);
- center->setText(tr::lng_manage_peer_reactions_max_slider(
- tr::now,
- lt_count,
- limit));
- center->setTextColorOverride(activeColor);
- right->setTextColorOverride((limit >= max)
- ? activeColor
- : inactiveColor);
- (*reactionsLimit) = limit;
- };
- const auto current = args.allowed.maxCount
- ? std::clamp(1, args.allowed.maxCount, max)
- : max / 2;
- slider->setPseudoDiscrete(
- max,
- [=](int index) { return index + 1; },
- current,
- updateLabels,
- updateLabels);
- updateLabels(current);
- wrap->toggleOn(rpl::single(
- optionInitial != Option::None
- ) | rpl::then(
- state->selectorState.value(
- ) | rpl::map(rpl::mappers::_1 == SelectorState::Active)));
- Ui::AddDividerText(inner, tr::lng_manage_peer_reactions_max_about());
- Ui::AddSkip(inner);
- const auto paid = inner->add(object_ptr<Ui::SettingsButton>(
- inner,
- tr::lng_manage_peer_reactions_paid(),
- st::manageGroupNoIconButton.button));
- paid->toggleOn(state->paidEnabled.value());
- paid->toggledValue(
- ) | rpl::start_with_next([=](bool value) {
- state->paidEnabled = value;
- }, paid->lifetime());
- Ui::AddSkip(inner);
- Ui::AddDividerText(
- inner,
- tr::lng_manage_peer_reactions_paid_about(
- lt_link,
- tr::lng_manage_peer_reactions_paid_link([=](QString text) {
- return Ui::Text::Link(
- text,
- u"https://telegram.org/tos/stars"_q);
- }),
- Ui::Text::WithEntities));
- }
- const auto collect = [=] {
- auto result = AllowedReactions();
- result.maxCount = (*reactionsLimit);
- if (isGroup
- ? (state->option.current() == Option::Some)
- : (enabled->toggled())) {
- result.some = state->selected;
- }
- if (!isGroup && enabled->toggled()) {
- result.paidEnabled = state->paidEnabled.current();
- }
- auto some = result.some;
- auto simple = all | ranges::views::transform(
- &Data::Reaction::id
- ) | ranges::to_vector;
- ranges::sort(some);
- ranges::sort(simple);
- result.type = isGroup
- ? (state->option.current() != Option::All
- ? AllowedReactionsType::Some
- : AllowedReactionsType::All)
- : (some == simple)
- ? AllowedReactionsType::Default
- : AllowedReactionsType::Some;
- return result;
- };
- box->addButton(tr::lng_settings_save(), [=] {
- const auto result = collect();
- if (!isGroup) {
- const auto custom = ranges::count_if(
- result.some,
- &Data::ReactionId::custom);
- if (custom > args.allowedCustomReactions) {
- args.askForBoosts(custom);
- return;
- }
- }
- box->closeBox();
- args.save(result);
- });
- box->addButton(tr::lng_cancel(), [=] {
- box->closeBox();
- });
- }
- void SaveAllowedReactions(
- not_null<PeerData*> peer,
- const Data::AllowedReactions &allowed) {
- auto ids = allowed.some | ranges::views::transform(
- Data::ReactionToMTP
- ) | ranges::to<QVector<MTPReaction>>;
- using Flag = MTPmessages_SetChatAvailableReactions::Flag;
- using Type = Data::AllowedReactionsType;
- const auto updated = (allowed.type != Type::Some)
- ? MTP_chatReactionsAll(MTP_flags((allowed.type == Type::Default)
- ? MTPDchatReactionsAll::Flag(0)
- : MTPDchatReactionsAll::Flag::f_allow_custom))
- : allowed.some.empty()
- ? MTP_chatReactionsNone()
- : MTP_chatReactionsSome(MTP_vector<MTPReaction>(ids));
- const auto editPaidEnabled = peer->isBroadcast();
- const auto paidEnabled = editPaidEnabled && allowed.paidEnabled;
- const auto maxCount = allowed.maxCount;
- peer->session().api().request(MTPmessages_SetChatAvailableReactions(
- MTP_flags(Flag()
- | (maxCount ? Flag::f_reactions_limit : Flag())
- | (editPaidEnabled ? Flag::f_paid_enabled : Flag())),
- peer->input,
- updated,
- MTP_int(maxCount),
- MTP_bool(paidEnabled)
- )).done([=](const MTPUpdates &result) {
- peer->session().api().applyUpdates(result);
- auto parsed = Data::Parse(updated, maxCount, paidEnabled);
- if (const auto chat = peer->asChat()) {
- chat->setAllowedReactions(parsed);
- } else if (const auto channel = peer->asChannel()) {
- channel->setAllowedReactions(parsed);
- } else {
- Unexpected("Invalid peer type in SaveAllowedReactions.");
- }
- }).fail([=](const MTP::Error &error) {
- if (error.type() == u"REACTION_INVALID"_q) {
- peer->updateFullForced();
- peer->owner().reactions().refreshDefault();
- }
- }).send();
- }
|