diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index c480a8579..a74b5df48 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -172,4 +172,34 @@ struct BusinessDetails { const BusinessDetails &b) = default; }; +enum class AwayScheduleType : uchar { + Never = 0, + Always = 1, + OutsideWorkingHours = 2, + Custom = 3, +}; + +struct AwaySchedule { + AwayScheduleType type = AwayScheduleType::Always; + WorkingInterval customInterval; + + friend inline bool operator==( + const AwaySchedule &a, + const AwaySchedule &b) = default; +}; + +struct AwaySettings { + BusinessRecipients recipients; + AwaySchedule schedule; + int shortcutId = 0; + + explicit operator bool() const { + return schedule.type != AwayScheduleType::Never; + } + + friend inline bool operator==( + const AwaySettings &a, + const AwaySettings &b) = default; +}; + } // namespace Data diff --git a/Telegram/SourceFiles/data/business/data_business_info.cpp b/Telegram/SourceFiles/data/business/data_business_info.cpp index 804b23deb..d054d3a56 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.cpp +++ b/Telegram/SourceFiles/data/business/data_business_info.cpp @@ -29,6 +29,51 @@ namespace { MTP_vector_from_range(list | ranges::views::transform(proj))); } +template +[[nodiscard]] auto RecipientsFlags( + const BusinessRecipients &data, + Flag) { + using Type = BusinessChatType; + const auto &chats = data.allButExcluded + ? data.excluded + : data.included; + return Flag() + | ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag()) + | ((chats.types & Type::ExistingChats) + ? Flag::f_existing_chats + : Flag()) + | ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag()) + | ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag()) + | (chats.list.empty() ? Flag() : Flag::f_users) + | (data.allButExcluded ? Flag::f_exclude_selected : Flag()); +} + +[[nodiscard]] MTPBusinessAwayMessageSchedule ToMTP( + const AwaySchedule &data) { + Expects(data.type != AwayScheduleType::Never); + + return (data.type == AwayScheduleType::Always) + ? MTP_businessAwayMessageScheduleAlways() + : (data.type == AwayScheduleType::OutsideWorkingHours) + ? MTP_businessAwayMessageScheduleOutsideWorkHours() + : MTP_businessAwayMessageScheduleCustom( + MTP_int(data.customInterval.start), + MTP_int(data.customInterval.end)); +} + +[[nodiscard]] MTPInputBusinessAwayMessage ToMTP(const AwaySettings &data) { + using Flag = MTPDinputBusinessAwayMessage::Flag; + return MTP_inputBusinessAwayMessage( + MTP_flags(RecipientsFlags(data.recipients, Flag())), + MTP_int(data.shortcutId), + ToMTP(data.schedule), + MTP_vector_from_range( + (data.recipients.allButExcluded + ? data.recipients.excluded + : data.recipients.included).list + | ranges::views::transform(&UserData::inputUser))); +} + } // namespace BusinessInfo::BusinessInfo(not_null owner) @@ -42,17 +87,51 @@ void BusinessInfo::saveWorkingHours(WorkingHours data) { if (details.hours == data) { return; } - details.hours = std::move(data); using Flag = MTPaccount_UpdateBusinessWorkHours::Flag; _owner->session().api().request(MTPaccount_UpdateBusinessWorkHours( - MTP_flags(details.hours ? Flag::f_business_work_hours : Flag()), - ToMTP(details.hours) + MTP_flags(data ? Flag::f_business_work_hours : Flag()), + ToMTP(data) )).send(); + details.hours = std::move(data); _owner->session().user()->setBusinessDetails(std::move(details)); } +void BusinessInfo::applyAwaySettings(AwaySettings data) { + if (_awaySettings == data) { + return; + } + _awaySettings = data; + _awaySettingsChanged.fire({}); +} + +void BusinessInfo::saveAwaySettings(AwaySettings data) { + if (_awaySettings == data) { + return; + } + using Flag = MTPaccount_UpdateBusinessAwayMessage::Flag; + _owner->session().api().request(MTPaccount_UpdateBusinessAwayMessage( + MTP_flags(data ? Flag::f_message : Flag()), + data ? ToMTP(data) : MTPInputBusinessAwayMessage() + )).send(); + + _awaySettings = std::move(data); + _awaySettingsChanged.fire({}); +} + +bool BusinessInfo::awaySettingsLoaded() const { + return _awaySettings.has_value(); +} + +AwaySettings BusinessInfo::awaySettings() const { + return _awaySettings.value_or(AwaySettings()); +} + +rpl::producer<> BusinessInfo::awaySettingsChanged() const { + return _awaySettingsChanged.events(); +} + void BusinessInfo::preload() { preloadTimezones(); } diff --git a/Telegram/SourceFiles/data/business/data_business_info.h b/Telegram/SourceFiles/data/business/data_business_info.h index ee4a2e043..9cd7f9681 100644 --- a/Telegram/SourceFiles/data/business/data_business_info.h +++ b/Telegram/SourceFiles/data/business/data_business_info.h @@ -18,9 +18,16 @@ public: explicit BusinessInfo(not_null owner); ~BusinessInfo(); + void preload(); + void saveWorkingHours(WorkingHours data); - void preload(); + void saveAwaySettings(AwaySettings data); + void applyAwaySettings(AwaySettings data); + [[nodiscard]] AwaySettings awaySettings() const; + [[nodiscard]] bool awaySettingsLoaded() const; + [[nodiscard]] rpl::producer<> awaySettingsChanged() const; + void preloadTimezones(); [[nodiscard]] rpl::producer timezonesValue() const; @@ -28,6 +35,8 @@ private: const not_null _owner; rpl::variable _timezones; + std::optional _awaySettings; + rpl::event_stream<> _awaySettingsChanged; mtpRequestId _timezonesRequestId = 0; int32 _timezonesHash = 0; diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 45f4f9e28..93346ec90 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/storage_user_photos.h" #include "main/main_session.h" #include "data/business/data_business_common.h" +#include "data/business/data_business_info.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_peer_bot_command.h" @@ -51,14 +52,66 @@ using UpdateFlag = Data::PeerUpdate::Flag; if (location) { const auto &data = location->data(); result.location.address = qs(data.vaddress()); - data.vgeo_point().match([&](const MTPDgeoPoint &data) { - result.location.point = Data::LocationPoint(data); - }, [&](const MTPDgeoPointEmpty &) { - }); + if (const auto point = data.vgeo_point()) { + point->match([&](const MTPDgeoPoint &data) { + result.location.point = Data::LocationPoint(data); + }, [&](const MTPDgeoPointEmpty &) { + }); + } } return result; } +template +Data::BusinessRecipients RecipientsFromMTP( + not_null owner, + const T &data) { + using Type = Data::BusinessChatType; + auto result = Data::BusinessRecipients{ + .allButExcluded = data.is_exclude_selected(), + }; + auto &chats = result.allButExcluded + ? result.excluded + : result.included; + chats.types = Type() + | (data.is_new_chats() ? Type::NewChats : Type()) + | (data.is_existing_chats() ? Type::ExistingChats : Type()) + | (data.is_contacts() ? Type::Contacts : Type()) + | (data.is_non_contacts() ? Type::NonContacts : Type()); + if (const auto users = data.vusers()) { + for (const auto &userId : users->v) { + chats.list.push_back(owner->user(UserId(userId.v))); + } + } + return result; +} + +[[nodiscard]] Data::AwaySettings FromMTP( + not_null owner, + const tl::conditional &message) { + if (!message) { + return Data::AwaySettings(); + } + const auto &data = message->data(); + auto result = Data::AwaySettings{ + .recipients = RecipientsFromMTP(owner, data), + .shortcutId = data.vshortcut_id().v, + }; + data.vschedule().match([&]( + const MTPDbusinessAwayMessageScheduleAlways &) { + result.schedule.type = Data::AwayScheduleType::Always; + }, [&](const MTPDbusinessAwayMessageScheduleOutsideWorkHours &) { + result.schedule.type = Data::AwayScheduleType::OutsideWorkingHours; + }, [&](const MTPDbusinessAwayMessageScheduleCustom &data) { + result.schedule.type = Data::AwayScheduleType::Custom; + result.schedule.customInterval = Data::WorkingInterval{ + data.vstart_date().v, + data.vend_date().v, + }; + }); + return result; +} + } // namespace BotInfo::BotInfo() = default; @@ -622,6 +675,10 @@ void ApplyUserUpdate(not_null user, const MTPDuserFull &update) { user->setBusinessDetails(FromMTP( update.vbusiness_work_hours(), update.vbusiness_location())); + if (user->isSelf()) { + user->owner().businessInfo().applyAwaySettings( + FromMTP(&user->owner(), update.vbusiness_away_message())); + } user->owner().stories().apply(user, update.vstories()); diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index 0e22a4c2f..b38e4e184 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -7,17 +7,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/business/settings_away_message.h" +#include "base/unixtime.h" #include "core/application.h" +#include "data/business/data_business_info.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "settings/business/settings_recipients_helper.h" +#include "ui/boxes/choose_date_time.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/checkbox.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_layers.h" #include "styles/style_settings.h" namespace Settings { @@ -37,9 +42,144 @@ private: void save(); rpl::variable _recipients; + rpl::variable _schedule; + rpl::variable _enabled; }; +[[nodiscard]] TimeId StartTimeMin() { + // Telegram was launched in August 2013 :) + return base::unixtime::serialize(QDateTime(QDate(2013, 8, 1))); +} + +[[nodiscard]] TimeId EndTimeMin() { + return StartTimeMin() + 3600; +} + +[[nodiscard]] bool BadCustomInterval(const Data::WorkingInterval &interval) { + return !interval + || (interval.start < StartTimeMin()) + || (interval.end < EndTimeMin()); +} + +struct AwayScheduleSelectorDescriptor { + not_null controller; + not_null*> data; +}; +void AddAwayScheduleSelector( + not_null container, + AwayScheduleSelectorDescriptor &&descriptor) { + using Type = Data::AwayScheduleType; + using namespace rpl::mappers; + + const auto controller = descriptor.controller; + const auto data = descriptor.data; + + Ui::AddSubsectionTitle(container, tr::lng_away_schedule()); + const auto group = std::make_shared>( + data->current().type); + + const auto add = [&](Type type, const QString &label) { + container->add( + object_ptr>( + container, + group, + type, + label), + st::boxRowPadding + st::settingsAwaySchedulePadding); + }; + add(Type::Always, tr::lng_away_schedule_always(tr::now)); + add(Type::OutsideWorkingHours, tr::lng_away_schedule_outside(tr::now)); + add(Type::Custom, tr::lng_away_schedule_custom(tr::now)); + + const auto customWrap = container->add( + object_ptr>( + container, + object_ptr(container))); + const auto customInner = customWrap->entity(); + customWrap->toggleOn(group->value() | rpl::map(_1 == Type::Custom)); + + group->changes() | rpl::start_with_next([=](Type value) { + auto copy = data->current(); + copy.type = value; + *data = copy; + }, customWrap->lifetime()); + + const auto chooseDate = [=]( + rpl::producer title, + TimeId now, + Fn min, + Fn max, + Fn done) { + using namespace Ui; + const auto box = std::make_shared>(); + const auto save = [=](TimeId time) { + done(time); + if (const auto strong = box->data()) { + strong->closeBox(); + } + }; + *box = controller->show(Box(ChooseDateTimeBox, ChooseDateTimeBoxArgs{ + .title = std::move(title), + .submit = tr::lng_settings_save(), + .done = save, + .min = min, + .time = now, + .max = max, + })); + }; + + Ui::AddSkip(customInner); + Ui::AddDivider(customInner); + Ui::AddSkip(customInner); + + auto startLabel = data->value( + ) | rpl::map([=](const Data::AwaySchedule &value) { + return langDateTime( + base::unixtime::parse(value.customInterval.start)); + }); + AddButtonWithLabel( + customInner, + tr::lng_away_custom_start(), + std::move(startLabel), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + chooseDate( + tr::lng_away_custom_start(), + data->current().customInterval.start, + StartTimeMin, + [=] { return data->current().customInterval.end - 1; }, + [=](TimeId time) { + auto copy = data->current(); + copy.customInterval.start = time; + *data = copy; + }); + }); + + auto endLabel = data->value( + ) | rpl::map([=](const Data::AwaySchedule &value) { + return langDateTime( + base::unixtime::parse(value.customInterval.end)); + }); + AddButtonWithLabel( + customInner, + tr::lng_away_custom_end(), + std::move(endLabel), + st::settingsButtonNoIcon + )->setClickedCallback([=] { + chooseDate( + tr::lng_away_custom_end(), + data->current().customInterval.end, + [=] { return data->current().customInterval.start + 1; }, + nullptr, + [=](TimeId time) { + auto copy = data->current(); + copy.customInterval.end = time; + *data = copy; + }); + }); +} + AwayMessage::AwayMessage( QWidget *parent, not_null controller) @@ -59,12 +199,27 @@ rpl::producer AwayMessage::title() { void AwayMessage::setupContent( not_null controller) { + using namespace Data; using namespace rpl::mappers; const auto content = Ui::CreateChild(this); - //const auto current = controller->session().data().chatbots().current(); + const auto info = &controller->session().data().businessInfo(); + const auto current = info->awaySettings(); + const auto disabled = (current.schedule.type == AwayScheduleType::Never); + + _recipients = current.recipients; + auto initialSchedule = disabled ? AwaySchedule{ + .type = AwayScheduleType::Always, + } : current.schedule; + if (BadCustomInterval(initialSchedule.customInterval)) { + const auto now = base::unixtime::now(); + initialSchedule.customInterval = WorkingInterval{ + .start = now, + .end = now + 24 * 60 * 60, + }; + } + _schedule = initialSchedule; - //_recipients = current.recipients; AddDividerTextWithLottie(content, { .lottie = u"sleep"_q, .lottieSize = st::settingsCloudPasswordIconSize, @@ -79,7 +234,8 @@ void AwayMessage::setupContent( content, tr::lng_away_enable(), st::settingsButtonNoIcon - ))->toggleOn(rpl::single(false)); + ))->toggleOn(rpl::single(!disabled)); + _enabled = enabled->toggledValue(); const auto wrap = content->add( object_ptr>( @@ -90,8 +246,32 @@ void AwayMessage::setupContent( Ui::AddSkip(inner); Ui::AddDivider(inner); - wrap->toggleOn(enabled->toggledValue()); - wrap->finishAnimating(); + const auto createWrap = inner->add( + object_ptr>( + inner, + object_ptr(inner))); + const auto createInner = createWrap->entity(); + Ui::AddSkip(createInner); + const auto create = createInner->add(object_ptr( + createInner, + tr::lng_away_create(), + st::settingsButtonLightNoIcon + )); + create->setClickedCallback([=] { + + }); + Ui::AddSkip(createInner); + Ui::AddDivider(createInner); + + createWrap->toggleOn(rpl::single(true)); + + Ui::AddSkip(inner); + AddAwayScheduleSelector(inner, { + .controller = controller, + .data = &_schedule, + }); + Ui::AddSkip(inner); + Ui::AddDivider(inner); AddBusinessRecipientsSelector(inner, { .controller = controller, @@ -101,10 +281,18 @@ void AwayMessage::setupContent( Ui::AddSkip(inner, st::settingsChatbotsAccessSkip); + wrap->toggleOn(enabled->toggledValue()); + wrap->finishAnimating(); + Ui::ResizeFitChild(this, content); } void AwayMessage::save() { + controller()->session().data().businessInfo().saveAwaySettings( + _enabled.current() ? Data::AwaySettings{ + .recipients = _recipients.current(), + .schedule = _schedule.current(), + } : Data::AwaySettings()); } } // namespace diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 3e57d64cc..41fb908e0 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -614,3 +614,5 @@ settingsWorkingHoursWeek: SettingsButton(settingsButtonNoIcon) { settingsWorkingHoursDetails: settingsNotificationTypeDetails; settingsWorkingHoursPicker: 200px; settingsWorkingHoursPickerItemHeight: 40px; + +settingsAwaySchedulePadding: margins(0px, 8px, 0px, 8px);