Show location and working hours in profile.

This commit is contained in:
John Preston 2024-03-05 14:05:48 +04:00
parent 5e82860376
commit ea36345eee
9 changed files with 525 additions and 26 deletions

View file

@ -1313,6 +1313,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_info_link_label" = "Link";
"lng_info_location_label" = "Location";
"lng_info_about_label" = "About";
"lng_info_work_open" = "Open";
"lng_info_work_closed" = "Closed";
"lng_info_hours_label" = "Business hours";
"lng_info_hours_closed" = "closed";
"lng_info_hours_opens_in_minutes#one" = "opens in {count} minute";
"lng_info_hours_opens_in_minutes#other" = "opens in {count} minutes";
"lng_info_hours_opens_in_hours#one" = "opens in {count} hour";
"lng_info_hours_opens_in_hours#other" = "opens in {count} hours";
"lng_info_hours_opens_in_days#one" = "opens in {count} day";
"lng_info_hours_opens_in_days#other" = "opens in {count} days";
"lng_info_hours_open_full" = "open 24 hours";
"lng_info_hours_next_day" = "{time} (next day)";
"lng_info_hours_local_time" = "local time";
"lng_info_hours_my_time" = "my time";
"lng_info_user_title" = "User Info";
"lng_info_bot_title" = "Bot Info";
"lng_info_group_title" = "Group Info";
@ -2190,7 +2204,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_hours_sunday" = "Sunday";
"lng_hours_closed" = "Closed";
"lng_hours_open_full" = "Open 24 hours";
"lng_hours_next_day" = "Next day, {time}";
"lng_hours_next_day" = "{time} (Next day)";
"lng_hours_time_zone_title" = "Choose Time Zone";
"lng_hours_add_button" = "Add a Set of Hours";
"lng_hours_opening" = "Opening Time";

View file

@ -106,6 +106,11 @@ WorkingIntervals ExtractDayIntervals(
return result;
}
bool IsFullOpen(const WorkingIntervals &extractedDay) {
return extractedDay
&& (extractedDay.list.front() == WorkingInterval{ 0, kDay });
}
WorkingIntervals RemoveDayIntervals(
const WorkingIntervals &intervals,
int dayIndex) {

View file

@ -138,6 +138,7 @@ struct WorkingHours {
[[nodiscard]] WorkingIntervals ExtractDayIntervals(
const WorkingIntervals &intervals,
int dayIndex);
[[nodiscard]] bool IsFullOpen(const WorkingIntervals &extractedDay);
[[nodiscard]] WorkingIntervals RemoveDayIntervals(
const WorkingIntervals &intervals,
int dayIndex);
@ -148,7 +149,7 @@ struct WorkingHours {
struct BusinessLocation {
QString address;
LocationPoint point;
std::optional<LocationPoint> point;
explicit operator bool() const {
return !address.isEmpty();

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/business/data_business_info.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "data/business/data_business_common.h"
#include "data/data_session.h"
#include "data/data_user.h"
@ -270,4 +271,24 @@ rpl::producer<Timezones> BusinessInfo::timezonesValue() const {
return _timezones.value();
}
QString FindClosestTimezoneId(const std::vector<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 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;
}
} // namespace Data

View file

@ -55,4 +55,7 @@ private:
};
[[nodiscard]] QString FindClosestTimezoneId(
const std::vector<Timezone> &list);
} // namespace Data

View file

@ -280,6 +280,7 @@ const Data::BusinessDetails &UserData::businessDetails() const {
}
void UserData::setBusinessDetails(Data::BusinessDetails details) {
details.hours = details.hours.normalized();
if ((!details && !_businessDetails)
|| (details && _businessDetails && details == *_businessDetails)) {
return;

View file

@ -1008,3 +1008,21 @@ similarChannelsLockAbout: FlatLabel(defaultFlatLabel) {
minWidth: 128px;
}
similarChannelsLockAboutPadding: margins(12px, 12px, 12px, 12px);
infoHoursState: FlatLabel(infoLabeled) {
minWidth: 0px;
}
infoHoursValue: FlatLabel(infoHoursState) {
textFg: windowSubTextFg;
align: align(topright);
}
infoHoursDayLabel: infoHoursState;
infoHoursOuter: RoundButton(defaultActiveButton) {
textBg: transparent;
textBgOver: transparent;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
}
infoHoursOuterMargin: margins(8px, 4px, 8px, 4px);
infoHoursDaySkip: 6px;

View file

@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_chat_participants.h"
#include "base/options.h"
#include "base/timer_rpl.h"
#include "base/unixtime.h"
#include "data/business/data_business_common.h"
#include "data/business/data_business_info.h"
#include "data/data_peer_values.h"
#include "data/data_session.h"
#include "data/data_folder.h"
@ -71,6 +75,8 @@ namespace Info {
namespace Profile {
namespace {
constexpr auto kDay = Data::WorkingInterval::kDay;
base::options::toggle ShowPeerIdBelowAbout({
.id = kOptionShowPeerIdBelowAbout,
.name = "Show Peer IDs in Profile",
@ -159,6 +165,435 @@ base::options::toggle ShowPeerIdBelowAbout({
});
}
[[nodiscard]] bool AreNonTrivialHours(const Data::WorkingHours &hours) {
if (!hours) {
return false;
}
const auto &intervals = hours.intervals.list;
for (auto i = 0; i != 7; ++i) {
const auto day = Data::WorkingInterval{ i * kDay, (i + 1) * kDay };
for (const auto &interval : intervals) {
const auto intersection = interval.intersected(day);
if (intersection && intersection != day) {
return true;
}
}
}
return false;
}
[[nodiscard]] TimeId OpensIn(
const Data::WorkingIntervals &intervals,
TimeId now) {
using namespace Data;
while (now < 0) {
now += WorkingInterval::kWeek;
}
while (now > WorkingInterval::kWeek) {
now -= WorkingInterval::kWeek;
}
auto closest = WorkingInterval::kWeek;
for (const auto &interval : intervals.list) {
if (interval.start <= now && interval.end > now) {
return TimeId(0);
} else if (interval.start > now && interval.start - now < closest) {
closest = interval.start - now;
} else if (interval.start < now) {
const auto next = interval.start + WorkingInterval::kWeek - now;
if (next < closest) {
closest = next;
}
}
}
return closest;
}
[[nodiscard]] rpl::producer<QString> OpensInText(
rpl::producer<TimeId> in,
rpl::producer<QString> fallback) {
return rpl::combine(
std::move(in),
std::move(fallback)
) | rpl::map([](TimeId in, QString fallback) {
return !in
? std::move(fallback)
: (in >= 86400)
? tr::lng_info_hours_opens_in_days(tr::now, lt_count, in / 86400)
: (in >= 3600)
? tr::lng_info_hours_opens_in_hours(tr::now, lt_count, in / 3600)
: tr::lng_info_hours_opens_in_minutes(
tr::now,
lt_count,
std::max(in / 60, 1));
});
}
[[nodiscard]] QString FormatDayTime(TimeId time) {
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)
? tr::lng_info_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('\n');
}
[[nodiscard]] QString FormatDayHours(
const Data::WorkingHours &hours,
const Data::WorkingIntervals &mine,
bool my,
int day) {
using namespace Data;
const auto local = ExtractDayIntervals(hours.intervals, day);
if (IsFullOpen(local)) {
return tr::lng_info_hours_open_full(tr::now);
}
const auto use = my ? ExtractDayIntervals(mine, day) : local;
if (!use) {
return tr::lng_info_hours_closed(tr::now);
}
return JoinIntervals(use);
}
[[nodiscard]] Data::WorkingIntervals ShiftedIntervals(
Data::WorkingIntervals intervals,
int delta) {
auto &list = intervals.list;
if (!delta || list.empty()) {
return { std::move(list) };
}
for (auto &interval : list) {
interval.start += delta;
interval.end += delta;
}
while (list.front().start < 0) {
constexpr auto kWeek = Data::WorkingInterval::kWeek;
const auto first = list.front();
if (first.end > 0) {
list.push_back({ first.start + kWeek, kWeek });
list.front().start = 0;
} else {
list.push_back(first.shifted(kWeek));
list.erase(list.begin());
}
}
return intervals.normalized();
}
[[nodiscard]] object_ptr<Ui::SlideWrap<>> CreateWorkingHours(
not_null<QWidget*> parent,
not_null<UserData*> user) {
using namespace Data;
auto result = object_ptr<Ui::SlideWrap<Ui::RoundButton>>(
parent,
object_ptr<Ui::RoundButton>(
parent,
rpl::single(QString()),
st::infoHoursOuter),
st::infoProfileLabeledPadding - st::infoHoursOuterMargin);
const auto button = result->entity();
const auto inner = Ui::CreateChild<Ui::VerticalLayout>(button);
button->widthValue() | rpl::start_with_next([=](int width) {
const auto margin = st::infoHoursOuterMargin;
inner->resizeToWidth(width - margin.left() - margin.right());
inner->move(margin.left(), margin.top());
}, inner->lifetime());
inner->heightValue() | rpl::start_with_next([=](int height) {
const auto margin = st::infoHoursOuterMargin;
height += margin.top() + margin.bottom();
button->resize(button->width(), height);
}, inner->lifetime());
const auto info = &user->owner().businessInfo();
struct State {
rpl::variable<WorkingHours> hours;
rpl::variable<TimeId> time;
rpl::variable<int> day;
rpl::variable<int> timezoneDelta;
rpl::variable<WorkingIntervals> mine;
rpl::variable<WorkingIntervals> mineByDays;
rpl::variable<TimeId> opensIn;
rpl::variable<bool> opened;
rpl::variable<bool> expanded;
rpl::variable<bool> nonTrivial;
rpl::variable<bool> myTimezone;
rpl::event_stream<> recounts;
};
const auto state = inner->lifetime().make_state<State>();
auto recounts = state->recounts.events_starting_with_copy(rpl::empty);
const auto recount = [=] {
state->recounts.fire({});
};
state->hours = user->session().changes().peerFlagsValue(
user,
PeerUpdate::Flag::BusinessDetails
) | rpl::map([=] {
return user->businessDetails().hours;
});
state->nonTrivial = state->hours.value() | rpl::map(AreNonTrivialHours);
const auto seconds = QTime::currentTime().msecsSinceStartOfDay() / 1000;
const auto inMinute = seconds % 60;
const auto firstTick = inMinute ? (61 - inMinute) : 1;
state->time = rpl::single(rpl::empty) | rpl::then(
base::timer_once(firstTick * crl::time(1000))
) | rpl::then(
base::timer_each(60 * crl::time(1000))
) | rpl::map([] {
const auto local = QDateTime::currentDateTime();
const auto day = local.date().dayOfWeek() - 1;
const auto seconds = local.time().msecsSinceStartOfDay() / 1000;
return day * kDay + seconds;
});
state->day = state->time.value() | rpl::map([](TimeId time) {
return time / kDay;
});
state->timezoneDelta = rpl::combine(
state->hours.value(),
info->timezonesValue()
) | rpl::filter([](
const WorkingHours &hours,
const Timezones &timezones) {
return ranges::contains(
timezones.list,
hours.timezoneId,
&Timezone::id);
}) | rpl::map([](WorkingHours &&hours, const Timezones &timezones) {
const auto &list = timezones.list;
const auto closest = FindClosestTimezoneId(list);
const auto i = ranges::find(list, closest, &Timezone::id);
const auto j = ranges::find(list, hours.timezoneId, &Timezone::id);
Assert(i != end(list));
Assert(j != end(list));
return i->utcOffset - j->utcOffset;
});
state->mine = rpl::combine(
state->hours.value(),
state->timezoneDelta.value()
) | rpl::map([](WorkingHours &&hours, int delta) {
return ShiftedIntervals(hours.intervals, delta);
});
state->opensIn = rpl::combine(
state->mine.value(),
state->time.value()
) | rpl::map([](const WorkingIntervals &mine, TimeId time) {
return OpensIn(mine, time);
});
state->opened = state->opensIn.value() | rpl::map(rpl::mappers::_1 == 0);
state->mineByDays = rpl::combine(
state->hours.value(),
state->timezoneDelta.value()
) | rpl::map([](WorkingHours &&hours, int delta) {
auto full = std::array<bool, 7>();
auto withoutFullDays = hours.intervals;
for (auto i = 0; i != 7; ++i) {
if (IsFullOpen(ExtractDayIntervals(hours.intervals, i))) {
full[i] = true;
withoutFullDays = ReplaceDayIntervals(
withoutFullDays,
i,
Data::WorkingIntervals());
}
}
auto result = ShiftedIntervals(withoutFullDays, delta);
for (auto i = 0; i != 7; ++i) {
if (full[i]) {
result = ReplaceDayIntervals(
result,
i,
Data::WorkingIntervals{ { { 0, kDay } } });
}
}
return result;
});
const auto dayHoursText = [=](int day) {
return rpl::combine(
state->hours.value(),
state->mineByDays.value(),
state->myTimezone.value()
) | rpl::map([=](
const WorkingHours &hours,
const WorkingIntervals &mine,
bool my) {
return FormatDayHours(hours, mine, my, day);
});
};
const auto dayHoursTextValue = [=](rpl::producer<int> day) {
return std::move(day)
| rpl::map(dayHoursText)
| rpl::flatten_latest();
};
const auto openedWrap = inner->add(object_ptr<Ui::RpWidget>(inner));
const auto opened = Ui::CreateChild<Ui::FlatLabel>(
openedWrap,
rpl::conditional(
state->opened.value(),
tr::lng_info_work_open(),
tr::lng_info_work_closed()
) | rpl::after_next(recount),
st::infoHoursState);
opened->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto timing = Ui::CreateChild<Ui::FlatLabel>(
openedWrap,
OpensInText(
state->opensIn.value(),
dayHoursTextValue(state->day.value())
) | rpl::after_next(recount),
st::infoHoursValue);
timing->setAttribute(Qt::WA_TransparentForMouseEvents);
state->opened.value() | rpl::start_with_next([=](bool value) {
opened->setTextColorOverride(value
? st::boxTextFgGood->c
: st::boxTextFgError->c);
}, opened->lifetime());
rpl::combine(
openedWrap->widthValue(),
opened->heightValue(),
timing->sizeValue()
) | rpl::start_with_next([=](int width, int h1, QSize size) {
opened->moveToLeft(0, 0, width);
timing->moveToRight(0, 0, width);
const auto margins = opened->getMargins();
const auto added = margins.top() + margins.bottom();
openedWrap->resize(width, std::max(h1, size.height()) - added);
}, openedWrap->lifetime());
const auto labelWrap = inner->add(object_ptr<Ui::RpWidget>(inner));
const auto label = Ui::CreateChild<Ui::FlatLabel>(
labelWrap,
tr::lng_info_hours_label(),
st::infoLabel);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto link = Ui::CreateChild<Ui::LinkButton>(
labelWrap,
QString());
rpl::combine(
state->nonTrivial.value(),
state->hours.value(),
state->mine.value(),
state->myTimezone.value()
) | rpl::map([=](
bool complex,
const WorkingHours &hours,
const WorkingIntervals &mine,
bool my) {
return (!complex || hours.intervals == mine)
? rpl::single(QString())
: my
? tr::lng_info_hours_my_time()
: tr::lng_info_hours_local_time();
}) | rpl::flatten_latest(
) | rpl::start_with_next([=](const QString &text) {
link->setText(text);
}, link->lifetime());
link->setClickedCallback([=] {
state->myTimezone = !state->myTimezone.current();
});
rpl::combine(
labelWrap->widthValue(),
label->heightValue(),
link->sizeValue()
) | rpl::start_with_next([=](int width, int h1, QSize size) {
label->moveToLeft(0, 0, width);
link->moveToRight(0, 0, width);
const auto margins = label->getMargins();
const auto added = margins.top() + margins.bottom();
labelWrap->resize(width, std::max(h1, size.height()) - added);
}, labelWrap->lifetime());
const auto other = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
other->toggleOn(state->expanded.value(), anim::type::normal);
other->finishAnimating();
const auto days = other->entity();
for (auto i = 1; i != 7; ++i) {
const auto dayWrap = days->add(
object_ptr<Ui::RpWidget>(other),
QMargins(0, st::infoHoursDaySkip, 0, 0));
auto label = state->day.value() | rpl::map([=](int day) {
switch ((day + i) % 7) {
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 working hours.");
}) | rpl::flatten_latest();
const auto dayLabel = Ui::CreateChild<Ui::FlatLabel>(
dayWrap,
std::move(label),
st::infoHoursDayLabel);
dayLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto dayHours = Ui::CreateChild<Ui::FlatLabel>(
dayWrap,
dayHoursTextValue(state->day.value()
| rpl::map((rpl::mappers::_1 + i) % 7)),
st::infoHoursValue);
dayHours->setAttribute(Qt::WA_TransparentForMouseEvents);
rpl::combine(
dayWrap->widthValue(),
dayLabel->heightValue(),
dayHours->sizeValue()
) | rpl::start_with_next([=](int width, int h1, QSize size) {
dayLabel->moveToLeft(0, 0, width);
dayHours->moveToRight(0, 0, width);
const auto margins = dayLabel->getMargins();
const auto added = margins.top() + margins.bottom();
dayWrap->resize(width, std::max(h1, size.height()) - added);
}, dayWrap->lifetime());
}
button->setClickedCallback([=] {
state->expanded = !state->expanded.current();
});
result->toggleOn(state->hours.value(
) | rpl::map([](const WorkingHours &data) {
return bool(data);
}));
return result;
}
template <typename Text, typename ToggleOn, typename Callback>
auto AddActionButton(
not_null<Ui::VerticalLayout*> parent,
@ -563,6 +998,28 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
}
return false;
});
} else {
tracker.track(result->add(CreateWorkingHours(result, user)));
auto locationText = user->session().changes().peerFlagsValue(
user,
Data::PeerUpdate::Flag::BusinessDetails
) | rpl::map([=] {
const auto &details = user->businessDetails();
if (!details.location) {
return TextWithEntities();
} else if (!details.location.point) {
return TextWithEntities{ details.location.address };
}
return Ui::Text::Link(
TextUtilities::SingleLine(details.location.address),
LocationClickHandler::Url(*details.location.point));
});
addInfoOneLine(
tr::lng_info_location_label(),
std::move(locationText),
QString()
).text->setLinksTrusted();
}
AddMainButton(

View file

@ -70,27 +70,6 @@ private:
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) {
@ -372,7 +351,7 @@ void ChooseTimezoneBox(
});
if (!ranges::contains(list, id, &Data::Timezone::id)) {
id = FindClosestTimezoneId(list);
id = Data::FindClosestTimezoneId(list);
}
const auto i = ranges::find(list, id, &Data::Timezone::id);
const auto value = int(i - begin(list));
@ -472,7 +451,7 @@ void AddWeekButton(
}
if (!intervals) {
return tr::lng_hours_closed();
} else if (intervals.list.front() == WorkingInterval{ 0, kDay }) {
} else if (IsFullOpen(intervals)) {
return tr::lng_hours_open_full();
}
return rpl::single(JoinIntervals(intervals));
@ -613,7 +592,7 @@ void WorkingHours::setupContent(
const auto now = _hours.current().timezoneId;
if (!ranges::contains(value.list, now, &Data::Timezone::id)) {
auto copy = _hours.current();
copy.timezoneId = FindClosestTimezoneId(value.list);
copy.timezoneId = Data::FindClosestTimezoneId(value.list);
_hours = std::move(copy);
}
}, inner->lifetime());