init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,267 @@
/*
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/auto_delete_settings.h"
#include "ui/widgets/checkbox.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
namespace Ui {
namespace {
object_ptr<Ui::RpWidget> CreateSliderForTTL(
not_null<QWidget*> parent,
std::vector<QString> labels,
int dashedAfterIndex,
int selected,
Fn<void(int)> callback) {
Expects(labels.size() > 1);
Expects(selected >= 0 && selected < labels.size());
Expects(dashedAfterIndex >= 0 && dashedAfterIndex < labels.size());
struct State {
std::vector<int> points;
std::vector<QString> labels;
int selected = 0;
};
static const auto st = &st::defaultSliderForTTL;
const auto height = st->font->height + st->skip + st->chosenSize;
const auto count = int(labels.size());
auto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), height);
const auto raw = result.data();
const auto slider = Ui::CreateChild<Ui::FixedHeightWidget>(
raw,
st->chosenSize);
slider->setCursor(style::cur_pointer);
slider->move(0, height - slider->height());
auto &lifetime = raw->lifetime();
const auto state = lifetime.make_state<State>(State{
.labels = std::move(labels),
.selected = selected
});
state->points.resize(count, 0);
raw->widthValue(
) | rpl::on_next([=](int width) {
for (auto i = 0; i != count; ++i) {
state->points[i] = (width * i) / (count - 1);
}
slider->resize(width, slider->height());
}, lifetime);
raw->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(raw);
p.setFont(st->font);
for (auto i = 0; i != count; ++i) {
// Label
p.setPen(st->textFg);
const auto &text = state->labels[i];
const auto textWidth = st->font->width(text);
const auto shift = (i == count - 1)
? textWidth
: (i > 0)
? (textWidth / 2)
: 0;
const auto x = state->points[i] - shift;
const auto y = st->font->ascent;
p.drawText(x, y, text);
}
}, lifetime);
slider->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(slider);
auto hq = PainterHighQualityEnabler(p);
p.setFont(st->font);
for (auto i = 0; i != count; ++i) {
const auto middle = (st->chosenSize / 2.);
// Point
const auto size = (i == state->selected)
? st->chosenSize
: st->pointSize;
const auto pointfg = (i <= state->selected)
? st->activeFg
: st->inactiveFg;
const auto shift = (i == count - 1)
? float64(size)
: (i > 0)
? (size / 2.)
: 0.;
const auto pointx = state->points[i] - shift;
const auto pointy = middle - (size / 2.);
p.setPen(Qt::NoPen);
p.setBrush(pointfg);
p.drawEllipse(QRectF{ pointx, pointy, size * 1., size * 1. });
// Line
if (i + 1 == count) {
break;
}
const auto nextSize = (i + 1 == state->selected)
? st->chosenSize
: st->pointSize;
const auto nextShift = (i + 1 == count - 1)
? float64(nextSize)
: (nextSize / 2.);
const auto &linefg = (i + 1 <= state->selected)
? st->activeFg
: st->inactiveFg;
const auto from = pointx + size + st->stroke * 1.5;
const auto till = state->points[i + 1] - nextShift - st->stroke * 1.5;
auto pen = linefg->p;
pen.setWidthF(st->stroke);
if (i >= dashedAfterIndex) {
// Try to fill the line with exact number of dash segments.
// UPD Doesn't work so well because it changes when clicking.
//const auto length = till - from;
//const auto offSegmentsCount = int(base::SafeRound(
// (length - st->dashOn) / (st->dashOn + st->dashOff)));
//const auto onSegmentsCount = offSegmentsCount + 1;
//const auto idealLength = offSegmentsCount * st->dashOff
// + onSegmentsCount * st->dashOn;
//const auto multiplier = length / float64(idealLength);
const auto multiplier = 1.;
auto dashPattern = QVector<qreal>{
st->dashOn * multiplier / st->stroke,
st->dashOff * multiplier / st->stroke
};
pen.setDashPattern(dashPattern);
}
pen.setCapStyle(Qt::RoundCap);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
p.drawLine(QPointF(from, middle), QPointF(till, middle));
}
}, lifetime);
slider->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::MouseButtonPress)
&& (static_cast<QMouseEvent*>(e.get())->button()
== Qt::LeftButton)
&& (state->points[1] > 0);
}) | rpl::map([=](not_null<QEvent*> e) {
return rpl::single(
static_cast<QMouseEvent*>(e.get())->pos()
) | rpl::then(slider->events(
) | rpl::take_while([=](not_null<QEvent*> e) {
return (e->type() != QEvent::MouseButtonRelease)
|| (static_cast<QMouseEvent*>(e.get())->button()
!= Qt::LeftButton);
}) | rpl::filter([=](not_null<QEvent*> e) {
return (e->type() == QEvent::MouseMove);
}) | rpl::map([=](not_null<QEvent*> e) {
return static_cast<QMouseEvent*>(e.get())->pos();
}));
}) | rpl::flatten_latest(
) | rpl::on_next([=](QPoint position) {
state->selected = std::clamp(
(position.x() + (state->points[1] / 2)) / state->points[1],
0,
count - 1);
slider->update();
callback(state->selected);
}, lifetime);
return result;
}
} // namespace
void AutoDeleteSettingsBox(
not_null<Ui::GenericBox*> box,
TimeId ttlPeriod,
rpl::producer<QString> about,
Fn<void(TimeId)> callback) {
box->setTitle(tr::lng_manage_messages_ttl_title());
struct State {
TimeId period = 0;
};
const auto state = box->lifetime().make_state<State>(State{
.period = ttlPeriod,
});
const auto options = std::vector<QString>{
tr::lng_manage_messages_ttl_disable(tr::now),
//u"5 seconds"_q, AssertIsDebug()
tr::lng_manage_messages_ttl_after1(tr::now),
tr::lng_manage_messages_ttl_after2(tr::now),
tr::lng_manage_messages_ttl_after3(tr::now),
};
const auto periodToIndex = [&](TimeId period) {
return !period
? 0
//: (period == 5) AssertIsDebug()
//? 1 AssertIsDebug()
: (period < 2 * 86400)
? 1
: (period < 8 * 86400)
? 2
: 3;
};
const auto indexToPeriod = [&](int index) {
return !index
? 0
//: (index == 1) AssertIsDebug()
//? 5 AssertIsDebug()
: (index == 1)
? 86400
: (index == 2)
? 7 * 86400
: 31 * 86400;
};
const auto sliderCallback = [=](int index) {
state->period = indexToPeriod(index);
};
box->addRow(
CreateSliderForTTL(
box,
options | ranges::to_vector,
options.size() - 1,
periodToIndex(ttlPeriod),
sliderCallback),
{
st::boxRowPadding.left(),
0,
st::boxRowPadding.right(),
st::boxMediumSkip });
box->addRow(
object_ptr<Ui::DividerLabel>(
box,
object_ptr<Ui::FlatLabel>(
box,
std::move(about),
st::boxDividerLabel),
st::ttlDividerLabelPadding),
style::margins());
box->addButton(tr::lng_settings_save(), [=] {
const auto period = state->period;
box->closeBox();
callback(period);
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace Ui

View File

@@ -0,0 +1,20 @@
/*
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 "ui/layers/generic_box.h"
namespace Ui {
void AutoDeleteSettingsBox(
not_null<Ui::GenericBox*> box,
TimeId ttlPeriod,
rpl::producer<QString> about,
Fn<void(TimeId)> callback);
} // namespace Ui

View File

@@ -0,0 +1,986 @@
/*
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/boost_box.h"
#include "info/profile/info_profile_icon.h"
#include "lang/lang_keys.h"
#include "ui/boxes/confirm_box.h"
#include "ui/effects/fireworks_animation.h"
#include "ui/effects/premium_bubble.h"
#include "ui/effects/premium_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "styles/style_giveaway.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
#include <QtGui/QGuiApplication>
namespace Ui {
namespace {
[[nodiscard]] BoostCounters AdjustByReached(BoostCounters data) {
const auto exact = (data.boosts == data.thisLevelBoosts);
const auto reached = !data.nextLevelBoosts || (exact && data.mine > 0);
if (reached) {
--data.level;
data.boosts = data.nextLevelBoosts = std::max({
data.boosts,
data.thisLevelBoosts,
1
});
data.thisLevelBoosts = 0;
} else {
data.boosts = std::max(data.thisLevelBoosts, data.boosts);
data.nextLevelBoosts = std::max(
data.nextLevelBoosts,
data.boosts + 1);
}
return data;
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeTitle(
not_null<Ui::RpWidget*> parent,
rpl::producer<QString> title,
rpl::producer<QString> repeated,
bool centered = true) {
auto result = object_ptr<Ui::RpWidget>(parent);
struct State {
not_null<Ui::FlatLabel*> title;
not_null<Ui::FlatLabel*> repeated;
};
const auto notEmpty = [](const QString &text) {
return !text.isEmpty();
};
const auto state = parent->lifetime().make_state<State>(State{
.title = Ui::CreateChild<Ui::FlatLabel>(
result.data(),
rpl::duplicate(title),
st::boostTitle),
.repeated = Ui::CreateChild<Ui::FlatLabel>(
result.data(),
rpl::duplicate(repeated) | rpl::filter(notEmpty),
st::boostTitleBadge),
});
state->title->show();
state->repeated->showOn(std::move(repeated) | rpl::map(notEmpty));
result->resize(result->width(), st::boostTitle.style.font->height);
rpl::combine(
result->widthValue(),
rpl::duplicate(title),
state->repeated->shownValue(),
state->repeated->widthValue()
) | rpl::on_next([=](int outer, auto&&, bool shown, int badge) {
const auto repeated = shown ? badge : 0;
const auto skip = st::boostTitleBadgeSkip;
const auto available = outer - repeated - skip;
const auto use = std::min(state->title->textMaxWidth(), available);
state->title->resizeToWidth(use);
const auto left = centered
? (outer - use - skip - repeated) / 2
: 0;
state->title->moveToLeft(left, 0);
const auto mleft = st::boostTitleBadge.margin.left();
const auto mtop = st::boostTitleBadge.margin.top();
state->repeated->moveToLeft(left + use + skip + mleft, mtop);
}, result->lifetime());
const auto badge = state->repeated;
badge->paintRequest() | rpl::on_next([=] {
auto p = QPainter(badge);
auto hq = PainterHighQualityEnabler(p);
const auto radius = std::min(badge->width(), badge->height()) / 2;
p.setPen(Qt::NoPen);
p.setBrush(st::premiumButtonBg2);
p.drawRoundedRect(badge->rect(), radius, radius);
}, badge->lifetime());
return result;
}
[[nodiscard]] object_ptr<Ui::FlatLabel> MakeFeaturesBadge(
not_null<QWidget*> parent,
rpl::producer<QString> text) {
return MakeBoostFeaturesBadge(parent, std::move(text), [](QRect rect) {
auto gradient = QLinearGradient(
rect.topLeft(),
rect.topRight());
gradient.setStops(Ui::Premium::GiftGradientStops());
return QBrush(gradient);
});
}
void AddFeaturesList(
not_null<Ui::VerticalLayout*> container,
const Ui::BoostFeatures &features,
int startFromLevel,
bool group) {
const auto add = [&](
rpl::producer<TextWithEntities> text,
const style::icon &st) {
const auto label = container->add(
object_ptr<Ui::FlatLabel>(
container,
std::move(text),
st::boostFeatureLabel),
st::boostFeaturePadding);
object_ptr<Info::Profile::FloatingIcon>(
label,
st,
st::boostFeatureIconPosition);
};
const auto lowMax = std::max({
features.linkLogoLevel,
features.profileIconLevel,
features.autotranslateLevel,
features.transcribeLevel,
features.emojiPackLevel,
features.emojiStatusLevel,
features.wallpaperLevel,
features.customWallpaperLevel,
(features.nameColorsByLevel.empty()
? 0
: features.nameColorsByLevel.back().first),
(features.linkStylesByLevel.empty()
? 0
: features.linkStylesByLevel.back().first),
(features.profileColorsByLevel.empty()
? 0
: features.profileColorsByLevel.back().first),
});
const auto highMax = std::max(lowMax, features.sponsoredLevel);
auto nameColors = 0;
auto linkStyles = 0;
auto profileColors = 0;
for (auto i = std::max(startFromLevel, 1); i <= highMax; ++i) {
if ((i > lowMax) && (i < highMax)) {
continue;
}
const auto unlocks = (i == startFromLevel);
{
const auto badge = container->add(
MakeFeaturesBadge(
container,
(unlocks
? tr::lng_boost_level_unlocks
: tr::lng_boost_level)(
lt_count,
rpl::single(float64(i)))),
st::boostLevelBadgePadding,
style::al_top);
const auto padding = st::boxRowPadding;
const auto line = Ui::CreateChild<Ui::RpWidget>(container);
badge->geometryValue() | rpl::on_next([=](const QRect &r) {
line->setGeometry(
padding.left(),
r.y(),
container->width() - rect::m::sum::h(padding),
r.height());
}, line->lifetime());
const auto shift = st::lineWidth * 10;
line->paintRequest() | rpl::on_next([=] {
auto p = QPainter(line);
p.setPen(st::windowSubTextFg);
const auto y = line->height() / 2;
const auto left = badge->x() - shift - padding.left();
const auto right = left + badge->width() + shift * 2;
if (left > 0) {
p.drawLine(0, y, left, y);
}
if (right < line->width()) {
p.drawLine(right, y, line->width(), y);
}
}, line->lifetime());
}
if (i >= features.sponsoredLevel) {
add(
tr::lng_channel_earn_off(tr::rich),
st::boostFeatureOffSponsored);
}
if (i >= features.customWallpaperLevel) {
add(
(group
? tr::lng_feature_custom_background_group
: tr::lng_feature_custom_background_channel)(tr::rich),
st::boostFeatureCustomBackground);
}
if (i >= features.wallpaperLevel) {
add(
(group
? tr::lng_feature_backgrounds_group
: tr::lng_feature_backgrounds_channel)(
lt_count,
rpl::single(float64(features.wallpapersCount)),
tr::rich),
st::boostFeatureBackground);
}
if (i >= features.emojiStatusLevel) {
add(
tr::lng_feature_emoji_status(tr::rich),
st::boostFeatureEmojiStatus);
}
if (const auto j = features.profileColorsByLevel.find(i)
; j != end(features.profileColorsByLevel)) {
profileColors += j->second;
}
if (i >= features.profileIconLevel) {
add(
(group
? tr::lng_feature_profile_icon_group
: tr::lng_feature_profile_icon_channel)(tr::rich),
st::boostFeatureProfileIcon);
}
if (profileColors > 0) {
add((group
? tr::lng_feature_profile_color_group
: tr::lng_feature_profile_color_channel)(
lt_count,
rpl::single(float64(profileColors)),
tr::rich
), st::boostFeatureProfileColor);
}
if (!group) {
if (const auto j = features.linkStylesByLevel.find(i)
; j != end(features.linkStylesByLevel)) {
linkStyles += j->second;
}
if (i >= features.linkLogoLevel) {
add(
tr::lng_feature_link_emoji(tr::rich),
st::boostFeatureCustomLink);
}
if (linkStyles > 0) {
add(tr::lng_feature_link_style_channel(
lt_count,
rpl::single(float64(linkStyles)),
tr::rich
), st::boostFeatureLink);
}
if (const auto j = features.nameColorsByLevel.find(i)
; j != end(features.nameColorsByLevel)) {
nameColors += j->second;
}
if (nameColors > 0) {
add(tr::lng_feature_name_color_channel(
lt_count,
rpl::single(float64(nameColors)),
tr::rich
), st::boostFeatureName);
}
add(tr::lng_feature_reactions(
lt_count,
rpl::single(float64(i)),
tr::rich
), st::boostFeatureCustomReactions);
}
add(
tr::lng_feature_stories(lt_count, rpl::single(1. * i), tr::rich),
st::boostFeatureStories);
if (!group && i >= features.autotranslateLevel) {
add(
tr::lng_feature_autotranslate(tr::rich),
st::boostFeatureAutoTranslate);
}
if (group && i >= features.transcribeLevel) {
add(
tr::lng_feature_transcribe(tr::rich),
st::boostFeatureTranscribe);
}
if (group && i >= features.emojiPackLevel) {
add(
tr::lng_feature_custom_emoji_pack(tr::rich),
st::boostFeatureCustomEmoji);
}
}
}
} // namespace
void StartFireworks(not_null<QWidget*> parent) {
const auto result = Ui::CreateChild<RpWidget>(parent.get());
result->setAttribute(Qt::WA_TransparentForMouseEvents);
result->setGeometry(parent->rect());
result->show();
auto &lifetime = result->lifetime();
const auto animation = lifetime.make_state<FireworksAnimation>([=] {
result->update();
});
result->paintRequest() | rpl::on_next([=] {
auto p = QPainter(result);
if (!animation->paint(p, result->rect())) {
crl::on_main(result, [=] { delete result; });
}
}, lifetime);
}
void BoostBox(
not_null<GenericBox*> box,
BoostBoxData data,
Fn<void(Fn<void(BoostCounters)>)> boost) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::boostBox);
//AssertIsDebug();
//data.boost = {
// .level = 2,
// .boosts = 3,
// .thisLevelBoosts = 2,
// .nextLevelBoosts = 5,
// .mine = 2,
//};
struct State {
rpl::variable<BoostCounters> data;
rpl::variable<bool> full;
bool submitted = false;
};
const auto state = box->lifetime().make_state<State>();
state->data = std::move(data.boost);
FillBoostLimit(
BoxShowFinishes(box),
box->verticalLayout(),
state->data.value(),
st::boxRowPadding);
box->setMaxHeight(st::boostBoxMaxHeight);
const auto close = box->addTopButton(
st::boxTitleClose,
[=] { box->closeBox(); });
const auto name = data.name;
auto title = state->data.value(
) | rpl::map([=](BoostCounters counters) {
return (counters.mine > 0)
? tr::lng_boost_channel_you_title(
lt_channel,
rpl::single(name))
: !counters.nextLevelBoosts
? tr::lng_boost_channel_title_max()
: counters.level
? (data.group
? tr::lng_boost_channel_title_more_group()
: tr::lng_boost_channel_title_more())
: (data.group
? tr::lng_boost_channel_title_first_group()
: tr::lng_boost_channel_title_first());
}) | rpl::flatten_latest();
auto repeated = state->data.value(
) | rpl::map([=](BoostCounters counters) {
return (counters.mine > 1) ? u"x%1"_q.arg(counters.mine) : u""_q;
});
const auto wasMine = state->data.current().mine;
const auto wasLifting = data.lifting;
auto text = state->data.value(
) | rpl::map([=](BoostCounters counters) {
const auto lifting = wasLifting
? (wasLifting
- std::clamp(counters.mine - wasMine, 0, wasLifting - 1))
: 0;
const auto bold = tr::bold(name);
const auto now = counters.boosts;
const auto full = !counters.nextLevelBoosts;
const auto left = (counters.nextLevelBoosts > now)
? (counters.nextLevelBoosts - now)
: 0;
auto post = tr::lng_boost_channel_post_stories(
lt_count,
rpl::single(float64(counters.level + (left ? 1 : 0))),
tr::rich);
return (lifting > 1)
? tr::lng_boost_group_lift_restrictions_many(
lt_count,
rpl::single(float64(lifting)),
tr::rich)
: lifting
? tr::lng_boost_group_lift_restrictions(tr::rich)
: (counters.mine || full)
? (left
? tr::lng_boost_channel_needs_unlock(
lt_count,
rpl::single(float64(left)),
lt_channel,
rpl::single(bold),
tr::rich)
: (!counters.level
? (data.group
? tr::lng_boost_channel_reached_first_group
: tr::lng_boost_channel_reached_first)(
tr::rich)
: (data.group
? tr::lng_boost_channel_reached_more_group
: tr::lng_boost_channel_reached_more)(
lt_count,
rpl::single(float64(counters.level)),
lt_post,
std::move(post),
tr::rich)))
: tr::lng_boost_channel_needs_unlock(
lt_count,
rpl::single(float64(left)),
lt_channel,
rpl::single(bold),
tr::rich);
}) | rpl::flatten_latest();
if (wasLifting) {
state->data.value(
) | rpl::on_next([=](BoostCounters counters) {
if (counters.mine - wasMine >= wasLifting) {
box->closeBox();
}
}, box->lifetime());
}
auto faded = object_ptr<Ui::FadeWrap<>>(
close->parentWidget(),
MakeTitle(
box,
(data.group
? tr::lng_boost_group_button
: tr::lng_boost_channel_button)(),
rpl::duplicate(repeated),
false));
const auto titleInner = faded.data();
titleInner->move(st::boxTitlePosition);
titleInner->resizeToWidth(st::boxWideWidth
- st::boxTitleClose.width
- st::boxTitlePosition.x());
titleInner->hide(anim::type::instant);
crl::on_main(titleInner, [=] {
titleInner->raise();
titleInner->toggleOn(rpl::single(
rpl::empty
) | rpl::then(
box->scrolls()
) | rpl::map([=] {
return box->scrollTop() > 0;
}));
});
box->addRow(
MakeTitle(box, std::move(title), std::move(repeated)),
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0));
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(text),
st::boostText),
(st::boxRowPadding
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)));
const auto current = state->data.current();
box->setTitle(rpl::single(QString()));
AddFeaturesList(
box->verticalLayout(),
data.features,
current.level + (current.nextLevelBoosts ? 1 : 0),
data.group);
const auto allowMulti = data.allowMulti;
auto submit = state->data.value(
) | rpl::map([=](BoostCounters counters) {
return (!counters.nextLevelBoosts || (counters.mine && !allowMulti))
? tr::lng_box_ok()
: (counters.mine > 0)
? tr::lng_boost_again_button()
: data.group
? tr::lng_boost_group_button()
: tr::lng_boost_channel_button();
}) | rpl::flatten_latest();
box->addButton(rpl::duplicate(submit), [=] {
if (state->submitted) {
return;
} else if (state->data.current().nextLevelBoosts > 0
&& (allowMulti || !state->data.current().mine)) {
state->submitted = true;
const auto was = state->data.current().mine;
//AssertIsDebug();
//state->submitted = false;
//if (state->data.current().level == 5
// && state->data.current().boosts == 11) {
// state->data = BoostCounters{
// .level = 5,
// .boosts = 14,
// .thisLevelBoosts = 9,
// .nextLevelBoosts = 15,
// .mine = 14,
// };
//} else if (state->data.current().level == 5) {
// state->data = BoostCounters{
// .level = 7,
// .boosts = 16,
// .thisLevelBoosts = 15,
// .nextLevelBoosts = 19,
// .mine = 16,
// };
//} else if (state->data.current().level == 4) {
// state->data = BoostCounters{
// .level = 5,
// .boosts = 11,
// .thisLevelBoosts = 9,
// .nextLevelBoosts = 15,
// .mine = 9,
// };
//} else if (state->data.current().level == 3) {
// state->data = BoostCounters{
// .level = 4,
// .boosts = 7,
// .thisLevelBoosts = 7,
// .nextLevelBoosts = 9,
// .mine = 5,
// };
//} else {
// state->data = BoostCounters{
// .level = 3,
// .boosts = 5,
// .thisLevelBoosts = 5,
// .nextLevelBoosts = 7,
// .mine = 3,
// };
//}
//return;
boost(crl::guard(box, [=](BoostCounters result) {
state->submitted = false;
if (result.thisLevelBoosts || result.nextLevelBoosts) {
if (result.mine > was) {
StartFireworks(box->parentWidget());
}
state->data = result;
}
}));
} else {
box->closeBox();
}
});
}
object_ptr<Ui::RpWidget> MakeLinkLabel(
not_null<QWidget*> parent,
rpl::producer<QString> text,
rpl::producer<QString> link,
std::shared_ptr<Ui::Show> show,
object_ptr<Ui::RpWidget> right) {
auto result = object_ptr<Ui::AbstractButton>(parent);
const auto raw = result.data();
const auto rawRight = right.release();
if (rawRight) {
rawRight->setParent(raw);
rawRight->show();
}
struct State {
State(
not_null<QWidget*> parent,
rpl::producer<QString> value,
rpl::producer<QString> link)
: text(std::move(value))
, link(std::move(link))
, label(parent, text.value(), st::giveawayGiftCodeLink)
, bg(st::roundRadiusLarge, st::windowBgOver) {
}
rpl::variable<QString> text;
rpl::variable<QString> link;
Ui::FlatLabel label;
Ui::RoundRect bg;
};
const auto state = raw->lifetime().make_state<State>(
raw,
rpl::duplicate(text),
std::move(link));
state->label.setSelectable(true);
rpl::combine(
raw->widthValue(),
std::move(text)
) | rpl::on_next([=](int outer, const auto&) {
const auto textWidth = state->label.textMaxWidth();
const auto skipLeft = st::giveawayGiftCodeLink.margin.left();
const auto skipRight = rawRight
? rawRight->width()
: st::giveawayGiftCodeLink.margin.right();
const auto available = outer - skipRight - skipLeft;
const auto use = std::min(textWidth, available);
state->label.resizeToWidth(use);
const auto forCenter = (outer - use) / 2;
const auto x = (forCenter < skipLeft)
? skipLeft
: (forCenter > outer - skipRight - use)
? (outer - skipRight - use)
: forCenter;
state->label.moveToLeft(x, st::giveawayGiftCodeLink.margin.top());
}, raw->lifetime());
raw->paintRequest() | rpl::on_next([=] {
auto p = QPainter(raw);
state->bg.paint(p, raw->rect());
}, raw->lifetime());
state->label.setAttribute(Qt::WA_TransparentForMouseEvents);
raw->resize(raw->width(), st::giveawayGiftCodeLinkHeight);
if (rawRight) {
raw->widthValue() | rpl::on_next([=](int width) {
rawRight->move(width - rawRight->width(), 0);
}, raw->lifetime());
}
raw->setClickedCallback([=] {
QGuiApplication::clipboard()->setText(state->link.current());
show->showToast(tr::lng_username_copied(tr::now));
});
return result;
}
void BoostBoxAlready(not_null<GenericBox*> box, bool group) {
ConfirmBox(box, {
.text = (group
? tr::lng_boost_error_already_text_group
: tr::lng_boost_error_already_text)(tr::rich),
.title = tr::lng_boost_error_already_title(),
.inform = true,
});
}
void GiftForBoostsBox(
not_null<GenericBox*> box,
QString channel,
int receive,
bool again) {
ConfirmBox(box, {
.text = (again
? tr::lng_boost_need_more_again
: tr::lng_boost_need_more_text)(
lt_count,
rpl::single(receive) | tr::to_count(),
lt_channel,
rpl::single(TextWithEntities{ channel }),
tr::rich),
.title = tr::lng_boost_need_more(),
.inform = true,
});
}
void GiftedNoBoostsBox(not_null<GenericBox*> box, bool group) {
InformBox(box, {
.text = (group
? tr::lng_boost_error_gifted_text_group
: tr::lng_boost_error_gifted_text)(tr::rich),
.title = tr::lng_boost_error_gifted_title(),
});
}
void PremiumForBoostsBox(
not_null<GenericBox*> box,
bool group,
Fn<void()> buyPremium) {
ConfirmBox(box, {
.text = (group
? tr::lng_boost_error_premium_text_group
: tr::lng_boost_error_premium_text)(tr::rich),
.confirmed = buyPremium,
.confirmText = tr::lng_boost_error_premium_yes(),
.title = tr::lng_boost_error_premium_title(),
});
}
void AskBoostBox(
not_null<GenericBox*> box,
AskBoostBoxData data,
Fn<void()> openStatistics,
Fn<void()> startGiveaway) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::boostBox);
box->setNoContentMargin(true);
box->addSkip(st::boxRowPadding.left());
FillBoostLimit(
BoxShowFinishes(box),
box->verticalLayout(),
rpl::single(data.boost),
st::boxRowPadding);
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
auto title = v::match(data.reason.data, [](AskBoostChannelColor) {
return tr::lng_boost_channel_title_color();
}, [](AskBoostAutotranslate) {
return tr::lng_boost_channel_title_autotranslate();
}, [](AskBoostWallpaper) {
return tr::lng_boost_channel_title_wallpaper();
}, [](AskBoostEmojiStatus) {
return tr::lng_boost_channel_title_status();
}, [](AskBoostEmojiPack) {
return tr::lng_boost_group_title_emoji();
}, [](AskBoostCustomReactions) {
return tr::lng_boost_channel_title_reactions();
}, [](AskBoostCpm) {
return tr::lng_boost_channel_title_cpm();
}, [](AskBoostWearCollectible) {
return tr::lng_boost_channel_title_wear();
});
auto isGroup = false;
auto reasonText = v::match(data.reason.data, [&](
AskBoostChannelColor data) {
return tr::lng_boost_channel_needs_level_color(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostAutotranslate data) {
return tr::lng_boost_channel_needs_level_autotranslate(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostWallpaper data) {
isGroup = data.group;
return (data.group
? tr::lng_boost_group_needs_level_wallpaper
: tr::lng_boost_channel_needs_level_wallpaper)(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostEmojiStatus data) {
isGroup = data.group;
return (data.group
? tr::lng_boost_group_needs_level_status
: tr::lng_boost_channel_needs_level_status)(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostEmojiPack data) {
isGroup = true;
return tr::lng_boost_group_needs_level_emoji(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostCustomReactions data) {
return tr::lng_boost_channel_needs_level_reactions(
lt_count,
rpl::single(float64(data.count)),
lt_same_count,
rpl::single(TextWithEntities{ QString::number(data.count) }),
tr::rich);
}, [&](AskBoostCpm data) {
return tr::lng_boost_channel_needs_level_cpm(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
}, [&](AskBoostWearCollectible data) {
return tr::lng_boost_channel_needs_level_wear(
lt_count,
rpl::single(float64(data.requiredLevel)),
tr::rich);
});
auto text = rpl::combine(
std::move(reasonText),
(isGroup ? tr::lng_boost_group_ask : tr::lng_boost_channel_ask)(
tr::rich)
) | rpl::map([](TextWithEntities &&text, TextWithEntities &&ask) {
return text.append(u"\n\n"_q).append(std::move(ask));
});
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(title),
st::boostCenteredTitle),
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0),
style::al_top);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(text),
st::boostText),
(st::boxRowPadding
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)),
style::al_top);
auto stats = object_ptr<Ui::IconButton>(box, st::boostLinkStatsButton);
stats->setClickedCallback(openStatistics);
box->addRow(MakeLinkLabel(
box,
rpl::single(data.link),
rpl::single(data.link),
box->uiShow(),
std::move(stats)));
AddFeaturesList(
box->verticalLayout(),
data.features,
data.boost.level + (data.boost.nextLevelBoosts ? 1 : 0),
data.group);
auto submit = tr::lng_boost_channel_ask_button();
box->addButton(rpl::duplicate(submit), [=] {
QGuiApplication::clipboard()->setText(data.link);
box->uiShow()->showToast(tr::lng_username_copied(tr::now));
});
}
void FillBoostLimit(
rpl::producer<> showFinished,
not_null<VerticalLayout*> container,
rpl::producer<BoostCounters> data,
style::margins limitLinePadding) {
const auto addSkip = [&](int skip) {
container->add(object_ptr<Ui::FixedHeightWidget>(container, skip));
};
const auto ratio = [=](BoostCounters counters) {
const auto min = counters.thisLevelBoosts;
const auto max = counters.nextLevelBoosts;
Assert(counters.boosts >= min && counters.boosts <= max);
const auto count = (max - min);
const auto index = (counters.boosts - min);
if (!index) {
return 0.;
} else if (index == count) {
return 1.;
} else if (count == 2) {
return 0.5;
}
const auto available = st::boxWideWidth
- st::boxPadding.left()
- st::boxPadding.right();
const auto average = available / float64(count);
const auto levelWidth = [&](int add) {
return st::normalFont->width(
tr::lng_boost_level(
tr::now,
lt_count,
counters.level + add));
};
const auto paddings = 2 * st::premiumLineTextSkip;
const auto labelLeftWidth = paddings + levelWidth(0);
const auto labelRightWidth = paddings + levelWidth(1);
const auto first = std::max(average, labelLeftWidth * 1.);
const auto last = std::max(average, labelRightWidth * 1.);
const auto other = (available - first - last) / (count - 2);
return (first + (index - 1) * other) / available;
};
auto adjustedData = rpl::duplicate(data) | rpl::map(AdjustByReached);
auto bubbleRowState = rpl::duplicate(
adjustedData
) | rpl::combine_previous(
BoostCounters()
) | rpl::map([=](BoostCounters previous, BoostCounters counters) {
return Premium::BubbleRowState{
.counter = counters.boosts,
.ratio = ratio(counters),
.animateFromZero = (counters.level != previous.level),
.dynamic = true,
};
});
Premium::AddBubbleRow(
container,
st::boostBubble,
std::move(showFinished),
rpl::duplicate(bubbleRowState),
Premium::BubbleType::Premium,
nullptr,
&st::premiumIconBoost,
limitLinePadding);
addSkip(st::premiumLineTextSkip);
const auto level = [](int level) {
return tr::lng_boost_level(tr::now, lt_count, level);
};
auto limitState = std::move(
bubbleRowState
) | rpl::map([](const Premium::BubbleRowState &state) {
return Premium::LimitRowState{
.ratio = state.ratio,
.animateFromZero = state.animateFromZero,
.dynamic = state.dynamic
};
});
auto left = rpl::duplicate(
adjustedData
) | rpl::map([=](BoostCounters counters) {
return level(counters.level);
});
auto right = rpl::duplicate(
adjustedData
) | rpl::map([=](BoostCounters counters) {
return level(counters.level + 1);
});
Premium::AddLimitRow(
container,
st::boostLimits,
Premium::LimitRowLabels{
.leftLabel = std::move(left),
.rightLabel = std::move(right),
},
std::move(limitState),
limitLinePadding);
}
object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(
not_null<QWidget*> parent,
rpl::producer<QString> text,
Fn<QBrush(QRect)> bg) {
auto result = object_ptr<Ui::FlatLabel>(
parent,
std::move(text),
st::boostLevelBadge);
const auto label = result.data();
label->show();
label->paintRequest() | rpl::on_next([=] {
const auto size = label->textMaxWidth();
const auto rect = QRect(
(label->width() - size) / 2,
st::boostLevelBadge.margin.top(),
size,
st::boostLevelBadge.style.font->height
).marginsAdded(st::boostLevelBadge.margin);
auto p = QPainter(label);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(bg(rect));
p.setPen(Qt::NoPen);
p.drawRoundedRect(rect, rect.height() / 2., rect.height() / 2.);
const auto &lineFg = st::windowBgRipple;
const auto line = st::boostLevelBadgeLine;
const auto top = st::boostLevelBadge.margin.top()
+ ((st::boostLevelBadge.style.font->height - line) / 2);
const auto left = 0;
const auto skip = st::boostLevelBadgeSkip;
if (const auto right = rect.x() - skip; right > left) {
p.fillRect(left, top, right - left, line, lineFg);
}
const auto right = label->width();
if (const auto left = rect.x() + rect.width() + skip
; left < right) {
p.fillRect(left, top, right - left, line, lineFg);
}
}, label->lifetime());
return result;
}
} // namespace Ui

View File

@@ -0,0 +1,154 @@
/*
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 "base/object_ptr.h"
namespace Ui {
void StartFireworks(not_null<QWidget*> parent);
class Show;
class RpWidget;
class GenericBox;
class VerticalLayout;
class FlatLabel;
struct BoostCounters {
int level = 0;
int boosts = 0;
int thisLevelBoosts = 0;
int nextLevelBoosts = 0; // Zero means no next level is available.
int mine = 0;
friend inline constexpr bool operator==(
BoostCounters,
BoostCounters) = default;
};
struct BoostFeatures {
base::flat_map<int, int> nameColorsByLevel;
base::flat_map<int, int> linkStylesByLevel;
base::flat_map<int, int> profileColorsByLevel;
int linkLogoLevel = 0;
int profileIconLevel = 0;
int autotranslateLevel = 0;
int transcribeLevel = 0;
int emojiPackLevel = 0;
int emojiStatusLevel = 0;
int wallpaperLevel = 0;
int wallpapersCount = 0;
int customWallpaperLevel = 0;
int sponsoredLevel = 0;
};
struct BoostBoxData {
QString name;
BoostCounters boost;
BoostFeatures features;
int lifting = 0;
bool allowMulti = false;
bool group = false;
};
void BoostBox(
not_null<GenericBox*> box,
BoostBoxData data,
Fn<void(Fn<void(BoostCounters)>)> boost);
void BoostBoxAlready(not_null<GenericBox*> box, bool group);
void GiftForBoostsBox(
not_null<GenericBox*> box,
QString channel,
int receive,
bool again);
void GiftedNoBoostsBox(not_null<GenericBox*> box, bool group);
void PremiumForBoostsBox(
not_null<GenericBox*> box,
bool group,
Fn<void()> buyPremium);
struct AskBoostChannelColor {
int requiredLevel = 0;
};
struct AskBoostAutotranslate {
int requiredLevel = 0;
};
struct AskBoostWallpaper {
int requiredLevel = 0;
bool group = false;
};
struct AskBoostEmojiStatus {
int requiredLevel = 0;
bool group = false;
};
struct AskBoostEmojiPack {
int requiredLevel = 0;
};
struct AskBoostCustomReactions {
int count = 0;
};
struct AskBoostCpm {
int requiredLevel = 0;
};
struct AskBoostWearCollectible {
int requiredLevel = 0;
};
struct AskBoostReason {
std::variant<
AskBoostChannelColor,
AskBoostAutotranslate,
AskBoostWallpaper,
AskBoostEmojiStatus,
AskBoostEmojiPack,
AskBoostCustomReactions,
AskBoostCpm,
AskBoostWearCollectible> data;
};
struct AskBoostBoxData {
QString link;
BoostCounters boost;
BoostFeatures features;
AskBoostReason reason;
bool group = false;
};
void AskBoostBox(
not_null<GenericBox*> box,
AskBoostBoxData data,
Fn<void()> openStatistics,
Fn<void()> startGiveaway);
[[nodiscard]] object_ptr<RpWidget> MakeLinkLabel(
not_null<QWidget*> parent,
rpl::producer<QString> text,
rpl::producer<QString> link,
std::shared_ptr<Show> show,
object_ptr<RpWidget> right);
void FillBoostLimit(
rpl::producer<> showFinished,
not_null<VerticalLayout*> container,
rpl::producer<BoostCounters> data,
style::margins limitLinePadding);
[[nodiscard]] object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(
not_null<QWidget*> parent,
rpl::producer<QString> text,
Fn<QBrush(QRect)> bg);
} // namespace Ui

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
/*
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 "ui/layers/box_content.h"
#include "ui/widgets/tooltip.h"
#include "base/required.h"
#include "base/timer.h"
#include <QtCore/QDate>
namespace style {
struct CalendarSizes;
struct CalendarColors;
} // namespace style
namespace st {
extern const style::CalendarSizes &defaultCalendarSizes;
extern const style::CalendarColors &defaultCalendarColors;
} // namespace st
namespace Ui {
class IconButton;
class ScrollArea;
class CalendarBox;
struct CalendarBoxArgs {
template <typename T>
using required = base::required<T>;
required<QDate> month;
required<QDate> highlighted;
required<Fn<void(QDate date)>> callback;
FnMut<void(not_null<CalendarBox*>)> finalize;
const style::CalendarSizes &st = st::defaultCalendarSizes;
QDate minDate;
QDate maxDate;
bool allowsSelection = false;
Fn<void(
not_null<Ui::CalendarBox*>,
std::optional<int>)> selectionChanged;
const style::CalendarColors &stColors = st::defaultCalendarColors;
};
class CalendarBox final : public BoxContent, private AbstractTooltipShower {
public:
CalendarBox(QWidget*, CalendarBoxArgs &&args);
~CalendarBox();
void toggleSelectionMode(bool enabled);
[[nodiscard]] QDate selectedFirstDate() const;
[[nodiscard]] QDate selectedLastDate() const;
protected:
void prepare() override;
void keyPressEvent(QKeyEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
void monthChanged(QDate month);
bool isPreviousEnabled() const;
bool isNextEnabled() const;
void goPreviousMonth();
void goNextMonth();
void setExactScroll();
void processScroll();
void createButtons();
void showJumpTooltip(not_null<IconButton*> button);
void jumpAfterDelay(not_null<IconButton*> button);
void jump(QPointer<IconButton> button);
QString tooltipText() const override;
QPoint tooltipPos() const override;
bool tooltipWindowActive() const override;
const style::CalendarSizes &_st;
const style::CalendarColors &_styleColors;
class Context;
std::unique_ptr<Context> _context;
std::unique_ptr<ScrollArea> _scroll;
class Inner;
not_null<Inner*> _inner;
class FloatingDate;
std::unique_ptr<FloatingDate> _floatingDate;
class Title;
object_ptr<Title> _title;
object_ptr<IconButton> _previous;
object_ptr<IconButton> _next;
bool _previousEnabled = false;
bool _nextEnabled = false;
Fn<void(QDate date)> _callback;
FnMut<void(not_null<CalendarBox*>)> _finalize;
bool _watchScroll = false;
QPointer<IconButton> _tooltipButton;
QPointer<IconButton> _jumpButton;
base::Timer _jumpTimer;
bool _selectionMode = false;
Fn<void(
not_null<Ui::CalendarBox*>,
std::optional<int>)> _selectionChanged;
};
} // namespace Ui

View File

@@ -0,0 +1,341 @@
/*
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/choose_date_time.h"
#include "base/unixtime.h"
#include "base/event_filter.h"
#include "ui/boxes/calendar_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/time_input.h"
#include "ui/ui_utility.h"
#include "lang/lang_keys.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include <QtWidgets/QTextEdit>
namespace Ui {
namespace {
constexpr auto kMinimalSchedule = TimeId(10);
QString DayString(const QDate &date) {
return tr::lng_month_day(
tr::now,
lt_month,
Lang::MonthDay(date.month())(tr::now),
lt_day,
QString::number(date.day()));
}
QString TimeString(QTime time) {
return QString("%1:%2"
).arg(time.hour()
).arg(time.minute(), 2, 10, QLatin1Char('0'));
}
} // namespace
ChooseDateTimeStyleArgs::ChooseDateTimeStyleArgs()
: labelStyle(&st::boxLabel)
, dateFieldStyle(&st::scheduleDateField)
, timeFieldStyle(&st::scheduleTimeField)
, separatorStyle(&st::scheduleTimeSeparator)
, atStyle(&st::scheduleAtLabel)
, calendarStyle(&st::defaultCalendarColors) {
}
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
not_null<GenericBox*> box,
ChooseDateTimeBoxArgs &&args) {
struct State {
rpl::variable<QDate> date;
rpl::variable<int> width;
not_null<InputField*> day;
not_null<TimeInput*> time;
not_null<FlatLabel*> at;
};
box->setTitle(std::move(args.title));
box->setWidth(st::boxWideWidth);
const auto content = box->addRow(
object_ptr<FixedHeightWidget>(box, st::scheduleHeight),
style::al_top);
if (args.description) {
box->addRow(object_ptr<FlatLabel>(
box,
std::move(args.description),
*args.style.labelStyle));
}
const auto parsed = base::unixtime::parse(args.time);
const auto state = box->lifetime().make_state<State>(State{
.date = parsed.date(),
.day = CreateChild<InputField>(
content,
*args.style.dateFieldStyle),
.time = CreateChild<TimeInput>(
content,
TimeString(parsed.time()),
*args.style.timeFieldStyle,
*args.style.dateFieldStyle,
*args.style.separatorStyle,
st::scheduleTimeSeparatorPadding),
.at = CreateChild<FlatLabel>(
content,
tr::lng_schedule_at(),
*args.style.atStyle),
});
state->date.value(
) | rpl::on_next([=](QDate date) {
state->day->setText(DayString(date));
state->time->setFocusFast();
}, state->day->lifetime());
const auto min = args.min ? args.min : [] {
return base::unixtime::now() + kMinimalSchedule;
};
const auto max = args.max ? args.max : [] {
return base::unixtime::serialize(
QDateTime::currentDateTime().addYears(1)) - 1;
};
const auto minDate = [=] {
return base::unixtime::parse(min()).date();
};
const auto maxDate = [=] {
return base::unixtime::parse(max()).date();
};
const auto &dayViewport = state->day->rawTextEdit()->viewport();
base::install_event_filter(dayViewport, [=](not_null<QEvent*> event) {
if (event->type() == QEvent::Wheel) {
const auto e = static_cast<QWheelEvent*>(event.get());
const auto direction = Ui::WheelDirection(e);
if (!direction) {
return base::EventFilterResult::Continue;
}
const auto d = state->date.current().addDays(direction);
state->date = std::clamp(d, minDate(), maxDate());
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
state->at->widthValue() | rpl::on_next([=](int width) {
const auto full = st::scheduleDateWidth
+ st::scheduleAtSkip
+ width
+ st::scheduleAtSkip
+ st::scheduleTimeWidth;
content->setNaturalWidth(full);
state->width = full;
}, state->at->lifetime());
content->widthValue(
) | rpl::on_next([=](int width) {
const auto paddings = width
- state->at->width()
- 2 * st::scheduleAtSkip
- st::scheduleDateWidth
- st::scheduleTimeWidth;
const auto left = paddings / 2;
state->day->resizeToWidth(st::scheduleDateWidth);
state->day->moveToLeft(left, st::scheduleDateTop, width);
state->at->moveToLeft(
left + st::scheduleDateWidth + st::scheduleAtSkip,
st::scheduleAtTop,
width);
state->time->resizeToWidth(st::scheduleTimeWidth);
state->time->moveToLeft(
width - left - st::scheduleTimeWidth,
st::scheduleDateTop,
width);
}, content->lifetime());
const auto calendar
= content->lifetime().make_state<base::weak_qptr<CalendarBox>>();
const auto calendarStyle = args.style.calendarStyle;
state->day->focusedChanges(
) | rpl::on_next([=](bool focused) {
if (*calendar || !focused) {
return;
}
*calendar = box->getDelegate()->show(
Box<CalendarBox>(Ui::CalendarBoxArgs{
.month = state->date.current(),
.highlighted = state->date.current(),
.callback = crl::guard(box, [=](QDate chosen) {
state->date = chosen;
(*calendar)->closeBox();
}),
.minDate = minDate(),
.maxDate = maxDate(),
.stColors = *calendarStyle,
}));
(*calendar)->boxClosing(
) | rpl::on_next(crl::guard(state->time, [=] {
state->time->setFocusFast();
}), (*calendar)->lifetime());
}, state->day->lifetime());
const auto collect = [=] {
const auto timeValue = state->time->valueCurrent().split(':');
if (timeValue.size() != 2) {
return 0;
}
const auto time = QTime(timeValue[0].toInt(), timeValue[1].toInt());
if (!time.isValid()) {
return 0;
}
const auto result = base::unixtime::serialize(
QDateTime(state->date.current(), time));
if (result < min() || result > max()) {
return 0;
}
return result;
};
const auto save = [=, done = args.done] {
if (const auto result = collect()) {
done(result);
} else {
state->time->showError();
}
};
state->time->submitRequests(
) | rpl::on_next(save, state->time->lifetime());
auto result = ChooseDateTimeBoxDescriptor();
box->setFocusCallback([=] { state->time->setFocusFast(); });
result.width = state->width.value();
result.submit = box->addButton(std::move(args.submit), save);
result.collect = [=] {
if (const auto result = collect()) {
return result;
}
state->time->showError();
return 0;
};
result.values = rpl::combine(
state->date.value(),
state->time->value()
) | rpl::map(collect);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
return result;
}
object_ptr<Ui::RpWidget> ChooseRepeatPeriod(
not_null<Ui::RpWidget*> parent,
ChooseRepeatPeriodArgs &&args) {
auto result = object_ptr<Ui::RpWidget>(parent.get());
const auto raw = result.data();
struct Entry {
TimeId value = 0;
QString text;
};
auto map = std::vector<Entry>{
{ 0, tr::lng_schedule_repeat_never(tr::now) },
{ 24 * 60 * 60, tr::lng_schedule_repeat_daily(tr::now) },
{ 7 * 24 * 60 * 60, tr::lng_schedule_repeat_weekly(tr::now) },
{ 14 * 24 * 60 * 60, tr::lng_schedule_repeat_biweekly(tr::now) },
{ 30 * 24 * 60 * 60, tr::lng_schedule_repeat_monthly(tr::now) },
{
91 * 24 * 60 * 60,
tr::lng_schedule_repeat_every_month(tr::now, lt_count, 3)
},
{
182 * 24 * 60 * 60,
tr::lng_schedule_repeat_every_month(tr::now, lt_count, 6)
},
{ 365 * 24 * 60 * 60, tr::lng_schedule_repeat_yearly(tr::now) },
};
if (args.test) {
map.insert(begin(map) + 1, Entry{ 300, u"Every 5 minutes"_q });
map.insert(begin(map) + 1, Entry{ 60, u"Every minute"_q });
}
const auto label = Ui::CreateChild<Ui::FlatLabel>(raw, QString());
rpl::combine(
raw->widthValue(),
label->naturalWidthValue()
) | rpl::on_next([=](int outer, int natural) {
label->resizeToWidth(std::min(outer, natural));
}, raw->lifetime());
label->heightValue() | rpl::on_next([=](int height) {
raw->resize(raw->width(), height);
}, label->lifetime());
struct State {
rpl::variable<TimeId> value;
rpl::variable<bool> locked;
std::unique_ptr<Ui::PopupMenu> menu;
};
const auto state = raw->lifetime().make_state<State>(State{
.value = args.value,
.locked = std::move(args.locked),
});
rpl::combine(
state->value.value(),
state->locked.value()
) | rpl::on_next([=](TimeId value, bool locked) {
auto result = tr::lng_schedule_repeat_label(
tr::now,
tr::marked);
const auto text = [&] {
const auto i = ranges::lower_bound(
map,
value,
ranges::less{},
&Entry::value);
return (i != end(map)) ? i->text : map.back().text;
}();
label->setMarkedText(result.append(' ').append(tr::link(
tr::bold(text).append(
Ui::Text::IconEmoji(locked
? &st::scheduleRepeatDropdownLock
: &st::scheduleRepeatDropdownArrow))
)));
return result;
}, label->lifetime());
label->setClickHandlerFilter([=](const auto &...) {
if (args.filter && args.filter()) {
return false;
}
const auto changed = args.changed;
state->menu = std::make_unique<Ui::PopupMenu>(label);
const auto menu = state->menu.get();
menu->setDestroyedCallback(crl::guard(label, [=] {
if (state->menu.get() == menu) {
state->menu.release();
}
}));
for (const auto &entry : map) {
const auto value = entry.value;
menu->addAction(entry.text, [=] {
state->value = value;
changed(value);
});
}
menu->popup(QCursor::pos());
return false;
});
return result;
}
} // namespace Ui

View File

@@ -0,0 +1,66 @@
/*
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 "ui/layers/generic_box.h"
namespace style {
struct FlatLabel;
struct InputField;
struct CalendarColors;
} // namespace style
namespace Ui {
class RoundButton;
struct ChooseDateTimeBoxDescriptor {
QPointer<RoundButton> submit;
Fn<TimeId()> collect;
rpl::producer<TimeId> values;
rpl::producer<int> width;
};
struct ChooseDateTimeStyleArgs {
ChooseDateTimeStyleArgs();
const style::FlatLabel *labelStyle;
const style::InputField *dateFieldStyle;
const style::InputField *timeFieldStyle;
const style::FlatLabel *separatorStyle;
const style::FlatLabel *atStyle;
const style::CalendarColors *calendarStyle;
};
struct ChooseDateTimeBoxArgs {
rpl::producer<QString> title;
rpl::producer<QString> submit;
Fn<void(TimeId)> done;
Fn<TimeId()> min;
TimeId time = 0;
Fn<TimeId()> max;
rpl::producer<QString> description;
ChooseDateTimeStyleArgs style;
};
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
not_null<GenericBox*> box,
ChooseDateTimeBoxArgs &&args);
struct ChooseRepeatPeriodArgs {
TimeId value = 0;
rpl::variable<bool> locked;
Fn<bool()> filter;
Fn<void(TimeId)> changed;
bool test = false;
};
[[nodiscard]] object_ptr<Ui::RpWidget> ChooseRepeatPeriod(
not_null<Ui::RpWidget*> parent,
ChooseRepeatPeriodArgs &&args);
} // namespace Ui

View File

@@ -0,0 +1,967 @@
/*
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/choose_font_box.h"
#include "base/event_filter.h"
#include "lang/lang_keys.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/style/style_core_font.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/multi_select.h"
#include "ui/widgets/scroll_area.h"
#include "ui/cached_round_corners.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_settings.h"
#include "styles/style_layers.h"
#include "styles/style_window.h"
#include <QtGui/QFontDatabase>
namespace Ui {
namespace {
constexpr auto kMinTextWidth = 120;
constexpr auto kMaxTextWidth = 320;
constexpr auto kMaxTextLines = 3;
struct PreviewRequest {
QString family;
QColor msgBg;
QColor msgShadow;
QColor replyBar;
QColor replyNameFg;
QColor textFg;
QImage bubbleTail;
};
class PreviewPainter {
public:
PreviewPainter(const QImage &bg, PreviewRequest request);
QImage takeResult();
private:
void layout();
void paintBubble(Painter &p);
void paintContent(Painter &p);
void paintReply(Painter &p);
void paintMessage(Painter &p);
void validateBubbleCache();
const PreviewRequest _request;
const style::owned_color _msgBg;
const style::owned_color _msgShadow;
style::owned_font _nameFontOwned;
style::font _nameFont;
style::TextStyle _nameStyle;
style::owned_font _textFontOwned;
style::font _textFont;
style::TextStyle _textStyle;
Ui::Text::String _nameText;
Ui::Text::String _replyText;
Ui::Text::String _messageText;
QRect _replyRect;
QRect _name;
QRect _reply;
QRect _message;
QRect _content;
QRect _bubble;
QSize _outer;
Ui::CornersPixmaps _bubbleCorners;
QPixmap _bubbleShadowBottomRight;
QImage _result;
};
class Selector final : public Ui::RpWidget {
public:
Selector(
not_null<QWidget*> parent,
const QString &now,
rpl::producer<QString> filter,
rpl::producer<> submits,
Fn<void(QString)> chosen,
Fn<void(Ui::ScrollToRequest, anim::type)> scrollTo);
void initScroll(anim::type animated);
void setMinHeight(int height);
void selectSkip(Qt::Key direction);
private:
struct Entry {
QString id;
QString key;
QString text;
QStringList keywords;
QImage cache;
std::unique_ptr<Ui::RadioView> check;
std::unique_ptr<Ui::RippleAnimation> ripple;
int paletteVersion = 0;
};
[[nodiscard]] static std::vector<Entry> FullList(const QString &now);
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
[[nodiscard]] bool searching() const;
[[nodiscard]] int shownRowsCount() const;
[[nodiscard]] Entry &shownRowAt(int index);
void applyFilter(const QString &query);
void updateSelected(int selected);
void updatePressed(int pressed);
void updateRow(int index);
void updateRow(not_null<Entry*> row, int hint);
void addRipple(int index, QPoint position);
void validateCache(Entry &row);
void choose(Entry &row);
const style::SettingsButton &_st;
std::vector<Entry> _rows;
std::vector<not_null<Entry*>> _filtered;
QString _chosen;
int _selected = -1;
int _pressed = -1;
std::optional<QPoint> _lastGlobalPoint;
bool _selectedByKeyboard = false;
Fn<void(QString)> _callback;
Fn<void(Ui::ScrollToRequest, anim::type)> _scrollTo;
int _rowsSkip = 0;
int _rowHeight = 0;
int _minHeight = 0;
QString _query;
QStringList _queryWords;
rpl::lifetime _lifetime;
};
Selector::Selector(
not_null<QWidget*> parent,
const QString &now,
rpl::producer<QString> filter,
rpl::producer<> submits,
Fn<void(QString)> chosen,
Fn<void(Ui::ScrollToRequest, anim::type)> scrollTo)
: RpWidget(parent)
, _st(st::settingsButton)
, _rows(FullList(now))
, _chosen(now)
, _callback(std::move(chosen))
, _scrollTo(std::move(scrollTo))
, _rowsSkip(st::settingsInfoPhotoSkip)
, _rowHeight(_st.height + _st.padding.top() + _st.padding.bottom()) {
setMouseTracking(true);
std::move(filter) | rpl::on_next([=](const QString &query) {
applyFilter(query);
}, _lifetime);
std::move(
submits
) | rpl::on_next([=] {
if (_selected >= 0) {
choose(shownRowAt(_selected));
} else if (searching() && !_filtered.empty()) {
choose(*_filtered.front());
}
}, _lifetime);
}
void Selector::applyFilter(const QString &query) {
if (_query == query) {
return;
}
_query = query;
updateSelected(-1);
updatePressed(-1);
_queryWords = TextUtilities::PrepareSearchWords(_query);
const auto skip = [](
const QStringList &haystack,
const QStringList &needles) {
const auto find = [](
const QStringList &haystack,
const QString &needle) {
for (const auto &item : haystack) {
if (item.startsWith(needle)) {
return true;
}
}
return false;
};
for (const auto &needle : needles) {
if (!find(haystack, needle)) {
return true;
}
}
return false;
};
_filtered.clear();
if (!_queryWords.isEmpty()) {
_filtered.reserve(_rows.size());
for (auto &row : _rows) {
if (!skip(row.keywords, _queryWords)) {
_filtered.push_back(&row);
} else {
row.ripple = nullptr;
}
}
}
resizeToWidth(width());
Ui::SendPendingMoveResizeEvents(this);
update();
}
void Selector::updateSelected(int selected) {
if (_selected == selected) {
return;
}
const auto was = (_selected >= 0);
updateRow(_selected);
_selected = selected;
updateRow(_selected);
const auto now = (_selected >= 0);
if (was != now) {
setCursor(now ? style::cur_pointer : style::cur_default);
}
if (_selectedByKeyboard) {
const auto top = (_selected > 0)
? (_rowsSkip + _selected * _rowHeight)
: 0;
const auto bottom = (_selected > 0)
? (top + _rowHeight)
: _selected
? 0
: _rowHeight;
_scrollTo({ top, bottom }, anim::type::instant);
}
}
void Selector::updatePressed(int pressed) {
if (_pressed == pressed) {
return;
} else if (_pressed >= 0) {
if (auto &ripple = shownRowAt(_pressed).ripple) {
ripple->lastStop();
}
}
updateRow(_pressed);
_pressed = pressed;
updateRow(_pressed);
}
void Selector::updateRow(int index) {
if (index >= 0) {
update(0, _rowsSkip + index * _rowHeight, width(), _rowHeight);
}
}
void Selector::updateRow(not_null<Entry*> row, int hint) {
if (hint >= 0 && hint < shownRowsCount() && &shownRowAt(hint) == row) {
updateRow(hint);
} else if (searching()) {
const auto i = ranges::find(_filtered, row);
if (i != end(_filtered)) {
updateRow(int(i - begin(_filtered)));
}
} else {
const auto index = int(row.get() - &_rows[0]);
Assert(index >= 0 && index < _rows.size());
updateRow(index);
}
}
void Selector::validateCache(Entry &row) {
const auto version = style::PaletteVersion();
if (row.cache.isNull()) {
const auto ratio = style::DevicePixelRatio();
row.cache = QImage(
QSize(width(), _rowHeight) * ratio,
QImage::Format_ARGB32_Premultiplied);
row.cache.setDevicePixelRatio(ratio);
} else if (row.paletteVersion == version) {
return;
}
row.paletteVersion = version;
row.cache.fill(Qt::transparent);
auto owned = style::owned_font(row.id, 0, st::boxFontSize);
const auto font = owned.font();
auto p = QPainter(&row.cache);
p.setFont(font);
p.setPen(st::windowFg);
const auto textw = width() - _st.padding.left() - _st.padding.right();
const auto textt = (_rowHeight - font->height) / 2.;
p.drawText(
_st.padding.left(),
textt + font->ascent,
font->elided(row.text, textw));
}
bool Selector::searching() const {
return !_queryWords.isEmpty();
}
int Selector::shownRowsCount() const {
return searching() ? int(_filtered.size()) : int(_rows.size());
}
Selector::Entry &Selector::shownRowAt(int index) {
return searching() ? *_filtered[index] : _rows[index];
}
void Selector::setMinHeight(int height) {
_minHeight = height;
if (_minHeight > 0) {
resizeToWidth(width());
}
}
void Selector::selectSkip(Qt::Key key) {
const auto count = shownRowsCount();
if (key == Qt::Key_Down) {
if (_selected + 1 < count) {
_selectedByKeyboard = true;
updateSelected(_selected + 1);
}
} else if (key == Qt::Key_Up) {
if (_selected >= 0) {
_selectedByKeyboard = true;
updateSelected(_selected - 1);
}
} else if (key == Qt::Key_PageDown) {
const auto change = _minHeight / _rowHeight;
if (_selected + 1 < count) {
_selectedByKeyboard = true;
updateSelected(std::min(_selected + change, count - 1));
}
} else if (key == Qt::Key_PageUp) {
const auto change = _minHeight / _rowHeight;
if (_selected > 0) {
_selectedByKeyboard = true;
updateSelected(std::max(_selected - change, 0));
} else if (!_selected) {
_selectedByKeyboard = true;
updateSelected(-1);
}
}
}
void Selector::initScroll(anim::type animated) {
const auto index = [&] {
if (searching()) {
const auto i = ranges::find(_filtered, _chosen, &Entry::id);
if (i != end(_filtered)) {
return int(i - begin(_filtered));
}
return -1;
}
const auto i = ranges::find(_rows, _chosen, &Entry::id);
Assert(i != end(_rows));
return int(i - begin(_rows));
}();
if (index >= 0) {
const auto top = _rowsSkip + index * _rowHeight;
const auto use = std::max(top - (_minHeight - _rowHeight) / 2, 0);
_scrollTo({ use, use + _minHeight }, animated);
}
}
int Selector::resizeGetHeight(int newWidth) {
const auto added = 2 * _rowsSkip;
return std::max(added + shownRowsCount() * _rowHeight, _minHeight);
}
void Selector::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto rows = shownRowsCount();
if (!rows) {
p.setFont(st::normalFont);
p.setPen(st::windowSubTextFg);
p.drawText(
QRect(0, 0, width(), height() * 2 / 3),
tr::lng_font_not_found(tr::now),
style::al_center);
return;
}
const auto clip = e->rect();
const auto clipped = std::max(clip.y() - _rowsSkip, 0);
const auto from = std::min(clipped / _rowHeight, rows);
const auto till = std::min(
(clip.y() + clip.height() - _rowsSkip + _rowHeight - 1) / _rowHeight,
rows);
const auto active = (_pressed >= 0) ? _pressed : _selected;
for (auto i = from; i != till; ++i) {
auto &row = shownRowAt(i);
const auto y = _rowsSkip + i * _rowHeight;
const auto bg = (i == active) ? st::windowBgOver : st::windowBg;
const auto rect = QRect(0, y, width(), _rowHeight);
p.fillRect(rect, bg);
if (row.ripple) {
row.ripple->paint(p, 0, y, width());
if (row.ripple->empty()) {
row.ripple = nullptr;
}
}
validateCache(row);
p.drawImage(0, y, row.cache);
if (!row.check) {
row.check = std::make_unique<Ui::RadioView>(
st::langsRadio,
(row.id == _chosen),
[=, row = &row] { updateRow(row, i); });
}
row.check->paint(
p,
_st.iconLeft,
y + (_rowHeight - st::langsRadio.diameter) / 2,
width());
}
}
void Selector::leaveEventHook(QEvent *e) {
_lastGlobalPoint = std::nullopt;
if (!_selectedByKeyboard) {
updateSelected(-1);
}
}
void Selector::mouseMoveEvent(QMouseEvent *e) {
if (!_lastGlobalPoint) {
_lastGlobalPoint = e->globalPos();
if (_selectedByKeyboard) {
return;
}
} else if (*_lastGlobalPoint == e->globalPos() && _selectedByKeyboard) {
return;
} else {
_lastGlobalPoint = e->globalPos();
}
_selectedByKeyboard = false;
const auto y = e->y() - _rowsSkip;
const auto index = (y >= 0) ? (y / _rowHeight) : -1;
updateSelected((index >= 0 && index < shownRowsCount()) ? index : -1);
}
void Selector::mousePressEvent(QMouseEvent *e) {
updatePressed(_selected);
if (_pressed >= 0) {
addRipple(_pressed, e->pos());
}
}
void Selector::mouseReleaseEvent(QMouseEvent *e) {
const auto pressed = _pressed;
updatePressed(-1);
if (pressed >= 0 && pressed == _selected) {
choose(shownRowAt(pressed));
}
}
void Selector::choose(Entry &row) {
const auto id = row.id;
if (_chosen != id) {
const auto i = ranges::find(_rows, _chosen, &Entry::id);
Assert(i != end(_rows));
if (i->check) {
i->check->setChecked(false, anim::type::normal);
}
_chosen = id;
if (row.check) {
row.check->setChecked(true, anim::type::normal);
}
}
const auto animated = searching()
? anim::type::instant
: anim::type::normal;
_callback(id);
initScroll(animated);
}
void Selector::addRipple(int index, QPoint position) {
Expects(index >= 0 && index < shownRowsCount());
const auto row = &shownRowAt(index);
if (!row->ripple) {
row->ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RectMask({ width(), _rowHeight }),
[=] { updateRow(row, index); });
}
row->ripple->add(position - QPoint(0, _rowsSkip + index * _rowHeight));
}
std::vector<Selector::Entry> Selector::FullList(const QString &now) {
using namespace TextUtilities;
auto database = QFontDatabase();
auto families = database.families();
auto result = std::vector<Entry>();
result.reserve(families.size() + 3);
const auto add = [&](const QString &text, const QString &id = {}) {
result.push_back({
.id = id,
.text = text,
.keywords = PrepareSearchWords(text),
});
};
add(tr::lng_font_default(tr::now));
add(tr::lng_font_system(tr::now), style::SystemFontTag());
for (const auto &family : families) {
if (database.isScalable(family)) {
result.push_back({ .id = family });
}
}
auto nowIt = ranges::find(result, now, &Entry::id);
if (nowIt == end(result)) {
result.push_back({ .id = now });
nowIt = end(result) - 1;
}
for (auto i = begin(result) + 2; i != end(result); ++i) {
i->key = TextUtilities::RemoveAccents(i->id).toLower();
i->text = i->id;
i->keywords = TextUtilities::PrepareSearchWords(i->id);
}
auto skip = 2;
if (nowIt - begin(result) >= skip) {
std::swap(result[2], *nowIt);
++skip;
}
ranges::sort(
begin(result) + skip, end(result),
std::less<>(),
&Entry::key);
return result;
}
[[nodiscard]] PreviewRequest PrepareRequest(const QString &family) {
return {
.family = family,
.msgBg = st::msgInBg->c,
.msgShadow = st::msgInShadow->c,
.replyBar = st::msgInReplyBarColor->c,
.replyNameFg = st::msgInServiceFg->c,
.textFg = st::historyTextInFg->c,
.bubbleTail = st::historyBubbleTailInLeft.instance(st::msgInBg->c),
};
}
PreviewPainter::PreviewPainter(const QImage &bg, PreviewRequest request)
: _request(request)
, _msgBg(_request.msgBg)
, _msgShadow(_request.msgShadow)
, _nameFontOwned(_request.family, style::FontFlag::Semibold, st::fsize)
, _nameFont(_nameFontOwned.font())
, _nameStyle(st::semiboldTextStyle)
, _textFontOwned(_request.family, 0, st::fsize)
, _textFont(_textFontOwned.font())
, _textStyle(st::defaultTextStyle) {
_nameStyle.font = _nameFont;
_textStyle.font = _textFont;
layout();
const auto ratio = style::DevicePixelRatio();
_result = QImage(
_outer * ratio,
QImage::Format_ARGB32_Premultiplied);
_result.setDevicePixelRatio(ratio);
auto p = Painter(&_result);
p.drawImage(0, 0, bg);
p.translate(_bubble.topLeft());
paintBubble(p);
}
void PreviewPainter::paintBubble(Painter &p) {
validateBubbleCache();
const auto bubble = QRect(QPoint(), _bubble.size());
const auto cornerShadow = _bubbleShadowBottomRight.size()
/ _bubbleShadowBottomRight.devicePixelRatio();
p.drawPixmap(
bubble.width() - cornerShadow.width(),
bubble.height() + st::msgShadow - cornerShadow.height(),
_bubbleShadowBottomRight);
Ui::FillRoundRect(p, bubble, _msgBg.color(), _bubbleCorners);
const auto &bubbleTail = _request.bubbleTail;
const auto tail = bubbleTail.size() / bubbleTail.devicePixelRatio();
p.drawImage(-tail.width(), bubble.height() - tail.height(), bubbleTail);
p.fillRect(
-tail.width(),
bubble.height(),
tail.width() + bubble.width() - cornerShadow.width(),
st::msgShadow,
_request.msgShadow);
p.translate(_content.topLeft());
const auto local = _content.translated(-_content.topLeft());
p.setClipRect(local);
paintContent(p);
}
void PreviewPainter::validateBubbleCache() {
if (!_bubbleCorners.p[0].isNull()) {
return;
}
const auto radius = st::bubbleRadiusLarge;
_bubbleCorners = Ui::PrepareCornerPixmaps(radius, _msgBg.color());
_bubbleCorners.p[2] = {};
_bubbleShadowBottomRight
= Ui::PrepareCornerPixmaps(radius, _msgShadow.color()).p[3];
}
void PreviewPainter::paintContent(Painter &p) {
paintReply(p);
p.translate(_message.topLeft());
const auto local = _message.translated(-_message.topLeft());
p.setClipRect(local);
paintMessage(p);
}
void PreviewPainter::paintReply(Painter &p) {
{
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(_request.replyBar);
const auto outline = st::messageTextStyle.blockquote.outline;
const auto radius = st::messageTextStyle.blockquote.radius;
p.setOpacity(Ui::kDefaultOutline1Opacity);
p.setClipRect(
_replyRect.x(),
_replyRect.y(),
outline,
_replyRect.height());
p.drawRoundedRect(_replyRect, radius, radius);
p.setOpacity(Ui::kDefaultBgOpacity);
p.setClipRect(
_replyRect.x() + outline,
_replyRect.y(),
_replyRect.width() - outline,
_replyRect.height());
p.drawRoundedRect(_replyRect, radius, radius);
}
p.setOpacity(1.);
p.setClipping(false);
p.setPen(_request.replyNameFg);
_nameText.drawLeftElided(
p,
_name.x(),
_name.y(),
_name.width(),
_outer.width());
p.setPen(_request.textFg);
_replyText.drawLeftElided(
p,
_reply.x(),
_reply.y(),
_reply.width(),
_outer.width());
}
void PreviewPainter::paintMessage(Painter &p) {
p.setPen(_request.textFg);
_messageText.drawLeft(p, 0, 0, _message.width(), _message.width());
}
QImage PreviewPainter::takeResult() {
return std::move(_result);
}
void PreviewPainter::layout() {
const auto skip = st::boxRowPadding.left();
const auto minTextWidth = style::ConvertScale(kMinTextWidth);
const auto maxTextWidth = st::boxWidth
- 2 * skip
- st::msgPadding.left()
- st::msgPadding.right();
_nameText = Ui::Text::String(
_nameStyle,
tr::lng_settings_chat_message_reply_from(tr::now));
_replyText = Ui::Text::String(
_textStyle,
tr::lng_background_text2(tr::now));
_messageText = Ui::Text::String(
_textStyle,
tr::lng_background_text1(tr::now),
kDefaultTextOptions,
st::msgMinWidth / 2);
const auto namePosition = QPoint(
st::historyReplyPadding.left(),
st::historyReplyPadding.top());
const auto replyPosition = QPoint(
st::historyReplyPadding.left(),
(st::historyReplyPadding.top() + _nameFont->height));
const auto paddingRight = st::historyReplyPadding.right();
const auto wantedWidth = std::max({
namePosition.x() + _nameText.maxWidth() + paddingRight,
replyPosition.x() + _replyText.maxWidth() + paddingRight,
_messageText.maxWidth()
});
const auto messageWidth = std::clamp(
wantedWidth,
minTextWidth,
maxTextWidth);
const auto messageHeight = _messageText.countHeight(messageWidth);
_replyRect = QRect(
st::msgReplyBarPos.x(),
st::historyReplyTop,
messageWidth,
(st::historyReplyPadding.top()
+ _nameFont->height
+ _textFont->height
+ st::historyReplyPadding.bottom()));
_name = QRect(
_replyRect.topLeft() + namePosition,
QSize(messageWidth - namePosition.x(), _nameFont->height));
_reply = QRect(
_replyRect.topLeft() + replyPosition,
QSize(messageWidth - replyPosition.x(), _textFont->height));
_message = QRect(0, 0, messageWidth, messageHeight);
const auto replySkip = _replyRect.y()
+ _replyRect.height()
+ st::historyReplyBottom;
_message.moveTop(replySkip);
_content = QRect(0, 0, messageWidth, replySkip + messageHeight);
const auto msgPadding = st::msgPadding;
_bubble = _content.marginsAdded(msgPadding);
_content.moveTopLeft(-_bubble.topLeft());
_bubble.moveTopLeft({});
_outer = QSize(st::boxWidth, st::boxWidth / 2);
_bubble.moveTopLeft({ skip, std::max(
(_outer.height() - _bubble.height()) / 2,
st::msgMargin.top()) });
}
[[nodiscard]] QImage GeneratePreview(
const QImage &bg,
PreviewRequest request) {
return PreviewPainter(bg, request).takeResult();
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakePreview(
not_null<QWidget*> parent,
Fn<QImage()> generatePreviewBg,
rpl::producer<QString> family) {
auto result = object_ptr<Ui::RpWidget>(parent.get());
const auto raw = result.data();
struct State {
QImage preview;
QImage bg;
QString family;
};
const auto state = raw->lifetime().make_state<State>();
state->bg = generatePreviewBg();
style::PaletteChanged() | rpl::on_next([=] {
state->bg = generatePreviewBg();
}, raw->lifetime());
rpl::combine(
rpl::single(rpl::empty) | rpl::then(style::PaletteChanged()),
std::move(family)
) | rpl::on_next([=](const auto &, QString family) {
state->family = family;
if (state->preview.isNull()) {
state->preview = GeneratePreview(
state->bg,
PrepareRequest(family));
const auto ratio = state->preview.devicePixelRatio();
raw->resize(state->preview.size() / int(ratio));
} else {
const auto weak = base::make_weak(raw);
const auto request = PrepareRequest(family);
crl::async([=, bg = state->bg] {
crl::on_main([
weak,
state,
preview = GeneratePreview(bg, request)
]() mutable {
if (const auto strong = weak.get()) {
state->preview = std::move(preview);
const auto ratio = state->preview.devicePixelRatio();
strong->resize(
strong->width(),
(state->preview.height() / int(ratio)));
strong->update();
}
});
});
}
}, raw->lifetime());
raw->paintRequest() | rpl::on_next([=](QRect clip) {
QPainter(raw).drawImage(0, 0, state->preview);
}, raw->lifetime());
return result;
}
} // namespace
void ChooseFontBox(
not_null<GenericBox*> box,
Fn<QImage()> generatePreviewBg,
const QString &family,
Fn<void(QString)> save) {
box->setTitle(tr::lng_font_box_title());
struct State {
rpl::variable<QString> family;
rpl::variable<QString> query;
rpl::event_stream<> submits;
};
const auto state = box->lifetime().make_state<State>(State{
.family = family,
});
const auto top = box->setPinnedToTopContent(
object_ptr<Ui::VerticalLayout>(box));
top->add(MakePreview(top, generatePreviewBg, state->family.value()));
const auto filter = top->add(object_ptr<Ui::MultiSelect>(
top,
st::defaultMultiSelect,
tr::lng_participant_filter()));
top->resizeToWidth(st::boxWidth);
filter->setSubmittedCallback([=](Qt::KeyboardModifiers) {
state->submits.fire({});
});
filter->setQueryChangedCallback([=](const QString &query) {
state->query = query;
});
filter->setCancelledCallback([=] {
filter->clearQuery();
});
const auto chosen = [=](const QString &value) {
state->family = value;
filter->clearQuery();
};
const auto scrollTo = [=](
Ui::ScrollToRequest request,
anim::type animated) {
box->scrollTo(request, animated);
};
const auto selector = box->addRow(
object_ptr<Selector>(
box,
state->family.current(),
state->query.value(),
state->submits.events(),
chosen,
scrollTo),
QMargins());
box->setMinHeight(st::boxMaxListHeight);
box->setMaxHeight(st::boxMaxListHeight);
base::install_event_filter(filter, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
const auto key = static_cast<QKeyEvent*>(e.get())->key();
if (key == Qt::Key_Up
|| key == Qt::Key_Down
|| key == Qt::Key_PageUp
|| key == Qt::Key_PageDown) {
selector->selectSkip(Qt::Key(key));
return base::EventFilterResult::Cancel;
}
}
return base::EventFilterResult::Continue;
});
rpl::combine(
box->heightValue(),
top->heightValue()
) | rpl::on_next([=](int box, int top) {
selector->setMinHeight(box - top);
}, selector->lifetime());
const auto apply = [=](QString chosen) {
if (chosen == family) {
box->closeBox();
return;
}
box->getDelegate()->show(Ui::MakeConfirmBox({
.text = tr::lng_settings_need_restart(),
.confirmed = [=] { save(chosen); },
.confirmText = tr::lng_settings_restart_now(),
}));
};
const auto refreshButtons = [=](QString chosen) {
box->clearButtons();
// Doesn't fit in most languages.
//if (!chosen.isEmpty()) {
// box->addLeftButton(tr::lng_background_reset_default(), [=] {
// apply(QString());
// });
//}
box->addButton(tr::lng_settings_save(), [=] {
apply(chosen);
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
};
state->family.value(
) | rpl::on_next(refreshButtons, box->lifetime());
box->setFocusCallback([=] {
filter->setInnerFocus();
});
box->setInitScrollCallback([=] {
SendPendingMoveResizeEvents(box);
selector->initScroll(anim::type::instant);
});
}
} // namespace Ui

View File

@@ -0,0 +1,20 @@
/*
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 Ui {
class GenericBox;
void ChooseFontBox(
not_null<GenericBox*> box,
Fn<QImage()> generatePreviewBg,
const QString &family,
Fn<void(QString)> save);
} // namespace Ui

View File

@@ -0,0 +1,382 @@
/*
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/choose_language_box.h"
#include "lang/lang_keys.h"
#include "spellcheck/spellcheck_types.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/multi_select.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/painter.h"
#include "base/debug_log.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
namespace Ui {
namespace {
const auto kLanguageNamePrefix = "cloud_lng_language_";
const auto kTranslateToPrefix = "cloud_lng_translate_to_";
[[nodiscard]] std::vector<LanguageId> TranslationLanguagesList() {
// If adding some languages here you need to check that it is
// supported on the server. Right now server supports those:
//
// 'af', 'sq', 'am', 'ar', 'hy', 'az', 'eu', 'be', 'bn', 'bs', 'bg',
// 'ca', 'ceb', 'zh-CN', 'zh', 'zh-TW', 'co', 'hr', 'cs', 'da', 'nl',
// 'en', 'eo', 'et', 'fi', 'fr', 'fy', 'gl', 'ka', 'de', 'el', 'gu',
// 'ht', 'ha', 'haw', 'he', 'iw', 'hi', 'hmn', 'hu', 'is', 'ig', 'id',
// 'ga', 'it', 'ja', 'jv', 'kn', 'kk', 'km', 'rw', 'ko', 'ku', 'ky',
// 'lo', 'la', 'lv', 'lt', 'lb', 'mk', 'mg', 'ms', 'ml', 'mt', 'mi',
// 'mr', 'mn', 'my', 'ne', 'no', 'ny', 'or', 'ps', 'fa', 'pl', 'pt',
// 'pa', 'ro', 'ru', 'sm', 'gd', 'sr', 'st', 'sn', 'sd', 'si', 'sk',
// 'sl', 'so', 'es', 'su', 'sw', 'sv', 'tl', 'tg', 'ta', 'tt', 'te',
// 'th', 'tr', 'tk', 'uk', 'ur', 'ug', 'uz', 'vi', 'cy', 'xh', 'yi',
// 'yo', 'zu',
return {
{ QLocale::English },
{ QLocale::Arabic },
{ QLocale::Belarusian },
{ QLocale::Catalan },
{ QLocale::Chinese },
{ QLocale::Dutch },
{ QLocale::French },
{ QLocale::German },
{ QLocale::Indonesian },
{ QLocale::Italian },
{ QLocale::Japanese },
{ QLocale::Korean },
{ QLocale::Polish },
{ QLocale::Portuguese },
{ QLocale::Russian },
{ QLocale::Spanish },
{ QLocale::Ukrainian },
{ QLocale::Afrikaans },
{ QLocale::Albanian },
{ QLocale::Amharic },
{ QLocale::Armenian },
{ QLocale::Azerbaijani },
{ QLocale::Basque },
{ QLocale::Bosnian },
{ QLocale::Bulgarian },
{ QLocale::Burmese },
{ QLocale::Croatian },
{ QLocale::Czech },
{ QLocale::Danish },
{ QLocale::Esperanto },
{ QLocale::Estonian },
{ QLocale::Finnish },
{ QLocale::Gaelic },
{ QLocale::Galician },
{ QLocale::Georgian },
{ QLocale::Greek },
{ QLocale::Gusii },
{ QLocale::Hausa },
{ QLocale::Hebrew },
{ QLocale::Hungarian },
{ QLocale::Icelandic },
{ QLocale::Igbo },
{ QLocale::Irish },
{ QLocale::Kazakh },
{ QLocale::Kinyarwanda },
{ QLocale::Kurdish },
{ QLocale::Lao },
{ QLocale::Latvian },
{ QLocale::Lithuanian },
{ QLocale::Luxembourgish },
{ QLocale::Macedonian },
{ QLocale::Malagasy },
{ QLocale::Malay },
{ QLocale::Maltese },
{ QLocale::Maori },
{ QLocale::Mongolian },
{ QLocale::Nepali },
{ QLocale::Pashto },
{ QLocale::Persian },
{ QLocale::Romanian },
{ QLocale::Serbian },
{ QLocale::Shona },
{ QLocale::Sindhi },
{ QLocale::Sinhala },
{ QLocale::Slovak },
{ QLocale::Slovenian },
{ QLocale::Somali },
{ QLocale::Sundanese },
{ QLocale::Swahili },
{ QLocale::Swedish },
{ QLocale::Tajik },
{ QLocale::Tatar },
{ QLocale::Teso },
{ QLocale::Thai },
{ QLocale::Turkish },
{ QLocale::Turkmen },
{ QLocale::Urdu },
{ QLocale::Uzbek },
{ QLocale::Vietnamese },
{ QLocale::Welsh },
{ QLocale::WesternFrisian },
{ QLocale::Xhosa },
{ QLocale::Yiddish },
};
}
class Row final : public SettingsButton {
public:
Row(not_null<RpWidget*> parent, LanguageId id);
[[nodiscard]] bool filtered(const QString &query) const;
[[nodiscard]] LanguageId id() const;
int resizeGetHeight(int newWidth) override;
protected:
void paintEvent(QPaintEvent *e) override;
private:
const style::PeerListItem &_st;
const LanguageId _id;
const QString _status;
const QString _titleText;
Text::String _title;
};
Row::Row(not_null<RpWidget*> parent, LanguageId id)
: SettingsButton(parent, rpl::never<QString>())
, _st(st::inviteLinkListItem)
, _id(id)
, _status(LanguageName(id))
, _titleText(LanguageNameNative(id))
, _title(_st.nameStyle, _titleText) {
}
LanguageId Row::id() const {
return _id;
}
bool Row::filtered(const QString &query) const {
return _status.startsWith(query, Qt::CaseInsensitive)
|| _titleText.startsWith(query, Qt::CaseInsensitive);
}
int Row::resizeGetHeight(int newWidth) {
return _st.height;
}
void Row::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto paintOver = (isOver() || isDown()) && !isDisabled();
SettingsButton::paintBg(p, e->rect(), paintOver);
SettingsButton::paintRipple(p, 0, 0);
SettingsButton::paintToggle(p, width());
const auto &color = st::windowSubTextFg;
p.setPen(Qt::NoPen);
p.setBrush(color);
const auto left = st::defaultSubsectionTitlePadding.left();
const auto toggleRect = SettingsButton::maybeToggleRect();
const auto right = left
+ (toggleRect.isEmpty() ? 0 : (width() - toggleRect.x()));
const auto availableWidth = std::min(
_title.maxWidth(),
width() - left - right);
p.setPen(_st.nameFg);
_title.drawLeft(
p,
left,
_st.namePosition.y(),
availableWidth,
width() - left - right);
p.setPen(paintOver ? _st.statusFgOver : _st.statusFg);
p.setFont(st::contactsStatusFont);
p.drawTextLeft(
left,
_st.statusPosition.y(),
width() - left - right,
_status);
}
} // namespace
QString LanguageNameTranslated(const QString &twoLetterCode) {
return Lang::GetNonDefaultValue(
kLanguageNamePrefix + twoLetterCode.toUtf8());
}
QString LanguageNameLocal(LanguageId id) {
return QLocale::languageToString(id.language());
}
QString LanguageName(LanguageId id) {
const auto translated = LanguageNameTranslated(id.twoLetterCode());
return translated.isEmpty() ? LanguageNameLocal(id) : translated;
}
QString LanguageNameNative(LanguageId id) {
const auto locale = id.locale();
if (locale.language() == QLocale::English
&& (locale.country() == QLocale::UnitedStates
|| locale.country() == QLocale::AnyCountry)) {
return u"English"_q;
} else if (locale.language() == QLocale::Spanish) {
return QString::fromUtf8("\x45\x73\x70\x61\xc3\xb1\x6f\x6c");
} else {
const auto name = locale.nativeLanguageName();
return name.left(1).toUpper() + name.mid(1);
}
}
rpl::producer<QString> TranslateBarTo(LanguageId id) {
const auto translated = Lang::GetNonDefaultValue(
kTranslateToPrefix + id.twoLetterCode().toUtf8());
return (translated.isEmpty()
? tr::lng_translate_bar_to_other
: tr::lng_translate_bar_to)(
lt_name,
rpl::single(translated.isEmpty()
? LanguageNameLocal(id)
: translated));
}
QString TranslateMenuDont(tr::now_t, LanguageId id) {
const auto translated = Lang::GetNonDefaultValue(
kTranslateToPrefix + id.twoLetterCode().toUtf8());
return (translated.isEmpty()
? tr::lng_translate_menu_dont_other
: tr::lng_translate_menu_dont)(
tr::now,
lt_name,
translated.isEmpty() ? LanguageNameLocal(id) : translated);
}
void ChooseLanguageBox(
not_null<GenericBox*> box,
rpl::producer<QString> title,
Fn<void(std::vector<LanguageId>)> callback,
std::vector<LanguageId> selected,
bool multiselect,
Fn<bool(LanguageId)> toggleCheck) {
box->setMinHeight(st::boxWidth);
box->setMaxHeight(st::boxWidth);
box->setTitle(std::move(title));
const auto multiSelect = box->setPinnedToTopContent(
object_ptr<MultiSelect>(
box,
st::defaultMultiSelect,
tr::lng_participant_filter()));
box->setFocusCallback([=] { multiSelect->setInnerFocus(); });
const auto container = box->verticalLayout();
const auto langs = [&] {
auto list = TranslationLanguagesList();
for (const auto id : list) {
LOG(("cloud_lng_language_%1").arg(id.twoLetterCode()));
}
const auto current = LanguageId{ QLocale(
Lang::LanguageIdOrDefault(Lang::Id())).language() };
if (const auto i = ranges::find(list, current); i != end(list)) {
base::reorder(list, std::distance(begin(list), i), 0);
}
ranges::stable_partition(list, [&](LanguageId id) {
return ranges::contains(selected, id);
});
return list;
}();
struct ToggleOne {
LanguageId id;
bool selected = false;
};
struct State {
rpl::event_stream<ToggleOne> toggles;
};
const auto state = box->lifetime().make_state<State>();
auto rows = std::vector<not_null<SlideWrap<Row>*>>();
rows.reserve(langs.size());
for (const auto &id : langs) {
const auto button = container->add(
object_ptr<SlideWrap<Row>>(
container,
object_ptr<Row>(container, id)));
if (multiselect) {
button->entity()->toggleOn(rpl::single(
ranges::contains(selected, id)
) | rpl::then(state->toggles.events(
) | rpl::filter([=](ToggleOne one) {
return one.id == id;
}) | rpl::map([=](ToggleOne one) {
return one.selected;
})));
button->entity()->toggledChanges(
) | rpl::on_next([=](bool value) {
if (toggleCheck && !toggleCheck(id)) {
state->toggles.fire({ .id = id, .selected = !value });
}
}, button->lifetime());
} else {
button->entity()->setClickedCallback([=] {
callback({ id });
box->closeBox();
});
}
rows.push_back(button);
}
multiSelect->setQueryChangedCallback([=](const QString &query) {
for (const auto &row : rows) {
const auto toggled = row->entity()->filtered(query);
if (toggled != row->toggled()) {
row->toggle(toggled, anim::type::instant);
}
}
});
{
const auto label = CreateChild<FlatLabel>(
box.get(),
tr::lng_languages_none(),
st::membersAbout);
box->verticalLayout()->geometryValue(
) | rpl::on_next([=](const QRect &geometry) {
const auto shown = (geometry.height() <= 0);
label->setVisible(shown);
if (shown) {
label->moveToLeft(
(geometry.width() - label->width()) / 2,
geometry.y() + st::membersAbout.style.font->height * 4);
label->stackUnder(box->verticalLayout());
}
}, label->lifetime());
}
if (multiselect) {
box->addButton(tr::lng_settings_save(), [=] {
auto result = ranges::views::all(
rows
) | ranges::views::filter([](const auto &row) {
return row->entity()->toggled();
}) | ranges::views::transform([](const auto &row) {
return row->entity()->id();
}) | ranges::to_vector;
if (!result.empty()) {
callback(std::move(result));
}
box->closeBox();
});
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace Ui

View File

@@ -0,0 +1,36 @@
/*
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
struct LanguageId;
namespace tr {
struct now_t;
} // namespace tr
namespace Ui {
class GenericBox;
[[nodiscard]] QString LanguageNameTranslated(const QString &twoLetterCode);
[[nodiscard]] QString LanguageNameLocal(LanguageId id);
[[nodiscard]] QString LanguageName(LanguageId id);
[[nodiscard]] QString LanguageNameNative(LanguageId id);
[[nodiscard]] rpl::producer<QString> TranslateBarTo(LanguageId id);
[[nodiscard]] QString TranslateMenuDont(tr::now_t, LanguageId id);
void ChooseLanguageBox(
not_null<GenericBox*> box,
rpl::producer<QString> title,
Fn<void(std::vector<LanguageId>)> callback,
std::vector<LanguageId> selected,
bool multiselect,
Fn<bool(LanguageId)> toggleCheck);
} // namespace Ui

View File

@@ -0,0 +1,145 @@
/*
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/choose_time.h"
#include "base/qt_signal_producer.h"
#include "ui/ui_utility.h"
#include "ui/widgets/fields/time_part_input_with_placeholder.h"
#include "ui/wrap/padding_wrap.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
namespace Ui {
ChooseTimeResult ChooseTimeWidget(
not_null<RpWidget*> parent,
TimeId startSeconds,
bool hiddenDaysInput) {
using TimeField = Ui::TimePartWithPlaceholder;
const auto putNext = [](not_null<TimeField*> field, QChar ch) {
field->setCursorPosition(0);
if (ch.unicode()) {
field->setText(ch + field->getLastText());
field->setCursorPosition(1);
}
field->onTextEdited();
field->setFocus();
};
const auto erasePrevious = [](not_null<TimeField*> field) {
const auto text = field->getLastText();
if (!text.isEmpty()) {
field->setCursorPosition(text.size() - 1);
field->setText(text.mid(0, text.size() - 1));
}
field->setFocus();
};
struct State {
not_null<TimeField*> day;
not_null<TimeField*> hour;
not_null<TimeField*> minute;
rpl::variable<int> valueInSeconds = 0;
};
auto content = object_ptr<Ui::FixedHeightWidget>(
parent,
st::scheduleHeight);
const auto startDays = startSeconds / 86400;
startSeconds -= startDays * 86400;
const auto startHours = startSeconds / 3600;
startSeconds -= startHours * 3600;
const auto startMinutes = startSeconds / 60;
const auto state = content->lifetime().make_state<State>(State{
.day = Ui::CreateChild<TimeField>(
content.data(),
st::muteBoxTimeField,
rpl::never<QString>(),
QString::number(startDays)),
.hour = Ui::CreateChild<TimeField>(
content.data(),
st::muteBoxTimeField,
rpl::never<QString>(),
QString::number(startHours)),
.minute = Ui::CreateChild<TimeField>(
content.data(),
st::muteBoxTimeField,
rpl::never<QString>(),
QString::number(startMinutes)),
});
const auto day = base::make_weak(state->day);
const auto hour = base::make_weak(state->hour);
const auto minute = base::make_weak(state->minute);
if (hiddenDaysInput) {
day->setVisible(false);
}
day->setPhrase(tr::lng_days);
day->setMaxValue(31);
day->setWheelStep(1);
day->putNext() | rpl::on_next([=](QChar ch) {
putNext(hour.get(), ch);
}, content->lifetime());
hour->setPhrase(tr::lng_hours);
hour->setMaxValue(23);
hour->setWheelStep(1);
hour->putNext() | rpl::on_next([=](QChar ch) {
putNext(minute.get(), ch);
}, content->lifetime());
hour->erasePrevious() | rpl::on_next([=] {
erasePrevious(day.get());
}, content->lifetime());
minute->setPhrase(tr::lng_minutes);
minute->setMaxValue(59);
minute->setWheelStep(10);
minute->erasePrevious() | rpl::on_next([=] {
erasePrevious(hour.get());
}, content->lifetime());
content->sizeValue(
) | rpl::on_next([=](const QSize &s) {
const auto inputWidth = s.width() / (hiddenDaysInput ? 2 : 3);
auto rect = QRect(
0,
(s.height() - day->height()) / 2,
inputWidth,
day->height());
for (const auto &input : { day, hour, minute }) {
if (input->isHidden()) {
continue;
}
input->setGeometry(rect - st::muteBoxTimeFieldPadding);
rect.translate(inputWidth, 0);
}
}, content->lifetime());
rpl::merge(
rpl::single(rpl::empty),
base::qt_signal_producer(day.get(), &MaskedInputField::changed),
base::qt_signal_producer(hour.get(), &MaskedInputField::changed),
base::qt_signal_producer(minute.get(), &MaskedInputField::changed)
) | rpl::on_next([=] {
state->valueInSeconds = 0
+ day->getLastText().toUInt() * 3600 * 24
+ hour->getLastText().toUInt() * 3600
+ minute->getLastText().toUInt() * 60;
}, content->lifetime());
return {
object_ptr<Ui::RpWidget>::fromRaw(content.release()),
state->valueInSeconds.value(),
};
}
} // namespace Ui

View File

@@ -0,0 +1,26 @@
/*
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 "base/object_ptr.h"
namespace Ui {
class RpWidget;
struct ChooseTimeResult {
object_ptr<RpWidget> widget;
rpl::producer<TimeId> secondsValue;
};
ChooseTimeResult ChooseTimeWidget(
not_null<RpWidget*> parent,
TimeId startSeconds,
bool hiddenDaysInput = false);
} // namespace Ui

View File

@@ -0,0 +1,277 @@
/*
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/collectible_info_box.h"
#include "base/unixtime.h"
#include "core/file_utilities.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "info/channel_statistics/earn/earn_format.h"
#include "ui/layers/generic_box.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/dynamic_image.h"
#include "ui/painter.h"
#include "settings/settings_common.h"
#include "styles/style_boxes.h"
#include "styles/style_credits.h"
#include "styles/style_layers.h"
#include <QtCore/QRegularExpression>
#include <QtGui/QGuiApplication>
namespace Ui {
namespace {
constexpr auto kTonMultiplier = uint64(1000000000);
[[nodiscard]] QString FormatEntity(CollectibleType type, QString entity) {
switch (type) {
case CollectibleType::Phone: {
static const auto kNonDigits = QRegularExpression(u"[^\\d]"_q);
entity.replace(kNonDigits, QString());
} return Ui::FormatPhone(entity);
case CollectibleType::Username:
return entity.startsWith('@') ? entity : ('@' + entity);
}
Unexpected("CollectibleType in FormatEntity.");
}
[[nodiscard]] QString FormatDate(TimeId date) {
return langDateTime(base::unixtime::parse(date));
}
[[nodiscard]] TextWithEntities FormatPrice(const CollectibleInfo &info) {
auto minor = Info::ChannelEarn::MinorPart(info.cryptoAmount);
if (minor.size() == 1 && minor.at(0) == '.') {
minor += '0';
}
auto price = (info.cryptoCurrency == u"TON"_q)
? Ui::Text::IconEmoji(
&st::tonIconEmoji
).append(
Info::ChannelEarn::MajorPart(info.cryptoAmount)
).append(minor)
: TextWithEntities{ ('{'
+ info.cryptoCurrency + ':' + QString::number(info.cryptoAmount)
+ '}') };
const auto fiat = Ui::FillAmountAndCurrency(info.amount, info.currency);
return Ui::Text::Wrapped(
price,
EntityType::Bold
).append(u" ("_q + fiat + ')');
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeOwnerCell(
not_null<QWidget*> parent,
const CollectibleInfo &info) {
const auto st = &st::defaultMultiSelectItem;
const auto size = st->height;
auto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), size);
const auto raw = result.data();
const auto name = info.ownerName;
const auto userpic = info.ownerUserpic;
const auto nameWidth = st->style.font->width(name);
const auto added = size + st->padding.left() + st->padding.right();
const auto subscribed = std::make_shared<bool>(false);
raw->paintRequest() | rpl::on_next([=] {
const auto use = std::min(nameWidth + added, raw->width());
const auto x = (raw->width() - use) / 2;
if (const auto available = use - added; available > 0) {
auto p = QPainter(raw);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st->textBg);
p.drawRoundedRect(x, 0, use, size, size / 2., size / 2.);
if (!*subscribed) {
*subscribed = true;
userpic->subscribeToUpdates([=] { raw->update(); });
}
p.drawImage(QRect(x, 0, size, size), userpic->image(size));
const auto textx = x + size + st->padding.left();
const auto texty = st->padding.top() + st->style.font->ascent;
const auto text = (use == nameWidth + added)
? name
: st->style.font->elided(name, available);
p.setPen(st->textFg);
p.setFont(st->style.font);
p.drawText(textx, texty, text);
}
}, raw->lifetime());
return result;
}
} // namespace
CollectibleType DetectCollectibleType(const QString &entity) {
return entity.startsWith('+')
? CollectibleType::Phone
: CollectibleType::Username;
}
void CollectibleInfoBox(
not_null<Ui::GenericBox*> box,
CollectibleInfo info) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::collectibleBox);
const auto type = DetectCollectibleType(info.entity);
const auto icon = box->addRow(
object_ptr<Ui::FixedHeightWidget>(box, st::collectibleIconDiameter),
st::collectibleIconPadding);
icon->paintRequest(
) | rpl::on_next([=](QRect clip) {
const auto size = icon->height();
const auto inner = QRect(
(icon->width() - size) / 2,
0,
size,
size);
if (!inner.intersects(clip)) {
return;
}
auto p = QPainter(icon);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(st::defaultActiveButton.textBg);
p.setPen(Qt::NoPen);
p.drawEllipse(inner);
}, icon->lifetime());
const auto lottieSize = st::collectibleIcon;
auto lottie = Settings::CreateLottieIcon(
icon,
{
.name = (type == CollectibleType::Phone
? u"collectible_phone"_q
: u"collectible_username"_q),
.color = &st::defaultActiveButton.textFg,
.sizeOverride = { lottieSize, lottieSize },
},
QMargins());
box->showFinishes(
) | rpl::on_next([animate = std::move(lottie.animate)] {
animate(anim::repeat::once);
}, box->lifetime());
const auto animation = lottie.widget.release();
icon->sizeValue() | rpl::on_next([=](QSize size) {
const auto skip = (type == CollectibleType::Phone)
? style::ConvertScale(2)
: 0;
animation->move(
(size.width() - animation->width()) / 2,
skip + (size.height() - animation->height()) / 2);
}, animation->lifetime());
const auto formatted = FormatEntity(type, info.entity);
const auto header = (type == CollectibleType::Phone)
? tr::lng_collectible_phone_title(
tr::now,
lt_phone,
tr::link(formatted),
tr::marked)
: tr::lng_collectible_username_title(
tr::now,
lt_username,
tr::link(formatted),
tr::marked);
const auto copyCallback = [box, type, formatted, text = info.copyText](
bool copyLink) {
QGuiApplication::clipboard()->setText((text.isEmpty() || !copyLink)
? formatted
: text);
box->uiShow()->showToast((type == CollectibleType::Phone)
? tr::lng_collectible_phone_copied(tr::now)
: copyLink
? tr::lng_username_copied(tr::now)
: tr::lng_username_text_copied(tr::now));
};
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
rpl::single(header),
st::collectibleHeader),
st::collectibleHeaderPadding,
style::al_top
)->setClickHandlerFilter([copyCallback](const auto &...) {
copyCallback(false);
return false;
});
box->addRow(MakeOwnerCell(box, info), st::collectibleOwnerPadding);
const auto text = ((type == CollectibleType::Phone)
? tr::lng_collectible_phone_info
: tr::lng_collectible_username_info)(
tr::now,
lt_date,
TextWithEntities{ FormatDate(info.date) },
lt_price,
FormatPrice(info),
tr::rich);
const auto label = box->addRow(
object_ptr<Ui::FlatLabel>(box, st::collectibleInfo),
st::collectibleInfoPadding,
style::al_top);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setMarkedText(text);
const auto more = box->addRow(
object_ptr<Ui::RoundButton>(
box,
tr::lng_collectible_learn_more(),
st::collectibleMore),
st::collectibleMorePadding);
more->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
more->setClickedCallback([url = info.url] {
File::OpenUrl(url);
});
const auto phrase = (type == CollectibleType::Phone)
? tr::lng_collectible_phone_copy
: tr::lng_collectible_username_copy;
auto owned = object_ptr<Ui::RoundButton>(
box,
phrase(),
st::collectibleCopy);
const auto copy = owned.data();
copy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
copy->setClickedCallback([copyCallback] {
copyCallback(true);
});
box->addButton(std::move(owned));
box->setNoContentMargin(true);
const auto buttonsParent = box->verticalLayout().get();
const auto close = Ui::CreateChild<Ui::IconButton>(
buttonsParent,
st::boxTitleClose);
close->setClickedCallback([=] {
box->closeBox();
});
box->widthValue(
) | rpl::on_next([=](int width) {
close->moveToRight(0, 0);
}, box->lifetime());
box->widthValue() | rpl::on_next([=](int width) {
more->setFullWidth(width
- st::collectibleMorePadding.left()
- st::collectibleMorePadding.right());
copy->setFullWidth(width
- st::collectibleBox.buttonPadding.left()
- st::collectibleBox.buttonPadding.right());
}, box->lifetime());
}
} // namespace Ui

View File

@@ -0,0 +1,37 @@
/*
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 Ui {
class GenericBox;
class DynamicImage;
enum class CollectibleType {
Phone,
Username,
};
[[nodiscard]] CollectibleType DetectCollectibleType(const QString &entity);
struct CollectibleInfo {
QString entity;
QString copyText;
std::shared_ptr<DynamicImage> ownerUserpic;
QString ownerName;
uint64 cryptoAmount = 0;
uint64 amount = 0;
QString cryptoCurrency;
QString currency;
QString url;
TimeId date = 0;
};
void CollectibleInfoBox(not_null<Ui::GenericBox*> box, CollectibleInfo info);
} // namespace Ui

View File

@@ -0,0 +1,156 @@
/*
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/confirm_box.h"
#include "lang/lang_keys.h"
#include "ui/rect.h"
#include "ui/widgets/buttons.h"
#include "styles/style_layers.h"
namespace Ui {
void ConfirmBox(not_null<Ui::GenericBox*> box, ConfirmBoxArgs &&args) {
const auto weak = base::make_weak(box);
const auto lifetime = box->lifetime().make_state<rpl::lifetime>();
const auto withTitle = !v::is_null(args.title);
if (withTitle) {
box->setTitle(v::text::take_marked(std::move(args.title)));
}
if (!v::is_null(args.text)) {
const auto padding = st::boxPadding;
const auto use = args.labelPadding
? *args.labelPadding
: withTitle
? QMargins(padding.left(), 0, padding.right(), padding.bottom())
: padding;
const auto label = box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
v::text::take_marked(std::move(args.text)),
args.labelStyle ? *args.labelStyle : st::boxLabel),
use);
if (args.labelFilter) {
label->setClickHandlerFilter(std::move(args.labelFilter));
}
}
const auto prepareCallback = [&](ConfirmBoxArgs::Callback &callback) {
return [=, confirmed = std::move(callback)]() {
if (const auto callbackPtr = std::get_if<1>(&confirmed)) {
if (auto callback = (*callbackPtr)) {
callback();
}
} else if (const auto callbackPtr = std::get_if<2>(&confirmed)) {
if (auto callback = (*callbackPtr)) {
callback(crl::guard(weak, [=] { weak->closeBox(); }));
}
} else if (weak) {
weak->closeBox();
}
};
};
const auto &defaultButtonStyle = box->getDelegate()->style().button;
const auto confirmTextPlain = v::is_null(args.confirmText)
|| v::is<rpl::producer<QString>>(args.confirmText)
|| v::is<QString>(args.confirmText);
const auto confirmButton = box->addButton(
(confirmTextPlain
? v::text::take_plain(
std::move(args.confirmText),
tr::lng_box_ok())
: rpl::single(QString())),
[=, c = prepareCallback(args.confirmed)]() {
lifetime->destroy();
c();
},
args.confirmStyle ? *args.confirmStyle : defaultButtonStyle);
if (!confirmTextPlain) {
confirmButton->setText(
v::text::take_marked(std::move(args.confirmText)));
}
box->events(
) | rpl::on_next([=](not_null<QEvent*> e) {
if ((e->type() != QEvent::KeyPress) || !confirmButton) {
return;
}
const auto k = static_cast<QKeyEvent*>(e.get());
if (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return) {
confirmButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);
}
}, box->lifetime());
if (!args.inform) {
const auto cancelButton = box->addButton(
v::text::take_plain(std::move(args.cancelText), tr::lng_cancel()),
crl::guard(weak, [=, c = prepareCallback(args.cancelled)]() {
lifetime->destroy();
c();
}),
args.cancelStyle ? *args.cancelStyle : defaultButtonStyle);
box->boxClosing(
) | rpl::on_next(crl::guard(cancelButton, [=] {
cancelButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);
}), *lifetime);
}
if (args.strictCancel) {
lifetime->destroy();
}
}
object_ptr<Ui::GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args) {
return Box(ConfirmBox, std::move(args));
}
void IconWithTitle(
not_null<VerticalLayout*> container,
not_null<RpWidget*> icon,
not_null<RpWidget*> title,
RpWidget *subtitle) {
const auto line = container->add(
object_ptr<RpWidget>(container),
st::boxRowPadding);
icon->setParent(line);
title->setParent(line);
if (subtitle) {
subtitle->setParent(line);
}
icon->heightValue(
) | rpl::on_next([=](int height) {
line->resize(line->width(), height);
}, icon->lifetime());
line->widthValue(
) | rpl::on_next([=](int width) {
icon->moveToLeft(0, 0);
const auto skip = st::defaultBoxCheckbox.textPosition.x();
title->resizeToWidth(width - rect::right(icon) - skip);
if (subtitle) {
subtitle->resizeToWidth(title->width());
title->moveToLeft(rect::right(icon) + skip, icon->y());
subtitle->moveToLeft(
title->x(),
icon->y() + icon->height() - subtitle->height());
} else {
title->moveToLeft(
rect::right(icon) + skip,
((icon->height() - title->height()) / 2));
}
}, title->lifetime());
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
title->setAttribute(Qt::WA_TransparentForMouseEvents);
}
} // namespace Ui

View File

@@ -0,0 +1,69 @@
/*
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 "ui/layers/generic_box.h"
#include "ui/text/text_variant.h"
namespace Ui {
struct ConfirmBoxArgs {
using Callback = std::variant<
v::null_t,
Fn<void()>,
Fn<void(Fn<void()>)>>;
v::text::data text = v::null;
Callback confirmed = v::null;
Callback cancelled = v::null;
v::text::data confirmText;
v::text::data cancelText;
const style::RoundButton *confirmStyle = nullptr;
const style::RoundButton *cancelStyle = nullptr;
const style::FlatLabel *labelStyle = nullptr;
Fn<bool(const ClickHandlerPtr&, Qt::MouseButton)> labelFilter;
std::optional<QMargins> labelPadding;
v::text::data title = v::null;
bool inform = false;
// If strict cancel is set the cancel.callback() is only called
// if the cancel button was pressed.
bool strictCancel = false;
};
void ConfirmBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args);
inline void InformBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args) {
args.inform = true;
ConfirmBox(box, std::move(args));
}
[[nodiscard]] object_ptr<GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args);
[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(
ConfirmBoxArgs &&args) {
args.inform = true;
return MakeConfirmBox(std::move(args));
}
[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(
v::text::data text) {
return MakeInformBox({ .text = std::move(text) });
}
void IconWithTitle(
not_null<VerticalLayout*> container,
not_null<RpWidget*> icon,
not_null<RpWidget*> title,
RpWidget *subtitle = nullptr);
} // namespace Ui

View File

@@ -0,0 +1,192 @@
/*
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/confirm_phone_box.h"
#include "core/file_utilities.h"
#include "ui/boxes/confirm_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/text/format_values.h" // Ui::FormatPhone
#include "ui/text/text_utilities.h"
#include "lang/lang_keys.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
namespace Ui {
ConfirmPhoneBox::ConfirmPhoneBox(
QWidget*,
const QString &phone,
int codeLength,
const QString &openUrl,
std::optional<int> timeout)
: _phone(phone)
, _sentCodeLength(codeLength)
, _call([=] { sendCall(); }, [=] { update(); }) {
if (!openUrl.isEmpty()) {
_fragment.create(
this,
tr::lng_intro_fragment_button(),
st::fragmentBoxButton);
_fragment->setClickedCallback([=] { File::OpenUrl(openUrl); });
_fragment->setTextTransform(
Ui::RoundButton::TextTransform::NoTransform);
}
if (timeout) {
_call.setStatus({ Ui::SentCodeCall::State::Waiting, *timeout });
}
}
void ConfirmPhoneBox::sendCall() {
_resendRequests.fire({});
}
void ConfirmPhoneBox::prepare() {
_about.create(
this,
tr::lng_confirm_phone_about(
lt_phone,
rpl::single(tr::bold(Ui::FormatPhone(_phone))),
tr::marked),
st::confirmPhoneAboutLabel);
_code.create(this, st::confirmPhoneCodeField, tr::lng_code_ph());
_code->setAutoSubmit(_sentCodeLength, [=] { sendCode(); });
_code->setChangedCallback([=] { showError(QString()); });
setTitle(tr::lng_confirm_phone_title());
addButton(tr::lng_confirm_phone_send(), [=] { sendCode(); });
addButton(tr::lng_cancel(), [=] { closeBox(); });
setDimensions(
st::boxWidth,
st::usernamePadding.top()
+ _code->height()
+ st::usernameSkip
+ _about->height()
+ st::usernameSkip
+ (_fragment ? (_fragment->height() + fragmentSkip()) : 0));
_code->submits(
) | rpl::on_next([=] { sendCode(); }, _code->lifetime());
showChildren();
}
void ConfirmPhoneBox::sendCode() {
if (_isWaitingCheck) {
return;
}
const auto code = _code->getDigitsOnly();
if (code.isEmpty()) {
_code->showError();
return;
}
_code->setDisabled(true);
setFocus();
showError(QString());
_checkRequests.fire_copy(code);
_isWaitingCheck = true;
}
void ConfirmPhoneBox::showError(const QString &error) {
_error = error;
if (!_error.isEmpty()) {
_code->showError();
}
update();
}
void ConfirmPhoneBox::paintEvent(QPaintEvent *e) {
BoxContent::paintEvent(e);
auto p = QPainter(this);
p.setFont(st::boxTextFont);
const auto callText = _call.getText();
if (!callText.isEmpty()) {
p.setPen(st::usernameDefaultFg);
const auto callTextRect = QRect(
st::usernamePadding.left(),
_about->y() + _about->height(),
width() - 2 * st::usernamePadding.left(),
st::usernameSkip);
p.drawText(callTextRect, callText, style::al_left);
}
auto errorText = _error;
if (errorText.isEmpty()) {
p.setPen(st::usernameDefaultFg);
errorText = tr::lng_confirm_phone_enter_code(tr::now);
} else {
p.setPen(st::boxTextFgError);
}
const auto errorTextRect = QRect(
st::usernamePadding.left(),
_code->y() + _code->height(),
width() - 2 * st::usernamePadding.left(),
st::usernameSkip);
p.drawText(errorTextRect, errorText, style::al_left);
}
void ConfirmPhoneBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
_code->resize(
width() - st::usernamePadding.left() - st::usernamePadding.right(),
_code->height());
_code->moveToLeft(st::usernamePadding.left(), st::usernamePadding.top());
if (_fragment) {
_fragment->setFullWidth(_code->width());
_fragment->moveToLeft(
(width() - _fragment->width()) / 2,
_code->y() + _code->height() + st::usernameSkip);
}
const auto aboutTop = _fragment
? (_fragment->y() + _fragment->height() + fragmentSkip())
: (_code->y() + _code->height() + st::usernameSkip);
_about->moveToLeft(st::usernamePadding.left(), aboutTop);
}
void ConfirmPhoneBox::setInnerFocus() {
_code->setFocusFast();
}
int ConfirmPhoneBox::fragmentSkip() const {
return st::usernamePadding.bottom();
}
rpl::producer<QString> ConfirmPhoneBox::checkRequests() const {
return _checkRequests.events();
}
rpl::producer<> ConfirmPhoneBox::resendRequests() const {
return _resendRequests.events();
}
void ConfirmPhoneBox::callDone() {
_call.callDone();
}
void ConfirmPhoneBox::showServerError(const QString &text) {
_isWaitingCheck = false;
_code->setDisabled(false);
_code->setFocus();
showError(text);
}
QString ConfirmPhoneBox::getPhone() const {
return _phone;
}
} // namespace Ui

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
*/
#pragma once
#include "ui/layers/box_content.h"
#include "ui/widgets/sent_code_field.h"
namespace Ui {
class FlatLabel;
class RoundButton;
class ConfirmPhoneBox final : public Ui::BoxContent {
public:
ConfirmPhoneBox(
QWidget*,
const QString &phone,
int codeLength,
const QString &openUrl,
std::optional<int> timeout);
[[nodiscard]] rpl::producer<QString> checkRequests() const;
[[nodiscard]] rpl::producer<> resendRequests() const;
void callDone();
void showServerError(const QString &text);
protected:
void prepare() override;
void setInnerFocus() override;
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
void sendCode();
void sendCall();
void checkPhoneAndHash();
[[nodiscard]] int fragmentSkip() const;
QString getPhone() const;
void showError(const QString &error);
// _hash from the link for account.sendConfirmPhoneCode call.
// _phoneHash from auth.sentCode for account.confirmPhone call.
const QString _phone;
// If we receive the code length, we autosubmit _code field when enough symbols is typed.
const int _sentCodeLength = 0;
bool _isWaitingCheck = false;
object_ptr<Ui::FlatLabel> _about = { nullptr };
object_ptr<Ui::SentCodeField> _code = { nullptr };
object_ptr<Ui::RoundButton> _fragment = { nullptr };
QString _error;
Ui::SentCodeCall _call;
rpl::event_stream<QString> _checkRequests;
rpl::event_stream<> _resendRequests;
};
} // namespace Ui

View File

@@ -0,0 +1,487 @@
/*
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/country_select_box.h"
#include "lang/lang_keys.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/multi_select.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "countries/countries_instance.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_intro.h"
#include <QtCore/QRegularExpression>
namespace Ui {
namespace {
QString LastValidISO;
} // namespace
class CountrySelectBox::Inner : public RpWidget {
public:
Inner(QWidget *parent, const QString &iso, Type type);
~Inner();
void updateFilter(QString filter = QString());
void selectSkip(int32 dir);
void selectSkipPage(int32 h, int32 dir);
void chooseCountry();
void refresh();
[[nodiscard]] rpl::producer<Entry> countryChosen() const {
return _countryChosen.events();
}
[[nodiscard]] rpl::producer<ScrollToRequest> mustScrollTo() const {
return _mustScrollTo.events();
}
protected:
void paintEvent(QPaintEvent *e) override;
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
private:
void init();
void updateSelected() {
updateSelected(mapFromGlobal(QCursor::pos()));
}
void updateSelected(QPoint localPos);
void updateSelectedRow();
void updateRow(int index);
void setPressed(int pressed);
const std::vector<Entry> &current() const;
Type _type = Type::Phones;
int _rowHeight = 0;
int _selected = -1;
int _pressed = -1;
QString _filter;
bool _mouseSelection = false;
std::vector<std::unique_ptr<RippleAnimation>> _ripples;
std::vector<Entry> _list;
std::vector<Entry> _filtered;
base::flat_map<QChar, std::vector<int>> _byLetter;
std::vector<std::vector<QString>> _namesList;
rpl::event_stream<Entry> _countryChosen;
rpl::event_stream<ScrollToRequest> _mustScrollTo;
};
CountrySelectBox::CountrySelectBox(QWidget*)
: CountrySelectBox(nullptr, QString(), Type::Phones) {
}
CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type)
: _select(this, st::defaultMultiSelect, tr::lng_country_ph())
, _ownedInner(this, iso, type) {
}
rpl::producer<QString> CountrySelectBox::countryChosen() const {
Expects(_ownedInner != nullptr || _inner != nullptr);
return (_ownedInner
? _ownedInner.data()
: _inner.data())->countryChosen() | rpl::map([](const Entry &e) {
return e.iso2;
});
}
rpl::producer<CountrySelectBox::Entry> CountrySelectBox::entryChosen() const {
Expects(_ownedInner != nullptr || _inner != nullptr);
return (_ownedInner
? _ownedInner.data()
: _inner.data())->countryChosen();
}
void CountrySelectBox::prepare() {
setTitle(tr::lng_country_select());
_select->resizeToWidth(st::boxWidth);
_select->setQueryChangedCallback([=](const QString &query) {
applyFilterUpdate(query);
});
_select->setSubmittedCallback([=](Qt::KeyboardModifiers) {
submit();
});
_inner = setInnerWidget(
std::move(_ownedInner),
st::countriesScroll,
_select->height());
addButton(tr::lng_close(), [=] { closeBox(); });
setDimensions(st::boxWidth, st::boxMaxListHeight);
_inner->mustScrollTo(
) | rpl::on_next([=](ScrollToRequest request) {
scrollToY(request.ymin, request.ymax);
}, lifetime());
}
void CountrySelectBox::submit() {
_inner->chooseCountry();
}
void CountrySelectBox::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Down) {
_inner->selectSkip(1);
} else if (e->key() == Qt::Key_Up) {
_inner->selectSkip(-1);
} else if (e->key() == Qt::Key_PageDown) {
_inner->selectSkipPage(height() - _select->height(), 1);
} else if (e->key() == Qt::Key_PageUp) {
_inner->selectSkipPage(height() - _select->height(), -1);
} else {
BoxContent::keyPressEvent(e);
}
}
void CountrySelectBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
_select->resizeToWidth(width());
_select->moveToLeft(0, 0);
_inner->resizeToWidth(width());
}
void CountrySelectBox::applyFilterUpdate(const QString &query) {
scrollToY(0);
_inner->updateFilter(query);
}
void CountrySelectBox::setInnerFocus() {
_select->setInnerFocus();
}
CountrySelectBox::Inner::Inner(
QWidget *parent,
const QString &iso,
Type type)
: RpWidget(parent)
, _type(type)
, _rowHeight(st::countryRowHeight) {
setAttribute(Qt::WA_OpaquePaintEvent);
const auto &byISO2 = Countries::Instance().byISO2();
if (byISO2.contains(iso)) {
LastValidISO = iso;
}
rpl::single(
) | rpl::then(
Countries::Instance().updated()
) | rpl::on_next([=] {
_mustScrollTo.fire(ScrollToRequest(0, 0));
_list.clear();
_namesList.clear();
init();
const auto filter = _filter;
_filter = u"a"_q;
updateFilter(filter);
}, lifetime());
}
void CountrySelectBox::Inner::init() {
const auto &byISO2 = Countries::Instance().byISO2();
const auto extractEntries = [&](const Countries::Info &info) {
for (const auto &code : info.codes) {
_list.push_back(Entry{
.country = info.name,
.iso2 = info.iso2,
.code = code.callingCode,
.alternativeName = info.alternativeName,
});
}
};
_list.reserve(byISO2.size());
_namesList.reserve(byISO2.size());
const auto l = byISO2.constFind(LastValidISO);
const auto lastValid = (l != byISO2.cend()) ? (*l) : nullptr;
if (lastValid) {
extractEntries(*lastValid);
}
for (const auto &entry : Countries::Instance().list()) {
if (&entry != lastValid) {
extractEntries(entry);
}
}
auto index = 0;
for (const auto &info : _list) {
static const auto RegExp = QRegularExpression("[\\s\\-]");
auto full = info.country
+ ' '
+ (!info.alternativeName.isEmpty()
? info.alternativeName
: QString());
const auto namesList = std::move(full).toLower().split(
RegExp,
Qt::SkipEmptyParts);
auto &names = _namesList.emplace_back();
names.reserve(namesList.size());
for (const auto &name : namesList) {
const auto part = name.trimmed();
if (part.isEmpty()) {
continue;
}
const auto ch = part[0];
auto &byLetter = _byLetter[ch];
if (byLetter.empty() || byLetter.back() != index) {
byLetter.push_back(index);
}
names.push_back(part);
}
++index;
}
}
void CountrySelectBox::Inner::paintEvent(QPaintEvent *e) {
Painter p(this);
QRect r(e->rect());
p.setClipRect(r);
const auto &list = current();
if (list.empty()) {
p.fillRect(r, st::boxBg);
p.setFont(st::noContactsFont);
p.setPen(st::noContactsColor);
p.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_country_none(tr::now), style::al_center);
return;
}
const auto l = int(list.size());
if (r.intersects(QRect(0, 0, width(), st::countriesSkip))) {
p.fillRect(r.intersected(QRect(0, 0, width(), st::countriesSkip)), st::countryRowBg);
}
int32 from = std::clamp((r.y() - st::countriesSkip) / _rowHeight, 0, l);
int32 to = std::clamp((r.y() + r.height() - st::countriesSkip + _rowHeight - 1) / _rowHeight, 0, l);
for (int32 i = from; i < to; ++i) {
auto selected = (i == (_pressed >= 0 ? _pressed : _selected));
auto y = st::countriesSkip + i * _rowHeight;
p.fillRect(0, y, width(), _rowHeight, selected ? st::countryRowBgOver : st::countryRowBg);
if (_ripples.size() > i && _ripples[i]) {
_ripples[i]->paint(p, 0, y, width());
if (_ripples[i]->empty()) {
_ripples[i].reset();
}
}
auto code = QString("+") + list[i].code;
auto codeWidth = st::countryRowCodeFont->width(code);
auto name = list[i].country;
auto nameWidth = st::countryRowNameFont->width(name);
auto availWidth = width() - st::countryRowPadding.left() - st::countryRowPadding.right() - codeWidth - st::boxScroll.width;
if (nameWidth > availWidth) {
name = st::countryRowNameFont->elided(name, availWidth);
nameWidth = st::countryRowNameFont->width(name);
}
p.setFont(st::countryRowNameFont);
p.setPen(st::countryRowNameFg);
p.drawTextLeft(st::countryRowPadding.left(), y + st::countryRowPadding.top(), width(), name);
if (_type == Type::Phones) {
p.setFont(st::countryRowCodeFont);
p.setPen(selected ? st::countryRowCodeFgOver : st::countryRowCodeFg);
p.drawTextLeft(st::countryRowPadding.left() + nameWidth + st::countryRowPadding.right(), y + st::countryRowPadding.top(), width(), code);
}
}
}
void CountrySelectBox::Inner::enterEventHook(QEnterEvent *e) {
setMouseTracking(true);
}
void CountrySelectBox::Inner::leaveEventHook(QEvent *e) {
_mouseSelection = false;
setMouseTracking(false);
if (_selected >= 0) {
updateSelectedRow();
_selected = -1;
}
}
void CountrySelectBox::Inner::mouseMoveEvent(QMouseEvent *e) {
_mouseSelection = true;
updateSelected(e->pos());
}
void CountrySelectBox::Inner::mousePressEvent(QMouseEvent *e) {
_mouseSelection = true;
updateSelected(e->pos());
setPressed(_selected);
const auto &list = current();
if (_pressed >= 0 && _pressed < list.size()) {
if (_ripples.size() <= _pressed) {
_ripples.reserve(_pressed + 1);
while (_ripples.size() <= _pressed) {
_ripples.push_back(nullptr);
}
}
if (!_ripples[_pressed]) {
auto mask = RippleAnimation::RectMask(QSize(width(), _rowHeight));
_ripples[_pressed] = std::make_unique<RippleAnimation>(st::countryRipple, std::move(mask), [this, index = _pressed] {
updateRow(index);
});
_ripples[_pressed]->add(e->pos() - QPoint(0, st::countriesSkip + _pressed * _rowHeight));
}
}
}
void CountrySelectBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
auto pressed = _pressed;
setPressed(-1);
updateSelectedRow();
if (e->button() == Qt::LeftButton) {
if ((pressed >= 0) && pressed == _selected) {
chooseCountry();
}
}
}
void CountrySelectBox::Inner::updateFilter(QString filter) {
const auto words = TextUtilities::PrepareSearchWords(filter);
filter = words.isEmpty() ? QString() : words.join(' ');
if (_filter == filter) {
return;
}
_filter = filter;
const auto findWord = [&](
const std::vector<QString> &names,
const QString &word) {
for (const auto &name : names) {
if (name.startsWith(word)) {
return true;
}
}
return false;
};
const auto hasAllWords = [&](const std::vector<QString> &names) {
for (const auto &word : words) {
if (!findWord(names, word)) {
return false;
}
}
return true;
};
if (!_filter.isEmpty()) {
_filtered.clear();
for (const auto index : _byLetter[_filter[0].toLower()]) {
if (hasAllWords(_namesList[index])) {
_filtered.push_back(_list[index]);
}
}
}
refresh();
_selected = current().empty() ? -1 : 0;
update();
}
void CountrySelectBox::Inner::selectSkip(int32 dir) {
_mouseSelection = false;
const auto &list = current();
int cur = (_selected >= 0) ? _selected : -1;
cur += dir;
if (cur <= 0) {
_selected = list.empty() ? -1 : 0;
} else if (cur >= list.size()) {
_selected = -1;
} else {
_selected = cur;
}
if (_selected >= 0) {
_mustScrollTo.fire(ScrollToRequest(
st::countriesSkip + _selected * _rowHeight,
st::countriesSkip + (_selected + 1) * _rowHeight));
}
update();
}
void CountrySelectBox::Inner::selectSkipPage(int32 h, int32 dir) {
int32 points = h / _rowHeight;
if (!points) return;
selectSkip(points * dir);
}
void CountrySelectBox::Inner::chooseCountry() {
const auto &list = current();
_countryChosen.fire_copy((_selected >= 0 && _selected < list.size())
? list[_selected]
: Entry());
}
void CountrySelectBox::Inner::refresh() {
const auto &list = current();
resize(width(), list.empty() ? st::noContactsHeight : (list.size() * _rowHeight + st::countriesSkip));
}
void CountrySelectBox::Inner::updateSelected(QPoint localPos) {
if (!_mouseSelection) return;
auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(QCursor::pos()));
const auto &list = current();
auto selected = (in && localPos.y() >= st::countriesSkip && localPos.y() < st::countriesSkip + list.size() * _rowHeight) ? ((localPos.y() - st::countriesSkip) / _rowHeight) : -1;
if (_selected != selected) {
updateSelectedRow();
_selected = selected;
updateSelectedRow();
}
}
auto CountrySelectBox::Inner::current() const
-> const std::vector<CountrySelectBox::Entry> & {
return _filter.isEmpty() ? _list : _filtered;
}
void CountrySelectBox::Inner::updateSelectedRow() {
updateRow(_selected);
}
void CountrySelectBox::Inner::updateRow(int index) {
if (index >= 0) {
update(0, st::countriesSkip + index * _rowHeight, width(), _rowHeight);
}
}
void CountrySelectBox::Inner::setPressed(int pressed) {
if (_pressed >= 0 && _pressed < _ripples.size() && _ripples[_pressed]) {
_ripples[_pressed]->lastStop();
}
_pressed = pressed;
}
CountrySelectBox::Inner::~Inner() = default;
} // namespace Ui

View File

@@ -0,0 +1,59 @@
/*
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 "ui/layers/box_content.h"
namespace Countries {
struct Info;
} // namespace Countries
namespace Ui {
class MultiSelect;
class RippleAnimation;
class CountrySelectBox : public BoxContent {
public:
enum class Type {
Phones,
Countries,
};
struct Entry {
QString country;
QString iso2;
QString code;
QString alternativeName;
};
CountrySelectBox(QWidget*);
CountrySelectBox(QWidget*, const QString &iso, Type type);
[[nodiscard]] rpl::producer<QString> countryChosen() const;
[[nodiscard]] rpl::producer<Entry> entryChosen() const;
protected:
void prepare() override;
void setInnerFocus() override;
void keyPressEvent(QKeyEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
void submit();
void applyFilterUpdate(const QString &query);
object_ptr<MultiSelect> _select;
class Inner;
object_ptr<Inner> _ownedInner;
QPointer<Inner> _inner;
};
} // namespace Ui

View File

@@ -0,0 +1,218 @@
/*
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 "ui/ui_utility.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
#include <QtCore/QDate>
namespace Ui {
class GenericBox;
void EditBirthdayBox(
not_null<Ui::GenericBox*> box,
Data::Birthday current,
Fn<void(Data::Birthday)> save,
EditBirthdayType type) {
box->setWidth(st::boxWideWidth);
const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::settingsWorkingHoursPicker));
const auto font = st::boxTextFont;
const auto itemHeight = st::settingsWorkingHoursPickerItemHeight;
const auto picker = [=](
int count,
int startIndex,
Fn<void(QPainter &p, QRectF rect, int index)> paint) {
return Ui::CreateChild<Ui::VerticalDrumPicker>(
content,
Ui::VerticalDrumPicker::DefaultPaintCallback(
font,
itemHeight,
paint),
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<Ui::VerticalDrumPicker*> months;
rpl::variable<Ui::VerticalDrumPicker*> days;
};
const auto state = content->lifetime().make_state<State>();
// years->value() is valid only after size is set.
rpl::combine(
content->sizeValue(),
state->months.value(),
state->days.value()
) | rpl::on_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::on_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::on_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::on_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<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
years->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));
}
return base::EventFilterResult::Continue;
});
auto confirmText = (type == EditBirthdayType::Suggest)
? tr::lng_suggest_birthday_box_confirm()
: tr::lng_settings_save();
box->addButton(std::move(confirmText), [=] {
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 && type == EditBirthdayType::Edit) {
box->addLeftButton(tr::lng_settings_birthday_reset(), [=] {
box->closeBox();
save(Data::Birthday());
});
}
}
} // namespace Ui

View File

@@ -0,0 +1,30 @@
/*
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;
enum class EditBirthdayType {
Edit,
Suggest,
ConfirmSuggestion,
};
void EditBirthdayBox(
not_null<Ui::GenericBox*> box,
Data::Birthday current,
Fn<void(Data::Birthday)> save,
EditBirthdayType type = EditBirthdayType::Edit);
} // namespace Ui

View File

@@ -0,0 +1,89 @@
/*
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_factcheck_box.h"
#include "lang/lang_keys.h"
#include "ui/widgets/fields/input_field.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
void EditFactcheckBox(
not_null<Ui::GenericBox*> box,
TextWithEntities current,
int limit,
Fn<void(TextWithEntities)> save,
Fn<void(not_null<Ui::InputField*>)> initField) {
box->setTitle(tr::lng_factcheck_title());
const auto field = box->addRow(object_ptr<Ui::InputField>(
box,
st::factcheckField,
Ui::InputField::Mode::NoNewlines,
tr::lng_factcheck_placeholder(),
TextWithTags{
current.text,
TextUtilities::ConvertEntitiesToTextTags(current.entities)
}));
AddLengthLimitLabel(field, limit);
initField(field);
enum class State {
Initial,
Changed,
Removed,
};
const auto state = box->lifetime().make_state<rpl::variable<State>>(
State::Initial);
field->changes() | rpl::on_next([=] {
const auto now = field->getLastText().trimmed();
*state = !now.isEmpty()
? State::Changed
: current.empty()
? State::Initial
: State::Removed;
}, field->lifetime());
state->value() | rpl::on_next([=](State state) {
box->clearButtons();
if (state == State::Removed) {
box->addButton(tr::lng_box_remove(), [=] {
box->closeBox();
save({});
}, st::attentionBoxButton);
} else if (state == State::Initial) {
box->addButton(tr::lng_settings_save(), [=] {
if (current.empty()) {
field->showError();
} else {
box->closeBox();
}
});
} else {
box->addButton(tr::lng_settings_save(), [=] {
auto result = field->getTextWithAppliedMarkdown();
if (result.text.size() > limit) {
field->showError();
return;
}
box->closeBox();
save({
result.text,
TextUtilities::ConvertTextTagsToEntities(result.tags)
});
});
}
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}, box->lifetime());
box->setFocusCallback([=] {
field->setFocusFast();
});
}

View File

@@ -0,0 +1,21 @@
/*
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 "ui/layers/generic_box.h"
namespace Ui {
class InputField;
} // namespace Ui
void EditFactcheckBox(
not_null<Ui::GenericBox*> box,
TextWithEntities current,
int limit,
Fn<void(TextWithEntities)> save,
Fn<void(not_null<Ui::InputField*>)> initField);

View File

@@ -0,0 +1,391 @@
/*
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_invite_link.h"
#include "base/unixtime.h"
#include "lang/lang_keys.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/layers/generic_box.h"
#include "ui/vertical_list.h"
#include "ui/text/format_values.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/effects/credits_graphics.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h"
#include "styles/style_settings.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
namespace Ui {
namespace {
constexpr auto kMaxLimit = std::numeric_limits<int>::max();
constexpr auto kHour = 3600;
constexpr auto kDay = 86400;
constexpr auto kMaxLabelLength = 32;
[[nodiscard]] QString FormatExpireDate(TimeId date) {
if (date > 0) {
return langDateTime(base::unixtime::parse(date));
} else if (-date < kDay) {
return tr::lng_hours(tr::now, lt_count, (-date / kHour));
} else if (-date < 7 * kDay) {
return tr::lng_days(tr::now, lt_count, (-date / kDay));
} else {
return tr::lng_weeks(tr::now, lt_count, (-date / (7 * kDay)));
}
}
} // namespace
void EditInviteLinkBox(
not_null<GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
const InviteLinkFields &data,
Fn<void(InviteLinkFields)> done) {
using namespace rpl::mappers;
const auto link = data.link;
const auto isGroup = data.isGroup;
const auto isPublic = data.isPublic;
const auto subscriptionLocked = data.subscriptionCredits > 0;
box->setTitle(link.isEmpty()
? tr::lng_group_invite_new_title()
: tr::lng_group_invite_edit_title());
const auto container = box->verticalLayout();
const auto addTitle = [&](
not_null<VerticalLayout*> container,
rpl::producer<QString> text) {
container->add(
object_ptr<FlatLabel>(
container,
std::move(text),
st::defaultSubsectionTitle),
(st::defaultSubsectionTitlePadding
+ style::margins(0, st::defaultVerticalListSkip, 0, 0)));
};
const auto addDivider = [&](
not_null<VerticalLayout*> container,
rpl::producer<QString> text,
style::margins margins = style::margins()) {
container->add(
object_ptr<DividerLabel>(
container,
object_ptr<FlatLabel>(
container,
std::move(text),
st::boxDividerLabel),
st::defaultBoxDividerLabelPadding),
margins);
};
const auto now = base::unixtime::now();
const auto expire = data.expireDate ? data.expireDate : kMaxLimit;
const auto expireGroup = std::make_shared<RadiobuttonGroup>(expire);
const auto usage = data.usageLimit ? data.usageLimit : kMaxLimit;
const auto usageGroup = std::make_shared<RadiobuttonGroup>(usage);
using Buttons = base::flat_map<int, base::unique_qptr<Radiobutton>>;
struct State {
Buttons expireButtons;
Buttons usageButtons;
int expireValue = 0;
int usageValue = 0;
rpl::variable<bool> requestApproval = false;
rpl::variable<bool> subscription = false;
};
const auto state = box->lifetime().make_state<State>(State{
.expireValue = expire,
.usageValue = usage,
.requestApproval = (data.requestApproval && !isPublic),
.subscription = false,
});
const auto requestApproval = (isPublic || subscriptionLocked)
? nullptr
: container->add(
object_ptr<SettingsButton>(
container,
tr::lng_group_invite_request_approve(),
st::settingsButtonNoIcon),
style::margins{ 0, 0, 0, st::defaultVerticalListSkip });
if (requestApproval) {
requestApproval->toggleOn(state->requestApproval.value(), true);
requestApproval->setClickedCallback([=] {
state->requestApproval.force_assign(!requestApproval->toggled());
state->subscription.force_assign(false);
});
addDivider(container, rpl::conditional(
state->requestApproval.value(),
(isGroup
? tr::lng_group_invite_about_approve()
: tr::lng_group_invite_about_approve_channel()),
(isGroup
? tr::lng_group_invite_about_no_approve()
: tr::lng_group_invite_about_no_approve_channel())));
}
auto credits = (Ui::NumberInput*)(nullptr);
if (!isPublic && fillSubscription) {
Ui::AddSkip(container);
const auto &[subscription, input] = fillSubscription();
credits = input.get();
subscription->toggleOn(state->subscription.value(), true);
if (subscriptionLocked) {
input->setText(QString::number(data.subscriptionCredits));
input->setReadOnly(true);
state->subscription.force_assign(true);
state->requestApproval.force_assign(false);
subscription->setToggleLocked(true);
subscription->finishAnimating();
}
subscription->setClickedCallback([=, show = box->uiShow()] {
if (subscriptionLocked) {
show->showToast(
tr::lng_group_invite_subscription_toast(tr::now));
return;
}
state->subscription.force_assign(!subscription->toggled());
state->requestApproval.force_assign(false);
});
}
const auto labelField = container->add(
object_ptr<Ui::InputField>(
container,
st::defaultInputField,
tr::lng_group_invite_label_header(),
data.label),
style::margins(
st::defaultSubsectionTitlePadding.left(),
st::defaultVerticalListSkip,
st::defaultSubsectionTitlePadding.right(),
st::defaultVerticalListSkip * 2));
labelField->setMaxLength(kMaxLabelLength);
addDivider(container, tr::lng_group_invite_label_about());
const auto &saveLabel = link.isEmpty()
? tr::lng_formatting_link_create
: tr::lng_settings_save;
box->addButton(saveLabel(), [=] {
const auto label = labelField->getLastText();
const auto expireDate = (state->expireValue == kMaxLimit)
? 0
: (state->expireValue < 0)
? (base::unixtime::now() - state->expireValue)
: state->expireValue;
const auto usageLimit = (state->usageValue == kMaxLimit)
? 0
: state->usageValue;
done(InviteLinkFields{
.link = link,
.label = label,
.expireDate = expireDate,
.usageLimit = usageLimit,
.subscriptionCredits = credits
? credits->getLastText().toInt()
: 0,
.requestApproval = state->requestApproval.current(),
.isGroup = isGroup,
.isPublic = isPublic,
});
});
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
if (subscriptionLocked) {
return;
}
addTitle(container, tr::lng_group_invite_expire_title());
const auto expiresWrap = container->add(
object_ptr<VerticalLayout>(container),
style::margins(0, 0, 0, st::defaultVerticalListSkip));
addDivider(
container,
tr::lng_group_invite_expire_about());
const auto usagesSlide = container->add(
object_ptr<SlideWrap<VerticalLayout>>(
container,
object_ptr<VerticalLayout>(container)));
const auto usagesInner = usagesSlide->entity();
addTitle(usagesInner, tr::lng_group_invite_usage_title());
const auto usagesWrap = usagesInner->add(
object_ptr<VerticalLayout>(usagesInner),
style::margins(0, 0, 0, st::defaultVerticalListSkip));
addDivider(usagesInner, tr::lng_group_invite_usage_about());
static const auto addButton = [](
not_null<VerticalLayout*> container,
const std::shared_ptr<RadiobuttonGroup> &group,
int value,
const QString &text) {
return container->add(
object_ptr<Radiobutton>(
container,
group,
value,
text),
st::inviteLinkLimitMargin);
};
const auto regenerate = [=] {
expireGroup->setValue(state->expireValue);
usageGroup->setValue(state->usageValue);
auto expires = std::vector{ kMaxLimit, -kHour, -kDay, -kDay * 7, 0 };
auto usages = std::vector{ kMaxLimit, 1, 10, 100, 0 };
auto defaults = State();
for (auto i = begin(expires); i != end(expires); ++i) {
if (*i == state->expireValue) {
break;
} else if (*i == kMaxLimit) {
continue;
} else if (!*i || (now - *i >= state->expireValue)) {
expires.insert(i, state->expireValue);
break;
}
}
for (auto i = begin(usages); i != end(usages); ++i) {
if (*i == state->usageValue) {
break;
} else if (*i == kMaxLimit) {
continue;
} else if (!*i || *i > state->usageValue) {
usages.insert(i, state->usageValue);
break;
}
}
state->expireButtons.clear();
state->usageButtons.clear();
for (const auto limit : expires) {
const auto text = (limit == kMaxLimit)
? tr::lng_group_invite_expire_never(tr::now)
: !limit
? tr::lng_group_invite_expire_custom(tr::now)
: FormatExpireDate(limit);
state->expireButtons.emplace(
limit,
addButton(expiresWrap, expireGroup, limit, text));
}
for (const auto limit : usages) {
const auto text = (limit == kMaxLimit)
? tr::lng_group_invite_usage_any(tr::now)
: !limit
? tr::lng_group_invite_usage_custom(tr::now)
: Lang::FormatCountDecimal(limit);
state->usageButtons.emplace(
limit,
addButton(usagesWrap, usageGroup, limit, text));
}
};
const auto guard = base::make_weak(box);
expireGroup->setChangedCallback([=](int value) {
if (value) {
state->expireValue = value;
return;
}
expireGroup->setValue(state->expireValue);
box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
const auto save = [=](TimeId result) {
if (!result) {
return;
}
if (guard) {
state->expireValue = result;
regenerate();
}
box->closeBox();
};
const auto now = base::unixtime::now();
const auto time = (state->expireValue == kMaxLimit)
? (now + kDay)
: (state->expireValue > now)
? state->expireValue
: (state->expireValue < 0)
? (now - state->expireValue)
: (now + kDay);
ChooseDateTimeBox(box, {
.title = tr::lng_group_invite_expire_after(),
.submit = tr::lng_settings_save(),
.done = save,
.time = time,
});
}));
});
usageGroup->setChangedCallback([=](int value) {
if (value) {
state->usageValue = value;
return;
}
usageGroup->setValue(state->usageValue);
box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
const auto height = st::boxPadding.bottom()
+ st::defaultInputField.heightMin
+ st::boxPadding.bottom();
box->setTitle(tr::lng_group_invite_expire_after());
const auto wrap = box->addRow(object_ptr<FixedHeightWidget>(
box,
height));
const auto input = CreateChild<NumberInput>(
wrap,
st::defaultInputField,
tr::lng_group_invite_custom_limit(),
(state->usageValue == kMaxLimit
? QString()
: QString::number(state->usageValue)),
200'000);
wrap->widthValue(
) | rpl::on_next([=](int width) {
input->resize(width, input->height());
input->moveToLeft(0, st::boxPadding.bottom());
}, input->lifetime());
box->setFocusCallback([=] {
input->setFocusFast();
});
const auto save = [=] {
const auto value = input->getLastText().toInt();
if (value <= 0) {
input->showError();
return;
}
if (guard) {
state->usageValue = value;
regenerate();
}
box->closeBox();
};
QObject::connect(input, &NumberInput::submitted, save);
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}));
});
regenerate();
usagesSlide->toggleOn(state->requestApproval.value() | rpl::map(!_1));
usagesSlide->finishAnimating();
}
void CreateInviteLinkBox(
not_null<GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
bool isGroup,
bool isPublic,
Fn<void(InviteLinkFields)> done) {
EditInviteLinkBox(
box,
std::move(fillSubscription),
InviteLinkFields{ .isGroup = isGroup, .isPublic = isPublic },
std::move(done));
}
} // namespace Ui

View File

@@ -0,0 +1,45 @@
/*
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 Ui {
class GenericBox;
class NumberInput;
class SettingsButton;
struct InviteLinkFields {
QString link;
QString label;
TimeId expireDate = 0;
int usageLimit = 0;
int subscriptionCredits = 0;
bool requestApproval = false;
bool isGroup = false;
bool isPublic = false;
};
struct InviteLinkSubscriptionToggle final {
not_null<Ui::SettingsButton*> button;
not_null<Ui::NumberInput*> amount;
};
void EditInviteLinkBox(
not_null<Ui::GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
const InviteLinkFields &data,
Fn<void(InviteLinkFields)> done);
void CreateInviteLinkBox(
not_null<Ui::GenericBox*> box,
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
bool isGroup,
bool isPublic,
Fn<void(InviteLinkFields)> done);
} // namespace Ui

View File

@@ -0,0 +1,155 @@
/*
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_invite_link_session.h"
#include "core/ui_integration.h" // TextContext
#include "data/components/credits.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/stickers/data_custom_emoji.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "ui/boxes/edit_invite_link.h" // InviteLinkSubscriptionToggle
#include "ui/effects/credits_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/number_input.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "styles/style_channel_earn.h"
#include "styles/style_chat.h"
#include "styles/style_settings.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
namespace Ui {
InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer) {
struct State final {
rpl::variable<float64> usdRate = 0;
};
const auto state = box->lifetime().make_state<State>();
const auto currency = u"USD"_q;
const auto container = box->verticalLayout();
const auto toggle = container->add(
object_ptr<SettingsButton>(
container,
tr::lng_group_invite_subscription(),
st::settingsButtonNoIconLocked),
style::margins{ 0, 0, 0, st::defaultVerticalListSkip });
const auto maxCredits = peer->session().appConfig().get<int>(
u"stars_subscription_amount_max"_q,
2500);
const auto &st = st::inviteLinkCreditsField;
const auto skip = st.textMargins.top() / 2;
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
box->setShowFinishedCallback([=] {
wrap->toggleOn(toggle->toggledValue());
wrap->finishAnimating();
});
const auto inputContainer = wrap->entity()->add(
CreateSkipWidget(container, st.heightMin - skip));
const auto input = CreateChild<NumberInput>(
inputContainer,
st,
tr::lng_group_invite_subscription_ph(),
QString(),
std::pow(QString::number(maxCredits).size(), 10));
wrap->toggledValue() | rpl::on_next([=](bool shown) {
if (shown) {
input->setFocus();
}
}, input->lifetime());
const auto icon = CreateSingleStarWidget(
inputContainer,
st.style.font->height);
const auto priceOverlay = Ui::CreateChild<Ui::RpWidget>(inputContainer);
priceOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
inputContainer->sizeValue(
) | rpl::on_next([=](const QSize &size) {
input->resize(
size.width() - rect::m::sum::h(st::boxRowPadding),
st.heightMin);
input->moveToLeft(st::boxRowPadding.left(), -skip);
icon->moveToLeft(
st::boxRowPadding.left(),
input->pos().y() + st.textMargins.top());
priceOverlay->resize(size);
}, input->lifetime());
ToggleChildrenVisibility(inputContainer, true);
QObject::connect(input, &Ui::MaskedInputField::changed, [=] {
const auto amount = input->getLastText().toDouble();
if (amount > maxCredits) {
input->setText(QString::number(maxCredits));
}
priceOverlay->update();
});
priceOverlay->paintRequest(
) | rpl::on_next([=, right = st::boxRowPadding.right()] {
if (state->usdRate.current() <= 0) {
return;
}
const auto amount = input->getLastText().toDouble();
if (amount <= 0) {
return;
}
const auto text = tr::lng_group_invite_subscription_price(
tr::now,
lt_cost,
Ui::FillAmountAndCurrency(
amount * state->usdRate.current(),
currency));
auto p = QPainter(priceOverlay);
p.setFont(st.placeholderFont);
p.setPen(st.placeholderFg);
p.setBrush(Qt::NoBrush);
const auto m = QMargins(0, skip, right, 0);
p.drawText(priceOverlay->rect() - m, text, style::al_right);
}, priceOverlay->lifetime());
state->usdRate = peer->session().credits().rateValue(peer);
auto about = object_ptr<Ui::FlatLabel>(
container,
tr::lng_group_invite_subscription_about(
lt_link,
tr::lng_group_invite_subscription_about_link(
lt_emoji,
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
tr::rich
) | rpl::map([](TextWithEntities text) {
return tr::link(
std::move(text),
tr::lng_group_invite_subscription_about_url(tr::now));
}),
tr::rich),
st::boxDividerLabel);
Ui::AddSkip(wrap->entity());
Ui::AddSkip(wrap->entity());
container->add(object_ptr<Ui::DividerLabel>(
container,
std::move(about),
st::defaultBoxDividerLabelPadding));
return { toggle, input };
}
} // namespace Ui

View File

@@ -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
class PeerData;
namespace Ui {
class GenericBox;
class SettingsButton;
struct InviteLinkSubscriptionToggle;
InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle(
not_null<Ui::GenericBox*> box,
not_null<PeerData*> peer);
} // namespace Ui

View File

@@ -0,0 +1,997 @@
/*
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/peer_qr_box.h"
#include "core/application.h"
#include "data/data_cloud_themes.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
#include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "qr/qr_generate.h"
#include "ui/controls/userpic_button.h"
#include "ui/dynamic_image.h"
#include "ui/dynamic_thumbnails.h"
#include "ui/effects/animations.h"
#include "ui/image/image_prepare.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "ui/vertical_list.h"
#include "ui/widgets/box_content_divider.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_giveaway.h"
#include "styles/style_credits.h"
#include "styles/style_intro.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
#include "styles/style_widgets.h"
#include "styles/style_window.h"
#include <QtCore/QMimeData>
#include <QtGui/QGuiApplication>
#include <QtSvg/QSvgRenderer>
namespace Ui {
namespace {
using Colors = std::vector<QColor>;
[[nodiscard]] QMargins NoPhotoBackgroundMargins() {
return QMargins(
st::profileQrBackgroundMargins.left(),
st::profileQrBackgroundMargins.left(),
st::profileQrBackgroundMargins.right(),
st::profileQrBackgroundMargins.bottom());
}
[[nodiscard]] style::font CreateFont(int size, int scale) {
return style::font(
style::ConvertScale(size, scale),
st::profileQrFont->flags(),
st::profileQrFont->family());
}
[[nodiscard]] QImage TelegramQr(
const Qr::Data &data,
int pixel,
int max,
bool hasWhiteBackground) {
Expects(data.size > 0);
constexpr auto kCenterRatio = 0.175;
if (max > 0 && data.size * pixel > max) {
pixel = std::max(max / data.size, 1);
}
auto qr = Qr::Generate(
data,
pixel * style::DevicePixelRatio(),
hasWhiteBackground ? Qt::transparent : Qt::black,
hasWhiteBackground ? Qt::white : Qt::transparent);
{
auto p = QPainter(&qr);
auto hq = PainterHighQualityEnabler(p);
auto svg = QSvgRenderer(u":/gui/plane_white.svg"_q);
const auto size = qr.rect().size();
const auto centerRect = Rect(size)
- Margins((size.width() - (size.width() * kCenterRatio)) / 2);
p.setPen(Qt::NoPen);
p.setBrush(Qt::white);
if (hasWhiteBackground) {
p.setCompositionMode(QPainter::CompositionMode_Clear);
p.drawEllipse(centerRect);
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
svg.render(&p, centerRect);
} else {
p.drawEllipse(centerRect);
p.setCompositionMode(QPainter::CompositionMode_Clear);
svg.render(&p, centerRect);
}
}
return qr;
}
[[nodiscard]] QMargins RoundedMargins(
const QMargins &backgroundMargins,
int photoSize,
int textMaxHeight) {
return (textMaxHeight
? (backgroundMargins + QMargins(0, photoSize / 2, 0, textMaxHeight))
: photoSize
? backgroundMargins + QMargins(0, photoSize / 2, 0, photoSize / 2)
: Margins(backgroundMargins.left()));
}
void Paint(
QPainter &p,
const style::font &font,
const QString &text,
const Colors &backgroundColors,
const QMargins &backgroundMargins,
const QImage &qrImage,
const QRect &qrRect,
int qrMaxSize,
int qrPixel,
int radius,
int textMaxHeight,
int photoSize,
bool hasWhiteBackground) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(hasWhiteBackground ? Qt::white : Qt::transparent);
const auto roundedRect = qrRect
+ RoundedMargins(backgroundMargins, photoSize, textMaxHeight);
p.drawRoundedRect(roundedRect, radius, radius);
if (!qrImage.isNull() && !backgroundColors.empty()) {
constexpr auto kDuration = crl::time(10000);
const auto angle = (crl::now() % kDuration)
/ float64(kDuration) * 360.0;
const auto gradientRotation = int(angle / 45.) * 45;
const auto gradientRotationAdd = angle - gradientRotation;
const auto textAdditionalWidth = backgroundMargins.left();
auto back = Images::GenerateGradient(
qrRect.size() + QSize(textAdditionalWidth, 0),
backgroundColors,
gradientRotation,
1. - (gradientRotationAdd / 45.));
if (hasWhiteBackground) {
p.drawImage(qrRect, back);
}
const auto coloredSize = QSize(back.width(), textMaxHeight);
auto colored = QImage(
coloredSize * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
colored.setDevicePixelRatio(style::DevicePixelRatio());
colored.fill(Qt::transparent);
if (textMaxHeight) {
// '@' + QString(32, 'W');
auto p = QPainter(&colored);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::black);
p.setFont(font);
auto option = QTextOption(style::al_center);
option.setWrapMode(QTextOption::WrapAnywhere);
p.drawText(Rect(coloredSize), text, option);
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
p.drawImage(0, -back.height() + textMaxHeight, back);
}
if (!hasWhiteBackground) {
auto copy = qrImage;
{
auto p = QPainter(&copy);
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
p.drawImage(Rect(copy.size()), back);
}
p.drawImage(qrRect, copy);
} else {
p.drawImage(qrRect, qrImage);
}
if (textMaxHeight) {
p.drawImage(
qrRect.x() - textAdditionalWidth / 2,
rect::bottom(qrRect)
+ ((rect::bottom(roundedRect) - rect::bottom(qrRect))
- textMaxHeight) / 2,
colored);
}
}
}
not_null<Ui::RpWidget*> PrepareQrWidget(
not_null<Ui::VerticalLayout*> container,
not_null<Ui::RpWidget*> topWidget,
rpl::producer<int> fontSizeValue,
rpl::producer<bool> userpicToggled,
rpl::producer<bool> backgroundToggled,
rpl::producer<QString> username,
rpl::producer<QString> links,
rpl::producer<Colors> bgs,
rpl::producer<QString> about) {
const auto divider = container->add(
object_ptr<Ui::BoxContentDivider>(container));
struct State final {
explicit State(Fn<void()> callback) : updating(callback) {
updating.start();
}
Ui::Animations::Basic updating;
style::font font;
QImage qrImage;
Colors backgroundColors;
QString text;
QMargins backgroundMargins;
int textWidth = 0;
int textMaxHeight = 0;
int photoSize = 0;
bool backgroundToggled = false;
};
const auto result = Ui::CreateChild<Ui::RpWidget>(divider);
topWidget->setParent(result);
topWidget->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto state = result->lifetime().make_state<State>(
[=] { result->update(); });
const auto qrMaxSize = st::boxWideWidth
- rect::m::sum::h(st::boxRowPadding)
- rect::m::sum::h(st::profileQrBackgroundMargins);
const auto aboutLabel = Ui::CreateChild<Ui::FlatLabel>(
divider,
st::creditsBoxAboutDivider);
rpl::combine(
std::move(fontSizeValue),
std::move(userpicToggled),
std::move(backgroundToggled),
std::move(username),
std::move(bgs),
std::move(links),
std::move(about),
rpl::single(rpl::empty) | rpl::then(style::PaletteChanged())
) | rpl::on_next([=](
int fontSize,
bool userpicToggled,
bool backgroundToggled,
const QString &username,
const Colors &backgroundColors,
const QString &link,
const QString &about,
const auto &) {
state->font = CreateFont(fontSize, style::Scale());
state->backgroundToggled = backgroundToggled;
state->backgroundMargins = userpicToggled
? st::profileQrBackgroundMargins
: NoPhotoBackgroundMargins();
state->photoSize = userpicToggled
? st::defaultUserpicButton.photoSize
: 0;
state->backgroundColors = backgroundColors;
state->text = username.toUpper();
state->textWidth = state->font->width(state->text);
if (!link.isEmpty()) {
const auto remainder = qrMaxSize % st::introQrPixel;
const auto downTo = remainder
? qrMaxSize - remainder
: qrMaxSize;
state->qrImage = TelegramQr(
Qr::Encode(link.toUtf8(), Qr::Redundancy::Default),
st::introQrPixel,
downTo,
backgroundToggled).scaled(
Size(qrMaxSize * style::DevicePixelRatio()),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
} else {
auto image = QImage(
Size(qrMaxSize * style::DevicePixelRatio()),
QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::white);
image.setDevicePixelRatio(style::DevicePixelRatio());
state->qrImage = std::move(image);
}
const auto resultWidth = qrMaxSize
+ rect::m::sum::h(state->backgroundMargins);
{
aboutLabel->setText(about);
aboutLabel->resizeToWidth(resultWidth);
}
const auto textMaxWidth = state->backgroundMargins.left()
+ (state->qrImage.width() / style::DevicePixelRatio());
const auto lines = int(state->textWidth / textMaxWidth) + 1;
state->textMaxHeight = state->textWidth
? (state->font->height * lines)
: 0;
const auto whiteMargins = RoundedMargins(
state->backgroundMargins,
state->photoSize,
state->textMaxHeight);
result->resize(
qrMaxSize + rect::m::sum::h(whiteMargins),
qrMaxSize
+ rect::m::sum::v(whiteMargins) // White.
+ rect::m::sum::v(st::profileQrBackgroundPadding) // Gray.
+ state->photoSize / 2
+ aboutLabel->height());
divider->resize(container->width(), result->height());
result->moveToLeft((container->width() - result->width()) / 2, 0);
topWidget->setVisible(userpicToggled);
topWidget->moveToLeft(0, std::numeric_limits<int>::min());
topWidget->raise();
aboutLabel->raise();
aboutLabel->moveToLeft(
result->x(),
divider->height()
- aboutLabel->height()
- st::defaultBoxDividerLabelPadding.top());
}, container->lifetime());
result->paintRequest(
) | rpl::on_next([=](QRect clip) {
auto p = QPainter(result);
const auto size = (state->qrImage.size() / style::DevicePixelRatio());
const auto qrRect = Rect(
(result->width() - size.width()) / 2,
state->backgroundMargins.top() + state->photoSize / 2,
size);
p.translate(
0,
st::profileQrBackgroundPadding.top() + state->photoSize / 2);
Paint(
p,
state->font,
state->text,
state->backgroundColors,
state->backgroundMargins,
state->qrImage,
qrRect,
qrMaxSize,
st::introQrPixel,
st::profileQrBackgroundRadius,
state->textMaxHeight,
state->photoSize,
state->backgroundToggled);
if (!state->photoSize) {
return;
}
const auto photoSize = state->photoSize;
const auto top = Ui::GrabWidget(
topWidget,
QRect(),
Qt::transparent).scaled(
Size(photoSize * style::DevicePixelRatio()),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
p.drawPixmap((result->width() - photoSize) / 2, -photoSize / 2, top);
}, result->lifetime());
return result;
}
[[nodiscard]] Fn<void(int)> AddDotsToSlider(
not_null<Ui::ContinuousSlider*> slider,
const style::MediaSlider &st,
int count) {
const auto lineWidth = st::lineWidth;
const auto smallSize = Size(st.seekSize.height() - st.width);
auto smallDots = std::vector<not_null<Ui::RpWidget*>>();
smallDots.reserve(count - 1);
const auto paintSmall = [=](QPainter &p, const QBrush &brush) {
auto hq = PainterHighQualityEnabler(p);
auto pen = st::boxBg->p;
pen.setWidth(st.width);
p.setPen(pen);
p.setBrush(brush);
p.drawEllipse(Rect(smallSize) - Margins(lineWidth));
};
for (auto i = 0; i < count - 1; i++) {
smallDots.push_back(
Ui::CreateChild<Ui::RpWidget>(slider->parentWidget()));
const auto dot = smallDots.back();
dot->resize(smallSize);
dot->setAttribute(Qt::WA_TransparentForMouseEvents);
dot->paintRequest() | rpl::on_next([=] {
auto p = QPainter(dot);
const auto fg = (slider->value() > (i / float64(count - 1)))
? st.activeFg
: st.inactiveFg;
paintSmall(p, fg);
}, dot->lifetime());
}
const auto bigDot = Ui::CreateChild<Ui::RpWidget>(slider->parentWidget());
bigDot->resize(st.seekSize);
bigDot->setAttribute(Qt::WA_TransparentForMouseEvents);
bigDot->paintRequest() | rpl::on_next([=] {
auto p = QPainter(bigDot);
auto hq = PainterHighQualityEnabler(p);
auto pen = st::boxBg->p;
pen.setWidth(st.width);
p.setPen(pen);
p.setBrush(st.activeFg);
p.drawEllipse(Rect(st.seekSize) - Margins(lineWidth));
}, bigDot->lifetime());
return [=](int index) {
const auto g = slider->geometry();
const auto bigTop = g.y() + (g.height() - bigDot->height()) / 2;
const auto smallTop = g.y()
+ (g.height() - smallSize.height()) / 2;
for (auto i = 0; i < count; ++i) {
if (index == i) {
const auto x = ((g.width() - bigDot->width()) * i)
/ float64(count - 1);
bigDot->move(g.x() + std::round(x), bigTop);
} else {
const auto k = (i < index) ? i : i - 1;
const auto w = smallDots[k]->width();
smallDots[k]->move(
g.x() + ((g.width() - w) * i) / (count - 1),
smallTop);
}
}
};
}
} // namespace
void FillPeerQrBox(
not_null<Ui::GenericBox*> box,
PeerData *peer,
std::optional<QString> customLink,
rpl::producer<QString> about) {
const auto window = Core::App().findWindow(box);
const auto controller = window ? window->sessionController() : nullptr;
if (!controller) {
return;
}
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
box->setWidth(st::aboutWidth);
box->setTitle(tr::lng_group_invite_context_qr());
box->verticalLayout()->resizeToWidth(box->width());
struct State {
Ui::RpWidget* saveButton = nullptr;
rpl::variable<bool> saveButtonBusy = false;
rpl::variable<bool> userpicToggled = true;
rpl::variable<bool> backgroundToggled = true;
rpl::variable<Colors> bgs;
Ui::Animations::Simple animation;
rpl::variable<int> chosen = 0;
rpl::variable<int> scaleValue = 0;
rpl::variable<int> fontSizeValue = 28;
};
const auto state = box->lifetime().make_state<State>();
state->userpicToggled = !(customLink || !peer);
const auto usernameValue = [=] {
return (customLink || !peer)
? (rpl::single(QString()) | rpl::type_erased)
: Info::Profile::UsernameValue(peer, true) | rpl::map(
[](const auto &username) { return username.text; });
};
const auto linkValue = [=] {
return customLink
? rpl::single(*customLink)
: peer
? Info::Profile::LinkValue(peer, true) | rpl::map(
[](const auto &link) { return link.text; })
: (rpl::single(QString()) | rpl::type_erased);
};
const auto userpic = Ui::CreateChild<Ui::RpWidget>(box);
const auto userpicSize = st::defaultUserpicButton.photoSize;
userpic->resize(Size(userpicSize));
const auto userpicMedia = Ui::MakeUserpicThumbnail(peer
? peer
: controller->session().user().get());
userpicMedia->subscribeToUpdates(
crl::guard(userpic, [=] { userpic->update(); }));
userpic->paintRequest() | rpl::on_next([=] {
auto p = QPainter(userpic);
p.drawImage(0, 0, userpicMedia->image(userpicSize));
}, userpic->lifetime());
linkValue() | rpl::on_next([=](const QString &link) {
if (link.isEmpty()) {
box->showFinishes() | rpl::on_next([=] {
box->closeBox();
}, box->lifetime());
box->closeBox();
}
}, box->lifetime());
userpic->setVisible(peer != nullptr);
PrepareQrWidget(
box->verticalLayout(),
userpic,
state->fontSizeValue.value(),
state->userpicToggled.value(),
state->backgroundToggled.value(),
usernameValue(),
linkValue(),
state->bgs.value(),
about ? std::move(about) : rpl::single(QString()));
Ui::AddSkip(box->verticalLayout());
Ui::AddSubsectionTitle(
box->verticalLayout(),
tr::lng_userpic_builder_color_subtitle());
const auto themesContainer = box->addRow(
object_ptr<Ui::VerticalLayout>(box));
const auto activewidth = int(
(st::defaultInputField.borderActive + st::lineWidth) * 0.9);
const auto size = st::chatThemePreviewSize.width();
const auto fill = [=](const std::vector<Data::CloudTheme> &cloudThemes) {
while (themesContainer->count()) {
delete themesContainer->widgetAt(0);
}
struct State {
Colors colors;
QImage image;
};
constexpr auto kMaxInRow = 4;
constexpr auto kMaxColors = 4;
auto row = (Ui::RpWidget*)(nullptr);
auto counter = 0;
const auto spacing = (0
+ (box->width() - rect::m::sum::h(st::boxRowPadding))
- (kMaxInRow * size)) / (kMaxInRow + 1);
auto colorsCollection = ranges::views::all(
cloudThemes
) | ranges::views::transform([](const auto &cloudTheme) -> Colors {
const auto it = cloudTheme.settings.find(
Data::CloudThemeType::Light);
if (it == end(cloudTheme.settings)) {
return Colors();
}
const auto colors = it->second.paper
? it->second.paper->backgroundColors()
: Colors();
if (colors.size() != kMaxColors) {
return Colors();
}
return colors;
}) | ranges::views::filter([](const Colors &colors) {
return !colors.empty();
}) | ranges::to_vector;
Expects(!colorsCollection.empty());
colorsCollection[0] = Colors{
st::premiumButtonBg1->c,
st::premiumButtonBg1->c,
st::premiumButtonBg2->c,
st::premiumButtonBg3->c,
};
// colorsCollection.push_back(Colors{
// st::creditsBg1->c,
// st::creditsBg2->c,
// st::creditsBg1->c,
// st::creditsBg2->c,
// });
for (const auto &colors : colorsCollection) {
if (state->bgs.current().empty()) {
state->bgs = colors;
}
if (counter % kMaxInRow == 0) {
Ui::AddSkip(themesContainer);
row = themesContainer->add(
object_ptr<Ui::RpWidget>(themesContainer));
row->resize(size, size);
}
const auto widget = Ui::CreateChild<Ui::AbstractButton>(row);
widget->setClickedCallback([=] {
state->chosen = counter;
widget->update();
state->animation.stop();
state->animation.start([=](float64 value) {
const auto was = state->bgs.current();
const auto &now = colors;
if (was.size() == now.size()
&& was.size() == kMaxColors) {
state->bgs = Colors({
anim::color(was[0], now[0], value),
anim::color(was[1], now[1], value),
anim::color(was[2], now[2], value),
anim::color(was[3], now[3], value),
});
}
},
0.,
1.,
st::shakeDuration);
});
state->chosen.value() | rpl::combine_previous(
) | rpl::filter([=](int i, int k) {
return i == counter || k == counter;
}) | rpl::on_next([=] {
widget->update();
}, widget->lifetime());
widget->resize(size, size);
widget->moveToLeft(
spacing + ((counter % kMaxInRow) * (size + spacing)),
0);
widget->show();
const auto cornersMask = Images::CornersMask(
st::roundRadiusLarge * style::DevicePixelRatio());
const auto back = [&] {
auto gradient = Images::GenerateGradient(
Size(size - activewidth * 5) * style::DevicePixelRatio(),
colors,
0,
0);
gradient.setDevicePixelRatio(style::DevicePixelRatio());
auto result = Images::Round(std::move(gradient), cornersMask);
const auto rect = Rect(
result.size() / style::DevicePixelRatio());
auto colored = result;
colored.fill(Qt::transparent);
{
auto p = QPainter(&colored);
auto hq = PainterHighQualityEnabler(p);
st::profileQrIcon.paintInCenter(p, rect);
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
p.drawImage(0, 0, result);
}
auto temp = result;
temp.fill(Qt::transparent);
{
auto p = QPainter(&temp);
auto hq = PainterHighQualityEnabler(p);
p.setPen(st::premiumButtonFg);
p.setBrush(st::premiumButtonFg);
const auto size = st::profileQrIcon.width() * 1.5;
const auto margins = Margins((rect.width() - size) / 2);
const auto inner = rect - margins;
p.drawRoundedRect(
inner,
st::roundRadiusLarge,
st::roundRadiusLarge);
p.drawImage(0, 0, colored);
}
{
auto p = QPainter(&result);
p.drawImage(0, 0, temp);
}
return result;
}();
widget->paintRequest() | rpl::on_next([=] {
auto p = QPainter(widget);
const auto rect = widget->rect() - Margins(activewidth * 2.5);
p.drawImage(rect.x(), rect.y(), back);
if (state->chosen.current() == counter) {
auto hq = PainterHighQualityEnabler(p);
auto pen = st::activeLineFg->p;
pen.setWidth(st::defaultInputField.borderActive);
p.setPen(pen);
const auto r = st::roundRadiusLarge
+ activewidth * 2.1 * style::DevicePixelRatio();
p.drawRoundedRect(
widget->rect() - Margins(pen.width()),
r,
r);
}
}, widget->lifetime());
counter++;
}
Ui::AddSkip(themesContainer);
Ui::AddSkip(themesContainer);
themesContainer->resizeToWidth(box->width());
};
const auto themes = &controller->session().data().cloudThemes();
const auto &list = themes->chatThemes();
if (!list.empty()) {
fill(list);
} else {
themes->refreshChatThemes();
themes->chatThemesUpdated(
) | rpl::take(1) | rpl::on_next([=] {
fill(themes->chatThemes());
}, box->lifetime());
}
Ui::AddSkip(box->verticalLayout());
Ui::AddDivider(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
Ui::AddSubsectionTitle(
box->verticalLayout(),
tr::lng_qr_box_quality());
Ui::AddSkip(box->verticalLayout());
constexpr auto kMaxQualities = 3;
{
const auto seekSize = st::settingsScale.seekSize.height();
const auto &labelSt = st::defaultFlatLabel;
const auto labels = box->verticalLayout()->add(
Ui::CreateSkipWidget(
box,
labelSt.style.font->height + labelSt.style.font->descent),
st::boxRowPadding);
const auto left = Ui::CreateChild<Ui::FlatLabel>(
labels,
tr::lng_qr_box_quality1(),
labelSt);
const auto middle = Ui::CreateChild<Ui::FlatLabel>(
labels,
tr::lng_qr_box_quality2(),
labelSt);
const auto right = Ui::CreateChild<Ui::FlatLabel>(
labels,
tr::lng_qr_box_quality3(),
labelSt);
labels->sizeValue(
) | rpl::on_next([=](const QSize &size) {
left->moveToLeft(0, 0);
middle->moveToLeft((size.width() - middle->width()) / 2, 0);
right->moveToRight(0, 0);
}, labels->lifetime());
const auto slider = box->verticalLayout()->add(
object_ptr<Ui::MediaSliderWheelless>(
box->verticalLayout(),
st::settingsScale),
st::boxRowPadding);
slider->resize(slider->width(), seekSize);
const auto active = st::windowActiveTextFg->c;
const auto inactive = st::windowSubTextFg->c;
const auto colorize = [=](int index) {
if (index == 0) {
left->setTextColorOverride(active);
middle->setTextColorOverride(inactive);
right->setTextColorOverride(inactive);
} else if (index == 1) {
left->setTextColorOverride(inactive);
middle->setTextColorOverride(active);
right->setTextColorOverride(inactive);
} else if (index == 2) {
left->setTextColorOverride(inactive);
middle->setTextColorOverride(inactive);
right->setTextColorOverride(active);
}
};
const auto updateGeometry = AddDotsToSlider(
slider,
st::settingsScale,
kMaxQualities);
slider->geometryValue(
) | rpl::on_next([=](const QRect &rect) {
updateGeometry(int(slider->value() * (kMaxQualities - 1)));
}, box->lifetime());
box->setShowFinishedCallback([=] {
colorize(0);
updateGeometry(0);
});
slider->setPseudoDiscrete(
kMaxQualities,
[=](int index) { return index; },
0,
[=](int scale) {
state->scaleValue = scale;
colorize(scale);
updateGeometry(scale);
},
[](int) {});
}
{
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
Ui::AddSubsectionTitle(
box->verticalLayout(),
tr::lng_qr_box_font_size());
Ui::AddSkip(box->verticalLayout());
const auto seekSize = st::settingsScale.seekSize.height();
const auto slider = box->verticalLayout()->add(
object_ptr<Ui::MediaSliderWheelless>(
box->verticalLayout(),
st::settingsScale),
st::boxRowPadding);
slider->resize(slider->width(), seekSize);
const auto kSizeAmount = 8;
const auto kMinSize = 20;
const auto kMaxSize = 36;
const auto kStep = (kMaxSize - kMinSize) / (kSizeAmount - 1);
const auto updateGeometry = AddDotsToSlider(
slider,
st::settingsScale,
kSizeAmount);
const auto fontSizeToIndex = [=](int fontSize) {
return (fontSize - kMinSize) / kStep;
};
const auto indexToFontSize = [=](int index) {
return kMinSize + index * kStep;
};
slider->geometryValue(
) | rpl::on_next([=](const QRect &rect) {
updateGeometry(fontSizeToIndex(state->fontSizeValue.current()));
}, box->lifetime());
box->setShowFinishedCallback([=] {
updateGeometry(fontSizeToIndex(state->fontSizeValue.current()));
});
slider->setPseudoDiscrete(
kSizeAmount,
[=](int index) { return indexToFontSize(index); },
state->fontSizeValue.current(),
[=](int fontSize) {
state->fontSizeValue = fontSize;
updateGeometry(fontSizeToIndex(fontSize));
},
[](int) {});
}
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
if (peer) {
const auto userpicToggle = box->verticalLayout()->add(
object_ptr<Ui::SettingsButton>(
box->verticalLayout(),
(peer->isUser()
? tr::lng_mediaview_profile_photo
: (peer->isChannel() && !peer->isMegagroup())
? tr::lng_mediaview_channel_photo
: tr::lng_mediaview_group_photo)(),
st::settingsButtonNoIcon));
userpicToggle->toggleOn(state->userpicToggled.value(), true);
userpicToggle->setClickedCallback([=] {
state->userpicToggled = !state->userpicToggled.current();
});
}
{
const auto backgroundToggle = box->verticalLayout()->add(
object_ptr<Ui::SettingsButton>(
box->verticalLayout(),
tr::lng_qr_box_transparent_background(),
st::settingsButtonNoIcon));
backgroundToggle->toggleOn(
state->backgroundToggled.value() | rpl::map(!rpl::mappers::_1),
true);
backgroundToggle->setClickedCallback([=] {
state->backgroundToggled = !state->backgroundToggled.current();
});
}
Ui::AddSkip(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
auto buttonText = rpl::conditional(
state->saveButtonBusy.value() | rpl::map(rpl::mappers::_1),
rpl::single(QString()),
tr::lng_chat_link_copy());
const auto show = controller->uiShow();
state->saveButton = box->addButton(std::move(buttonText), [=] {
if (state->saveButtonBusy.current()) {
return;
}
state->saveButtonBusy = true;
const auto userpicToggled = state->userpicToggled.current();
const auto backgroundToggled = state->backgroundToggled.current();
const auto scale = style::kScaleDefault
* (kMaxQualities + int(state->scaleValue.current() * 2));
const auto divider = std::max(100, style::Scale())
/ style::kScaleDefault;
const auto profileQrBackgroundRadius = style::ConvertScale(
st::profileQrBackgroundRadius / divider,
scale);
const auto introQrPixel = style::ConvertScale(
st::introQrPixel / divider,
scale);
const auto lineWidth = style::ConvertScale(
st::lineWidth / divider,
scale);
const auto boxWideWidth = style::ConvertScale(
st::boxWideWidth / divider,
scale);
const auto createMargins = [&](const style::margins &margins) {
return QMargins(
style::ConvertScale(margins.left() / divider, scale),
style::ConvertScale(margins.top() / divider, scale),
style::ConvertScale(margins.right() / divider, scale),
style::ConvertScale(margins.bottom() / divider, scale));
};
const auto boxRowPadding = createMargins(st::boxRowPadding);
const auto backgroundMargins = userpicToggled
? createMargins(st::profileQrBackgroundMargins)
: createMargins(NoPhotoBackgroundMargins());
const auto qrMaxSize = boxWideWidth
- rect::m::sum::h(boxRowPadding)
- rect::m::sum::h(backgroundMargins);
const auto photoSize = userpicToggled
? style::ConvertScale(
st::defaultUserpicButton.photoSize / divider,
scale)
: 0;
const auto font = CreateFont(state->fontSizeValue.current(), scale);
const auto username = rpl::variable<QString>(
usernameValue()).current().toUpper();
const auto link = rpl::variable<QString>(linkValue());
const auto textWidth = font->width(username);
const auto top = photoSize
? userpicMedia->image(photoSize)
: QImage();
const auto weak = base::make_weak(box);
crl::async([=] {
const auto qrImage = TelegramQr(
Qr::Encode(
link.current().toUtf8(),
Qr::Redundancy::Default),
introQrPixel,
qrMaxSize,
backgroundToggled);
const auto textMaxWidth = backgroundMargins.left()
+ (qrImage.width() / style::DevicePixelRatio());
const auto lines = int(textWidth / textMaxWidth) + 1;
const auto textMaxHeight = textWidth ? font->height * lines : 0;
const auto whiteMargins = RoundedMargins(
backgroundMargins,
photoSize,
textMaxHeight);
const auto resultSize = QSize(
qrMaxSize + rect::m::sum::h(whiteMargins),
qrMaxSize + rect::m::sum::v(whiteMargins) + photoSize / 2);
const auto qrImageSize = qrImage.size()
/ style::DevicePixelRatio();
const auto qrRect = Rect(
(resultSize.width() - qrImageSize.width()) / 2,
whiteMargins.top() + photoSize / 2,
qrImageSize);
auto image = QImage(
resultSize * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
image.setDevicePixelRatio(style::DevicePixelRatio());
{
auto p = QPainter(&image);
p.translate(0, lineWidth); // Bad.
Paint(
p,
font,
username,
state->bgs.current(),
backgroundMargins,
qrImage,
qrRect,
qrMaxSize,
introQrPixel,
profileQrBackgroundRadius,
textMaxHeight,
photoSize,
backgroundToggled);
if (userpicToggled) {
p.drawImage((resultSize.width() - photoSize) / 2, 0, top);
}
}
crl::on_main(weak, [=] {
state->saveButtonBusy = false;
auto mime = std::make_unique<QMimeData>();
mime->setImageData(std::move(image));
QGuiApplication::clipboard()->setMimeData(mime.release());
show->showToast(tr::lng_group_invite_qr_copied(tr::now));
});
});
});
if (const auto saveButton = state->saveButton) {
using namespace Info::Statistics;
const auto loadingAnimation = InfiniteRadialAnimationWidget(
saveButton,
saveButton->height() / 2);
AddChildToWidgetCenter(saveButton, loadingAnimation);
loadingAnimation->showOn(state->saveButtonBusy.value());
}
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
}
void DefaultShowFillPeerQrBoxCallback(
std::shared_ptr<Ui::Show> show,
PeerData *peer) {
if (peer && !peer->username().isEmpty()) {
show->show(Box(Ui::FillPeerQrBox, peer, std::nullopt, nullptr));
}
}
} // namespace Ui

View File

@@ -0,0 +1,27 @@
/*
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
class PeerData;
namespace Ui {
class GenericBox;
class Show;
void DefaultShowFillPeerQrBoxCallback(
std::shared_ptr<Ui::Show> show,
PeerData *peer);
void FillPeerQrBox(
not_null<Ui::GenericBox*> box,
PeerData *peer,
std::optional<QString> customLink,
rpl::producer<QString> about);
} // namespace Ui

View File

@@ -0,0 +1,149 @@
/*
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/rate_call_box.h"
#include "lang/lang_keys.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "styles/style_layers.h"
#include "styles/style_calls.h"
namespace Ui {
namespace {
constexpr auto kMaxRating = 5;
constexpr auto kRateCallCommentLengthMax = 200;
} // namespace
RateCallBox::RateCallBox(QWidget*, InputSubmitSettings sendWay)
: _sendWay(sendWay) {
}
void RateCallBox::prepare() {
setTitle(tr::lng_call_rate_label());
addButton(tr::lng_cancel(), [=] { closeBox(); });
for (auto i = 0; i < kMaxRating; ++i) {
_stars.emplace_back(this, st::callRatingStar);
_stars.back()->setClickedCallback([this, value = i + 1] {
ratingChanged(value);
});
_stars.back()->show();
}
updateMaxHeight();
}
void RateCallBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
const auto starsWidth = (_stars.size() * st::callRatingStar.width);
auto starLeft = (width() - starsWidth) / 2;
const auto starTop = st::callRatingStarTop;
for (auto &star : _stars) {
star->moveToLeft(starLeft, starTop);
starLeft += star->width();
}
if (_comment) {
_comment->moveToLeft(
st::callRatingPadding.left(),
_stars.back()->bottomNoMargins() + st::callRatingCommentTop);
}
}
void RateCallBox::ratingChanged(int value) {
Expects(value > 0 && value <= kMaxRating);
if (!_rating) {
clearButtons();
addButton(tr::lng_send_button(), [=] { send(); });
addButton(tr::lng_cancel(), [=] { closeBox(); });
}
_rating = value;
for (auto i = 0; i < kMaxRating; ++i) {
_stars[i]->setIconOverride((i < value)
? &st::callRatingStarFilled
: nullptr);
_stars[i]->setRippleColorOverride((i < value)
? &st::lightButtonBgOver
: nullptr);
}
if (value < kMaxRating) {
if (!_comment) {
_comment.create(
this,
st::callRatingComment,
Ui::InputField::Mode::MultiLine,
tr::lng_call_rate_comment());
_comment->show();
_comment->setSubmitSettings(_sendWay);
_comment->setMaxLength(kRateCallCommentLengthMax);
_comment->resize(
width()
- st::callRatingPadding.left()
- st::callRatingPadding.right(),
_comment->height());
updateMaxHeight();
_comment->heightChanges(
) | rpl::on_next([=] {
commentResized();
}, _comment->lifetime());
_comment->submits(
) | rpl::on_next([=] { send(); }, _comment->lifetime());
_comment->cancelled(
) | rpl::on_next([=] {
closeBox();
}, _comment->lifetime());
}
_comment->setFocusFast();
} else if (_comment) {
_comment.destroy();
updateMaxHeight();
}
}
void RateCallBox::setInnerFocus() {
if (_comment) {
_comment->setFocusFast();
} else {
BoxContent::setInnerFocus();
}
}
void RateCallBox::commentResized() {
updateMaxHeight();
update();
}
void RateCallBox::send() {
Expects(_rating > 0 && _rating <= kMaxRating);
_sends.fire({
.rating = _rating,
.comment = _comment ? _comment->getLastText().trimmed() : QString(),
});
}
void RateCallBox::updateMaxHeight() {
auto newHeight = st::callRatingPadding.top()
+ st::callRatingStarTop
+ _stars.back()->heightNoMargins()
+ st::callRatingPadding.bottom();
if (_comment) {
newHeight += st::callRatingCommentTop + _comment->height();
}
setDimensions(st::boxWideWidth, newHeight);
}
rpl::producer<RateCallBox::Result> RateCallBox::sends() const {
return _sends.events();
}
} // namespace Ui

View File

@@ -0,0 +1,51 @@
/*
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 "ui/layers/box_content.h"
namespace Ui {
class InputField;
class IconButton;
enum class InputSubmitSettings;
class RateCallBox : public Ui::BoxContent {
public:
RateCallBox(QWidget*, InputSubmitSettings sendWay);
struct Result {
int rating = 0;
QString comment;
};
[[nodiscard]] rpl::producer<Result> sends() const;
protected:
void prepare() override;
void setInnerFocus() override;
void resizeEvent(QResizeEvent *e) override;
private:
void updateMaxHeight();
void ratingChanged(int value);
void send();
void commentResized();
const InputSubmitSettings _sendWay;
int _rating = 0;
std::vector<object_ptr<Ui::IconButton>> _stars;
object_ptr<Ui::InputField> _comment = { nullptr };
rpl::event_stream<Result> _sends;
};
} // namespace Ui

View File

@@ -0,0 +1,223 @@
/*
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/report_box_graphics.h"
#include "info/profile/info_profile_icon.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "settings/settings_common.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/wrap/vertical_layout.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
#include "styles/style_channel_earn.h"
#include "styles/style_settings.h"
namespace Ui {
namespace {
constexpr auto kReportReasonLengthMax = 512;
using Source = ReportSource;
using Reason = ReportReason;
} // namespace
void ReportReasonBox(
not_null<GenericBox*> box,
const style::ReportBox &st,
ReportSource source,
Fn<void(Reason)> done) {
box->setTitle([&] {
switch (source) {
case Source::Message: return tr::lng_report_message_title();
case Source::Channel: return tr::lng_report_title();
case Source::Group: return tr::lng_report_group_title();
case Source::Bot: return tr::lng_report_bot_title();
case Source::ProfilePhoto:
return tr::lng_report_profile_photo_title();
case Source::ProfileVideo:
return tr::lng_report_profile_video_title();
case Source::GroupPhoto: return tr::lng_report_group_photo_title();
case Source::GroupVideo: return tr::lng_report_group_video_title();
case Source::ChannelPhoto:
return tr::lng_report_channel_photo_title();
case Source::ChannelVideo:
return tr::lng_report_channel_video_title();
case Source::Story:
return tr::lng_report_story();
}
Unexpected("'source' in ReportReasonBox.");
}());
auto margin = style::margins{ 0, st::reportReasonTopSkip, 0, 0 };
const auto add = [&](
Reason reason,
tr::phrase<> text,
const style::icon &icon) {
const auto layout = box->verticalLayout();
const auto button = layout->add(
object_ptr<Ui::SettingsButton>(layout.get(), text(), st.button),
margin);
margin = {};
button->setClickedCallback([=] {
done(reason);
});
const auto height = st.button.padding.top()
+ st.button.height
+ st.button.padding.bottom();
object_ptr<Info::Profile::FloatingIcon>(
button,
icon,
QPoint{
st::infoSharedMediaButtonIconPosition.x(),
(height - icon.height()) / 2,
});
};
add(Reason::Spam, tr::lng_report_reason_spam, st.spam);
if (source == Source::Channel
|| source == Source::Group
|| source == Source::Bot) {
add(Reason::Fake, tr::lng_report_reason_fake, st.fake);
}
add(
Reason::Violence,
tr::lng_report_reason_violence,
st.violence);
add(
Reason::ChildAbuse,
tr::lng_report_reason_child_abuse,
st.children);
add(
Reason::Pornography,
tr::lng_report_reason_pornography,
st.pornography);
add(
Reason::Copyright,
tr::lng_report_reason_copyright,
st.copyright);
if (source == Source::Message || source == Source::Story) {
add(
Reason::IllegalDrugs,
tr::lng_report_reason_illegal_drugs,
st.drugs);
add(
Reason::PersonalDetails,
tr::lng_report_reason_personal_details,
st.personal);
}
add(Reason::Other, tr::lng_report_reason_other, st.other);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void ReportDetailsBox(
not_null<GenericBox*> box,
const style::ReportBox &st,
Fn<void(QString)> done) {
box->setTitle(tr::lng_profile_report());
AddReportDetailsIconButton(box);
Ui::AddSkip(
box->verticalLayout(),
st::settingsBlockedListIconPadding.bottom());
box->addRow(
object_ptr<FlatLabel>(
box, // #TODO reports
tr::lng_report_details_about(),
st.label),
{
st::boxRowPadding.left(),
st::boxPadding.top(),
st::boxRowPadding.right(),
st::boxPadding.bottom(),
});
const auto details = box->addRow(
object_ptr<InputField>(
box,
st.field,
InputField::Mode::MultiLine,
tr::lng_report_details(),
QString()));
details->setMaxLength(kReportReasonLengthMax);
box->setFocusCallback([=] {
details->setFocusFast();
});
const auto submit = [=] {
const auto text = details->getLastText();
done(text);
};
details->submits() | rpl::on_next(submit, details->lifetime());
box->addButton(tr::lng_report_button(), submit);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
not_null<Ui::AbstractButton*> AddReportOptionButton(
not_null<Ui::VerticalLayout*> container,
const QString &text,
const style::ReportBox *stOverride) {
const auto button = container->add(
object_ptr<Ui::SettingsButton>(
container,
rpl::single(QString()),
(stOverride ? stOverride : &st::defaultReportBox)->noIconButton));
const auto textFg = (stOverride
? stOverride->label
: st::sponsoredReportLabel).textFg->c;
const auto label = Ui::CreateChild<Ui::FlatLabel>(
button,
rpl::single(text),
st::sponsoredReportLabel);
label->setTextColorOverride(textFg);
const auto icon = Ui::CreateChild<Ui::RpWidget>(button);
icon->resize(st::settingsPremiumArrow.size());
icon->paintRequest() | rpl::on_next([=, w = icon->width()] {
auto p = Painter(icon);
st::settingsPremiumArrow.paint(p, 0, 0, w, textFg);
}, icon->lifetime());
button->sizeValue() | rpl::on_next([=](const QSize &size) {
const auto left = button->st().padding.left();
const auto right = button->st().padding.right();
icon->moveToRight(right, (size.height() - icon->height()) / 2);
label->resizeToWidth(size.width()
- icon->width()
- left
- st::settingsButtonRightSkip
- right);
label->moveToLeft(left, (size.height() - label->height()) / 2);
button->resize(
button->width(),
rect::m::sum::v(button->st().padding) + label->height());
}, button->lifetime());
label->setAttribute(Qt::WA_TransparentForMouseEvents);
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
return button;
}
void AddReportDetailsIconButton(not_null<GenericBox*> box) {
auto icon = Settings::CreateLottieIcon(
box->verticalLayout(),
{
.name = u"blocked_peers_empty"_q,
.sizeOverride = st::normalBoxLottieSize,
},
{});
box->setShowFinishedCallback([animate = std::move(icon.animate)] {
animate(anim::repeat::once);
});
box->addRow(std::move(icon.widget));
}
} // namespace Ui

View File

@@ -0,0 +1,64 @@
/*
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 style {
struct ReportBox;
} // namespace style
namespace Ui {
class AbstractButton;
class GenericBox;
class VerticalLayout;
enum class ReportSource {
Message,
Channel,
Group,
Bot,
ProfilePhoto,
ProfileVideo,
GroupPhoto,
GroupVideo,
ChannelPhoto,
ChannelVideo,
Story,
};
enum class ReportReason {
Spam,
Fake,
Violence,
ChildAbuse,
Pornography,
Copyright,
IllegalDrugs,
PersonalDetails,
Other,
};
void ReportReasonBox(
not_null<GenericBox*> box,
const style::ReportBox &st,
ReportSource source,
Fn<void(ReportReason)> done);
void ReportDetailsBox(
not_null<GenericBox*> box,
const style::ReportBox &st,
Fn<void(QString)> done);
[[nodiscard]] not_null<Ui::AbstractButton*> AddReportOptionButton(
not_null<Ui::VerticalLayout*> container,
const QString &text,
const style::ReportBox *stOverride);
void AddReportDetailsIconButton(not_null<GenericBox*> box);
} // namespace Ui

View File

@@ -0,0 +1,219 @@
/*
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/show_or_premium_box.h"
#include "base/object_ptr.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "settings/settings_common.h"
#include "ui/effects/premium_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/gradient_round_button.h"
#include "ui/widgets/labels.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
#include "styles/style_boxes.h"
#include "styles/style_settings.h"
namespace Ui {
namespace {
constexpr auto kShowOrLineOpacity = 0.3;
} // namespace
object_ptr<RpWidget> MakeShowOrLabel(
not_null<RpWidget*> parent,
rpl::producer<QString> text) {
auto result = object_ptr<FlatLabel>(
parent,
std::move(text),
st::showOrLabel);
const auto raw = result.data();
raw->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(raw);
const auto full = st::showOrLineWidth;
const auto left = (raw->width() - full) / 2;
const auto text = raw->naturalWidth() + 2 * st::showOrLabelSkip;
const auto fill = (full - text) / 2;
const auto stroke = st::lineWidth;
const auto top = st::showOrLineTop;
p.setOpacity(kShowOrLineOpacity);
p.fillRect(left, top, fill, stroke, st::windowSubTextFg);
const auto start = left + full - fill;
p.fillRect(start, top, fill, stroke, st::windowSubTextFg);
}, raw->lifetime());
return result;
}
void ShowOrPremiumBox(
not_null<GenericBox*> box,
ShowOrPremium type,
QString shortName,
Fn<void()> justShow,
Fn<void()> toPremium) {
struct Skin {
rpl::producer<QString> showTitle;
rpl::producer<TextWithEntities> showAbout;
rpl::producer<QString> showButton;
rpl::producer<QString> orPremium;
rpl::producer<QString> premiumTitle;
rpl::producer<TextWithEntities> premiumAbout;
rpl::producer<QString> premiumButton;
QString toast;
QString lottie;
};
auto skin = (type == ShowOrPremium::LastSeen)
? Skin{
tr::lng_lastseen_show_title(),
tr::lng_lastseen_show_about(
lt_user,
rpl::single(TextWithEntities{ shortName }),
tr::rich),
tr::lng_lastseen_show_button(),
tr::lng_lastseen_or(),
tr::lng_lastseen_premium_title(),
tr::lng_lastseen_premium_about(
lt_user,
rpl::single(TextWithEntities{ shortName }),
tr::rich),
tr::lng_lastseen_premium_button(),
tr::lng_lastseen_shown_toast(tr::now),
u"show_or_premium_lastseen"_q,
}
: Skin{
tr::lng_readtime_show_title(),
tr::lng_readtime_show_about(
lt_user,
rpl::single(TextWithEntities{ shortName }),
tr::rich),
tr::lng_readtime_show_button(),
tr::lng_readtime_or(),
tr::lng_readtime_premium_title(),
tr::lng_readtime_premium_about(
lt_user,
rpl::single(TextWithEntities{ shortName }),
tr::rich),
tr::lng_readtime_premium_button(),
tr::lng_readtime_shown_toast(tr::now),
u"show_or_premium_readtime"_q,
};
box->setStyle(st::showOrBox);
box->setWidth(st::boxWideWidth);
box->addTopButton(st::boxTitleClose, [=] {
box->closeBox();
});
const auto buttonPadding = QMargins(
st::showOrBox.buttonPadding.left(),
0,
st::showOrBox.buttonPadding.right(),
0);
auto icon = Settings::CreateLottieIcon(
box,
{
.name = skin.lottie,
.sizeOverride = st::normalBoxLottieSize
- Size(st::showOrTitleIconMargin * 2),
},
{ 0, st::showOrTitleIconMargin, 0, st::showOrTitleIconMargin });
Settings::AddLottieIconWithCircle(
box->verticalLayout(),
std::move(icon.widget),
st::settingsBlockedListIconPadding,
st::normalBoxLottieSize);
Ui::AddSkip(box->verticalLayout());
box->addRow(
object_ptr<FlatLabel>(
box,
std::move(skin.showTitle),
st::boostCenteredTitle),
st::showOrTitlePadding + buttonPadding,
style::al_top);
box->addRow(
object_ptr<FlatLabel>(
box,
std::move(skin.showAbout),
st::boostText),
st::showOrAboutPadding + buttonPadding,
style::al_top);
const auto show = box->addRow(
object_ptr<RoundButton>(
box,
std::move(skin.showButton),
st::showOrShowButton),
buttonPadding);
show->setTextTransform(RoundButton::TextTransform::NoTransform);
box->addRow(
MakeShowOrLabel(box, std::move(skin.orPremium)),
st::showOrLabelPadding + buttonPadding,
style::al_justify);
box->addRow(
object_ptr<FlatLabel>(
box,
std::move(skin.premiumTitle),
st::boostCenteredTitle),
st::showOrTitlePadding + buttonPadding,
style::al_top);
box->addRow(
object_ptr<FlatLabel>(
box,
std::move(skin.premiumAbout),
st::boostText),
st::showOrPremiumAboutPadding + buttonPadding,
style::al_top);
const auto premium = CreateChild<GradientButton>(
box.get(),
Premium::ButtonGradientStops());
premium->resize(st::showOrShowButton.width, st::showOrShowButton.height);
const auto label = CreateChild<FlatLabel>(
premium,
std::move(skin.premiumButton),
st::premiumPreviewButtonLabel);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
rpl::combine(
premium->widthValue(),
label->widthValue()
) | rpl::on_next([=](int outer, int width) {
label->moveToLeft(
(outer - width) / 2,
st::premiumPreviewBox.button.textTop,
outer);
}, label->lifetime());
box->setShowFinishedCallback([=, animate = std::move(icon.animate)] {
premium->startGlareAnimation();
animate(anim::repeat::once);
});
box->addButton(
object_ptr<AbstractButton>::fromRaw(premium));
show->setClickedCallback([box, justShow, toast = skin.toast] {
justShow();
box->uiShow()->showToast(toast);
box->closeBox();
});
premium->setClickedCallback(std::move(toPremium));
}
} // namespace Ui

View File

@@ -0,0 +1,32 @@
/*
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 "base/object_ptr.h"
namespace Ui {
class RpWidget;
class GenericBox;
enum class ShowOrPremium : uchar {
LastSeen,
ReadTime,
};
void ShowOrPremiumBox(
not_null<GenericBox*> box,
ShowOrPremium type,
QString shortName,
Fn<void()> justShow,
Fn<void()> toPremium);
[[nodiscard]] object_ptr<RpWidget> MakeShowOrLabel(
not_null<RpWidget*> parent,
rpl::producer<QString> text);
} // namespace Ui

View File

@@ -0,0 +1,55 @@
/*
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/single_choice_box.h"
#include "lang/lang_keys.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/padding_wrap.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
void SingleChoiceBox(
not_null<Ui::GenericBox*> box,
SingleChoiceBoxArgs &&args) {
box->setTitle(std::move(args.title));
box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
const auto group = std::make_shared<Ui::RadiobuttonGroup>(
args.initialSelection);
const auto layout = box->verticalLayout();
layout->add(object_ptr<Ui::FixedHeightWidget>(
layout,
st::boxOptionListPadding.top() + st::autolockButton.margin.top()));
auto &&ints = ranges::views::ints(0, ranges::unreachable);
for (const auto &[i, text] : ranges::views::zip(ints, args.options)) {
layout->add(
object_ptr<Ui::Radiobutton>(
layout,
group,
i,
text,
args.st ? *args.st : st::defaultBoxCheckbox,
args.radioSt ? *args.radioSt : st::defaultRadio),
QMargins(
st::boxPadding.left() + st::boxOptionListPadding.left(),
0,
st::boxPadding.right(),
st::boxOptionListSkip));
}
const auto callback = args.callback.value();
group->setChangedCallback([=](int value) {
const auto weak = base::make_weak(box);
callback(value);
if (weak) {
box->closeBox();
}
});
}

View File

@@ -0,0 +1,32 @@
/*
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 "ui/layers/generic_box.h"
#include "base/required.h"
namespace style {
struct Checkbox;
struct Radio;
} // namespace style
struct SingleChoiceBoxArgs {
template <typename T>
using required = base::required<T>;
required<rpl::producer<QString>> title;
const std::vector<QString> &options;
int initialSelection = 0;
required<Fn<void(int)>> callback;
const style::Checkbox *st = nullptr;
const style::Radio *radioSt = nullptr;
};
void SingleChoiceBox(
not_null<Ui::GenericBox*> box,
SingleChoiceBoxArgs &&args);

View File

@@ -0,0 +1,133 @@
/*
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/time_picker_box.h"
#include "base/event_filter.h"
#include "lang/lang_keys.h"
#include "ui/layers/generic_box.h"
#include "ui/effects/animation_value.h"
#include "ui/ui_utility.h"
#include "ui/widgets/vertical_drum_picker.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
namespace Ui {
std::vector<TimeId> DefaultTimePickerValues() {
return {
(60 * 15),
(60 * 30),
(3600 * 1),
(3600 * 2),
(3600 * 3),
(3600 * 4),
(3600 * 8),
(3600 * 12),
(86400 * 1),
(86400 * 2),
(86400 * 3),
(86400 * 7 * 1),
(86400 * 7 * 2),
(86400 * 31 * 1),
(86400 * 31 * 2),
(86400 * 31 * 3),
};
}
Fn<TimeId()> TimePickerBox(
not_null<GenericBox*> box,
std::vector<TimeId> values,
std::vector<QString> phrases,
TimeId startValue) {
Expects(phrases.size() == values.size());
const auto startIndex = [&, &v = startValue] {
const auto it = ranges::lower_bound(values, v);
if (it == begin(values)) {
return 0;
}
const auto left = *(it - 1);
const auto right = *it;
const auto shift = (std::abs(v - left) < std::abs(v - right))
? -1
: 0;
return int(std::distance(begin(values), it - shift));
}();
const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(
box,
st::historyMessagesTTLPickerHeight));
const auto font = st::boxTextFont;
const auto maxPhraseWidth = [&] {
// We have to use QFontMetricsF instead of
// FontData::width for more precise calculation.
const auto mf = QFontMetricsF(font->f);
const auto maxPhrase = ranges::max_element(
phrases,
std::less<>(),
[&](const QString &s) { return mf.horizontalAdvance(s); });
return std::ceil(mf.horizontalAdvance(*maxPhrase));
}();
const auto itemHeight = st::historyMessagesTTLPickerItemHeight;
auto paintCallback = Ui::VerticalDrumPicker::DefaultPaintCallback(
font,
itemHeight,
[=](QPainter &p, QRectF r, int index) {
p.drawText(r, phrases[index], style::al_center);
});
const auto picker = Ui::CreateChild<Ui::VerticalDrumPicker>(
content,
std::move(paintCallback),
phrases.size(),
itemHeight,
startIndex);
content->sizeValue(
) | rpl::on_next([=](const QSize &s) {
picker->resize(maxPhraseWidth, s.height());
picker->moveToLeft((s.width() - picker->width()) / 2, 0);
}, content->lifetime());
content->paintRequest(
) | rpl::on_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;
});
return [=] { return values[picker->index()]; };
}
} // namespace Ui

View File

@@ -0,0 +1,22 @@
/*
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 Ui {
class GenericBox;
[[nodiscard]] std::vector<TimeId> DefaultTimePickerValues();
[[nodiscard]] Fn<TimeId()> TimePickerBox(
not_null<GenericBox*> box,
std::vector<TimeId> values,
std::vector<QString> phrases,
TimeId startValue);
} // namespace Ui