Support edit / save of away message settings.

This commit is contained in:
John Preston 2024-02-23 13:29:29 +04:00
parent f85c3c88f7
commit e6b9ac2267
6 changed files with 378 additions and 13 deletions

View file

@ -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

View file

@ -29,6 +29,51 @@ namespace {
MTP_vector_from_range(list | ranges::views::transform(proj)));
}
template <typename Flag>
[[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<Session*> 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();
}

View file

@ -18,9 +18,16 @@ public:
explicit BusinessInfo(not_null<Session*> 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<Timezones> timezonesValue() const;
@ -28,6 +35,8 @@ private:
const not_null<Session*> _owner;
rpl::variable<Timezones> _timezones;
std::optional<AwaySettings> _awaySettings;
rpl::event_stream<> _awaySettingsChanged;
mtpRequestId _timezonesRequestId = 0;
int32 _timezonesHash = 0;

View file

@ -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 <typename T>
Data::BusinessRecipients RecipientsFromMTP(
not_null<Data::Session*> 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<Data::Session*> owner,
const tl::conditional<MTPBusinessAwayMessage> &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<UserData*> 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());

View file

@ -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<Data::BusinessRecipients> _recipients;
rpl::variable<Data::AwaySchedule> _schedule;
rpl::variable<bool> _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<Window::SessionController*> controller;
not_null<rpl::variable<Data::AwaySchedule>*> data;
};
void AddAwayScheduleSelector(
not_null<Ui::VerticalLayout*> 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<Ui::RadioenumGroup<Type>>(
data->current().type);
const auto add = [&](Type type, const QString &label) {
container->add(
object_ptr<Ui::Radioenum<Type>>(
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<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(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<QString> title,
TimeId now,
Fn<TimeId()> min,
Fn<TimeId()> max,
Fn<void(TimeId)> done) {
using namespace Ui;
const auto box = std::make_shared<QPointer<Ui::BoxContent>>();
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<Window::SessionController*> controller)
@ -59,12 +199,27 @@ rpl::producer<QString> AwayMessage::title() {
void AwayMessage::setupContent(
not_null<Window::SessionController*> controller) {
using namespace Data;
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(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<Ui::SlideWrap<Ui::VerticalLayout>>(
@ -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<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto createInner = createWrap->entity();
Ui::AddSkip(createInner);
const auto create = createInner->add(object_ptr<Ui::SettingsButton>(
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

View file

@ -614,3 +614,5 @@ settingsWorkingHoursWeek: SettingsButton(settingsButtonNoIcon) {
settingsWorkingHoursDetails: settingsNotificationTypeDetails;
settingsWorkingHoursPicker: 200px;
settingsWorkingHoursPickerItemHeight: 40px;
settingsAwaySchedulePadding: margins(0px, 8px, 0px, 8px);