/* 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/settings_information.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/vertical_layout_reorder.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/box_content_divider.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" #include "ui/text/text_utilities.h" #include "ui/delayed_activation.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "ui/unread_badge_paint.h" #include "core/application.h" #include "core/core_settings.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "boxes/add_contact_box.h" #include "boxes/premium_limits_box.h" #include "boxes/username_box.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_peer_values.h" #include "data/data_changes.h" #include "data/data_premium_limits.h" #include "info/profile/info_profile_values.h" #include "info/profile/info_profile_badge.h" #include "lang/lang_keys.h" #include "main/main_account.h" #include "main/main_session.h" #include "main/main_domain.h" #include "mtproto/mtproto_dc_options.h" #include "window/window_session_controller.h" #include "window/window_controller.h" #include "window/window_peer_menu.h" #include "apiwrap.h" #include "api/api_peer_photo.h" #include "api/api_user_names.h" #include "base/call_delayed.h" #include "base/options.h" #include "base/unixtime.h" #include "base/random.h" #include "styles/style_chat.h" // popupMenuExpandedSeparator #include "styles/style_dialogs.h" // dialogsPremiumIcon #include "styles/style_layers.h" #include "styles/style_settings.h" #include "styles/style_menu_icons.h" #include "styles/style_window.h" #include #include namespace Settings { namespace { constexpr auto kSaveBioTimeout = 1000; constexpr auto kPlayStatusLimit = 2; class ComposedBadge final : public Ui::RpWidget { public: ComposedBadge( not_null parent, not_null button, not_null session, rpl::producer &&text, bool hasUnread, Fn animationPaused); private: rpl::variable _text; rpl::event_stream _unreadWidth; rpl::event_stream _premiumWidth; QPointer _unread; Info::Profile::Badge _badge; }; ComposedBadge::ComposedBadge( not_null parent, not_null button, not_null session, rpl::producer &&text, bool hasUnread, Fn animationPaused) : Ui::RpWidget(parent) , _text(std::move(text)) , _badge( this, st::settingsInfoPeerBadge, session->user(), nullptr, std::move(animationPaused), kPlayStatusLimit, Info::Profile::BadgeType::Premium) { if (hasUnread) { _unread = CreateUnread(this, rpl::single( rpl::empty ) | rpl::then( session->data().unreadBadgeChanges() ) | rpl::map([=] { auto &owner = session->data(); return Badge::UnreadBadge{ owner.unreadBadge(), owner.unreadBadgeMuted(), }; })); rpl::combine( _unread->shownValue(), _unread->widthValue() ) | rpl::map([=](bool shown, int width) { return shown ? width : 0; }) | rpl::start_to_stream(_unreadWidth, _unread->lifetime()); } _badge.updated( ) | rpl::start_with_next([=] { if (const auto button = _badge.widget()) { button->widthValue( ) | rpl::start_to_stream(_premiumWidth, button->lifetime()); } else { _premiumWidth.fire(0); } }, lifetime()); auto textWidth = _text.value() | rpl::map([=] { return button->fullTextWidth(); }); rpl::combine( _unreadWidth.events_starting_with(_unread ? _unread->width() : 0), _premiumWidth.events_starting_with(_badge.widget() ? _badge.widget()->width() : 0), std::move(textWidth), button->sizeValue() ) | rpl::start_with_next([=]( int unreadWidth, int premiumWidth, int textWidth, const QSize &buttonSize) { const auto &st = button->st(); const auto skip = st.style.font->spacew; const auto textRightPosition = st.padding.left() + textWidth + skip; const auto minWidth = unreadWidth + premiumWidth + skip; const auto maxTextWidth = buttonSize.width() - minWidth - st.padding.right(); const auto finalTextRight = std::min(textRightPosition, maxTextWidth); resize( buttonSize.width() - st.padding.right() - finalTextRight, buttonSize.height()); _badge.move( 0, st.padding.top(), buttonSize.height() - st.padding.top()); if (_unread) { _unread->moveToRight( 0, (buttonSize.height() - _unread->height()) / 2); } }, lifetime()); } class AccountsList final { public: AccountsList( not_null container, not_null controller); [[nodiscard]] rpl::producer<> closeRequests() const; private: void setup(); [[nodiscard]] not_null*> setupAdd(); void rebuild(); const not_null _controller; const not_null _outer; int _outerIndex = 0; Ui::SlideWrap *_addAccount = nullptr; base::flat_map< not_null, base::unique_qptr> _watched; base::unique_qptr _contextMenu; std::unique_ptr _reorder; int _reordering = 0; rpl::event_stream<> _closeRequests; base::binary_guard _accountSwitchGuard; }; [[nodiscard]] rpl::producer StatusValue( not_null user) { return [=](auto consumer) { auto lifetime = rpl::lifetime(); const auto timer = lifetime.make_state(); const auto push = [=] { const auto now = base::unixtime::now(); consumer.put_next(Data::OnlineTextActive(user, now) ? Ui::Text::Link(Data::OnlineText(user, now)) : Ui::Text::WithEntities(Data::OnlineText(user, now))); timer->callOnce(Data::OnlineChangeTimeout(user, now)); }; timer->setCallback(push); user->session().changes().peerFlagsValue( user, Data::PeerUpdate::Flag::OnlineStatus ) | rpl::start_with_next(push, lifetime); return lifetime; }; } void SetupPhoto( not_null container, not_null controller, not_null self) { const auto wrap = container->add(object_ptr( container, st::settingsInfoPhotoHeight)); const auto photo = Ui::CreateChild( wrap, controller, self, Ui::UserpicButton::Role::OpenPhoto, Ui::UserpicButton::Source::PeerPhoto, st::settingsInfoPhoto); const auto upload = CreateUploadSubButton(wrap, controller); upload->chosenImages( ) | rpl::start_with_next([=](Ui::UserpicButton::ChosenImage &&chosen) { auto &image = chosen.image; UpdatePhotoLocally(self, image); photo->showCustom(base::duplicate(image)); self->session().api().peerPhoto().upload( self, { std::move(image), chosen.markup.documentId, chosen.markup.colors, }); }, upload->lifetime()); const auto name = Ui::CreateChild( wrap, Info::Profile::NameValue(self), st::settingsCoverName); const auto status = Ui::CreateChild( wrap, StatusValue(self), st::settingsCoverStatus); status->setAttribute(Qt::WA_TransparentForMouseEvents); rpl::combine( wrap->widthValue(), photo->widthValue(), Info::Profile::NameValue(self), status->widthValue() ) | rpl::start_with_next([=]( int max, int photoWidth, const QString&, int statusWidth) { photo->moveToLeft( (max - photoWidth) / 2, st::settingsInfoPhotoTop); upload->moveToLeft( ((max - photoWidth) / 2 + photoWidth - upload->width() + st::settingsInfoUploadLeft), photo->y() + photo->height() - upload->height()); const auto skip = st::settingsButton.iconLeft; name->resizeToNaturalWidth(max - 2 * skip); name->moveToLeft( (max - name->width()) / 2, (photo->y() + photo->height() + st::settingsInfoPhotoSkip)); status->moveToLeft( (max - statusWidth) / 2, (name->y() + name->height() + st::settingsInfoNameSkip)); }, photo->lifetime()); } void ShowMenu( QWidget *parent, const QString ©Button, const QString &text) { const auto menu = Ui::CreateChild(parent); menu->addAction(copyButton, [=] { QGuiApplication::clipboard()->setText(text); }); menu->popup(QCursor::pos()); } void AddRow( not_null container, rpl::producer label, rpl::producer value, const QString ©Button, Fn edit, IconDescriptor &&descriptor) { const auto wrap = AddButtonWithLabel( container, std::move(label), std::move(value) | rpl::map([](const auto &t) { return t.text; }), st::settingsButton, std::move(descriptor)); const auto forcopy = Ui::CreateChild(wrap.get()); wrap->setAcceptBoth(); wrap->clicks( ) | rpl::filter([=] { return !wrap->isDisabled(); }) | rpl::start_with_next([=](Qt::MouseButton button) { if (button == Qt::LeftButton) { edit(); } else if (!forcopy->isEmpty()) { ShowMenu(wrap, copyButton, *forcopy); } }, wrap->lifetime()); auto existing = base::duplicate( value ) | rpl::map([](const TextWithEntities &text) { return text.entities.isEmpty(); }); base::duplicate( value ) | rpl::filter([](const TextWithEntities &text) { return text.entities.isEmpty(); }) | rpl::start_with_next([=](const TextWithEntities &text) { *forcopy = text.text; }, wrap->lifetime()); } void SetupRows( not_null container, not_null controller, not_null self) { const auto session = &self->session(); Ui::AddSkip(container); AddRow( container, tr::lng_settings_name_label(), Info::Profile::NameValue(self) | Ui::Text::ToWithEntities(), tr::lng_profile_copy_fullname(tr::now), [=] { controller->show(Box(self)); }, { &st::menuIconProfile }); const auto showChangePhone = [=] { controller->show( Ui::MakeInformBox(tr::lng_change_phone_error())); controller->window().activate(); }; AddRow( container, tr::lng_settings_phone_label(), Info::Profile::PhoneValue(self), tr::lng_profile_copy_phone(tr::now), showChangePhone, { &st::menuIconPhone }); auto username = Info::Profile::UsernameValue(self); auto empty = base::duplicate( username ) | rpl::map([](const TextWithEntities &username) { return username.text.isEmpty(); }); auto label = rpl::combine( tr::lng_settings_username_label(), std::move(empty) ) | rpl::map([](const QString &label, bool empty) { return empty ? "t.me/username" : label; }); auto value = rpl::combine( std::move(username), tr::lng_settings_username_add() ) | rpl::map([](const TextWithEntities &username, const QString &add) { if (!username.text.isEmpty()) { return username; } auto result = TextWithEntities{ add }; result.entities.push_back({ EntityType::CustomUrl, 0, int(add.size()), "internal:edit_username" }); return result; }); session->api().usernames().requestToCache(session->user()); AddRow( container, std::move(label), std::move(value), tr::lng_context_copy_mention(tr::now), [=] { const auto box = controller->show( Box(UsernamesBox, session->user())); box->boxClosing( ) | rpl::start_with_next([=] { session->api().usernames().requestToCache(session->user()); }, box->lifetime()); }, { &st::menuIconUsername }); Ui::AddSkip(container); Ui::AddDividerText(container, tr::lng_settings_username_about()); } void SetupBio( not_null container, not_null self) { const auto limits = Data::PremiumLimits(&self->session()); const auto defaultLimit = limits.aboutLengthDefault(); const auto premiumLimit = limits.aboutLengthPremium(); const auto bioStyle = [=] { auto result = st::settingsBio; result.textMargins.setRight(st::boxTextFont->spacew + st::boxTextFont->width('-' + QString::number(premiumLimit))); return result; }; const auto style = Ui::AttachAsChild(container, bioStyle()); const auto current = Ui::AttachAsChild(container, self->about()); const auto changed = Ui::CreateChild>( container.get()); const auto bio = container->add( object_ptr( container, *style, Ui::InputField::Mode::MultiLine, tr::lng_bio_placeholder(), *current), st::settingsBioMargins); const auto countdown = Ui::CreateChild( container.get(), QString(), st::settingsBioCountdown); rpl::combine( bio->geometryValue(), countdown->widthValue() ) | rpl::start_with_next([=](QRect geometry, int width) { countdown->move( geometry.x() + geometry.width() - width, geometry.y() + style->textMargins.top()); }, countdown->lifetime()); const auto assign = [=](QString text) { auto position = bio->textCursor().position(); bio->setText(text.replace('\n', ' ')); auto cursor = bio->textCursor(); cursor.setPosition(position); bio->setTextCursor(cursor); }; const auto updated = [=] { auto text = bio->getLastText(); if (text.indexOf('\n') >= 0) { assign(text); text = bio->getLastText(); } changed->fire(*current != text); const auto limit = self->isPremium() ? premiumLimit : defaultLimit; const auto countLeft = limit - bio->lastTextSizeWithoutSurrogatePairsCount(); countdown->setText(QString::number(countLeft)); countdown->setTextColorOverride( countLeft < 0 ? st::boxTextFgError->c : std::optional()); }; const auto save = [=] { self->session().api().saveSelfBio( TextUtilities::PrepareForSending(bio->getLastText())); }; Info::Profile::AboutValue( self ) | rpl::start_with_next([=](const TextWithEntities &text) { const auto wasChanged = (*current != bio->getLastText()); *current = text.text; if (wasChanged) { changed->fire(*current != bio->getLastText()); } else { assign(text.text); *current = bio->getLastText(); } }, bio->lifetime()); const auto generation = Ui::CreateChild(bio); changed->events( ) | rpl::start_with_next([=](bool changed) { if (changed) { const auto saved = *generation = std::abs(*generation) + 1; base::call_delayed(kSaveBioTimeout, bio, [=] { if (*generation == saved) { save(); *generation = 0; } }); } else if (*generation > 0) { *generation = -*generation; } }, bio->lifetime()); // We need 'bio' to still exist here as InputField, so we add this // to 'container' lifetime, not to the 'bio' lifetime. container->lifetime().add([=] { if (*generation > 0) { save(); } }); bio->setMaxLength(premiumLimit * 2); bio->setSubmitSettings(Ui::InputField::SubmitSettings::Both); auto cursor = bio->textCursor(); cursor.setPosition(bio->getLastText().size()); bio->setTextCursor(cursor); bio->submits() | rpl::start_with_next([=] { save(); }, bio->lifetime()); bio->changes() | rpl::start_with_next(updated, bio->lifetime()); bio->setInstantReplaces(Ui::InstantReplaces::Default()); bio->setInstantReplacesEnabled( Core::App().settings().replaceEmojiValue()); Ui::Emoji::SuggestionsController::Init( container->window(), bio, &self->session()); updated(); Ui::AddDividerText(container, tr::lng_settings_about_bio()); } void SetupAccountsWrap( not_null container, not_null controller) { Ui::AddSkip(container); SetupAccounts(container, controller); } [[nodiscard]] bool IsAltShift(Qt::KeyboardModifiers modifiers) { return (modifiers & Qt::ShiftModifier) && (modifiers & Qt::AltModifier); } [[nodiscard]] object_ptr MakeAccountButton( QWidget *parent, not_null window, not_null account, Fn callback, bool locked) { const auto active = (account == &window->session().account()); const auto session = &account->session(); const auto user = session->user(); auto text = rpl::single( user->name() ) | rpl::then(session->changes().realtimeNameUpdates( user ) | rpl::map([=] { return user->name(); })); auto result = object_ptr( parent, rpl::duplicate(text), st::mainMenuAddAccountButton); const auto raw = result.data(); { const auto container = Badge::AddRight(raw); const auto composedBadge = Ui::CreateChild( container.get(), raw, session, std::move(text), !active, [=] { return window->isGifPausedAtLeastFor( Window::GifPauseReason::Layer); }); composedBadge->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { container->resize(s); }, container->lifetime()); } struct State { State(QWidget *parent) : userpic(parent) { userpic.setAttribute(Qt::WA_TransparentForMouseEvents); } Ui::RpWidget userpic; Ui::PeerUserpicView view; base::unique_qptr menu; }; const auto state = raw->lifetime().make_state(raw); const auto userpicSkip = 2 * st::mainMenuAccountLine + st::lineWidth; const auto userpicSize = st::mainMenuAccountSize + userpicSkip * 2; raw->heightValue( ) | rpl::start_with_next([=](int height) { const auto left = st::mainMenuAddAccountButton.iconLeft + (st::settingsIconAdd.width() - userpicSize) / 2; const auto top = (height - userpicSize) / 2; state->userpic.setGeometry(left, top, userpicSize, userpicSize); }, state->userpic.lifetime()); state->userpic.paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(&state->userpic); const auto size = st::mainMenuAccountSize; const auto line = st::mainMenuAccountLine; const auto skip = 2 * line + st::lineWidth; const auto full = size + skip * 2; user->paintUserpicLeft(p, state->view, skip, skip, full, size); if (active) { const auto shift = st::lineWidth + (line * 0.5); const auto diameter = full - 2 * shift; const auto rect = QRectF(shift, shift, diameter, diameter); auto hq = PainterHighQualityEnabler(p); auto pen = st::windowBgActive->p; // The same as '+' in add. pen.setWidthF(line); p.setPen(pen); p.setBrush(Qt::NoBrush); p.drawEllipse(rect); } }, state->userpic.lifetime()); raw->setAcceptBoth(true); raw->clicks( ) | rpl::start_with_next([=](Qt::MouseButton which) { if (which == Qt::LeftButton) { callback(raw->clickModifiers()); return; } else if (which != Qt::RightButton) { return; } if (!state->menu && IsAltShift(raw->clickModifiers()) && !locked) { state->menu = base::make_unique_q( raw, st::popupMenuWithIcons); Window::MenuAddMarkAsReadAllChatsAction( window, Ui::Menu::CreateAddActionCallback(state->menu)); state->menu->popup(QCursor::pos()); return; } if (session == &window->session() || state->menu) { return; } state->menu = base::make_unique_q( raw, st::popupMenuExpandedSeparator); const auto addAction = Ui::Menu::CreateAddActionCallback( state->menu); addAction(tr::lng_context_new_window(tr::now), [=] { Ui::PreventDelayedActivation(); callback(Qt::ControlModifier); }, &st::menuIconNewWindow); Window::AddSeparatorAndShiftUp(addAction); addAction(tr::lng_profile_copy_phone(tr::now), [=] { const auto phone = rpl::variable( Info::Profile::PhoneValue(session->user())); QGuiApplication::clipboard()->setText(phone.current().text); }, &st::menuIconCopy); if (!locked) { addAction(tr::lng_menu_activate(tr::now), [=] { callback({}); }, &st::menuIconProfile); } auto logoutCallback = [=] { const auto callback = [=](Fn &&close) { close(); Core::App().logoutWithChecks(&session->account()); }; window->show( Ui::MakeConfirmBox({ .text = tr::lng_sure_logout(), .confirmed = crl::guard(session, callback), .confirmText = tr::lng_settings_logout(), .confirmStyle = &st::attentionBoxButton, }), Ui::LayerOption::CloseOther); }; addAction({ .text = tr::lng_settings_logout(tr::now), .handler = std::move(logoutCallback), .icon = &st::menuIconLeaveAttention, .isAttention = true, }); state->menu->popup(QCursor::pos()); }, raw->lifetime()); return result; } AccountsList::AccountsList( not_null container, not_null controller) : _controller(controller) , _outer(container) , _outerIndex(container->count()) { setup(); } rpl::producer<> AccountsList::closeRequests() const { return _closeRequests.events(); } void AccountsList::setup() { _addAccount = setupAdd(); rpl::single(rpl::empty) | rpl::then( Core::App().domain().accountsChanges() ) | rpl::start_with_next([=] { const auto &list = Core::App().domain().accounts(); const auto exists = [&](not_null account) { for (const auto &[index, existing] : list) { if (account == existing.get()) { return true; } } return false; }; for (auto i = _watched.begin(); i != _watched.end();) { if (!exists(i->first)) { i = _watched.erase(i); } else { ++i; } } for (const auto &[index, account] : list) { if (_watched.emplace(account.get()).second) { account->sessionChanges( ) | rpl::start_with_next([=] { rebuild(); }, _outer->lifetime()); } } rebuild(); }, _outer->lifetime()); Core::App().domain().maxAccountsChanges( ) | rpl::start_with_next([=] { // Full rebuild. for (auto i = _watched.begin(); i != _watched.end(); i++) { i->second = nullptr; } rebuild(); }, _outer->lifetime()); } not_null*> AccountsList::setupAdd() { const auto result = _outer->add( object_ptr>( _outer.get(), CreateButtonWithIcon( _outer.get(), tr::lng_menu_add_account(), st::mainMenuAddAccountButton, { &st::settingsIconAdd, IconType::Round, &st::windowBgActive })))->setDuration(0); const auto button = result->entity(); using Environment = MTP::Environment; const auto add = [=](Environment environment, bool newWindow = false) { auto &domain = _controller->session().domain(); auto found = false; for (const auto &[index, account] : domain.accounts()) { const auto raw = account.get(); if (!raw->sessionExists() && raw->mtp().environment() == environment) { found = true; } } if (!found && domain.accounts().size() >= domain.maxAccounts()) { _controller->show( Box(AccountsLimitBox, &_controller->session())); } else if (newWindow) { domain.addActivated(environment, true); } else { _controller->window().preventOrInvoke([=] { _controller->session().domain().addActivated(environment); }); } }; button->setAcceptBoth(true); button->clicks( ) | rpl::start_with_next([=](Qt::MouseButton which) { if (which == Qt::LeftButton) { const auto modifiers = button->clickModifiers(); const auto newWindow = (modifiers & Qt::ControlModifier); add(Environment::Production, newWindow); return; } else if (which != Qt::RightButton || !IsAltShift(button->clickModifiers())) { return; } _contextMenu = base::make_unique_q(_outer); _contextMenu->addAction("Production Server", [=] { add(Environment::Production); }); _contextMenu->addAction("Test Server", [=] { add(Environment::Test); }); _contextMenu->popup(QCursor::pos()); }, button->lifetime()); return result; } void AccountsList::rebuild() { const auto inner = _outer->insert( _outerIndex, object_ptr(_outer.get())); _reorder = std::make_unique(inner); _reorder->updates( ) | rpl::start_with_next([=](Ui::VerticalLayoutReorder::Single data) { using State = Ui::VerticalLayoutReorder::State; if (data.state == State::Started) { ++_reordering; } else { Ui::PostponeCall(inner, [=] { --_reordering; }); if (data.state == State::Applied) { std::vector order; order.reserve(inner->count()); for (auto i = 0; i < inner->count(); i++) { for (const auto &[account, button] : _watched) { if (button.get() == inner->widgetAt(i)) { order.push_back(account->session().uniqueId()); } } } Core::App().settings().setAccountsOrder(order); Core::App().saveSettings(); } } }, inner->lifetime()); const auto premiumLimit = _controller->session().domain().maxAccounts(); const auto list = _controller->session().domain().orderedAccounts(); for (const auto &account : list) { auto i = _watched.find(account); Assert(i != _watched.end()); auto &button = i->second; if (!account->sessionExists() || list.size() == 1) { button = nullptr; } else if (!button) { const auto nextIsLocked = (inner->count() >= premiumLimit); auto callback = [=](Qt::KeyboardModifiers modifiers) { if (_reordering) { return; } if (account == &_controller->session().account()) { _closeRequests.fire({}); return; } const auto newWindow = (modifiers & Qt::ControlModifier); auto activate = [=, guard = _accountSwitchGuard.make_guard()]{ if (guard) { _reorder->finishReordering(); if (newWindow) { _closeRequests.fire({}); Core::App().ensureSeparateWindowForAccount( account); } Core::App().domain().maybeActivate(account); } }; if (const auto window = Core::App().separateWindowForAccount(account)) { _closeRequests.fire({}); window->activate(); } else { base::call_delayed( st::defaultRippleAnimation.hideDuration, account, std::move(activate)); } }; button.reset(inner->add(MakeAccountButton( inner, _controller, account, std::move(callback), nextIsLocked))); } } inner->resizeToWidth(_outer->width()); const auto count = int(list.size()); _reorder->addPinnedInterval( premiumLimit, std::max(1, count - premiumLimit)); _addAccount->toggle( (count < Main::Domain::kPremiumMaxAccounts), anim::type::instant); _reorder->start(); } } // namespace Information::Information( QWidget *parent, not_null controller) : Section(parent) { setupContent(controller); } rpl::producer Information::title() { return tr::lng_settings_section_info(); } void Information::setupContent( not_null controller) { const auto content = Ui::CreateChild(this); const auto self = controller->session().user(); SetupPhoto(content, controller, self); SetupBio(content, self); SetupRows(content, controller, self); SetupAccountsWrap(content, controller); Ui::ResizeFitChild(this, content); } AccountsEvents SetupAccounts( not_null container, not_null controller) { const auto list = container->lifetime().make_state( container, controller); return { .closeRequests = list->closeRequests(), }; } void UpdatePhotoLocally(not_null user, const QImage &image) { auto bytes = QByteArray(); auto buffer = QBuffer(&bytes); image.save(&buffer, "JPG", 87); user->setUserpic( base::RandomValue(), ImageLocation( { .data = InMemoryLocation{ .bytes = bytes } }, image.width(), image.height()), false); } namespace Badge { Ui::UnreadBadgeStyle Style() { auto result = Ui::UnreadBadgeStyle(); result.font = st::mainMenuBadgeFont; result.size = st::mainMenuBadgeSize; result.sizeId = Ui::UnreadBadgeSize::MainMenu; return result; } not_null AddRight( not_null button) { const auto widget = Ui::CreateChild(button.get()); rpl::combine( button->sizeValue(), widget->sizeValue(), widget->shownValue() ) | rpl::start_with_next([=](QSize outer, QSize inner, bool shown) { auto padding = button->st().padding; if (shown) { widget->moveToRight( padding.right(), (outer.height() - inner.height()) / 2, outer.width()); padding.setRight(padding.right() + inner.width()); } button->setPaddingOverride(padding); button->update(); }, widget->lifetime()); return widget; } not_null CreateUnread( not_null container, rpl::producer value) { struct State { State(QWidget *parent) : widget(parent) { widget.setAttribute(Qt::WA_TransparentForMouseEvents); } Ui::RpWidget widget; Ui::UnreadBadgeStyle st = Style(); int count = 0; QString string; }; const auto state = container->lifetime().make_state(container); std::move( value ) | rpl::start_with_next([=](UnreadBadge badge) { state->st.muted = badge.muted; state->count = badge.count; if (!state->count) { state->widget.hide(); return; } state->string = Lang::FormatCountToShort(state->count).string; state->widget.resize(Ui::CountUnreadBadgeSize(state->string, state->st)); if (state->widget.isHidden()) { state->widget.show(); } }, state->widget.lifetime()); state->widget.paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(&state->widget); Ui::PaintUnreadBadge( p, state->string, state->widget.width(), 0, state->st); }, state->widget.lifetime()); return &state->widget; } void AddUnread( not_null button, rpl::producer value) { const auto container = AddRight(button); const auto badge = CreateUnread(container, std::move(value)); badge->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { container->resize(s); }, container->lifetime()); } } // namespace Badge } // namespace Settings