diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 6f530972c..7d8e4125c 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -794,6 +794,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_color_emoji_about" = "Make replies to your messages stand out by adding custom patterns to them."; "lng_settings_color_emoji_about_channel" = "Make replies to your channel's messages stand out by adding custom patterns to them."; "lng_settings_color_subscribe" = "Subscribe to {link} to choose a custom color for your name."; +"lng_settings_color_changed" = "Your name color has been updated!"; +"lng_settings_color_changed_channel" = "Your channel color has been updated!"; "lng_suggest_hide_new_title" = "Hide new chats?"; "lng_suggest_hide_new_about" = "You are receiving lots of new chats from users who are not in your Contact List.\n\nDo you want to have such chats **automatically muted** and **archived**?"; diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 71f3c42c4..84c2db7e6 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -251,68 +251,24 @@ struct GiftCodeLink { }; } -[[nodiscard]] object_ptr MakeLinkLabel( - not_null parent, - rpl::producer text, - rpl::producer link, - std::shared_ptr show) { - auto result = object_ptr(parent); +[[nodiscard]] object_ptr MakeLinkCopyIcon( + not_null parent) { + auto result = object_ptr(parent); const auto raw = result.data(); - struct State { - State( - not_null parent, - rpl::producer value, - rpl::producer link) - : text(std::move(value)) - , link(std::move(link)) - , label(parent, text.value(), st::giveawayGiftCodeLink) - , bg(st::roundRadiusLarge, st::windowBgOver) { - } - - rpl::variable text; - rpl::variable link; - Ui::FlatLabel label; - Ui::RoundRect bg; - }; - - const auto state = raw->lifetime().make_state( - raw, - rpl::duplicate(text), - std::move(link)); - state->label.setSelectable(true); - - rpl::combine( - raw->widthValue(), - std::move(text) - ) | rpl::start_with_next([=](int outer, const auto&) { - const auto textWidth = state->label.textMaxWidth(); - const auto skipLeft = st::giveawayGiftCodeLink.margin.left(); - const auto skipRight = st::giveawayGiftCodeLinkCopyWidth; - const auto available = outer - skipRight - skipLeft; - const auto use = std::min(textWidth, available); - state->label.resizeToWidth(use); - state->label.move(outer - skipRight - use - skipLeft, 0); - }, raw->lifetime()); - raw->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(raw); - state->bg.paint(p, raw->rect()); - const auto outer = raw->width(); - const auto width = st::giveawayGiftCodeLinkCopyWidth; const auto &icon = st::giveawayGiftCodeLinkCopy; - const auto left = outer - width + (width - icon.width()) / 2; + const auto left = (raw->width() - icon.width()) / 2; const auto top = (raw->height() - icon.height()) / 2; icon.paint(p, left, top, raw->width()); }, raw->lifetime()); - state->label.setAttribute(Qt::WA_TransparentForMouseEvents); + raw->resize( + st::giveawayGiftCodeLinkCopyWidth, + st::giveawayGiftCodeLinkHeight); - raw->resize(raw->width(), st::giveawayGiftCodeLinkHeight); - raw->setClickedCallback([=] { - QGuiApplication::clipboard()->setText(state->link.current()); - show->showToast(tr::lng_username_copied(tr::now)); - }); + raw->setAttribute(Qt::WA_TransparentForMouseEvents); return result; } @@ -502,11 +458,12 @@ void GiftCodeBox( const auto link = MakeGiftCodeLink(&controller->session(), slug); box->addRow( - MakeLinkLabel( + Ui::MakeLinkLabel( box, rpl::single(link.text), rpl::single(link.link), - box->uiShow()), + box->uiShow(), + MakeLinkCopyIcon(box)), st::giveawayGiftCodeLinkMargin); auto table = box->addRow( diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index f15ae47cb..11c674565 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -7,14 +7,38 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/peers/edit_peer_color_box.h" +#include "apiwrap.h" +#include "base/unixtime.h" +#include "chat_helpers/compose/compose_show.h" #include "data/data_changes.h" +#include "data/data_channel.h" #include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_web_page.h" +#include "history/view/history_view_element.h" +#include "history/history.h" +#include "history/history_item.h" +#include "info/boosts/info_boosts_widget.h" +#include "info/info_memento.h" #include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "settings/settings_common.h" +#include "settings/settings_premium.h" +#include "ui/boxes/boost_box.h" #include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/effects/path_shift_gradient.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/painter.h" +#include "window/themes/window_theme.h" +#include "window/section_widget.h" +#include "window/window_session_controller.h" +#include "styles/style_chat.h" +#include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_settings.h" #include "styles/style_widgets.h" @@ -23,32 +47,97 @@ namespace { using namespace Settings; -class ColorSample final : public Ui::RpWidget { +constexpr auto kFakeChannelId = ChannelId(0xFFFFFFF000ULL); +constexpr auto kFakeWebPageId = WebPageId(0xFFFFFFFF00000000ULL); +constexpr auto kSelectAnimationDuration = crl::time(150); + +class ColorSample final : public Ui::AbstractButton { public: ColorSample( not_null parent, - std::shared_ptr st, + std::shared_ptr style, rpl::producer colorIndex, const QString &name); + ColorSample( + not_null parent, + std::shared_ptr style, + uint8 colorIndex, + bool selected); + [[nodiscard]] uint8 index() const; int naturalWidth() const override; + void setSelected(bool selected); + private: void paintEvent(QPaintEvent *e) override; - std::shared_ptr _st; + std::shared_ptr _style; Ui::Text::String _name; uint8 _index = 0; + Ui::Animations::Simple _selectAnimation; + bool _selected = false; + bool _simple = false; + +}; + +class PreviewDelegate final : public HistoryView::DefaultElementDelegate { +public: + PreviewDelegate( + not_null parent, + not_null st, + Fn update); + + bool elementAnimationsPaused() override; + not_null elementPathShiftGradient() override; + HistoryView::Context elementContext() override; + +private: + const not_null _parent; + const std::unique_ptr _pathGradient; + +}; + +class PreviewWrap final : public Ui::RpWidget { +public: + PreviewWrap( + not_null box, + std::shared_ptr style, + std::shared_ptr theme, + not_null peer, + rpl::producer colorIndexValue); + ~PreviewWrap(); + +private: + using Element = HistoryView::Element; + + void paintEvent(QPaintEvent *e) override; + + void initElements(); + + const not_null _box; + const not_null _peer; + const not_null _fake; + const not_null _history; + const not_null _webpage; + const std::shared_ptr _theme; + const std::shared_ptr _style; + const std::unique_ptr _delegate; + const not_null _replyToItem; + const not_null _replyItem; + std::unique_ptr _element; + Ui::PeerUserpicView _userpic; + QPoint _position; }; ColorSample::ColorSample( not_null parent, - std::shared_ptr st, + std::shared_ptr style, rpl::producer colorIndex, const QString &name) -: RpWidget(parent) -, _st(st) +: AbstractButton(parent) +, _style(style) , _name(st::semiboldTextStyle, name) { std::move( colorIndex @@ -58,11 +147,35 @@ ColorSample::ColorSample( }, lifetime()); } +ColorSample::ColorSample( + not_null parent, + std::shared_ptr style, + uint8 colorIndex, + bool selected) +: AbstractButton(parent) +, _style(style) +, _index(colorIndex) +, _selected(selected) +, _simple(true) { +} + +void ColorSample::setSelected(bool selected) { + if (_selected == selected) { + return; + } + _selected = selected; + _selectAnimation.start( + [=] { update(); }, + _selected ? 0. : 1., + _selected ? 1. : 0., + kSelectAnimationDuration); +} + void ColorSample::paintEvent(QPaintEvent *e) { auto p = Painter(this); auto hq = PainterHighQualityEnabler(p); - const auto colors = _st->coloredValues(false, _index); - if (!colors.outlines[1].alpha()) { + const auto colors = _style->coloredValues(false, _index); + if (!_simple && !colors.outlines[1].alpha()) { const auto radius = height() / 2; p.setPen(Qt::NoPen); p.setBrush(colors.bg); @@ -81,32 +194,52 @@ void ColorSample::paintEvent(QPaintEvent *e) { 1, style::al_top); } else { - const auto size = width(); + const auto size = float64(width()); const auto half = size / 2.; + const auto full = QRectF(-half, -half, size, size); p.translate(size / 2., size / 2.); - p.rotate(-45.); p.setPen(Qt::NoPen); - p.setClipRect(-size, -size, 3 * size, size); + if (colors.outlines[1].alpha()) { + p.rotate(-45.); + p.setClipRect(-size, 0, 3 * size, size); + p.setBrush(colors.outlines[1]); + p.drawEllipse(full); + p.setClipRect(-size, -size, 3 * size, size); + } p.setBrush(colors.outlines[0]); - p.drawEllipse(-half, -half, size, size); - p.setClipRect(-size, 0, 3 * size, size); - p.setBrush(colors.outlines[1]); - p.drawEllipse(-half, -half, size, size); + p.drawEllipse(full); + p.setClipping(false); if (colors.outlines[2].alpha()) { - const auto center = st::settingsColorSampleCenter; - const auto radius = st::settingsColorSampleCenterRadius; - p.setClipping(false); + const auto multiplier = size / st::settingsColorSampleSize; + const auto center = st::settingsColorSampleCenter * multiplier; + const auto radius = st::settingsColorSampleCenterRadius + * multiplier; p.setBrush(colors.outlines[2]); p.drawRoundedRect( QRectF(-center / 2., -center / 2., center, center), radius, radius); } + const auto selected = _selectAnimation.value(_selected ? 1. : 0.); + if (selected > 0) { + const auto line = st::settingsColorRadioStroke * 1.; + const auto thickness = selected * line; + auto pen = st::boxBg->p; + pen.setWidthF(thickness); + p.setBrush(Qt::NoBrush); + p.setPen(pen); + const auto skip = 1.5 * line; + p.drawEllipse(full.marginsRemoved({ skip, skip, skip, skip })); + } } } +uint8 ColorSample::index() const { + return _index; +} + int ColorSample::naturalWidth() const { - if (_st->colorPatternIndex(_index)) { + if (_name.isEmpty() || _style->colorPatternIndex(_index)) { return st::settingsColorSampleSize; } const auto padding = st::settingsColorSamplePadding; @@ -115,14 +248,463 @@ int ColorSample::naturalWidth() const { padding.top() + st::semiboldFont->height + padding.bottom()); } +PreviewWrap::PreviewWrap( + not_null box, + std::shared_ptr style, + std::shared_ptr theme, + not_null peer, + rpl::producer colorIndexValue) +: RpWidget(box) +, _box(box) +, _peer(peer) +, _fake(_peer->owner().channel(kFakeChannelId)) +, _history(_fake->owner().history(_fake)) +, _webpage(_peer->owner().webpage( + kFakeWebPageId, + WebPageType::Article, + u"internal:peer-color-webpage-preview"_q, + u"internal:peer-color-webpage-preview"_q, + tr::lng_settings_color_link_name(tr::now), + tr::lng_settings_color_link_title(tr::now), + { tr::lng_settings_color_link_description(tr::now) }, + nullptr, // photo + nullptr, // document + WebPageCollage(), + 0, // duration + QString(), // author + false, // hasLargeMedia + 0)) // pendingTill +, _theme(theme) +, _style(style) +, _delegate(std::make_unique(box, _style.get(), [=] { + update(); +})) +, _replyToItem(_history->addNewLocalMessage( + _history->nextNonHistoryEntryId(), + (MessageFlag::FakeHistoryItem + | MessageFlag::HasFromId + | MessageFlag::Post), + UserId(), // via + FullReplyTo(), + base::unixtime::now(), // date + _fake->id, + QString(), // postAuthor + TextWithEntities{ _peer->isSelf() + ? tr::lng_settings_color_reply(tr::now) + : tr::lng_settings_color_reply_channel(tr::now), + }, + MTP_messageMediaEmpty(), + HistoryMessageMarkupData(), + uint64(0))) +, _replyItem(_history->addNewLocalMessage( + _history->nextNonHistoryEntryId(), + (MessageFlag::FakeHistoryItem + | MessageFlag::HasFromId + | MessageFlag::HasReplyInfo + | MessageFlag::Post), + UserId(), // via + FullReplyTo{ .messageId = _replyToItem->fullId() }, + base::unixtime::now(), // date + _fake->id, + QString(), // postAuthor + TextWithEntities{ _peer->isSelf() + ? tr::lng_settings_color_text(tr::now) + : tr::lng_settings_color_text_channel(tr::now), + }, + MTP_messageMediaWebPage( + MTP_flags(0), + MTP_webPagePending( + MTP_flags(0), + MTP_long(_webpage->id), + MTPstring(), + MTP_int(0))), + HistoryMessageMarkupData(), + uint64(0))) +, _element(_replyItem->createView(_delegate.get())) +, _position(0, st::msgMargin.bottom()) { + _style->apply(_theme.get()); + + _fake->setName(peer->name(), QString()); + std::move(colorIndexValue) | rpl::start_with_next([=](uint8 index) { + _fake->changeColorIndex(index); + update(); + }, lifetime()); + + const auto session = &_history->session(); + session->data().viewRepaintRequest( + ) | rpl::start_with_next([=](not_null view) { + if (view == _element.get()) { + update(); + } + }, lifetime()); + + initElements(); +} + +PreviewWrap::~PreviewWrap() { + _element = nullptr; + _replyItem->destroy(); + _replyToItem->destroy(); +} + +void PreviewWrap::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + const auto clip = e->rect(); + + p.setClipRect(clip); + Window::SectionWidget::PaintBackground( + p, + _theme.get(), + QSize(_box->width(), _box->window()->height()), + clip); + + auto context = _theme->preparePaintContext( + _style.get(), + rect(), + clip, + !window()->isActiveWindow()); + + p.translate(_position); + _element->draw(p, context); + + if (_element->displayFromPhoto()) { + auto userpicMinBottomSkip = st::historyPaddingBottom + + st::msgMargin.bottom(); + auto userpicBottom = height() + - _element->marginBottom() + - _element->marginTop(); + const auto item = _element->data(); + const auto userpicTop = userpicBottom - st::msgPhotoSize; + _peer->paintUserpicLeft( + p, + _userpic, + st::historyPhotoLeft, + userpicTop, + width(), + st::msgPhotoSize); + } +} + +void PreviewWrap::initElements() { + _element->initDimensions(); + + widthValue( + ) | rpl::filter([=](int width) { + return width > st::msgMinWidth; + }) | rpl::start_with_next([=](int width) { + const auto height = _position.y() + + _element->resizeGetHeight(width) + + st::msgMargin.top(); + resize(width, height); + }, lifetime()); +} + +PreviewDelegate::PreviewDelegate( + not_null parent, + not_null st, + Fn update) +: _parent(parent) +, _pathGradient(HistoryView::MakePathShiftGradient(st, update)) { +} + +bool PreviewDelegate::elementAnimationsPaused() { + return _parent->window()->isActiveWindow(); +} + +auto PreviewDelegate::elementPathShiftGradient() +-> not_null { + return _pathGradient.get(); +} + +HistoryView::Context PreviewDelegate::elementContext() { + return HistoryView::Context::AdminLog; +} + +void Set( + std::shared_ptr show, + not_null peer, + uint8 colorIndex) { + const auto was = peer->colorIndex(); + peer->changeColorIndex(colorIndex); + peer->session().changes().peerUpdated( + peer, + Data::PeerUpdate::Flag::Color); + const auto done = [=] { + show->showToast(peer->isSelf() + ? tr::lng_settings_color_changed(tr::now) + : tr::lng_settings_color_changed_channel(tr::now)); + }; + const auto fail = [=](const MTP::Error &error) { + peer->changeColorIndex(was); + peer->session().changes().peerUpdated( + peer, + Data::PeerUpdate::Flag::Color); + show->showToast(error.type()); + }; + const auto send = [&](auto &&request) { + peer->session().api().request( + std::move(request) + ).done(done).fail(fail).send(); + }; + if (peer->isSelf()) { + send(MTPaccount_UpdateColor( + MTP_flags( + MTPaccount_UpdateColor::Flag::f_background_emoji_id), + MTP_int(colorIndex), + MTP_long(peer->backgroundEmojiId()))); + } else if (const auto channel = peer->asChannel()) { + send(MTPchannels_UpdateColor( + MTP_flags( + MTPchannels_UpdateColor::Flag::f_background_emoji_id), + channel->inputChannel, + MTP_int(colorIndex), + MTP_long(peer->backgroundEmojiId()))); + } else { + Unexpected("Invalid peer type in Set(colorIndex)."); + } +} + +void Apply( + std::shared_ptr show, + not_null peer, + uint8 colorIndex, + Fn close, + Fn cancel) { + const auto session = &peer->session(); + if (peer->colorIndex() == colorIndex) { + close(); + } else if (peer->isSelf() && !session->premium()) { + Settings::ShowPremiumPromoToast( + show, + tr::lng_settings_color_subscribe( + tr::now, + lt_link, + Ui::Text::Link( + Ui::Text::Bold( + tr::lng_send_as_premium_required_link(tr::now))), + Ui::Text::WithEntities), + u"name_color"_q); + cancel(); + } else if (peer->isSelf()) { + Set(show, peer, colorIndex); + close(); + } else { + session->api().request(MTPpremium_GetBoostsStatus( + peer->input + )).done([=](const MTPpremium_BoostsStatus &result) { + const auto &data = result.data(); + const auto required = session->account().appConfig().get( + "channel_color_level_min", + 5); + if (data.vlevel().v >= required) { + Set(show, peer, colorIndex); + close(); + return; + } + const auto next = data.vnext_level_boosts().value_or_empty(); + const auto openStatistics = [=] { + if (const auto controller = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo)) { + controller->showSection(Info::Boosts::Make(peer)); + } + }; + show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{ + .link = qs(data.vboost_url()), + .boost = { + .level = data.vlevel().v, + .boosts = data.vboosts().v, + .thisLevelBoosts = data.vcurrent_level_boosts().v, + .nextLevelBoosts = next, + }, + .requiredLevel = required, + }, openStatistics, nullptr)); + cancel(); + }).fail([=](const MTP::Error &error) { + show->showToast(error.type()); + cancel(); + }).send(); + } +} + +class ColorSelector final : public Ui::RpWidget { +public: + ColorSelector( + not_null box, + std::shared_ptr style, + rpl::producer> indices, + uint8 index, + Fn callback); + +private: + void fillFrom(std::vector indices); + + int resizeGetHeight(int newWidth) override; + + const std::shared_ptr _style; + std::vector> _samples; + const Fn _callback; + uint8 _index = 0; + +}; + +ColorSelector::ColorSelector( + not_null box, + std::shared_ptr style, + rpl::producer> indices, + uint8 index, + Fn callback) +: RpWidget(box) +, _style(style) +, _callback(std::move(callback)) +, _index(index) { + std::move( + indices + ) | rpl::start_with_next([=](std::vector indices) { + fillFrom(std::move(indices)); + }, lifetime()); +} + +void ColorSelector::fillFrom(std::vector indices) { + auto samples = std::vector>(); + const auto add = [&](uint8 index) { + auto i = ranges::find(_samples, index, &ColorSample::index); + if (i != end(_samples)) { + samples.push_back(std::move(*i)); + _samples.erase(i); + } else { + samples.push_back(std::make_unique( + this, + _style, + index, + index == _index)); + samples.back()->show(); + samples.back()->setClickedCallback([=] { + if (_index != index) { + _callback(index); + + ranges::find( + _samples, + _index, + &ColorSample::index + )->get()->setSelected(false); + _index = index; + ranges::find( + _samples, + _index, + &ColorSample::index + )->get()->setSelected(true); + } + }); + } + }; + for (const auto index : indices) { + add(index); + } + if (!ranges::contains(indices, _index)) { + add(_index); + } + _samples = std::move(samples); + if (width() > 0) { + resizeToWidth(width()); + } +} + +int ColorSelector::resizeGetHeight(int newWidth) { + if (newWidth <= 0) { + return 0; + } + const auto count = int(_samples.size()); + const auto columns = Ui::kSimpleColorIndexCount; + const auto rows = (count + columns - 1) / columns; + const auto skip = st::settingsColorRadioSkip; + const auto size = (newWidth - skip * (columns - 1)) / float64(columns); + const auto isize = int(base::SafeRound(size)); + auto top = 0; + auto left = 0.; + for (auto i = 0; i != count; ++i) { + const auto row = i / columns; + const auto column = i % columns; + _samples[i]->resize(isize, isize); + _samples[i]->move(int(base::SafeRound(left)), top); + left += size + skip; + if (!((i + 1) % columns)) { + top += isize + skip; + left = 0.; + } + } + return (top - skip) + ((count % columns) ? (isize + skip) : 0); +} + } // namespace void EditPeerColorBox( - not_null box, - std::shared_ptr show, - not_null peer, - std::shared_ptr st) { + not_null box, + std::shared_ptr show, + not_null peer, + std::shared_ptr style, + std::shared_ptr theme) { + box->setTitle(tr::lng_settings_color_title()); + box->setWidth(st::boxWideWidth); + struct State { + rpl::variable index; + bool changing = false; + bool applying = false; + }; + const auto state = box->lifetime().make_state(); + state->index = peer->colorIndex(); + + box->addRow(object_ptr( + box, + style, + theme, + peer, + state->index.value() + ), {}); + + const auto appConfig = &peer->session().account().appConfig(); + auto indices = rpl::single( + rpl::empty + ) | rpl::then( + appConfig->refreshed() + ) | rpl::map([=] { + const auto list = appConfig->get>( + "peer_colors_available", + { 0, 1, 2, 3, 4, 5, 6 }); + return list | ranges::views::transform([](int i) { + return uint8(i); + }) | ranges::to_vector; + }); + const auto margin = st::settingsColorRadioMargin; + const auto skip = st::settingsColorRadioSkip; + box->addRow( + object_ptr( + box, + style, + std::move(indices), + state->index.current(), + [=](uint8 index) { state->index = index; }), + { margin, skip, margin, skip }); + + const auto container = box->verticalLayout(); + AddDividerText(container, peer->isSelf() + ? tr::lng_settings_color_about() + : tr::lng_settings_color_about_channel()); + + box->addButton(tr::lng_settings_apply(), [=] { + if (state->applying) { + return; + } + state->applying = true; + Apply(show, peer, state->index.current(), crl::guard(box, [=] { + box->closeBox(); + }), crl::guard(box, [=] { + state->applying = false; + })); + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); } void AddPeerColorButton( @@ -144,11 +726,16 @@ void AddPeerColorButton( return peer->colorIndex(); }); const auto name = peer->shortName(); - const auto st = std::make_shared( + + const auto style = std::make_shared( peer->session().colorIndicesValue()); + const auto theme = std::shared_ptr( + Window::Theme::DefaultChatThemeOn(button->lifetime())); + style->apply(theme.get()); + const auto sample = Ui::CreateChild( button.get(), - st, + style, rpl::duplicate(colorIndexValue), name); sample->show(); @@ -167,7 +754,7 @@ void AddPeerColorButton( - (st::settingsColorButton.padding.right() - sampleSize) - st::settingsButton.style.font->width(button) - st::settingsButtonRightSkip; - if (st->colorPatternIndex(colorIndex)) { + if (style->colorPatternIndex(colorIndex)) { sample->resize(sampleSize, sampleSize); } else { const auto padding = st::settingsColorSamplePadding; @@ -188,7 +775,7 @@ void AddPeerColorButton( const auto right = st::settingsColorButton.padding.right() - st::settingsColorSampleSkip - st::settingsColorSampleSize - - (st->colorPatternIndex(colorIndex) + - (style->colorPatternIndex(colorIndex) ? 0 : st::settingsColorSamplePadding.right()); sample->move( @@ -197,4 +784,8 @@ void AddPeerColorButton( }, sample->lifetime()); sample->setAttribute(Qt::WA_TransparentForMouseEvents); + + button->setClickedCallback([=] { + show->show(Box(EditPeerColorBox, show, peer, style, theme)); + }); } diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h index b69e9e04f..68aecaab2 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.h @@ -14,6 +14,7 @@ class Show; namespace Ui { class GenericBox; class ChatStyle; +class ChatTheme; class VerticalLayout; } // namespace Ui @@ -21,7 +22,8 @@ void EditPeerColorBox( not_null box, std::shared_ptr show, not_null peer, - std::shared_ptr st = nullptr); + std::shared_ptr style = nullptr, + std::shared_ptr theme = nullptr); void AddPeerColorButton( not_null container, diff --git a/Telegram/SourceFiles/history/history_item_components.cpp b/Telegram/SourceFiles/history/history_item_components.cpp index 5cf6ecb4c..b75292aa0 100644 --- a/Telegram/SourceFiles/history/history_item_components.cpp +++ b/Telegram/SourceFiles/history/history_item_components.cpp @@ -838,6 +838,8 @@ void HistoryMessageReply::paint( ? resolvedMessage->displayFrom() : resolvedStory ? resolvedStory->peer().get() + : _externalSender + ? _externalSender : nullptr; const auto backgroundEmojiId = colorPeer ? colorPeer->backgroundEmojiId() diff --git a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp index 0b2fad3d7..49a5dabd4 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_draft_options.cpp @@ -70,39 +70,6 @@ private: }; -[[nodiscard]] std::unique_ptr DefaultThemeOn( - rpl::lifetime &lifetime) { - auto result = std::make_unique(); - - using namespace Window::Theme; - const auto push = [=, raw = result.get()] { - const auto background = Background(); - const auto &paper = background->paper(); - raw->setBackground({ - .prepared = background->prepared(), - .preparedForTiled = background->preparedForTiled(), - .gradientForFill = background->gradientForFill(), - .colorForFill = background->colorForFill(), - .colors = paper.backgroundColors(), - .patternOpacity = paper.patternOpacity(), - .gradientRotation = paper.gradientRotation(), - .isPattern = paper.isPattern(), - .tile = background->tile(), - }); - }; - - push(); - Background()->updates( - ) | rpl::start_with_next([=](const BackgroundUpdate &update) { - if (update.type == BackgroundUpdate::Type::New - || update.type == BackgroundUpdate::Type::Changed) { - push(); - } - }, lifetime); - - return result; -} - [[nodiscard]] TextWithEntities HighlightParsedLinks( TextWithEntities text, const std::vector &links) { @@ -124,7 +91,6 @@ private: return text; } - class PreviewWrap final : public Ui::RpWidget { public: PreviewWrap( @@ -195,7 +161,7 @@ PreviewWrap::PreviewWrap( : RpWidget(box) , _box(box) , _history(history) -, _theme(DefaultThemeOn(lifetime())) +, _theme(Window::Theme::DefaultChatThemeOn(lifetime())) , _style(std::make_unique( history->session().colorIndicesValue())) , _delegate(std::make_unique( @@ -376,7 +342,6 @@ void PreviewWrap::paintEvent(QPaintEvent *e) { } auto p = Painter(this); - auto hq = PainterHighQualityEnabler(p); auto context = _theme->preparePaintContext( _style.get(), diff --git a/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp index 26b12a690..7528c093b 100644 --- a/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp +++ b/Telegram/SourceFiles/info/boosts/info_boosts_inner_widget.cpp @@ -210,16 +210,14 @@ void InnerWidget::fill() { fakeShowed->events(), rpl::single(status.overview.isBoosted), dividerContent.data(), - Ui::BoostBoxData{ - .boost = Ui::BoostCounters{ - .level = status.overview.level, - .boosts = status.overview.boostCount, - .thisLevelBoosts - = status.overview.currentLevelBoostCount, - .nextLevelBoosts - = status.overview.nextLevelBoostCount, - .mine = status.overview.isBoosted, - } + Ui::BoostCounters{ + .level = status.overview.level, + .boosts = status.overview.boostCount, + .thisLevelBoosts + = status.overview.currentLevelBoostCount, + .nextLevelBoosts + = status.overview.nextLevelBoostCount, + .mine = status.overview.isBoosted, }, st::statisticsLimitsLinePadding); inner->add(object_ptr( diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 368973e21..7ce12a06b 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -567,10 +567,13 @@ filterLinkChatsList: PeerList(peerListBox) { } settingsColorSampleSize: 20px; -settingsColorSampleCenter: 8px; +settingsColorSampleCenter: 6px; settingsColorSampleCenterRadius: 2px; settingsColorSamplePadding: margins(8px, 2px, 8px, 2px); settingsColorSampleSkip: 6px; settingsColorButton: SettingsButton(settingsButton) { padding: margins(60px, 10px, 48px, 10px); -} \ No newline at end of file +} +settingsColorRadioMargin: 17px; +settingsColorRadioSkip: 13px; +settingsColorRadioStroke: 2px; diff --git a/Telegram/SourceFiles/settings/settings_premium.cpp b/Telegram/SourceFiles/settings/settings_premium.cpp index 3dd312074..cb3eeccd8 100644 --- a/Telegram/SourceFiles/settings/settings_premium.cpp +++ b/Telegram/SourceFiles/settings/settings_premium.cpp @@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/widgets/checkbox.h" // Ui::RadiobuttonGroup. #include "ui/widgets/gradient_round_button.h" #include "ui/widgets/labels.h" @@ -1466,6 +1467,36 @@ QString LookupPremiumRef(PremiumPreview section) { return QString(); } +void ShowPremiumPromoToast( + std::shared_ptr show, + TextWithEntities textWithLink, + const QString &ref) { + using WeakToast = base::weak_ptr; + const auto toast = std::make_shared(); + (*toast) = show->showToast({ + .text = std::move(textWithLink), + .st = &st::defaultMultilineToast, + .duration = Ui::Toast::kDefaultDuration * 2, + .multiline = true, + .filter = crl::guard(&show->session(), [=]( + const ClickHandlerPtr &, + Qt::MouseButton button) { + if (button == Qt::LeftButton) { + if (const auto strong = toast->get()) { + strong->hideAnimated(); + (*toast) = nullptr; + if (const auto controller = show->resolveWindow( + ChatHelpers::WindowUsage::PremiumPromo)) { + Settings::ShowPremium(controller, ref); + } + return true; + } + } + return false; + }), + }); +} + not_null CreateSubscribeButton( SubscribeButtonArgs &&args) { Expects(args.show || args.controller); diff --git a/Telegram/SourceFiles/settings/settings_premium.h b/Telegram/SourceFiles/settings/settings_premium.h index 0ad7b036c..6bc1775fd 100644 --- a/Telegram/SourceFiles/settings/settings_premium.h +++ b/Telegram/SourceFiles/settings/settings_premium.h @@ -51,6 +51,11 @@ void StartPremiumPayment( [[nodiscard]] QString LookupPremiumRef(PremiumPreview section); +void ShowPremiumPromoToast( + std::shared_ptr show, + TextWithEntities textWithLink, + const QString &ref); + struct SubscribeButtonArgs final { Window::SessionController *controller = nullptr; not_null parent; diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index 20e2e71b6..66f504f1d 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -16,6 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_premium.h" +#include + namespace Ui { void StartFireworks(not_null parent) { @@ -57,7 +59,7 @@ void BoostBox( BoxShowFinishes(box), state->you.value(), box->verticalLayout(), - data, + data.boost, st::boxRowPadding); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); @@ -170,31 +172,188 @@ void BoostBox( }, button->lifetime()); } +object_ptr MakeLinkLabel( + not_null parent, + rpl::producer text, + rpl::producer link, + std::shared_ptr show, + object_ptr right) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + const auto rawRight = right.release(); + if (rawRight) { + rawRight->setParent(raw); + rawRight->show(); + } + + struct State { + State( + not_null parent, + rpl::producer value, + rpl::producer link) + : text(std::move(value)) + , link(std::move(link)) + , label(parent, text.value(), st::giveawayGiftCodeLink) + , bg(st::roundRadiusLarge, st::windowBgOver) { + } + + rpl::variable text; + rpl::variable link; + Ui::FlatLabel label; + Ui::RoundRect bg; + }; + + const auto state = raw->lifetime().make_state( + raw, + rpl::duplicate(text), + std::move(link)); + state->label.setSelectable(true); + + rpl::combine( + raw->widthValue(), + std::move(text) + ) | rpl::start_with_next([=](int outer, const auto&) { + const auto textWidth = state->label.textMaxWidth(); + const auto skipLeft = st::giveawayGiftCodeLink.margin.left(); + const auto skipRight = rawRight + ? rawRight->width() + : st::giveawayGiftCodeLink.margin.right(); + const auto available = outer - skipRight - skipLeft; + const auto use = std::min(textWidth, available); + state->label.resizeToWidth(use); + const auto forCenter = (outer - use) / 2; + const auto x = (forCenter < skipLeft) + ? skipLeft + : (forCenter > outer - skipRight - use) + ? (outer - skipRight - use) + : forCenter; + state->label.moveToLeft(x, st::giveawayGiftCodeLink.margin.top()); + }, raw->lifetime()); + + raw->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(raw); + state->bg.paint(p, raw->rect()); + }, raw->lifetime()); + + state->label.setAttribute(Qt::WA_TransparentForMouseEvents); + + raw->resize(raw->width(), st::giveawayGiftCodeLinkHeight); + if (rawRight) { + raw->widthValue() | rpl::start_with_next([=](int width) { + rawRight->move(width - rawRight->width(), 0); + }, raw->lifetime()); + } + raw->setClickedCallback([=] { + QGuiApplication::clipboard()->setText(state->link.current()); + show->showToast(tr::lng_username_copied(tr::now)); + }); + + return result; +} + +void AskBoostBox( + not_null box, + AskBoostBoxData data, + Fn openStatistics, + Fn startGiveaway) { + box->setWidth(st::boxWideWidth); + box->setStyle(st::boostBox); + + const auto full = !data.boost.nextLevelBoosts; + + struct State { + rpl::variable you = false; + bool submitted = false; + }; + const auto state = box->lifetime().make_state(State{ + .you = data.boost.mine, + }); + + FillBoostLimit( + BoxShowFinishes(box), + state->you.value(), + box->verticalLayout(), + data.boost, + st::boxRowPadding); + + box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); + + auto title = tr::lng_boost_channel_title_color(); + auto text = rpl::combine( + tr::lng_boost_channel_needs_level_color( + lt_count, + rpl::single(float64(data.requiredLevel)), + Ui::Text::RichLangValue), + tr::lng_boost_channel_ask(Ui::Text::RichLangValue) + ) | rpl::map([](TextWithEntities &&text, TextWithEntities &&ask) { + return text.append(u"\n\n"_q).append(std::move(ask)); + }); + box->addRow( + object_ptr( + box, + std::move(title), + st::boostTitle), + st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0)); + box->addRow( + object_ptr( + box, + std::move(text), + st::boostText), + (st::boxRowPadding + + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip))); + + auto stats = object_ptr(box, st::boostLinkStatsButton); + stats->setClickedCallback(openStatistics); + box->addRow(MakeLinkLabel( + box, + rpl::single(data.link), + rpl::single(data.link), + box->uiShow(), + std::move(stats))); + + auto submit = tr::lng_boost_channel_ask_button(); + const auto button = box->addButton(rpl::duplicate(submit), [=] { + QGuiApplication::clipboard()->setText(data.link); + box->uiShow()->showToast(tr::lng_username_copied(tr::now)); + }); + rpl::combine( + std::move(submit), + box->widthValue() + ) | rpl::start_with_next([=](const QString &, int width) { + const auto &padding = st::boostBox.buttonPadding; + button->resizeToWidth(width + - padding.left() + - padding.right()); + button->moveToLeft(padding.left(), button->y()); + }, button->lifetime()); +} + void FillBoostLimit( rpl::producer<> showFinished, rpl::producer you, not_null container, - BoostBoxData data, + BoostCounters data, style::margins limitLinePadding) { - const auto full = !data.boost.nextLevelBoosts; + const auto full = !data.nextLevelBoosts; - if (data.boost.mine && data.boost.boosts > 0) { - --data.boost.boosts; + if (data.mine && data.boosts > 0) { + --data.boosts; } if (full) { - data.boost.nextLevelBoosts = data.boost.boosts - + (data.boost.mine ? 1 : 0); - data.boost.thisLevelBoosts = 0; - if (data.boost.level > 0) { - --data.boost.level; + data.nextLevelBoosts = data.boosts + + (data.mine ? 1 : 0); + data.thisLevelBoosts = 0; + if (data.level > 0) { + --data.level; } - } else if (data.boost.mine - && data.boost.level > 0 - && data.boost.boosts < data.boost.thisLevelBoosts) { - --data.boost.level; - data.boost.nextLevelBoosts = data.boost.thisLevelBoosts; - data.boost.thisLevelBoosts = 0; + } else if (data.mine + && data.level > 0 + && data.boosts < data.thisLevelBoosts) { + --data.level; + data.nextLevelBoosts = data.thisLevelBoosts; + data.thisLevelBoosts = 0; } const auto addSkip = [&](int skip) { @@ -205,18 +364,18 @@ void FillBoostLimit( const auto levelWidth = [&](int add) { return st::normalFont->width( - tr::lng_boost_level(tr::now, lt_count, data.boost.level + add)); + tr::lng_boost_level(tr::now, lt_count, data.level + add)); }; const auto paddings = 2 * st::premiumLineTextSkip; const auto labelLeftWidth = paddings + levelWidth(0); const auto labelRightWidth = paddings + levelWidth(1); const auto ratio = [=](int boosts) { const auto min = std::min( - data.boost.boosts, - data.boost.thisLevelBoosts); + data.boosts, + data.thisLevelBoosts); const auto max = std::max({ - data.boost.boosts, - data.boost.nextLevelBoosts, + data.boosts, + data.nextLevelBoosts, 1, }); Assert(boosts >= min && boosts <= max); @@ -239,12 +398,12 @@ void FillBoostLimit( return (first + (index - 1) * other) / available; }; - const auto min = std::min(data.boost.boosts, data.boost.thisLevelBoosts); - const auto now = data.boost.boosts; - const auto max = (data.boost.nextLevelBoosts > min) - ? (data.boost.nextLevelBoosts) - : (data.boost.boosts > 0) - ? data.boost.boosts + const auto min = std::min(data.boosts, data.thisLevelBoosts); + const auto now = data.boosts; + const auto max = (data.nextLevelBoosts > min) + ? (data.nextLevelBoosts) + : (data.boosts > 0) + ? data.boosts : 1; auto bubbleRowState = ( std::move(you) @@ -280,8 +439,8 @@ void FillBoostLimit( container, st::boostLimits, Premium::LimitRowLabels{ - .leftLabel = level(data.boost.level), - .rightLabel = level(data.boost.level + 1), + .leftLabel = level(data.level), + .rightLabel = level(data.level + 1), .dynamic = true, }, std::move(ratioValue), diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.h b/Telegram/SourceFiles/ui/boxes/boost_box.h index 2c688acda..da8d57572 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.h +++ b/Telegram/SourceFiles/ui/boxes/boost_box.h @@ -7,10 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/object_ptr.h" + namespace Ui { void StartFireworks(not_null parent); +class Show; +class RpWidget; class GenericBox; class VerticalLayout; @@ -32,11 +36,30 @@ void BoostBox( BoostBoxData data, Fn)> boost); +struct AskBoostBoxData { + QString link; + BoostCounters boost; + int requiredLevel = 0; +}; + +void AskBoostBox( + not_null box, + AskBoostBoxData data, + Fn openStatistics, + Fn startGiveaway); + +[[nodiscard]] object_ptr MakeLinkLabel( + not_null parent, + rpl::producer text, + rpl::producer link, + std::shared_ptr show, + object_ptr right); + void FillBoostLimit( rpl::producer<> showFinished, rpl::producer you, not_null container, - BoostBoxData data, + BoostCounters data, style::margins limitLinePadding); } // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/choose_send_as.cpp b/Telegram/SourceFiles/ui/chat/choose_send_as.cpp index fc24f1aab..0856b044a 100644 --- a/Telegram/SourceFiles/ui/chat/choose_send_as.cpp +++ b/Telegram/SourceFiles/ui/chat/choose_send_as.cpp @@ -14,7 +14,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "ui/controls/send_as_button.h" #include "ui/text/text_utilities.h" -#include "ui/toast/toast.h" #include "ui/painter.h" #include "window/window_session_controller.h" #include "main/main_session.h" @@ -168,39 +167,6 @@ rpl::producer> ListController::clicked() const { return _clicked.events(); } -void ShowPremiumPromoToast(not_null controller) { - using WeakToast = base::weak_ptr; - const auto toast = std::make_shared(); - - auto link = Ui::Text::Link( - tr::lng_send_as_premium_required_link(tr::now)); - link.entities.push_back( - EntityInText(EntityType::Semibold, 0, link.text.size())); - (*toast) = controller->showToast({ - .text = tr::lng_send_as_premium_required( - tr::now, - lt_link, - link, - Ui::Text::WithEntities), - .st = &st::defaultMultilineToast, - .duration = Ui::Toast::kDefaultDuration * 2, - .multiline = true, - .filter = crl::guard(&controller->session(), [=]( - const ClickHandlerPtr &, - Qt::MouseButton button) { - if (button == Qt::LeftButton) { - if (const auto strong = toast->get()) { - strong->hideAnimated(); - (*toast) = nullptr; - Settings::ShowPremium(controller, "send_as"); - return true; - } - } - return false; - }), - }); -} - } // namespace void ChooseSendAsBox( @@ -272,7 +238,17 @@ void SetupSendAsButton( if (i != end(list) && i->premiumRequired && !sendAs->session().premium()) { - ShowPremiumPromoToast(window); + Settings::ShowPremiumPromoToast( + window->uiShow(), + tr::lng_send_as_premium_required( + tr::now, + lt_link, + Ui::Text::Link( + Ui::Text::Bold( + tr::lng_send_as_premium_required_link( + tr::now))), + Ui::Text::WithEntities), + u"send_as"_q); return false; } session->sendAsPeers().saveChosen(peer, sendAs); diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index d7521700e..1891e26e0 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -288,6 +288,14 @@ giveawayGiftCodeLinkHeight: 42px; giveawayGiftCodeLinkCopyWidth: 40px; giveawayGiftCodeLinkMargin: margins(24px, 8px, 24px, 12px); +boostLinkStatsButton: IconButton(defaultIconButton) { + width: giveawayGiftCodeLinkCopyWidth; + height: giveawayGiftCodeLinkHeight; + icon: icon{{ "menu/stats", menuIconColor }}; + iconOver: icon{{ "menu/stats", menuIconColor }}; + ripple: emptyRippleAnimation; +} + giveawayGiftCodeTable: Table(defaultTable) { labelMinWidth: 91px; } diff --git a/Telegram/SourceFiles/window/themes/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp index e8b06a5a6..be3d87846 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme.cpp @@ -1591,5 +1591,36 @@ SendMediaReady PrepareWallPaper(MTP::DcId dcId, const QImage &image) { QByteArray()); } +std::unique_ptr DefaultChatThemeOn(rpl::lifetime &lifetime) { + auto result = std::make_unique(); + + const auto push = [=, raw = result.get()] { + const auto background = Background(); + const auto &paper = background->paper(); + raw->setBackground({ + .prepared = background->prepared(), + .preparedForTiled = background->preparedForTiled(), + .gradientForFill = background->gradientForFill(), + .colorForFill = background->colorForFill(), + .colors = paper.backgroundColors(), + .patternOpacity = paper.patternOpacity(), + .gradientRotation = paper.gradientRotation(), + .isPattern = paper.isPattern(), + .tile = background->tile(), + }); + }; + + push(); + Background()->updates( + ) | rpl::start_with_next([=](const BackgroundUpdate &update) { + if (update.type == BackgroundUpdate::Type::New + || update.type == BackgroundUpdate::Type::Changed) { + push(); + } + }, lifetime); + + return result; +} + } // namespace Theme } // namespace Window diff --git a/Telegram/SourceFiles/window/themes/window_theme.h b/Telegram/SourceFiles/window/themes/window_theme.h index 32f43977c..1425e4d07 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.h +++ b/Telegram/SourceFiles/window/themes/window_theme.h @@ -28,6 +28,7 @@ class Controller; namespace Ui { struct ChatThemeBackground; +class ChatTheme; } // namespace Ui namespace Webview { @@ -309,5 +310,8 @@ bool ReadPaletteValues( [[nodiscard]] Webview::ThemeParams WebViewParams(); +[[nodiscard]] std::unique_ptr DefaultChatThemeOn( + rpl::lifetime &lifetime); + } // namespace Theme } // namespace Window