/* 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/sections/settings_information.h" #include "settings/sections/settings_main.h" #include "settings/settings_builder.h" #include "settings/settings_common_session.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 "ui/ui_utility.h" #include "core/application.h" #include "core/click_handler_types.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 "boxes/peers/edit_peer_color_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_channel.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 "api/api_user_privacy.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 // AyuGram includes #include "ayu/ui/ayu_userpic.h" #include "ayu/utils/telegram_helpers.h" #include "styles/style_info.h" namespace Settings { namespace { using namespace Builder; struct InformationHighlightTargets { QPointer photo; QPointer uploadPhoto; QPointer bio; QPointer colorButton; QPointer channelButton; QPointer addAccount; QPointer name; QPointer phone; QPointer username; QPointer birthday; }; constexpr auto kSaveBioTimeout = 1000; constexpr auto kPlayStatusLimit = 12; class ComposedBadge final : public Ui::RpWidget { public: ComposedBadge( not_null parent, not_null button, not_null<::Main::Session*> session, rpl::producer &&text, bool hasUnread, Fn animationPaused); private: rpl::variable _text; rpl::event_stream _unreadWidth; rpl::event_stream _premiumWidth; rpl::event_stream _exteraWidth; QPointer _unread; Info::Profile::Badge _badge; Info::Profile::Badge _exteraBadge; }; ComposedBadge::ComposedBadge( not_null parent, not_null button, not_null<::Main::Session*> session, rpl::producer &&text, bool hasUnread, Fn animationPaused) : Ui::RpWidget(parent) , _text(std::move(text)) , _badge( this, st::settingsInfoPeerBadge, session, Info::Profile::BadgeContentForPeer(session->user()), nullptr, animationPaused, kPlayStatusLimit, Info::Profile::BadgeType::Premium) , _exteraBadge( this, st::infoPeerBadge, session, ExteraBadgeTypeFromPeer(session->user()), nullptr, std::move(animationPaused), 0, Info::Profile::BadgeType::Extera | Info::Profile::BadgeType::ExteraSupporter | Info::Profile::BadgeType::ExteraCustom) { if (hasUnread) { _unread = Badge::CreateUnread(this, rpl::single( rpl::empty ) | rpl::then( session->data().unreadBadgeChanges() ) | rpl::map([=] { auto &owner = session->data(); return Badge::UnreadBadge{ owner.unreadWithMentionsBadge(), owner.unreadWithMentionsBadgeMuted(), }; })); 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::on_next([=] { if (const auto button = _badge.widget()) { button->widthValue( ) | rpl::start_to_stream(_premiumWidth, button->lifetime()); } else { _premiumWidth.fire(0); } }, lifetime()); _exteraBadge.updated( ) | rpl::on_next([=] { if (const auto widget = _exteraBadge.widget()) { widget->widthValue( ) | rpl::start_to_stream(_exteraWidth, widget->lifetime()); } else { _exteraWidth.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), _exteraWidth.events_starting_with(_exteraBadge.widget() ? _exteraBadge.widget()->width() : 0), std::move(textWidth), button->sizeValue() ) | rpl::on_next([=]( int unreadWidth, int premiumWidth, int exteraWidth, 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 exteraGap = exteraWidth ? st::infoVerifiedCheckPosition.x() : 0; const auto minWidth = unreadWidth + premiumWidth + exteraGap + exteraWidth + 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()); _exteraBadge.move( premiumWidth, 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; [[nodiscard]] Ui::RpWidget *addAccountButton() 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<::Main::Account*>, 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) ? tr::link(Data::OnlineText(user, now)) : tr::marked(Data::OnlineText(user, now))); timer->callOnce(Data::OnlineChangeTimeout(user, now)); }; timer->setCallback(push); user->session().changes().peerFlagsValue( user, Data::PeerUpdate::Flag::OnlineStatus ) | rpl::on_next(push, lifetime); return lifetime; }; } void SetupPhoto( not_null container, not_null controller, not_null self, InformationHighlightTargets *targets) { Ui::AddSkip(container); // fix avatar cutting on top 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); if (targets) { targets->photo = photo; targets->uploadPhoto = upload; } upload->chosenImages( ) | rpl::on_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::on_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()); } not_null 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::on_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::on_next([=](const TextWithEntities &text) { *forcopy = text.text; }, wrap->lifetime()); return wrap; } void SetupBirthday( not_null container, not_null controller, not_null self, InformationHighlightTargets *targets) { const auto session = &self->session(); Ui::AddSkip(container); auto value = rpl::combine( Info::Profile::BirthdayValue(self), tr::lng_settings_birthday_add() ) | rpl::map([](Data::Birthday birthday, const QString &add) { const auto text = Data::BirthdayText(birthday); return TextWithEntities{ !text.isEmpty() ? text : add }; }); const auto edit = [=] { Core::App().openInternalUrl( u"internal:edit_birthday"_q, QVariant::fromValue(ClickHandlerContext{ .sessionWindow = base::make_weak(controller), })); }; const auto birthdayButton = AddRow( container, tr::lng_settings_birthday_label(), std::move(value), tr::lng_mediaview_copy(tr::now), edit, { &st::menuIconGiftPremium }); if (targets) { targets->birthday = birthdayButton; } const auto key = Api::UserPrivacy::Key::Birthday; session->api().userPrivacy().reload(key); auto isExactlyContacts = session->api().userPrivacy().value( key ) | rpl::map([=](const Api::UserPrivacy::Rule &value) { return (value.option == Api::UserPrivacy::Option::Contacts) && value.always.peers.empty() && !value.always.premiums && value.never.peers.empty(); }) | rpl::distinct_until_changed(); Ui::AddSkip(container); Ui::AddDividerText(container, rpl::conditional( std::move(isExactlyContacts), tr::lng_settings_birthday_contacts( lt_link, tr::lng_settings_birthday_contacts_link( tr::url(u"internal:edit_privacy_birthday"_q)), tr::marked), tr::lng_settings_birthday_about( lt_link, tr::lng_settings_birthday_about_link( tr::url(u"internal:edit_privacy_birthday"_q)), tr::marked))); } void SetupPersonalChannel( not_null container, not_null controller, not_null self, InformationHighlightTargets *targets) { Ui::AddSkip(container); auto value = rpl::combine( Info::Profile::PersonalChannelValue(self), tr::lng_settings_channel_add() ) | rpl::map([](ChannelData *channel, const QString &add) { return TextWithEntities{ channel ? channel->name() : add }; }); const auto edit = [=] { Core::App().openInternalUrl( u"internal:edit_personal_channel"_q, QVariant::fromValue(ClickHandlerContext{ .sessionWindow = base::make_weak(controller), })); }; const auto channelButton = AddRow( container, tr::lng_settings_channel_label(), std::move(value), tr::lng_mediaview_copy(tr::now), edit, { &st::menuIconChannel }); const auto colorButton = AddPeerColorButton( container, controller->uiShow(), self, st::settingsColorButton); if (targets) { targets->channelButton = channelButton; targets->colorButton = colorButton; } Ui::AddSkip(container); Ui::AddDivider(container); } void SetupRows( not_null container, not_null controller, not_null self, InformationHighlightTargets *targets) { const auto session = &self->session(); Ui::AddSkip(container); const auto showEditName = [=] { if (controller->showFrozenError()) { return; } controller->show(Box(self)); }; const auto nameButton = AddRow( container, tr::lng_settings_name_label(), Info::Profile::NameValue(self) | rpl::map(tr::marked), tr::lng_profile_copy_fullname(tr::now), showEditName, { &st::menuIconProfile }); if (targets) { targets->name = nameButton; } const auto copyPhone = [=] { QGuiApplication::clipboard()->setText(self->phone()); controller->showToast(tr::lng_text_copied(tr::now), 500); }; const auto phoneButton = AddRow( container, tr::lng_settings_phone_label(), Info::Profile::PhoneValue(self), tr::lng_profile_copy_phone(tr::now), copyPhone, { &st::menuIconPhone }); if (targets) { targets->phone = phoneButton; } 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 usernameValue = 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()); const auto usernameButton = AddRow( container, std::move(label), std::move(usernameValue), tr::lng_context_copy_mention(tr::now), [=] { if (controller->showFrozenError()) { return; } const auto box = controller->show( Box(UsernamesBox, session->user())); box->boxClosing( ) | rpl::on_next([=] { session->api().usernames().requestToCache(session->user()); }, box->lifetime()); }, { &st::menuIconUsername }); if (targets) { targets->username = usernameButton; } Ui::AddSkip(container); Ui::AddDividerText(container, tr::lng_settings_username_about()); } void SetupBio( not_null container, not_null self, InformationHighlightTargets *targets) { 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); if (targets) { targets->bio = bio; } const auto countdown = Ui::CreateChild( container.get(), QString(), st::settingsBioCountdown); rpl::combine( bio->geometryValue(), countdown->widthValue() ) | rpl::on_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 - Ui::ComputeFieldCharacterCount(bio); 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::on_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::on_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()); 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::on_next([=] { save(); }, bio->lifetime()); bio->changes() | rpl::on_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, InformationHighlightTargets *targets) { Ui::AddSkip(container); auto events = SetupAccounts(container, controller); if (targets) { targets->addAccount = events.addAccountButton; } } [[nodiscard]] bool IsAltShift(Qt::KeyboardModifiers modifiers) { return (modifiers & Qt::ShiftModifier) && (modifiers & Qt::AltModifier); } [[nodiscard]] object_ptr MakeAccountButton( QWidget *parent, not_null window, not_null<::Main::Account*> 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, st::mainMenuAccountLine); const auto composedBadge = Ui::CreateChild( container.get(), raw, session, std::move(text), !active, [=] { return window->isGifPausedAtLeastFor( Window::GifPauseReason::Layer); }); composedBadge->sizeValue( ) | rpl::on_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::on_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::on_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); AyuUserpic::PaintShape(p, rect); } }, state->userpic.lifetime()); raw->setAcceptBoth(true); raw->clicks( ) | rpl::on_next([=](Qt::MouseButton which) { if (which == Qt::LeftButton) { callback(raw->clickModifiers()); return; } else if (which == Qt::MiddleButton) { callback(Qt::ControlModifier); return; } else if (which != Qt::RightButton) { return; } if (state->menu) { return; } const auto isActive = session == &window->session(); state->menu = base::make_unique_q( raw, st::popupMenuExpandedSeparator); const auto addAction = Ui::Menu::CreateAddActionCallback( state->menu); if (!isActive) { 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) { if (!isActive) { addAction(tr::lng_menu_activate(tr::now), [=] { callback({}); }, &st::menuIconProfile); } Window::MenuAddMarkAsReadAllChatsAction( session, window->uiShow(), addAction); } if (!isActive) { 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(); } Ui::RpWidget *AccountsList::addAccountButton() const { return _addAccount ? _addAccount->entity() : nullptr; } void AccountsList::setup() { _addAccount = setupAdd(); rpl::single(rpl::empty) | rpl::then( Core::App().domain().accountsChanges() ) | rpl::on_next([=] { const auto &list = Core::App().domain().accounts(); const auto exists = [&](not_null<::Main::Account*> 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::on_next([=] { rebuild(); }, _outer->lifetime()); } } rebuild(); }, _outer->lifetime()); Core::App().domain().maxAccountsChanges( ) | rpl::on_next([=] { 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::on_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())) { #ifdef _DEBUG if (which != Qt::RightButton) { return; } #else // _DEBUG return; #endif // !_DEBUG } _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::on_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().ensureSeparateWindowFor(account); } Core::App().domain().maybeActivate(account); } }; if (const auto window = Core::App().separateWindowFor( 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(); } void BuildInformationSection(SectionBuilder &builder) { builder.add(nullptr, [] { return SearchEntry{ .id = u"edit/bio"_q, .title = tr::lng_bio_placeholder(tr::now), .keywords = { u"bio"_q, u"about"_q, u"description"_q }, }; }); builder.add(nullptr, [] { return SearchEntry{ .id = u"edit/name"_q, .title = tr::lng_settings_name_label(tr::now), .keywords = { u"name"_q, u"first"_q, u"last"_q }, }; }); builder.add(nullptr, [] { return SearchEntry{ .id = u"edit/phone"_q, .title = tr::lng_settings_phone_label(tr::now), .keywords = { u"phone"_q, u"number"_q, u"mobile"_q }, }; }); builder.add(nullptr, [] { return SearchEntry{ .id = u"edit/username"_q, .title = tr::lng_settings_username_label(tr::now), .keywords = { u"username"_q, u"link"_q, u"t.me"_q }, }; }); builder.add(nullptr, [] { return SearchEntry{ .id = u"edit/your-color"_q, .title = tr::lng_settings_theme_name_color(tr::now), .keywords = { u"color"_q, u"theme"_q, u"name"_q }, }; }); builder.add(nullptr, [] { return SearchEntry{ .id = u"edit/channel"_q, .title = tr::lng_settings_channel_label(tr::now), .keywords = { u"channel"_q, u"personal"_q }, }; }); builder.add(nullptr, [] { return SearchEntry{ .id = u"edit/birthday"_q, .title = tr::lng_settings_birthday_label(tr::now), .keywords = { u"birthday"_q, u"date"_q, u"birth"_q }, }; }); builder.add(nullptr, [] { return SearchEntry{ .id = u"edit/add-account"_q, .title = tr::lng_menu_add_account(tr::now), .keywords = { u"account"_q, u"add"_q, u"switch"_q, u"multiple"_q }, }; }); } class Information : public Section { public: Information( QWidget *parent, not_null controller); [[nodiscard]] rpl::producer title() override; void showFinished() override; private: void setupContent(); QPointer _photo; QPointer _uploadPhoto; QPointer _bio; QPointer _colorButton; QPointer _channelButton; QPointer _addAccount; QPointer _name; QPointer _phone; QPointer _username; QPointer _birthday; }; Information::Information( QWidget *parent, not_null controller) : Section(parent, controller) { setupContent(); } rpl::producer Information::title() { return tr::lng_settings_section_info(); } void Information::showFinished() { Section::showFinished(); } void Information::setupContent() { const auto content = Ui::CreateChild(this); const SectionBuildMethod buildMethod = [ photo = &_photo, uploadPhoto = &_uploadPhoto, bio = &_bio, colorButton = &_colorButton, channelButton = &_channelButton, addAccount = &_addAccount, name = &_name, phone = &_phone, username = &_username, birthday = &_birthday ]( not_null container, not_null controller, Fn showOther, rpl::producer<> showFinished) { auto &lifetime = container->lifetime(); const auto highlights = lifetime.make_state(); const auto isPaused = Window::PausedIn( controller, Window::GifPauseReason::Layer); auto builder = SectionBuilder(WidgetContext{ .container = container, .controller = controller, .showOther = std::move(showOther), .isPaused = isPaused, .highlights = highlights, }); const auto self = controller->session().user(); auto targets = InformationHighlightTargets(); SetupPhoto(container, controller, self, &targets); SetupBio(container, self, &targets); SetupRows(container, controller, self, &targets); SetupPersonalChannel(container, controller, self, &targets); SetupBirthday(container, controller, self, &targets); SetupAccountsWrap(container, controller, &targets); *photo = targets.photo; *uploadPhoto = targets.uploadPhoto; *bio = targets.bio; *colorButton = targets.colorButton; *channelButton = targets.channelButton; *addAccount = targets.addAccount; *name = targets.name; *phone = targets.phone; *username = targets.username; *birthday = targets.birthday; if (highlights) { if (*photo) { highlights->push_back({ u"profile-photo"_q, { photo->data(), { .shape = HighlightShape::Ellipse } }, }); } if (*uploadPhoto) { highlights->push_back({ u"profile-photo/use-emoji"_q, { uploadPhoto->data(), { .shape = HighlightShape::Ellipse } }, }); } if (*bio) { highlights->push_back({ u"edit/bio"_q, { bio->data(), { .margin = st::settingsBioHighlightMargin } }, }); } if (*colorButton) { highlights->push_back({ u"edit/your-color"_q, { colorButton->data(), { .rippleShape = true } }, }); } if (*channelButton) { highlights->push_back({ u"edit/channel"_q, { channelButton->data(), { .rippleShape = true } }, }); } if (*addAccount) { highlights->push_back({ u"edit/add-account"_q, { addAccount->data(), { .rippleShape = true } }, }); } if (*name) { highlights->push_back({ u"edit/name"_q, { name->data(), { .rippleShape = true } }, }); } if (*phone) { highlights->push_back({ u"edit/phone"_q, { phone->data(), { .rippleShape = true } }, }); } if (*username) { highlights->push_back({ u"edit/username"_q, { username->data(), { .rippleShape = true } }, }); } if (*birthday) { highlights->push_back({ u"edit/birthday"_q, { birthday->data(), { .rippleShape = true } }, }); } } std::move(showFinished) | rpl::on_next([=] { for (const auto &[id, entry] : *highlights) { if (entry.widget) { controller->checkHighlightControl( id, entry.widget, base::duplicate(entry.args)); } } }, lifetime); }; build(content, buildMethod); Ui::ResizeFitChild(this, content); } const auto kMeta = BuildHelper({ .id = Information::Id(), .parentId = MainId(), .title = &tr::lng_settings_section_info, .icon = &st::menuIconProfile, }, [](SectionBuilder &builder) { BuildInformationSection(builder); }); } // namespace Type InformationId() { return Information::Id(); } AccountsEvents SetupAccounts( not_null container, not_null controller) { const auto list = container->lifetime().make_state( container, controller); return { .closeRequests = list->closeRequests(), .addAccountButton = list->addAccountButton(), }; } 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, int rightPadding) { const auto widget = Ui::CreateChild(button.get()); rpl::combine( button->sizeValue(), widget->sizeValue(), widget->shownValue() ) | rpl::on_next([=](QSize outer, QSize inner, bool shown) { auto padding = button->st().padding; if (shown) { widget->moveToRight( padding.right() + rightPadding, (outer.height() - inner.height()) / 2, outer.width()); padding.setRight(padding.right() + inner.width() + rightPadding); } 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::on_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::on_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::on_next([=](const QSize &s) { container->resize(s); }, container->lifetime()); } } // namespace Badge namespace Builder { SectionBuildMethod InformationSection = kMeta.build; } // namespace Builder } // namespace Settings