Initial working hours editing.

This commit is contained in:
John Preston 2024-02-21 22:25:52 +04:00
parent 1fe641c458
commit 4d12f1c0ef
11 changed files with 978 additions and 2 deletions

View File

@ -450,7 +450,10 @@ PRIVATE
countries/countries_manager.h
data/business/data_business_chatbots.cpp
data/business/data_business_chatbots.h
data/business/data_business_common.cpp
data/business/data_business_common.h
data/business/data_business_info.cpp
data/business/data_business_info.h
data/notify/data_notify_settings.cpp
data/notify/data_notify_settings.h
data/notify/data_peer_notify_settings.cpp

View File

@ -2189,6 +2189,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_hours_saturday" = "Saturday";
"lng_hours_sunday" = "Sunday";
"lng_hours_closed" = "Closed";
"lng_hours_open_full" = "Open 24 hours";
"lng_hours_next_day" = "Next day, {time}";
"lng_hours_time_zone_title" = "Choose Time Zone";
"lng_hours_add_button" = "Add a Set of Hours";
"lng_hours_opening" = "Opening Time";
"lng_hours_closing" = "Closing Time";
"lng_hours_remove" = "Remove";
"lng_hours_about_day" = "Specify your working hours during the day.";
"lng_replies_title" = "Quick Replies";
"lng_replies_about" = "Set up shortcuts with rich text and media to respond to messages faster.";

View File

@ -0,0 +1,158 @@
/*
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 "data/business/data_business_common.h"
namespace Data {
namespace {
constexpr auto kDay = WorkingInterval::kDay;
constexpr auto kWeek = WorkingInterval::kWeek;
constexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax;
[[nodiscard]] WorkingIntervals SortAndMerge(WorkingIntervals intervals) {
auto &list = intervals.list;
ranges::sort(list, ranges::less(), &WorkingInterval::start);
for (auto i = 0, count = int(list.size()); i != count; ++i) {
if (i && list[i].intersected(list[i - 1])) {
list[i - 1] = list[i - 1].united(list[i]);
list[i] = {};
}
if (!list[i]) {
list.erase(list.begin() + i);
--i;
--count;
}
}
return intervals;
}
[[nodiscard]] WorkingIntervals MoveTailToFront(WorkingIntervals intervals) {
auto &list = intervals.list;
auto after = WorkingInterval{ kWeek, kWeek + kDay };
while (!list.empty()) {
if (const auto tail = list.back().intersected(after)) {
list.back().end = tail.start;
if (!list.back()) {
list.pop_back();
}
list.insert(begin(list), tail.shifted(-kWeek));
} else {
break;
}
}
return intervals;
}
} // namespace
WorkingIntervals WorkingIntervals::normalized() const {
return SortAndMerge(MoveTailToFront(SortAndMerge(*this)));
}
Data::WorkingIntervals ExtractDayIntervals(
const Data::WorkingIntervals &intervals,
int dayIndex) {
Expects(dayIndex >= 0 && dayIndex < 7);
auto result = Data::WorkingIntervals();
auto &list = result.list;
for (const auto &interval : intervals.list) {
const auto now = interval.intersected(
{ (dayIndex - 1) * kDay, (dayIndex + 2) * kDay });
const auto after = interval.intersected(
{ (dayIndex + 6) * kDay, (dayIndex + 9) * kDay });
const auto before = interval.intersected(
{ (dayIndex - 8) * kDay, (dayIndex - 5) * kDay });
if (now) {
list.push_back(now.shifted(-dayIndex * kDay));
}
if (after) {
list.push_back(after.shifted(-(dayIndex + 7) * kDay));
}
if (before) {
list.push_back(before.shifted(-(dayIndex - 7) * kDay));
}
}
result = result.normalized();
const auto outside = [&](Data::WorkingInterval interval) {
return (interval.end <= 0) || (interval.start >= kDay);
};
list.erase(ranges::remove_if(list, outside), end(list));
if (!list.empty() && list.back().start <= 0 && list.back().end >= kDay) {
list.back() = { 0, kDay };
} else if (!list.empty() && (list.back().end > kDay + kInNextDayMax)) {
list.back() = list.back().intersected({ 0, kDay });
}
if (!list.empty() && list.front().start <= 0) {
if (list.front().start < 0
&& list.front().end <= kInNextDayMax
&& list.front().start > -kDay) {
list.erase(begin(list));
} else {
list.front() = list.front().intersected({ 0, kDay });
if (!list.front()) {
list.erase(begin(list));
}
}
}
return result;
}
Data::WorkingIntervals RemoveDayIntervals(
const Data::WorkingIntervals &intervals,
int dayIndex) {
auto result = intervals.normalized();
auto &list = result.list;
const auto day = Data::WorkingInterval{ 0, kDay };
const auto shifted = day.shifted(dayIndex * kDay);
auto before = Data::WorkingInterval{ 0, shifted.start };
auto after = Data::WorkingInterval{ shifted.end, kWeek };
for (auto i = 0, count = int(list.size()); i != count; ++i) {
if (list[i].end <= shifted.start || list[i].start >= shifted.end) {
continue;
} else if (list[i].end <= shifted.start + kInNextDayMax
&& (list[i].start < shifted.start
|| (!dayIndex // This 'Sunday' finishing on next day <= 6:00.
&& list[i].start == shifted.start
&& list.back().end >= kWeek))) {
continue;
} else if (const auto first = list[i].intersected(before)) {
list[i] = first;
if (const auto second = list[i].intersected(after)) {
list.push_back(second);
}
} else if (const auto second = list[i].intersected(after)) {
list[i] = second;
} else {
list.erase(list.begin() + i);
--i;
--count;
}
}
return result.normalized();
}
Data::WorkingIntervals ReplaceDayIntervals(
const Data::WorkingIntervals &intervals,
int dayIndex,
Data::WorkingIntervals replacement) {
auto result = RemoveDayIntervals(intervals, dayIndex);
const auto first = result.list.insert(
end(result.list),
begin(replacement.list),
end(replacement.list));
for (auto &interval : ranges::subrange(first, end(result.list))) {
interval = interval.shifted(dayIndex * kDay);
}
return result.normalized();
}
} // namespace Data

View File

@ -42,4 +42,103 @@ struct BusinessRecipients {
const BusinessRecipients &b) = default;
};
struct Timezone {
QString id;
QString name;
TimeId utcOffset = 0;
friend inline bool operator==(
const Timezone &a,
const Timezone &b) = default;
};
struct Timezones {
std::vector<Timezone> list;
friend inline bool operator==(
const Timezones &a,
const Timezones &b) = default;
};;
struct WorkingInterval {
static constexpr auto kDay = 24 * 3600;
static constexpr auto kWeek = 7 * kDay;
static constexpr auto kInNextDayMax = 6 * 3600;
TimeId start = 0;
TimeId end = 0;
explicit operator bool() const {
return start < end;
}
[[nodiscard]] WorkingInterval shifted(TimeId offset) const {
return { start + offset, end + offset };
}
[[nodiscard]] WorkingInterval united(WorkingInterval other) const {
if (!*this) {
return other;
} else if (!other) {
return *this;
}
return {
std::min(start, other.start),
std::max(end, other.end),
};
}
[[nodiscard]] WorkingInterval intersected(WorkingInterval other) const {
const auto result = WorkingInterval{
std::max(start, other.start),
std::min(end, other.end),
};
return result ? result : WorkingInterval();
}
friend inline bool operator==(
const WorkingInterval &a,
const WorkingInterval &b) = default;
};
struct WorkingIntervals {
std::vector<WorkingInterval> list;
[[nodiscard]] WorkingIntervals normalized() const;
explicit operator bool() const {
for (const auto &interval : list) {
if (interval) {
return true;
}
}
return false;
}
friend inline bool operator==(
const WorkingIntervals &a,
const WorkingIntervals &b) = default;
};
struct WorkingHours {
WorkingIntervals intervals;
QString timezoneId;
[[nodiscard]] WorkingHours normalized() const {
return { intervals.normalized(), timezoneId };
}
friend inline bool operator==(
const WorkingHours &a,
const WorkingHours &b) = default;
};
[[nodiscard]] Data::WorkingIntervals ExtractDayIntervals(
const Data::WorkingIntervals &intervals,
int dayIndex);
[[nodiscard]] Data::WorkingIntervals RemoveDayIntervals(
const Data::WorkingIntervals &intervals,
int dayIndex);
[[nodiscard]] Data::WorkingIntervals ReplaceDayIntervals(
const Data::WorkingIntervals &intervals,
int dayIndex,
Data::WorkingIntervals replacement);
} // namespace Data

View File

@ -0,0 +1,71 @@
/*
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 "data/business/data_business_info.h"
#include "apiwrap.h"
#include "data/data_session.h"
#include "main/main_session.h"
namespace Data {
BusinessInfo::BusinessInfo(not_null<Session*> owner)
: _owner(owner) {
}
BusinessInfo::~BusinessInfo() = default;
const WorkingHours &BusinessInfo::workingHours() const {
return _workingHours.current();
}
rpl::producer<WorkingHours> BusinessInfo::workingHoursValue() const {
return _workingHours.value();
}
void BusinessInfo::saveWorkingHours(WorkingHours data) {
_workingHours = std::move(data);
}
void BusinessInfo::preload() {
preloadTimezones();
}
void BusinessInfo::preloadTimezones() {
if (!_timezones.current().list.empty() || _timezonesRequestId) {
return;
}
_timezonesRequestId = _owner->session().api().request(
MTPhelp_GetTimezonesList(MTP_int(_timezonesHash))
).done([=](const MTPhelp_TimezonesList &result) {
result.match([&](const MTPDhelp_timezonesList &data) {
_timezonesHash = data.vhash().v;
const auto proj = [](const MTPtimezone &result) {
return Timezone{
.id = qs(result.data().vid()),
.name = qs(result.data().vname()),
.utcOffset = result.data().vutc_offset().v,
};
};
_timezones = Timezones{
.list = ranges::views::all(
data.vtimezones().v
) | ranges::views::transform(
proj
) | ranges::to_vector,
};
}, [](const MTPDhelp_timezonesListNotModified &) {
});
}).send();
}
rpl::producer<Timezones> BusinessInfo::timezonesValue() const {
const_cast<BusinessInfo*>(this)->preloadTimezones();
return _timezones.value();
}
} // namespace Data

View File

@ -0,0 +1,41 @@
/*
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
#include "data/business/data_business_common.h"
namespace Data {
class Session;
class BusinessInfo final {
public:
explicit BusinessInfo(not_null<Session*> owner);
~BusinessInfo();
[[nodiscard]] const WorkingHours &workingHours() const;
[[nodiscard]] rpl::producer<WorkingHours> workingHoursValue() const;
void saveWorkingHours(WorkingHours data);
void preload();
void preloadTimezones();
[[nodiscard]] rpl::producer<Timezones> timezonesValue() const;
private:
const not_null<Session*> _owner;
rpl::variable<WorkingHours> _workingHours;
rpl::variable<Timezones> _timezones;
mtpRequestId _timezonesRequestId = 0;
int32 _timezonesHash = 0;
};
} // namespace Data

View File

@ -38,6 +38,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "passport/passport_form_controller.h"
#include "lang/lang_keys.h" // tr::lng_deleted(tr::now) in user name
#include "data/business/data_business_chatbots.h"
#include "data/business/data_business_info.h"
#include "data/stickers/data_stickers.h"
#include "data/notify/data_notify_settings.h"
#include "data/data_bot_app.h"
@ -270,7 +271,8 @@ Session::Session(not_null<Main::Session*> session)
, _customEmojiManager(std::make_unique<CustomEmojiManager>(this))
, _stories(std::make_unique<Stories>(this))
, _savedMessages(std::make_unique<SavedMessages>(this))
, _chatbots(std::make_unique<Chatbots>(this)) {
, _chatbots(std::make_unique<Chatbots>(this))
, _businessInfo(std::make_unique<BusinessInfo>(this)) {
_cache->open(_session->local().cacheKey());
_bigFileCache->open(_session->local().cacheBigFileKey());

View File

@ -63,6 +63,7 @@ class CustomEmojiManager;
class Stories;
class SavedMessages;
class Chatbots;
class BusinessInfo;
struct ReactionId;
struct RepliesReadTillUpdate {
@ -146,6 +147,9 @@ public:
[[nodiscard]] Chatbots &chatbots() const {
return *_chatbots;
}
[[nodiscard]] BusinessInfo &businessInfo() const {
return *_businessInfo;
}
[[nodiscard]] MsgId nextNonHistoryEntryId() {
return ++_nonHistoryEntryId;
@ -1070,6 +1074,7 @@ private:
const std::unique_ptr<Stories> _stories;
const std::unique_ptr<SavedMessages> _savedMessages;
const std::unique_ptr<Chatbots> _chatbots;
const std::unique_ptr<BusinessInfo> _businessInfo;
MsgId _nonHistoryEntryId = ServerMaxMsgId.bare + ScheduledMsgIdsRange;

View File

@ -7,22 +7,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "settings/business/settings_working_hours.h"
#include "base/event_filter.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/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/vertical_drum_picker.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace Settings {
namespace {
constexpr auto kDay = Data::WorkingInterval::kDay;
constexpr auto kWeek = Data::WorkingInterval::kWeek;
constexpr auto kInNextDayMax = Data::WorkingInterval::kInNextDayMax;
class WorkingHours : public BusinessSection<WorkingHours> {
public:
WorkingHours(
@ -36,8 +49,492 @@ private:
void setupContent(not_null<Window::SessionController*> controller);
void save();
rpl::variable<Data::WorkingHours> _hours;
};
[[nodiscard]] QString TimezoneFullName(const Data::Timezone &data) {
const auto abs = std::abs(data.utcOffset);
const auto hours = abs / 3600;
const auto minutes = (abs % 3600) / 60;
const auto seconds = abs % 60;
const auto sign = (data.utcOffset < 0) ? '-' : '+';
const auto prefix = u"(UTC"_q
+ sign
+ QString::number(hours)
+ u":"_q
+ QString::number(minutes).rightJustified(2, u'0')
+ u")"_q;
return prefix + ' ' + data.name;
}
[[nodiscard]] QString FindClosestTimezoneId(
const std::vector<Data::Timezone> &list) {
const auto local = QDateTime::currentDateTime();
const auto utc = QDateTime(local.date(), local.time(), Qt::UTC);
const auto shift = base::unixtime::now() - (TimeId)::time(nullptr);
const auto delta = int(utc.toSecsSinceEpoch())
- int(local.toSecsSinceEpoch())
- shift;
const auto proj = [&](const Data::Timezone &value) {
auto distance = value.utcOffset - delta;
while (distance > 12 * 3600) {
distance -= 24 * 3600;
}
while (distance < -12 * 3600) {
distance += 24 * 3600;
}
return std::abs(distance);
};
return ranges::min_element(list, ranges::less(), proj)->id;
}
[[nodiscard]] QString FormatDayTime(
TimeId time,
bool showEndAsNextDay = false) {
const auto wrap = [](TimeId value) {
const auto hours = value / 3600;
const auto minutes = (value % 3600) / 60;
return QString::number(hours).rightJustified(2, u'0')
+ ':'
+ QString::number(minutes).rightJustified(2, u'0');
};
return (time > kDay || (showEndAsNextDay && time == kDay))
? tr::lng_hours_next_day(tr::now, lt_time, wrap(time - kDay))
: wrap(time == kDay ? 0 : time);
}
[[nodiscard]] QString JoinIntervals(const Data::WorkingIntervals &data) {
auto result = QStringList();
result.reserve(data.list.size());
for (const auto &interval : data.list) {
const auto start = FormatDayTime(interval.start);
const auto end = FormatDayTime(interval.end);
result.push_back(start + u" - "_q + end);
}
return result.join(u", "_q);
}
void EditTimeBox(
not_null<Ui::GenericBox*> box,
TimeId low,
TimeId high,
TimeId value,
Fn<void(TimeId)> save) {
Expects(low <= high);
const auto values = (high - low + 60) / 60;
const auto startIndex = (value - low) / 60;
const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::settingsWorkingHoursPicker));
const auto font = st::boxTextFont;
const auto itemHeight = st::settingsWorkingHoursPickerItemHeight;
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);
p.drawText(r, FormatDayTime(low + index * 60, true), style::al_center);
p.restore();
};
const auto picker = Ui::CreateChild<Ui::VerticalDrumPicker>(
content,
std::move(paintCallback),
values,
itemHeight,
startIndex);
content->sizeValue(
) | rpl::start_with_next([=](const QSize &s) {
picker->resize(s.width(), s.height());
picker->moveToLeft((s.width() - picker->width()) / 2, 0);
}, content->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(content, [=](not_null<QEvent*> e) {
if ((e->type() == QEvent::MouseButtonPress)
|| (e->type() == QEvent::MouseButtonRelease)
|| (e->type() == QEvent::MouseMove)) {
picker->handleMouseEvent(static_cast<QMouseEvent*>(e.get()));
} else if (e->type() == QEvent::Wheel) {
picker->handleWheelEvent(static_cast<QWheelEvent*>(e.get()));
}
return base::EventFilterResult::Continue;
});
base::install_event_filter(box, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
picker->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));
}
return base::EventFilterResult::Continue;
});
box->addButton(tr::lng_settings_save(), [=] {
const auto weak = Ui::MakeWeak(box);
save(std::clamp(low + picker->index() * 60, low, high));
if (const auto strong = weak.data()) {
strong->closeBox();
}
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
void EditDayBox(
not_null<Ui::GenericBox*> box,
rpl::producer<QString> title,
Data::WorkingIntervals intervals,
Fn<void(Data::WorkingIntervals)> save) {
box->setTitle(std::move(title));
box->setWidth(st::boxWideWidth);
struct State {
rpl::variable<Data::WorkingIntervals> data;
};
const auto state = box->lifetime().make_state<State>(State{
.data = std::move(intervals),
});
const auto container = box->verticalLayout();
const auto rows = container->add(
object_ptr<Ui::VerticalLayout>(container));
const auto makeRow = [=](
Data::WorkingInterval interval,
TimeId min,
TimeId max) {
auto result = object_ptr<Ui::VerticalLayout>(rows);
const auto raw = result.data();
AddDivider(raw);
AddSkip(raw);
AddButtonWithLabel(
raw,
tr::lng_hours_opening(),
rpl::single(FormatDayTime(interval.start, true)),
st::settingsButtonNoIcon
)->setClickedCallback([=] {
const auto max = std::max(min, interval.end - 60);
const auto now = std::clamp(interval.start, min, max);
const auto save = crl::guard(box, [=](TimeId value) {
auto now = state->data.current();
const auto i = ranges::find(now.list, interval);
if (i != end(now.list)) {
i->start = value;
state->data = now.normalized();
}
});
box->getDelegate()->show(Box(EditTimeBox, min, max, now, save));
});
AddButtonWithLabel(
raw,
tr::lng_hours_closing(),
rpl::single(FormatDayTime(interval.end, true)),
st::settingsButtonNoIcon
)->setClickedCallback([=] {
const auto min = std::min(max, interval.start + 60);
const auto now = std::clamp(interval.end, min, max);
const auto save = crl::guard(box, [=](TimeId value) {
auto now = state->data.current();
const auto i = ranges::find(now.list, interval);
if (i != end(now.list)) {
i->end = value;
state->data = now.normalized();
}
});
box->getDelegate()->show(Box(EditTimeBox, min, max, now, save));
});
raw->add(object_ptr<Ui::SettingsButton>(
raw,
tr::lng_hours_remove(),
st::settingsAttentionButton
))->setClickedCallback([=] {
auto now = state->data.current();
const auto i = ranges::find(now.list, interval);
if (i != end(now.list)) {
now.list.erase(i);
state->data = std::move(now);
}
});
AddSkip(raw);
return result;
};
const auto addWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
AddDivider(addWrap->entity());
AddSkip(addWrap->entity());
const auto add = addWrap->entity()->add(
object_ptr<Ui::SettingsButton>(
container,
tr::lng_hours_add_button(),
st::settingsButtonLightNoIcon));
add->setClickedCallback([=] {
auto now = state->data.current();
if (now.list.empty()) {
now.list.push_back({ 8 * 3600, 20 * 3600 });
} else if (const auto last = now.list.back().end; last + 60 < kDay) {
const auto from = std::max(
std::min(last + 30 * 60, kDay - 30 * 60),
last + 60);
const auto till = std::min(from + 4 * 3600, kDay + 30 * 60);
now.list.push_back({ from, from + 4 * 3600 });
}
state->data = std::move(now);
});
state->data.value(
) | rpl::start_with_next([=](const Data::WorkingIntervals &data) {
const auto count = int(data.list.size());
for (auto i = 0; i != count; ++i) {
const auto min = (i == 0) ? 0 : (data.list[i - 1].end + 60);
const auto max = (i == count - 1)
? (kDay + kInNextDayMax)
: (data.list[i + 1].start - 60);
rows->insert(i, makeRow(data.list[i], min, max));
if (rows->count() > i + 1) {
delete rows->widgetAt(i + 1);
}
}
while (rows->count() > count) {
delete rows->widgetAt(count);
}
rows->resizeToWidth(st::boxWideWidth);
addWrap->toggle(data.list.empty()
|| data.list.back().end + 60 < kDay, anim::type::instant);
add->clearState();
}, add->lifetime());
addWrap->finishAnimating();
AddSkip(container);
AddDividerText(container, tr::lng_hours_about_day());
box->addButton(tr::lng_settings_save(), [=] {
const auto weak = Ui::MakeWeak(box);
save(state->data.current());
if (const auto strong = weak.data()) {
strong->closeBox();
}
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
void ChooseTimezoneBox(
not_null<Ui::GenericBox*> box,
std::vector<Data::Timezone> list,
QString id,
Fn<void(QString)> save) {
Expects(!list.empty());
box->setWidth(st::boxWideWidth);
box->setTitle(tr::lng_hours_time_zone_title());
const auto height = st::boxWideWidth;
box->setMaxHeight(height);
ranges::sort(list, ranges::less(), [](const Data::Timezone &value) {
return std::pair(value.utcOffset, value.name);
});
if (!ranges::contains(list, id, &Data::Timezone::id)) {
id = FindClosestTimezoneId(list);
}
const auto i = ranges::find(list, id, &Data::Timezone::id);
const auto value = int(i - begin(list));
const auto group = std::make_shared<Ui::RadiobuttonGroup>(value);
const auto radioPadding = st::defaultCheckbox.margin;
const auto max = std::max(radioPadding.top(), radioPadding.bottom());
auto index = 0;
auto padding = st::boxRowPadding + QMargins(0, max, 0, max);
auto selected = (Ui::Radiobutton*)nullptr;
for (const auto &entry : list) {
const auto button = box->addRow(
object_ptr<Ui::Radiobutton>(
box,
group,
index++,
TimezoneFullName(entry)),
padding);
if (index == value + 1) {
selected = button;
}
padding = st::boxRowPadding + QMargins(0, 0, 0, max);
}
if (selected) {
box->verticalLayout()->resizeToWidth(st::boxWideWidth);
const auto y = selected->y() - (height - selected->height()) / 2;
box->setInitScrollCallback([=] {
box->scrollToY(y);
});
}
group->setChangedCallback([=](int index) {
const auto weak = Ui::MakeWeak(box);
save(list[index].id);
if (const auto strong = weak.data()) {
strong->closeBox();
}
});
box->addButton(tr::lng_close(), [=] {
box->closeBox();
});
}
void AddWeekButton(
not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
int index,
not_null<rpl::variable<Data::WorkingHours>*> data) {
auto label = [&] {
switch (index) {
case 0: return tr::lng_hours_monday();
case 1: return tr::lng_hours_tuesday();
case 2: return tr::lng_hours_wednesday();
case 3: return tr::lng_hours_thursday();
case 4: return tr::lng_hours_friday();
case 5: return tr::lng_hours_saturday();
case 6: return tr::lng_hours_sunday();
}
Unexpected("Index in AddWeekButton.");
}();
const auto &st = st::settingsWorkingHoursWeek;
const auto button = AddButtonWithIcon(
container,
rpl::duplicate(label),
st);
button->setClickedCallback([=] {
const auto done = [=](Data::WorkingIntervals intervals) {
auto now = data->current();
now.intervals = ReplaceDayIntervals(
now.intervals,
index,
std::move(intervals));
*data = now.normalized();
};
controller->show(Box(
EditDayBox,
rpl::duplicate(label),
ExtractDayIntervals(data->current().intervals, index),
crl::guard(button, done)));
});
const auto toggleButton = Ui::CreateChild<Ui::SettingsButton>(
container.get(),
nullptr,
st);
const auto checkView = button->lifetime().make_state<Ui::ToggleView>(
st.toggle,
false,
[=] { toggleButton->update(); });
auto status = data->value(
) | rpl::map([=](const Data::WorkingHours &data) {
using namespace Data;
const auto intervals = ExtractDayIntervals(data.intervals, index);
const auto empty = intervals.list.empty();
if (checkView->checked() == empty) {
checkView->setChecked(!empty, anim::type::instant);
}
if (!intervals) {
return tr::lng_hours_closed();
} else if (intervals.list.front() == WorkingInterval{ 0, kDay }) {
return tr::lng_hours_open_full();
}
return rpl::single(JoinIntervals(intervals));
}) | rpl::flatten_latest();
const auto details = Ui::CreateChild<Ui::FlatLabel>(
button.get(),
std::move(status),
st::settingsWorkingHoursDetails);
details->show();
details->moveToLeft(
st.padding.left(),
st.padding.top() + st.height - details->height());
details->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto separator = Ui::CreateChild<Ui::RpWidget>(container.get());
separator->paintRequest(
) | rpl::start_with_next([=, bg = st.textBgOver] {
auto p = QPainter(separator);
p.fillRect(separator->rect(), bg);
}, separator->lifetime());
const auto separatorHeight = st.height - 2 * st.toggle.border;
button->geometryValue(
) | rpl::start_with_next([=](const QRect &r) {
const auto w = st::rightsButtonToggleWidth;
toggleButton->setGeometry(
r.x() + r.width() - w,
r.y(),
w,
r.height());
separator->setGeometry(
toggleButton->x() - st::lineWidth,
r.y() + (r.height() - separatorHeight) / 2,
st::lineWidth,
separatorHeight);
}, toggleButton->lifetime());
const auto checkWidget = Ui::CreateChild<Ui::RpWidget>(toggleButton);
checkWidget->resize(checkView->getSize());
checkWidget->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(checkWidget);
checkView->paint(p, 0, 0, checkWidget->width());
}, checkWidget->lifetime());
toggleButton->sizeValue(
) | rpl::start_with_next([=](const QSize &s) {
checkWidget->moveToRight(
st.toggleSkip,
(s.height() - checkWidget->height()) / 2);
}, toggleButton->lifetime());
toggleButton->setClickedCallback([=] {
const auto enabled = !checkView->checked();
checkView->setChecked(enabled, anim::type::normal);
auto now = data->current();
now.intervals = ReplaceDayIntervals(
now.intervals,
index,
(enabled
? Data::WorkingIntervals{ { { 0, kDay } } }
: Data::WorkingIntervals()));
*data = now.normalized();
});
}
WorkingHours::WorkingHours(
QWidget *parent,
not_null<Window::SessionController*> controller)
@ -56,11 +553,21 @@ rpl::producer<QString> WorkingHours::title() {
}
void WorkingHours::setupContent(
not_null<Window::SessionController*> controller) {
not_null<Window::SessionController*> controller) {
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
struct State {
rpl::variable<Data::Timezones> timezones;
bool timezoneEditPending = false;
};
const auto info = &controller->session().data().businessInfo();
const auto state = content->lifetime().make_state<State>(State{
.timezones = info->timezonesValue(),
});
_hours = info->workingHours();
AddDividerTextWithLottie(content, {
.lottie = u"hours"_q,
.lottieSize = st::settingsCloudPasswordIconSize,
@ -85,6 +592,75 @@ void WorkingHours::setupContent(
Ui::AddSkip(inner);
Ui::AddDivider(inner);
Ui::AddSkip(inner);
for (auto i = 0; i != 7; ++i) {
AddWeekButton(inner, controller, i, &_hours);
}
Ui::AddSkip(inner);
Ui::AddDivider(inner);
Ui::AddSkip(inner);
state->timezones.value(
) | rpl::filter([=](const Data::Timezones &value) {
return !value.list.empty();
}) | rpl::start_with_next([=](const Data::Timezones &value) {
const auto now = _hours.current().timezoneId;
if (!ranges::contains(value.list, now, &Data::Timezone::id)) {
auto copy = _hours.current();
copy.timezoneId = FindClosestTimezoneId(value.list);
_hours = std::move(copy);
}
}, inner->lifetime());
auto timezoneLabel = rpl::combine(
_hours.value(),
state->timezones.value()
) | rpl::map([](
const Data::WorkingHours &hours,
const Data::Timezones &timezones) {
const auto i = ranges::find(
timezones.list,
hours.timezoneId,
&Data::Timezone::id);
return (i != end(timezones.list)) ? TimezoneFullName(*i) : QString();
});
const auto editTimezone = [=](const std::vector<Data::Timezone> &list) {
const auto was = _hours.current().timezoneId;
controller->show(Box(ChooseTimezoneBox, list, was, [=](QString id) {
if (id != was) {
auto copy = _hours.current();
copy.timezoneId = id;
_hours = std::move(copy);
}
}));
};
AddButtonWithLabel(
inner,
tr::lng_hours_time_zone(),
std::move(timezoneLabel),
st::settingsButtonNoIcon
)->setClickedCallback([=] {
const auto &list = state->timezones.current().list;
if (!list.empty()) {
editTimezone(list);
} else {
state->timezoneEditPending = true;
}
});
if (state->timezones.current().list.empty()) {
state->timezones.value(
) | rpl::filter([](const Data::Timezones &value) {
return !value.list.empty();
}) | rpl::start_with_next([=](const Data::Timezones &value) {
if (state->timezoneEditPending) {
state->timezoneEditPending = false;
editTimezone(value.list);
}
}, inner->lifetime());
}
wrap->toggleOn(enabled->toggledValue());
wrap->finishAnimating();
@ -93,6 +669,8 @@ void WorkingHours::setupContent(
}
void WorkingHours::save() {
controller()->session().data().businessInfo().saveWorkingHours(
_hours.current());
}
} // namespace

View File

@ -607,3 +607,10 @@ settingsChatbotsBottomTextMargin: margins(22px, 8px, 22px, 3px);
settingsChatbotsAdd: SettingsButton(settingsButton) {
iconLeft: 22px;
}
settingsWorkingHoursWeek: SettingsButton(settingsButtonNoIcon) {
height: 40px;
padding: margins(22px, 4px, 22px, 4px);
}
settingsWorkingHoursDetails: settingsNotificationTypeDetails;
settingsWorkingHoursPicker: 200px;
settingsWorkingHoursPickerItemHeight: 40px;

View File

@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/premium_preview_box.h"
#include "core/click_handler_types.h"
#include "data/data_peer_values.h" // AmPremiumValue.
#include "data/data_session.h"
#include "data/business/data_business_info.h"
#include "info/info_wrap_widget.h" // Info::Wrap.
#include "info/settings/info_settings_widget.h" // SectionCustomTopBarData.
#include "lang/lang_keys.h"
@ -356,6 +358,8 @@ void Business::setStepDataReference(std::any &data) {
void Business::setupContent() {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
_controller->session().data().businessInfo().preloadTimezones();
Ui::AddSkip(content, st::settingsFromFileTop);
AddBusinessSummary(content, _controller, [=](BusinessFeature feature) {