diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index ca78a2d9d..df0fd736e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -671,6 +671,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_birthday_about_link" = "Settings"; "lng_settings_birthday_contacts" = "Only your contacts can see your birthday. {link}"; "lng_settings_birthday_contacts_link" = "Change >"; +"lng_settings_birthday_saved" = "Your date of birth was updated."; +"lng_settings_birthday_reset" = "Reset"; "lng_settings_add_account_about" = "You can add up to four accounts with different phone numbers."; "lng_settings_peer_to_peer_about" = "Disabling peer-to-peer will relay all calls through Telegram servers to avoid revealing your IP address, but may slightly decrease audio quality."; "lng_settings_advanced" = "Advanced"; diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index d94187094..c5502085a 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -21,17 +21,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/click_handler_types.h" #include "boxes/background_preview_box.h" #include "ui/boxes/confirm_box.h" +#include "ui/boxes/edit_birthday_box.h" #include "boxes/share_box.h" #include "boxes/connection_box.h" +#include "boxes/edit_privacy_box.h" #include "boxes/premium_preview_box.h" #include "boxes/sticker_set_box.h" #include "boxes/sessions_box.h" #include "boxes/language_box.h" #include "passport/passport_form_controller.h" #include "ui/toast/toast.h" -#include "data/data_session.h" -#include "data/data_document.h" +#include "data/data_birthday.h" #include "data/data_channel.h" +#include "data/data_document.h" +#include "data/data_session.h" +#include "data/data_user.h" #include "media/player/media_player_instance.h" #include "media/view/media_view_open_common.h" #include "window/window_session_controller.h" @@ -44,6 +48,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_global_ttl.h" #include "settings/settings_folders.h" #include "settings/settings_main.h" +#include "settings/settings_privacy_controllers.h" #include "settings/settings_privacy_security.h" #include "settings/settings_chat.h" #include "settings/settings_premium.h" @@ -669,6 +674,59 @@ bool ShowSearchTagsPromo( return true; } +bool ShowEditBirthday( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto user = controller->session().user(); + const auto save = [=](Data::Birthday result) { + user->setBirthday(result); + + using Flag = MTPaccount_UpdateBirthday::Flag; + using BFlag = MTPDbirthday::Flag; + user->session().api().request(MTPaccount_UpdateBirthday( + MTP_flags(result ? Flag::f_birthday : Flag()), + MTP_birthday( + MTP_flags(result.year() ? BFlag::f_year : BFlag()), + MTP_int(result.day()), + MTP_int(result.month()), + MTP_int(result.year())) + )).done(crl::guard(controller, [=] { + controller->showToast(tr::lng_settings_birthday_saved(tr::now)); + })).fail(crl::guard(controller, [=](const MTP::Error &error) { + controller->showToast(u"Error: "_q + error.type()); + })).send(); + }; + controller->show(Box( + Ui::EditBirthdayBox, + user->birthday(), + save)); + return true; +} + +bool ShowEditBirthdayPrivacy( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + auto syncLifetime = controller->session().api().userPrivacy().value( + Api::UserPrivacy::Key::Birthday + ) | rpl::take( + 1 + ) | rpl::start_with_next([=](const Api::UserPrivacy::Rule &value) { + controller->show(Box( + controller, + std::make_unique<::Settings::BirthdayPrivacyController>(), + value)); + }); + return true; +} + void ExportTestChatTheme( not_null controller, not_null theme) { @@ -1034,9 +1092,17 @@ const std::vector &InternalUrlHandlers() { CopyPeerId }, { - u"about_tags"_q, + u"^about_tags$"_q, ShowSearchTagsPromo }, + { + u"^edit_birthday$"_q, + ShowEditBirthday, + }, + { + u"^edit_privacy_birthday$"_q, + ShowEditBirthdayPrivacy, + }, }; return Result; } diff --git a/Telegram/SourceFiles/data/data_birthday.cpp b/Telegram/SourceFiles/data/data_birthday.cpp index 4b9df693f..e29378cbe 100644 --- a/Telegram/SourceFiles/data/data_birthday.cpp +++ b/Telegram/SourceFiles/data/data_birthday.cpp @@ -11,7 +11,8 @@ namespace Data { namespace { [[nodiscard]] bool Validate(int day, int month, int year) { - if (year != 0 && (year < 1900 || year > 2100)) { + if (year != 0 + && (year < Birthday::kYearMin || year > Birthday::kYearMax)) { return false; } else if (day < 1) { return false; diff --git a/Telegram/SourceFiles/data/data_birthday.h b/Telegram/SourceFiles/data/data_birthday.h index 13a5e4cfe..fd36870cc 100644 --- a/Telegram/SourceFiles/data/data_birthday.h +++ b/Telegram/SourceFiles/data/data_birthday.h @@ -30,6 +30,9 @@ public: friend inline constexpr auto operator<=>(Birthday, Birthday) = default; friend inline constexpr bool operator==(Birthday, Birthday) = default; + static constexpr auto kYearMin = 1900; + static constexpr auto kYearMax = 2100; + private: int _value = 0; diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index e66a9839b..76636ddbd 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -343,6 +343,15 @@ rpl::producer CanAddContactValue(not_null user) { ) | rpl::map(!_1); } +rpl::producer BirthdayValue(not_null user) { + return user->session().changes().peerFlagsValue( + user, + UpdateFlag::Birthday + ) | rpl::map([=] { + return user->birthday(); + }); +} + rpl::producer AmInChannelValue(not_null channel) { return channel->session().changes().peerFlagsValue( channel, diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.h b/Telegram/SourceFiles/info/profile/info_profile_values.h index a74f22a42..b335949c7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.h +++ b/Telegram/SourceFiles/info/profile/info_profile_values.h @@ -17,6 +17,7 @@ struct ChannelLocation; namespace Data { class ForumTopic; class Thread; +class Birthday; } // namespace Data namespace Main { @@ -83,6 +84,8 @@ rpl::producer> MigratedOrMeValue( not_null user); [[nodiscard]] rpl::producer CanAddContactValue( not_null user); +[[nodiscard]] rpl::producer BirthdayValue( + not_null user); [[nodiscard]] rpl::producer AmInChannelValue( not_null channel); [[nodiscard]] rpl::producer MembersCountValue(not_null peer); diff --git a/Telegram/SourceFiles/settings/settings_information.cpp b/Telegram/SourceFiles/settings/settings_information.cpp index 9e4a66529..eef68335e 100644 --- a/Telegram/SourceFiles/settings/settings_information.cpp +++ b/Telegram/SourceFiles/settings/settings_information.cpp @@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/vertical_list.h" #include "ui/unread_badge_paint.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" @@ -48,6 +49,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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" @@ -353,6 +355,84 @@ void AddRow( }, wrap->lifetime()); } +void SetupBirthday( + not_null container, + not_null controller, + not_null self) { + 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 wrap = &Ui::Text::WithEntities; + if (const auto year = birthday.year()) { + return wrap(tr::lng_month_day_year( + tr::now, + lt_month, + Lang::MonthSmall(birthday.month())(tr::now), + lt_day, + QString::number(birthday.day()), + lt_year, + QString::number(year))); + } else if (birthday) { + return wrap(tr::lng_month_day( + tr::now, + lt_month, + Lang::MonthSmall(birthday.month())(tr::now), + lt_day, + QString::number(birthday.day()))); + } + auto result = TextWithEntities{ add }; + result.entities.push_back({ + EntityType::CustomUrl, + 0, + int(add.size()), + "internal:edit_username" }); + return result; + }); + const auto edit = [=] { + Core::App().openInternalUrl( + u"internal:edit_birthday"_q, + QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = base::make_weak(controller), + })); + }; + AddRow( + container, + tr::lng_settings_birthday_label(), + std::move(value), + tr::lng_mediaview_copy(tr::now), + edit, + { &st::menuIconGiftPremium }); + + 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.empty() + && value.never.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( + ) | Ui::Text::ToLink(u"internal:edit_privacy_birthday"_q), + Ui::Text::WithEntities), + tr::lng_settings_birthday_about( + lt_link, + tr::lng_settings_birthday_about_link( + ) | Ui::Text::ToLink(u"internal:edit_privacy_birthday"_q), + Ui::Text::WithEntities))); +} + void SetupRows( not_null container, not_null controller, @@ -954,6 +1034,7 @@ void Information::setupContent( SetupPhoto(content, controller, self); SetupBio(content, self); SetupRows(content, controller, self); + SetupBirthday(content, controller, self); SetupAccountsWrap(content, controller); Ui::ResizeFitChild(this, content); diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp index b0657d2b1..5308b9cf4 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp @@ -53,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat_helpers.h" #include "styles/style_settings.h" #include "styles/style_info.h" +#include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include @@ -1522,21 +1523,19 @@ object_ptr BirthdayPrivacyController::setupAboveWidget( not_null outerContainer) { const auto session = &controller->session(); const auto user = session->user(); - auto result = object_ptr>( + auto result = object_ptr>( parent, - object_ptr( + object_ptr( parent, - tr::lng_edit_privacy_birthday_yet( - lt_link, - tr::lng_edit_privacy_birthday_yet_link( - ) | Ui::Text::ToLink("internal:edit_birthday"), - Ui::Text::WithEntities), - st::settingsPrivacyAddBirthday), - st::boxRowPadding + style::margins( - 0, - st::defaultVerticalListSkip, - 0, - st::settingsPrivacySkipTop)); + object_ptr( + parent, + tr::lng_edit_privacy_birthday_yet( + lt_link, + tr::lng_edit_privacy_birthday_yet_link( + ) | Ui::Text::ToLink("internal:edit_birthday"), + Ui::Text::WithEntities), + st::boxDividerLabel), + st::defaultBoxDividerLabelPadding)); result->toggleOn(session->changes().peerFlagsValue( user, Data::PeerUpdate::Flag::Birthday diff --git a/Telegram/SourceFiles/settings/settings_privacy_controllers.h b/Telegram/SourceFiles/settings/settings_privacy_controllers.h index 23a727808..5e1083c20 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_controllers.h +++ b/Telegram/SourceFiles/settings/settings_privacy_controllers.h @@ -30,7 +30,7 @@ public: explicit BlockedBoxController( not_null window); - Main::Session &session() const override; + ::Main::Session &session() const override; void prepare() override; void rowClicked(not_null row) override; void rowRightActionClicked(not_null row) override; diff --git a/Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp b/Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp new file mode 100644 index 000000000..7d3641537 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp @@ -0,0 +1,232 @@ +/* +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 "ui/boxes/edit_birthday_box.h" + +#include "base/event_filter.h" +#include "data/data_birthday.h" +#include "lang/lang_keys.h" +#include "ui/layers/generic_box.h" +#include "ui/widgets/vertical_drum_picker.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +#include + +namespace Ui { + +class GenericBox; + +void EditBirthdayBox( + not_null box, + Data::Birthday current, + Fn save) { + box->setWidth(st::boxWideWidth); + const auto content = box->addRow(object_ptr( + box, + st::settingsWorkingHoursPicker)); + + const auto font = st::boxTextFont; + const auto itemHeight = st::settingsWorkingHoursPickerItemHeight; + const auto picker = [=]( + int count, + int startIndex, + Fn paint) { + auto paintCallback = [=]( + QPainter &p, + int index, + float64 y, + float64 distanceFromCenter, + int outerWidth) { + const auto r = QRectF(0, y, outerWidth, itemHeight); + const auto progress = std::abs(distanceFromCenter); + const auto revProgress = 1. - progress; + p.save(); + p.translate(r.center()); + constexpr auto kMinYScale = 0.2; + const auto yScale = kMinYScale + + (1. - kMinYScale) * anim::easeOutCubic(1., revProgress); + p.scale(1., yScale); + p.translate(-r.center()); + p.setOpacity(revProgress); + p.setFont(font); + p.setPen(st::defaultFlatLabel.textFg); + paint(p, r, index); + p.restore(); + }; + return Ui::CreateChild( + content, + std::move(paintCallback), + count, + itemHeight, + startIndex); + }; + + const auto nowDate = QDate::currentDate(); + const auto nowYear = nowDate.year(); + const auto nowMonth = nowDate.month(); + const auto nowDay = nowDate.day(); + const auto now = Data::Birthday(nowDay, nowMonth, nowYear); + const auto max = current.year() ? std::max(now, current) : now; + const auto maxYear = max.year(); + const auto minYear = Data::Birthday::kYearMin; + const auto yearsCount = (maxYear - minYear + 2); // Last - not set. + const auto yearsStartIndex = current.year() + ? (current.year() - minYear) + : (yearsCount - 1); + const auto yearsPaint = [=](QPainter &p, QRectF rect, int index) { + p.drawText( + rect, + (index < yearsCount - 1 + ? QString::number(minYear + index) + : QString::fromUtf8("\xe2\x80\x94")), + style::al_center); + }; + const auto years = picker(yearsCount, yearsStartIndex, yearsPaint); + + struct State { + rpl::variable months; + rpl::variable days; + }; + const auto state = content->lifetime().make_state(); + + // years->value() is valid only after size is set. + rpl::combine( + content->sizeValue(), + state->months.value(), + state->days.value() + ) | rpl::start_with_next([=]( + QSize s, + Ui::VerticalDrumPicker *months, + Ui::VerticalDrumPicker *days) { + const auto half = s.width() / 2; + years->setGeometry(half * 3 / 2, 0, half / 2, s.height()); + if (months) { + months->setGeometry(half / 2, 0, half, s.height()); + } + if (days) { + days->setGeometry(0, 0, half / 2, s.height()); + } + }, content->lifetime()); + + Ui::SendPendingMoveResizeEvents(years); + + years->value() | rpl::start_with_next([=](int yearsIndex) { + const auto year = (yearsIndex == yearsCount - 1) + ? 0 + : minYear + yearsIndex; + const auto monthsCount = (year == maxYear) + ? max.month() + : 12; + const auto monthsStartIndex = std::clamp( + (state->months.current() + ? state->months.current()->index() + : current.month() + ? (current.month() - 1) + : (now.month() - 1)), + 0, + monthsCount - 1); + const auto monthsPaint = [=](QPainter &p, QRectF rect, int index) { + p.drawText( + rect, + Lang::Month(index + 1)(tr::now), + style::al_center); + }; + const auto updated = picker( + monthsCount, + monthsStartIndex, + monthsPaint); + delete state->months.current(); + state->months = updated; + state->months.current()->show(); + }, years->lifetime()); + + Ui::SendPendingMoveResizeEvents(state->months.current()); + + state->months.value() | rpl::map([=](Ui::VerticalDrumPicker *picker) { + return picker ? picker->value() : rpl::single(current.month() + ? (current.month() - 1) + : (now.month() - 1)); + }) | rpl::flatten_latest() | rpl::start_with_next([=](int monthIndex) { + const auto month = monthIndex + 1; + const auto yearsIndex = years->index(); + const auto year = (yearsIndex == yearsCount - 1) + ? 0 + : minYear + yearsIndex; + const auto daysCount = (year == maxYear && month == max.month()) + ? max.day() + : (month == 2) + ? ((!year || ((year % 4) && (!(year % 100) || (year % 400)))) + ? 29 + : 28) + : ((month == 4) || (month == 6) || (month == 9) || (month == 11)) + ? 30 + : 31; + const auto daysStartIndex = std::clamp( + (state->days.current() + ? state->days.current()->index() + : current.day() + ? (current.day() - 1) + : (now.day() - 1)), + 0, + daysCount - 1); + const auto daysPaint = [=](QPainter &p, QRectF rect, int index) { + p.drawText(rect, QString::number(index + 1), style::al_center); + }; + const auto updated = picker( + daysCount, + daysStartIndex, + daysPaint); + delete state->days.current(); + state->days = updated; + state->days.current()->show(); + }, years->lifetime()); + + content->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = QPainter(content); + + p.fillRect(r, Qt::transparent); + + const auto lineRect = QRect( + 0, + content->height() / 2, + content->width(), + st::defaultInputField.borderActive); + p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg); + p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg); + }, content->lifetime()); + + base::install_event_filter(box, [=](not_null e) { + if (e->type() == QEvent::KeyPress) { + years->handleKeyEvent(static_cast(e.get())); + } + return base::EventFilterResult::Continue; + }); + + box->addButton(tr::lng_settings_save(), [=] { + const auto result = Data::Birthday( + state->days.current()->index() + 1, + state->months.current()->index() + 1, + ((years->index() == yearsCount - 1) + ? 0 + : minYear + years->index())); + box->closeBox(); + save(result); + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + if (current) { + box->addLeftButton(tr::lng_settings_birthday_reset(), [=] { + box->closeBox(); + save(Data::Birthday()); + }); + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/edit_birthday_box.h b/Telegram/SourceFiles/ui/boxes/edit_birthday_box.h new file mode 100644 index 000000000..bc96f8e0d --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/edit_birthday_box.h @@ -0,0 +1,23 @@ +/* +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 +*/ +#pragma once + +namespace Data { +class Birthday; +} // namespace Data + +namespace Ui { + +class GenericBox; + +void EditBirthdayBox( + not_null box, + Data::Birthday current, + Fn save); + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 5a4b2bd13..c2bb319bf 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -241,6 +241,8 @@ PRIVATE ui/boxes/confirm_phone_box.h ui/boxes/country_select_box.cpp ui/boxes/country_select_box.h + ui/boxes/edit_birthday_box.cpp + ui/boxes/edit_birthday_box.h ui/boxes/edit_invite_link.cpp ui/boxes/edit_invite_link.h ui/boxes/rate_call_box.cpp