/* 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 "settings/business/settings_chat_intro.h" #include "api/api_premium.h" #include "boxes/peers/edit_peer_color_box.h" // ButtonStyleWithRightEmoji #include "chat_helpers/stickers_lottie.h" #include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_selector.h" #include "core/application.h" #include "data/business/data_business_info.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_sticker_player.h" #include "history/view/history_view_about_view.h" #include "history/view/history_view_context_menu.h" #include "history/view/history_view_element.h" #include "history/history.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/effects/path_shift_gradient.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/buttons.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_layers.h" #include "styles/style_settings.h" namespace Settings { namespace { using namespace HistoryView; class PreviewDelegate final : public DefaultElementDelegate { public: PreviewDelegate( not_null parent, not_null st, Fn update); bool elementAnimationsPaused() override; not_null elementPathShiftGradient() override; Context elementContext() override; private: const not_null _parent; const std::unique_ptr _pathGradient; }; class PreviewWrap final : public Ui::RpWidget { public: PreviewWrap( not_null parent, not_null session, rpl::producer value); ~PreviewWrap(); private: void paintEvent(QPaintEvent *e) override; void resizeTo(int width); void prepare(rpl::producer value); const not_null _history; const std::unique_ptr _theme; const std::unique_ptr _style; const std::unique_ptr _delegate; std::unique_ptr _view; QPoint _position; }; class StickerPanel final { public: StickerPanel(); ~StickerPanel(); struct Descriptor { not_null controller; not_null button; }; void show(Descriptor &&descriptor); struct CustomChosen { not_null sticker; }; [[nodiscard]] rpl::producer someCustomChosen() const { return _someCustomChosen.events(); } private: void create(const Descriptor &descriptor); base::unique_qptr _panel; QPointer _panelButton; rpl::event_stream _someCustomChosen; }; class ChatIntro final : public BusinessSection { public: ChatIntro( QWidget *parent, not_null controller); ~ChatIntro(); [[nodiscard]] bool closeByOutsideClick() const override; [[nodiscard]] rpl::producer title() override; void setInnerFocus() override { _setFocus(); } private: void setupContent(not_null controller); void save(); Fn _setFocus; rpl::variable _intro; }; [[nodiscard]] int PartLimit( not_null session, const QString &key, int defaultValue) { return session->appConfig().get(key, defaultValue); } [[nodiscard]] not_null AddPartInput( not_null container, rpl::producer placeholder, QString current, int limit) { const auto field = container->add( object_ptr( container, st::settingsChatIntroField, std::move(placeholder), current), st::settingsChatIntroFieldMargins); field->setMaxLength(limit); AddLengthLimitLabel(field, limit); return field; } rpl::producer> IconPlayerValue( not_null sticker, Fn update) { const auto media = sticker->createMediaView(); media->checkStickerLarge(); media->goodThumbnailWanted(); return rpl::single() | rpl::then( sticker->owner().session().downloaderTaskFinished() ) | rpl::filter([=] { return media->loaded(); }) | rpl::take(1) | rpl::map([=] { auto result = std::shared_ptr(); const auto info = sticker->sticker(); const auto box = QSize(st::emojiSize, st::emojiSize); if (info->isLottie()) { result = std::make_shared( ChatHelpers::LottiePlayerFromDocument( media.get(), ChatHelpers::StickerLottieSize::StickerEmojiSize, box, Lottie::Quality::High)); } else if (info->isWebm()) { result = std::make_shared( media->owner()->location(), media->bytes(), box); } else { result = std::make_shared( media->owner()->location(), media->bytes(), box); } result->setRepaintCallback(update); return result; }); } [[nodiscard]] object_ptr CreateIntroStickerButton( not_null parent, std::shared_ptr show, rpl::producer stickerValue, Fn stickerChosen) { const auto button = ButtonStyleWithRightEmoji( parent, tr::lng_chat_intro_random_sticker(tr::now), st::settingsButtonNoIcon); auto result = Settings::CreateButtonWithIcon( parent, tr::lng_chat_intro_choose_sticker(), *button.st); const auto raw = result.data(); const auto right = Ui::CreateChild(raw); right->show(); struct State { StickerPanel panel; DocumentData *sticker = nullptr; std::shared_ptr player; rpl::lifetime playerLifetime; }; const auto state = right->lifetime().make_state(); state->panel.someCustomChosen( ) | rpl::start_with_next([=](StickerPanel::CustomChosen chosen) { stickerChosen(chosen.sticker); }, raw->lifetime()); std::move( stickerValue ) | rpl::start_with_next([=](DocumentData *sticker) { state->sticker = sticker; if (sticker) { right->resize(button.emojiWidth + button.added, right->height()); IconPlayerValue( sticker, [=] { right->update(); } ) | rpl::start_with_next([=]( std::shared_ptr player) { state->player = std::move(player); right->update(); }, state->playerLifetime); } else { state->playerLifetime.destroy(); state->player = nullptr; right->resize(button.noneWidth + button.added, right->height()); right->update(); } }, right->lifetime()); rpl::combine( raw->sizeValue(), right->widthValue() ) | rpl::start_with_next([=](QSize outer, int width) { right->resize(width, outer.height()); const auto skip = st::settingsButton.padding.right(); right->moveToRight(skip - button.added, 0, outer.width()); }, right->lifetime()); right->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(right); const auto height = right->height(); if (state->player) { if (state->player->ready()) { const auto frame = state->player->frame( QSize(st::emojiSize, st::emojiSize), QColor(0, 0, 0, 0), false, crl::now(), !right->window()->isActiveWindow()).image; const auto target = DownscaledSize( frame.size(), QSize(st::emojiSize, st::emojiSize)); p.drawImage( QRect( button.added + (st::emojiSize - target.width()) / 2, (height - target.height()) / 2, target.width(), target.height()), frame); state->player->markFrameShown(); } } else { const auto &font = st::normalFont; p.setFont(font); p.setPen(st::windowActiveTextFg); p.drawText( QPoint( button.added, (height - font->height) / 2 + font->ascent), tr::lng_chat_intro_random_sticker(tr::now)); } }, right->lifetime()); raw->setClickedCallback([=] { const auto controller = show->resolveWindow( ChatHelpers::WindowUsage::PremiumPromo); if (controller) { state->panel.show({ .controller = controller, .button = right, }); } }); return result; } PreviewDelegate::PreviewDelegate( not_null parent, not_null st, Fn update) : _parent(parent) , _pathGradient(MakePathShiftGradient(st, update)) { } bool PreviewDelegate::elementAnimationsPaused() { return _parent->window()->isActiveWindow(); } auto PreviewDelegate::elementPathShiftGradient() -> not_null { return _pathGradient.get(); } Context PreviewDelegate::elementContext() { return Context::History; } PreviewWrap::PreviewWrap( not_null parent, not_null session, rpl::producer value) : RpWidget(parent) , _history(session->data().history(session->userPeerId())) , _theme(Window::Theme::DefaultChatThemeOn(lifetime())) , _style(std::make_unique( _history->session().colorIndicesValue())) , _delegate(std::make_unique( parent, _style.get(), [=] { update(); })) , _position(0, st::msgMargin.bottom()) { _style->apply(_theme.get()); session->data().viewRepaintRequest( ) | rpl::start_with_next([=](not_null view) { if (view == _view->view()) { update(); } }, lifetime()); session->downloaderTaskFinished() | rpl::start_with_next([=] { update(); }, lifetime()); prepare(std::move(value)); } PreviewWrap::~PreviewWrap() { _view = nullptr; } void PreviewWrap::prepare(rpl::producer value) { _view = std::make_unique( _history.get(), _delegate.get()); std::move(value) | rpl::start_with_next([=](Data::ChatIntro intro) { _view->make(std::move(intro), true); if (width() >= st::msgMinWidth) { resizeTo(width()); } update(); }, lifetime()); widthValue( ) | rpl::filter([=](int width) { return width >= st::msgMinWidth; }) | rpl::start_with_next([=](int width) { resizeTo(width); }, lifetime()); } void PreviewWrap::resizeTo(int width) { const auto height = _position.y() + _view->view()->resizeGetHeight(width) + _position.y() + st::msgServiceMargin.top() + st::msgServiceGiftBoxTopSkip - st::msgServiceMargin.bottom(); resize(width, height); } void PreviewWrap::paintEvent(QPaintEvent *e) { auto p = Painter(this); const auto clip = e->rect(); if (!clip.isEmpty()) { p.setClipRect(clip); Window::SectionWidget::PaintBackground( p, _theme.get(), QSize(width(), window()->height()), clip); } auto context = _theme->preparePaintContext( _style.get(), rect(), e->rect(), !window()->isActiveWindow()); p.translate(_position); _view->view()->draw(p, context); } StickerPanel::StickerPanel() = default; StickerPanel::~StickerPanel() = default; void StickerPanel::show(Descriptor &&descriptor) { if (!_panel) { create(descriptor); _panel->shownValue( ) | rpl::filter([=] { return (_panelButton != nullptr); }) | rpl::start_with_next([=](bool shown) { if (shown) { _panelButton->installEventFilter(_panel.get()); } else { _panelButton->removeEventFilter(_panel.get()); } }, _panel->lifetime()); } const auto button = descriptor.button; if (const auto previous = _panelButton.data()) { if (previous != button) { previous->removeEventFilter(_panel.get()); } } _panelButton = button; const auto parent = _panel->parentWidget(); const auto global = button->mapToGlobal(QPoint()); const auto local = parent->mapFromGlobal(global); _panel->moveBottomRight( local.y() + (st::normalFont->height / 2), local.x() + button->width() * 3); _panel->toggleAnimated(); } void StickerPanel::create(const Descriptor &descriptor) { using Selector = ChatHelpers::TabbedSelector; using Descriptor = ChatHelpers::TabbedSelectorDescriptor; using Mode = ChatHelpers::TabbedSelector::Mode; const auto controller = descriptor.controller; const auto body = controller->window().widget()->bodyWidget(); _panel = base::make_unique_q( body, controller, object_ptr( nullptr, Descriptor{ .show = controller->uiShow(), .st = st::backgroundEmojiPan, .level = Window::GifPauseReason::Layer, .mode = Mode::StickersOnly, .features = { .megagroupSet = false, .stickersSettings = false, .openStickerSets = false, }, })); _panel->setDropDown(false); _panel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, st::emojiPanMinHeight); _panel->hide(); _panel->selector()->fileChosen( ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { _someCustomChosen.fire({ data.document }); _panel->hideAnimated(); }, _panel->lifetime()); } ChatIntro::ChatIntro( QWidget *parent, not_null controller) : BusinessSection(parent, controller) { setupContent(controller); } ChatIntro::~ChatIntro() { if (!Core::Quitting()) { save(); } } bool ChatIntro::closeByOutsideClick() const { return false; } rpl::producer ChatIntro::title() { return tr::lng_chat_intro_title(); } [[nodiscard]] rpl::producer IntroWithRandomSticker( not_null session, rpl::producer intro) { auto random = rpl::single( Api::RandomHelloStickerValue(session) ) | rpl::then(rpl::duplicate( intro ) | rpl::map([=](const Data::ChatIntro &intro) { return intro.sticker; }) | rpl::distinct_until_changed( ) | rpl::filter([](DocumentData *sticker) { return !sticker; }) | rpl::map([=] { return Api::RandomHelloStickerValue(session); })) | rpl::flatten_latest(); return rpl::combine( std::move(intro), std::move(random) ) | rpl::map([=](Data::ChatIntro intro, DocumentData *hello) { if (!intro.sticker) { intro.sticker = hello; } return intro; }); } void ChatIntro::setupContent( not_null controller) { using namespace rpl::mappers; const auto content = Ui::CreateChild(this); const auto session = &controller->session(); _intro = controller->session().user()->businessDetails().intro; const auto change = [=](Fn modify) { auto intro = _intro.current(); modify(intro); _intro = intro; }; content->add( object_ptr( content, session, IntroWithRandomSticker(session, _intro.value())), {}); const auto title = AddPartInput( content, tr::lng_chat_intro_enter_title(), _intro.current().title, PartLimit(session, u"intro_title_length_limit"_q, 32)); const auto description = AddPartInput( content, tr::lng_chat_intro_enter_message(), _intro.current().description, PartLimit(session, u"intro_description_length_limit"_q, 70)); content->add(CreateIntroStickerButton( content, controller->uiShow(), _intro.value() | rpl::map([](const Data::ChatIntro &intro) { return intro.sticker; }) | rpl::distinct_until_changed(), [=](DocumentData *sticker) { change([&](Data::ChatIntro &intro) { intro.sticker = sticker; }); })); Ui::AddSkip(content); title->changes() | rpl::start_with_next([=] { change([&](Data::ChatIntro &intro) { intro.title = title->getLastText(); }); }, title->lifetime()); description->changes() | rpl::start_with_next([=] { change([&](Data::ChatIntro &intro) { intro.description = description->getLastText(); }); }, description->lifetime()); _setFocus = [=] { title->setFocusFast(); }; Ui::AddDividerText( content, tr::lng_chat_intro_about(), st::peerAppearanceDividerTextMargin); Ui::AddSkip(content); const auto resetWrap = content->add( object_ptr>( content, object_ptr( content, tr::lng_chat_intro_reset(), st::settingsAttentionButton ))); resetWrap->toggleOn( _intro.value() | rpl::map([](const Data::ChatIntro &intro) { return !!intro; })); resetWrap->entity()->setClickedCallback([=] { _intro = Data::ChatIntro(); title->clear(); description->clear(); title->setFocus(); }); Ui::ResizeFitChild(this, content); } void ChatIntro::save() { const auto show = controller()->uiShow(); const auto fail = [=](QString error) { }; controller()->session().data().businessInfo().saveChatIntro( _intro.current(), fail); } } // namespace Type ChatIntroId() { return ChatIntro::Id(); } } // namespace Settings