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
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

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,376 @@
/*
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 "settings/business/settings_away_message.h"
#include "base/unixtime.h"
#include "core/application.h"
#include "data/business/data_business_info.h"
#include "data/business/data_shortcut_messages.h"
#include "data/data_session.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/business/settings_recipients_helper.h"
#include "settings/business/settings_shortcut_messages.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace Settings {
namespace {
class AwayMessage final : public BusinessSection<AwayMessage> {
public:
AwayMessage(
QWidget *parent,
not_null<Window::SessionController*> controller);
~AwayMessage();
[[nodiscard]] bool closeByOutsideClick() const override;
[[nodiscard]] rpl::producer<QString> title() override;
private:
void setupContent(not_null<Window::SessionController*> controller);
void save();
rpl::variable<bool> _canHave;
rpl::event_stream<> _deactivateOnAttempt;
rpl::variable<Data::BusinessRecipients> _recipients;
rpl::variable<Data::AwaySchedule> _schedule;
rpl::variable<bool> _offlineOnly;
rpl::variable<bool> _enabled;
};
[[nodiscard]] TimeId StartTimeMin() {
// Telegram was launched in August 2013 :)
return base::unixtime::serialize(QDateTime(QDate(2013, 8, 1), QTime(0, 0)));
}
[[nodiscard]] TimeId EndTimeMin() {
return StartTimeMin() + 3600;
}
[[nodiscard]] bool BadCustomInterval(const Data::WorkingInterval &interval) {
return !interval
|| (interval.start < StartTimeMin())
|| (interval.end < EndTimeMin());
}
struct AwayScheduleSelectorDescriptor {
not_null<Window::SessionController*> controller;
not_null<rpl::variable<Data::AwaySchedule>*> data;
};
void AddAwayScheduleSelector(
not_null<Ui::VerticalLayout*> container,
AwayScheduleSelectorDescriptor &&descriptor) {
using Type = Data::AwayScheduleType;
using namespace rpl::mappers;
const auto controller = descriptor.controller;
const auto data = descriptor.data;
Ui::AddSubsectionTitle(container, tr::lng_away_schedule());
const auto group = std::make_shared<Ui::RadioenumGroup<Type>>(
data->current().type);
const auto add = [&](Type type, const QString &label) {
container->add(
object_ptr<Ui::Radioenum<Type>>(
container,
group,
type,
label),
st::boxRowPadding + st::settingsAwaySchedulePadding);
};
add(Type::Always, tr::lng_away_schedule_always(tr::now));
add(Type::OutsideWorkingHours, tr::lng_away_schedule_outside(tr::now));
add(Type::Custom, tr::lng_away_schedule_custom(tr::now));
const auto customWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
const auto customInner = customWrap->entity();
customWrap->toggleOn(group->value() | rpl::map(_1 == Type::Custom));
group->changes() | rpl::on_next([=](Type value) {
auto copy = data->current();
copy.type = value;
*data = copy;
}, customWrap->lifetime());
const auto chooseDate = [=](
rpl::producer<QString> title,
TimeId now,
Fn<TimeId()> min,
Fn<TimeId()> max,
Fn<void(TimeId)> done) {
using namespace Ui;
const auto box = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
const auto save = [=](TimeId time) {
done(time);
if (const auto strong = box->get()) {
strong->closeBox();
}
};
*box = controller->show(Box(ChooseDateTimeBox, ChooseDateTimeBoxArgs{
.title = std::move(title),
.submit = tr::lng_settings_save(),
.done = save,
.min = min,
.time = now,
.max = max,
}));
};
Ui::AddSkip(customInner);
Ui::AddDivider(customInner);
Ui::AddSkip(customInner);
auto startLabel = data->value(
) | rpl::map([=](const Data::AwaySchedule &value) {
return langDateTime(
base::unixtime::parse(value.customInterval.start));
});
AddButtonWithLabel(
customInner,
tr::lng_away_custom_start(),
std::move(startLabel),
st::settingsButtonNoIcon
)->setClickedCallback([=] {
chooseDate(
tr::lng_away_custom_start(),
data->current().customInterval.start,
StartTimeMin,
[=] { return data->current().customInterval.end - 1; },
[=](TimeId time) {
auto copy = data->current();
copy.customInterval.start = time;
*data = copy;
});
});
auto endLabel = data->value(
) | rpl::map([=](const Data::AwaySchedule &value) {
return langDateTime(
base::unixtime::parse(value.customInterval.end));
});
AddButtonWithLabel(
customInner,
tr::lng_away_custom_end(),
std::move(endLabel),
st::settingsButtonNoIcon
)->setClickedCallback([=] {
chooseDate(
tr::lng_away_custom_end(),
data->current().customInterval.end,
[=] { return data->current().customInterval.start + 1; },
nullptr,
[=](TimeId time) {
auto copy = data->current();
copy.customInterval.end = time;
*data = copy;
});
});
}
AwayMessage::AwayMessage(
QWidget *parent,
not_null<Window::SessionController*> controller)
: BusinessSection(parent, controller) {
setupContent(controller);
}
AwayMessage::~AwayMessage() {
if (!Core::Quitting()) {
save();
}
}
bool AwayMessage::closeByOutsideClick() const {
return false;
}
rpl::producer<QString> AwayMessage::title() {
return tr::lng_away_title();
}
void AwayMessage::setupContent(
not_null<Window::SessionController*> controller) {
using namespace Data;
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
const auto info = &controller->session().data().businessInfo();
const auto current = info->awaySettings();
const auto disabled = (current.schedule.type == AwayScheduleType::Never);
_recipients = disabled
? Data::BusinessRecipients{ .allButExcluded = true }
: Data::BusinessRecipients::MakeValid(current.recipients);
auto initialSchedule = disabled ? AwaySchedule{
.type = AwayScheduleType::Always,
} : current.schedule;
if (BadCustomInterval(initialSchedule.customInterval)) {
const auto now = base::unixtime::now();
initialSchedule.customInterval = WorkingInterval{
.start = now,
.end = now + 24 * 60 * 60,
};
}
_schedule = initialSchedule;
AddDividerTextWithLottie(content, {
.lottie = u"sleep"_q,
.lottieSize = st::settingsCloudPasswordIconSize,
.lottieMargins = st::peerAppearanceIconPadding,
.showFinished = showFinishes(),
.about = tr::lng_away_about(tr::marked),
.aboutMargins = st::peerAppearanceCoverLabelMargin,
});
const auto session = &controller->session();
_canHave = rpl::combine(
ShortcutsCountValue(session),
ShortcutsLimitValue(session),
ShortcutExistsValue(session, u"away"_q),
(_1 < _2) || _3);
Ui::AddSkip(content);
const auto enabled = content->add(object_ptr<Ui::SettingsButton>(
content,
tr::lng_away_enable(),
st::settingsButtonNoIcon
))->toggleOn(rpl::single(
!disabled
) | rpl::then(rpl::merge(
_canHave.value() | rpl::filter(!_1),
_deactivateOnAttempt.events() | rpl::map_to(false)
)));
_enabled = enabled->toggledValue();
_enabled.value() | rpl::filter(_1) | rpl::on_next([=] {
if (!_canHave.current()) {
controller->showToast({
.text = { tr::lng_away_limit_reached(tr::now) },
.adaptive = true,
});
_deactivateOnAttempt.fire({});
}
}, lifetime());
const auto wrap = content->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
content,
object_ptr<Ui::VerticalLayout>(content)));
const auto inner = wrap->entity();
Ui::AddSkip(inner);
Ui::AddDivider(inner);
const auto createWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto createInner = createWrap->entity();
Ui::AddSkip(createInner);
const auto create = AddButtonWithLabel(
createInner,
rpl::conditional(
ShortcutExistsValue(session, u"away"_q),
tr::lng_business_edit_messages(),
tr::lng_away_create()),
ShortcutMessagesCountValue(
session,
u"away"_q
) | rpl::map([=](int count) {
return count
? tr::lng_forum_messages(tr::now, lt_count, count)
: QString();
}),
st::settingsButtonLightNoIcon);
create->setClickedCallback([=] {
const auto owner = &controller->session().data();
const auto id = owner->shortcutMessages().emplaceShortcut("away");
showOther(ShortcutMessagesId(id));
});
Ui::AddSkip(createInner);
Ui::AddDivider(createInner);
createWrap->toggleOn(rpl::single(true));
Ui::AddSkip(inner);
AddAwayScheduleSelector(inner, {
.controller = controller,
.data = &_schedule,
});
Ui::AddSkip(inner);
Ui::AddDivider(inner);
Ui::AddSkip(inner);
const auto offlineOnly = inner->add(
object_ptr<Ui::SettingsButton>(
inner,
tr::lng_away_offline_only(),
st::settingsButtonNoIcon)
)->toggleOn(rpl::single(current.offlineOnly));
_offlineOnly = offlineOnly->toggledValue();
Ui::AddSkip(inner);
Ui::AddDividerText(inner, tr::lng_away_offline_only_about());
AddBusinessRecipientsSelector(inner, {
.controller = controller,
.title = tr::lng_away_recipients(),
.data = &_recipients,
.type = Data::BusinessRecipientsType::Messages,
});
Ui::AddSkip(inner, st::settingsChatbotsAccessSkip);
wrap->toggleOn(enabled->toggledValue());
wrap->finishAnimating();
Ui::ResizeFitChild(this, content);
}
void AwayMessage::save() {
const auto show = controller()->uiShow();
const auto session = &controller()->session();
const auto fail = [=](QString error) {
if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) {
show->showToast(tr::lng_greeting_recipients_empty(tr::now));
} else if (error != u"SHORTCUT_INVALID"_q) {
show->showToast(error);
}
};
session->data().businessInfo().saveAwaySettings(
_enabled.current() ? Data::AwaySettings{
.recipients = _recipients.current(),
.schedule = _schedule.current(),
.shortcutId = LookupShortcutId(session, u"away"_q),
.offlineOnly = _offlineOnly.current(),
} : Data::AwaySettings(),
fail);
}
} // namespace
Type AwayMessageId() {
return AwayMessage::Id();
}
} // namespace Settings

View File

@@ -0,0 +1,16 @@
/*
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 "settings/settings_type.h"
namespace Settings {
[[nodiscard]] Type AwayMessageId();
} // namespace Settings

View File

@@ -0,0 +1,646 @@
/*
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 "settings/business/settings_chat_intro.h"
#include "api/api_premium.h"
#include "boxes/peers/edit_peer_color_box.h" // ButtonStyleWithRightEmoji
#include "chat_helpers/stickers_lottie.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/application.h"
#include "data/business/data_business_info.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/view/media/history_view_media_common.h"
#include "history/view/media/history_view_sticker_player.h"
#include "history/view/history_view_about_view.h"
#include "history/view/history_view_element.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "settings/business/settings_recipients_helper.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "window/themes/window_theme.h"
#include "window/section_widget.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace Settings {
namespace {
using namespace HistoryView;
class PreviewDelegate final : public DefaultElementDelegate {
public:
PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update);
bool elementAnimationsPaused() override;
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;
Context elementContext() override;
private:
const not_null<QWidget*> _parent;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
};
class PreviewWrap final : public Ui::RpWidget {
public:
PreviewWrap(
not_null<QWidget*> parent,
not_null<Main::Session*> session,
rpl::producer<Data::ChatIntro> value);
~PreviewWrap();
private:
void paintEvent(QPaintEvent *e) override;
void resizeTo(int width);
void prepare(rpl::producer<Data::ChatIntro> value);
const not_null<History*> _history;
const std::unique_ptr<Ui::ChatTheme> _theme;
const std::unique_ptr<Ui::ChatStyle> _style;
const std::unique_ptr<PreviewDelegate> _delegate;
std::unique_ptr<AboutView> _view;
QPoint _position;
};
class StickerPanel final {
public:
StickerPanel();
~StickerPanel();
struct Descriptor {
not_null<Window::SessionController*> controller;
not_null<QWidget*> button;
};
void show(Descriptor &&descriptor);
struct CustomChosen {
not_null<DocumentData*> sticker;
};
[[nodiscard]] rpl::producer<CustomChosen> someCustomChosen() const {
return _someCustomChosen.events();
}
private:
void create(const Descriptor &descriptor);
base::unique_qptr<ChatHelpers::TabbedPanel> _panel;
QPointer<QWidget> _panelButton;
rpl::event_stream<CustomChosen> _someCustomChosen;
};
class ChatIntro final : public BusinessSection<ChatIntro> {
public:
ChatIntro(
QWidget *parent,
not_null<Window::SessionController*> controller);
~ChatIntro();
[[nodiscard]] bool closeByOutsideClick() const override;
[[nodiscard]] rpl::producer<QString> title() override;
void setInnerFocus() override {
_setFocus();
}
private:
void setupContent(not_null<Window::SessionController*> controller);
void save();
Fn<void()> _setFocus;
rpl::variable<Data::ChatIntro> _intro;
};
[[nodiscard]] int PartLimit(
not_null<Main::Session*> session,
const QString &key,
int defaultValue) {
return session->appConfig().get<int>(key, defaultValue);
}
[[nodiscard]] not_null<Ui::InputField*> AddPartInput(
not_null<Ui::VerticalLayout*> container,
rpl::producer<QString> placeholder,
QString current,
int limit) {
const auto field = container->add(
object_ptr<Ui::InputField>(
container,
st::settingsChatIntroField,
std::move(placeholder),
current),
st::settingsChatIntroFieldMargins);
field->setMaxLength(limit);
Ui::AddLengthLimitLabel(field, limit);
return field;
}
rpl::producer<std::shared_ptr<StickerPlayer>> IconPlayerValue(
not_null<DocumentData*> sticker,
Fn<void()> update) {
const auto media = sticker->createMediaView();
media->checkStickerLarge();
media->goodThumbnailWanted();
return rpl::single() | rpl::then(
sticker->session().downloaderTaskFinished()
) | rpl::filter([=] {
return media->loaded();
}) | rpl::take(1) | rpl::map([=] {
auto result = std::shared_ptr<StickerPlayer>();
const auto info = sticker->sticker();
const auto box = QSize(st::emojiSize, st::emojiSize);
if (info->isLottie()) {
result = std::make_shared<LottiePlayer>(
ChatHelpers::LottiePlayerFromDocument(
media.get(),
ChatHelpers::StickerLottieSize::StickerEmojiSize,
box,
Lottie::Quality::High));
} else if (info->isWebm()) {
result = std::make_shared<WebmPlayer>(
media->owner()->location(),
media->bytes(),
box);
} else {
result = std::make_shared<StaticStickerPlayer>(
media->owner()->location(),
media->bytes(),
box);
}
result->setRepaintCallback(update);
return result;
});
}
[[nodiscard]] object_ptr<Ui::SettingsButton> CreateIntroStickerButton(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
rpl::producer<DocumentData*> stickerValue,
Fn<void(DocumentData*)> stickerChosen) {
const auto button = ButtonStyleWithRightEmoji(
parent,
tr::lng_chat_intro_random_sticker(tr::now),
st::settingsButtonNoIcon);
auto result = Settings::CreateButtonWithIcon(
parent,
tr::lng_chat_intro_choose_sticker(),
*button.st);
const auto raw = result.data();
const auto right = Ui::CreateChild<Ui::RpWidget>(raw);
right->show();
struct State {
StickerPanel panel;
DocumentData *sticker = nullptr;
std::shared_ptr<StickerPlayer> player;
rpl::lifetime playerLifetime;
};
const auto state = right->lifetime().make_state<State>();
state->panel.someCustomChosen(
) | rpl::on_next([=](StickerPanel::CustomChosen chosen) {
stickerChosen(chosen.sticker);
}, raw->lifetime());
std::move(
stickerValue
) | rpl::on_next([=](DocumentData *sticker) {
state->sticker = sticker;
if (sticker) {
right->resize(button.emojiWidth + button.added, right->height());
IconPlayerValue(
sticker,
[=] { right->update(); }
) | rpl::on_next([=](
std::shared_ptr<StickerPlayer> player) {
state->player = std::move(player);
right->update();
}, state->playerLifetime);
} else {
state->playerLifetime.destroy();
state->player = nullptr;
right->resize(button.noneWidth + button.added, right->height());
right->update();
}
}, right->lifetime());
rpl::combine(
raw->sizeValue(),
right->widthValue()
) | rpl::on_next([=](QSize outer, int width) {
right->resize(width, outer.height());
const auto skip = st::settingsButton.padding.right();
right->moveToRight(skip - button.added, 0, outer.width());
}, right->lifetime());
right->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(right);
const auto height = right->height();
if (state->player) {
if (state->player->ready()) {
const auto frame = state->player->frame(
QSize(st::emojiSize, st::emojiSize),
QColor(0, 0, 0, 0),
false,
crl::now(),
!right->window()->isActiveWindow()).image;
const auto target = DownscaledSize(
frame.size(),
QSize(st::emojiSize, st::emojiSize));
p.drawImage(
QRect(
button.added + (st::emojiSize - target.width()) / 2,
(height - target.height()) / 2,
target.width(),
target.height()),
frame);
state->player->markFrameShown();
}
} else {
const auto &font = st::normalFont;
p.setFont(font);
p.setPen(st::windowActiveTextFg);
p.drawText(
QPoint(
button.added,
(height - font->height) / 2 + font->ascent),
tr::lng_chat_intro_random_sticker(tr::now));
}
}, right->lifetime());
raw->setClickedCallback([=] {
if (const auto controller = show->resolveWindow()) {
state->panel.show({
.controller = controller,
.button = right,
});
}
});
return result;
}
PreviewDelegate::PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update)
: _parent(parent)
, _pathGradient(MakePathShiftGradient(st, update)) {
}
bool PreviewDelegate::elementAnimationsPaused() {
return _parent->window()->isActiveWindow();
}
auto PreviewDelegate::elementPathShiftGradient()
-> not_null<Ui::PathShiftGradient*> {
return _pathGradient.get();
}
Context PreviewDelegate::elementContext() {
return Context::History;
}
PreviewWrap::PreviewWrap(
not_null<QWidget*> parent,
not_null<Main::Session*> session,
rpl::producer<Data::ChatIntro> value)
: RpWidget(parent)
, _history(session->data().history(session->userPeerId()))
, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))
, _style(std::make_unique<Ui::ChatStyle>(
_history->session().colorIndicesValue()))
, _delegate(std::make_unique<PreviewDelegate>(
parent,
_style.get(),
[=] { update(); }))
, _position(0, st::msgMargin.bottom()) {
_style->apply(_theme.get());
session->data().viewRepaintRequest(
) | rpl::on_next([=](not_null<const Element*> view) {
if (view == _view->view()) {
update();
}
}, lifetime());
session->downloaderTaskFinished() | rpl::on_next([=] {
update();
}, lifetime());
prepare(std::move(value));
}
PreviewWrap::~PreviewWrap() {
_view = nullptr;
}
void PreviewWrap::prepare(rpl::producer<Data::ChatIntro> value) {
_view = std::make_unique<AboutView>(
_history.get(),
_delegate.get());
std::move(value) | rpl::on_next([=](Data::ChatIntro intro) {
_view->make(std::move(intro), true);
if (width() >= st::msgMinWidth) {
resizeTo(width());
}
update();
}, lifetime());
widthValue(
) | rpl::filter([=](int width) {
return width >= st::msgMinWidth;
}) | rpl::on_next([=](int width) {
resizeTo(width);
}, lifetime());
}
void PreviewWrap::resizeTo(int width) {
const auto height = _position.y()
+ _view->view()->resizeGetHeight(width)
+ _position.y()
+ st::msgServiceMargin.top()
+ st::msgServiceGiftBoxTopSkip
- st::msgServiceMargin.bottom();
resize(width, height);
}
void PreviewWrap::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto clip = e->rect();
if (!clip.isEmpty()) {
p.setClipRect(clip);
Window::SectionWidget::PaintBackground(
p,
_theme.get(),
QSize(width(), window()->height()),
clip);
}
auto context = _theme->preparePaintContext(
_style.get(),
rect(),
e->rect(),
!window()->isActiveWindow());
p.translate(_position);
_view->view()->draw(p, context);
}
StickerPanel::StickerPanel() = default;
StickerPanel::~StickerPanel() = default;
void StickerPanel::show(Descriptor &&descriptor) {
if (!_panel) {
create(descriptor);
_panel->shownValue(
) | rpl::filter([=] {
return (_panelButton != nullptr);
}) | rpl::on_next([=](bool shown) {
if (shown) {
_panelButton->installEventFilter(_panel.get());
} else {
_panelButton->removeEventFilter(_panel.get());
}
}, _panel->lifetime());
}
const auto button = descriptor.button;
if (const auto previous = _panelButton.data()) {
if (previous != button) {
previous->removeEventFilter(_panel.get());
}
}
_panelButton = button;
const auto parent = _panel->parentWidget();
const auto global = button->mapToGlobal(QPoint());
const auto local = parent->mapFromGlobal(global);
_panel->moveBottomRight(
local.y() + (st::normalFont->height / 2),
local.x() + button->width() * 3);
_panel->toggleAnimated();
}
void StickerPanel::create(const Descriptor &descriptor) {
using Selector = ChatHelpers::TabbedSelector;
using Descriptor = ChatHelpers::TabbedSelectorDescriptor;
using Mode = ChatHelpers::TabbedSelector::Mode;
const auto controller = descriptor.controller;
const auto body = controller->window().widget()->bodyWidget();
_panel = base::make_unique_q<ChatHelpers::TabbedPanel>(
body,
controller,
object_ptr<Selector>(
nullptr,
Descriptor{
.show = controller->uiShow(),
.st = st::backgroundEmojiPan,
.level = Window::GifPauseReason::Layer,
.mode = Mode::ChatIntro,
.features = {
.megagroupSet = false,
.stickersSettings = false,
.openStickerSets = false,
},
}));
_panel->setDropDown(false);
_panel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
_panel->hide();
_panel->selector()->fileChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
_someCustomChosen.fire({ data.document });
_panel->hideAnimated();
}, _panel->lifetime());
}
ChatIntro::ChatIntro(
QWidget *parent,
not_null<Window::SessionController*> controller)
: BusinessSection(parent, controller) {
setupContent(controller);
}
ChatIntro::~ChatIntro() {
if (!Core::Quitting()) {
save();
}
}
bool ChatIntro::closeByOutsideClick() const {
return false;
}
rpl::producer<QString> ChatIntro::title() {
return tr::lng_chat_intro_title();
}
[[nodiscard]] rpl::producer<Data::ChatIntro> IntroWithRandomSticker(
not_null<Main::Session*> session,
rpl::producer<Data::ChatIntro> intro) {
auto random = rpl::single(
Api::RandomHelloStickerValue(session)
) | rpl::then(rpl::duplicate(
intro
) | rpl::map([=](const Data::ChatIntro &intro) {
return intro.sticker;
}) | rpl::distinct_until_changed(
) | rpl::filter([](DocumentData *sticker) {
return !sticker;
}) | rpl::map([=] {
return Api::RandomHelloStickerValue(session);
})) | rpl::flatten_latest();
return rpl::combine(
std::move(intro),
std::move(random)
) | rpl::map([=](Data::ChatIntro intro, DocumentData *hello) {
if (!intro.sticker) {
intro.sticker = hello;
}
return intro;
});
}
void ChatIntro::setupContent(
not_null<Window::SessionController*> controller) {
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
const auto session = &controller->session();
_intro = controller->session().user()->businessDetails().intro;
const auto change = [=](Fn<void(Data::ChatIntro &)> modify) {
auto intro = _intro.current();
modify(intro);
_intro = intro;
};
content->add(
object_ptr<PreviewWrap>(
content,
session,
IntroWithRandomSticker(session, _intro.value())),
style::margins());
const auto title = AddPartInput(
content,
tr::lng_chat_intro_enter_title(),
_intro.current().title,
PartLimit(session, u"intro_title_length_limit"_q, 32));
const auto description = AddPartInput(
content,
tr::lng_chat_intro_enter_message(),
_intro.current().description,
PartLimit(session, u"intro_description_length_limit"_q, 70));
content->add(CreateIntroStickerButton(
content,
controller->uiShow(),
_intro.value() | rpl::map([](const Data::ChatIntro &intro) {
return intro.sticker;
}) | rpl::distinct_until_changed(),
[=](DocumentData *sticker) {
change([&](Data::ChatIntro &intro) {
intro.sticker = sticker;
});
}));
Ui::AddSkip(content);
title->changes() | rpl::on_next([=] {
change([&](Data::ChatIntro &intro) {
intro.title = title->getLastText();
});
}, title->lifetime());
description->changes() | rpl::on_next([=] {
change([&](Data::ChatIntro &intro) {
intro.description = description->getLastText();
});
}, description->lifetime());
_setFocus = [=] {
title->setFocusFast();
};
Ui::AddDividerText(
content,
tr::lng_chat_intro_about(),
st::peerAppearanceDividerTextMargin);
Ui::AddSkip(content);
const auto resetWrap = content->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
content,
object_ptr<Ui::SettingsButton>(
content,
tr::lng_chat_intro_reset(),
st::settingsAttentionButton
)));
resetWrap->toggleOn(
_intro.value() | rpl::map([](const Data::ChatIntro &intro) {
return !!intro;
}));
resetWrap->entity()->setClickedCallback([=] {
_intro = Data::ChatIntro();
title->clear();
description->clear();
title->setFocus();
});
Ui::ResizeFitChild(this, content);
}
void ChatIntro::save() {
const auto fail = [=](QString error) {
};
controller()->session().data().businessInfo().saveChatIntro(
_intro.current(),
fail);
}
} // namespace
Type ChatIntroId() {
return ChatIntro::Id();
}
} // namespace Settings

View File

@@ -0,0 +1,16 @@
/*
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 "settings/settings_type.h"
namespace Settings {
[[nodiscard]] Type ChatIntroId();
} // namespace Settings

View File

@@ -0,0 +1,823 @@
/*
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 "settings/business/settings_chat_links.h"
#include "api/api_chat_links.h"
#include "apiwrap.h"
#include "base/event_filter.h"
#include "boxes/peers/edit_peer_invite_link.h"
#include "boxes/peers/edit_peer_invite_links.h"
#include "boxes/premium_preview_box.h"
#include "boxes/peer_list_box.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/application.h"
#include "core/ui_integration.h"
#include "core/core_settings.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_document.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "settings/business/settings_recipients_helper.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/emoji_button.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/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include <QtGui/QGuiApplication>
namespace Settings {
namespace {
constexpr auto kChangesDebounceTimeout = crl::time(1000);
using ChatLinkData = Api::ChatLink;
class ChatLinks final : public BusinessSection<ChatLinks> {
public:
ChatLinks(
QWidget *parent,
not_null<Window::SessionController*> controller);
~ChatLinks();
[[nodiscard]] rpl::producer<QString> title() override;
const Ui::RoundRect *bottomSkipRounding() const override {
return &_bottomSkipRounding;
}
private:
void setupContent(not_null<Window::SessionController*> controller);
Ui::RoundRect _bottomSkipRounding;
};
struct ChatLinkAction {
enum class Type {
Copy,
Share,
Rename,
Delete,
};
QString link;
Type type = Type::Copy;
};
class Row;
class RowDelegate {
public:
virtual not_null<Main::Session*> rowSession() = 0;
virtual void rowUpdateRow(not_null<Row*> row) = 0;
virtual void rowPaintIcon(
QPainter &p,
int x,
int y,
int size) = 0;
};
class Row final : public PeerListRow {
public:
Row(not_null<RowDelegate*> delegate, const ChatLinkData &data);
void update(const ChatLinkData &data);
[[nodiscard]] ChatLinkData data() const;
QString generateName() override;
QString generateShortName() override;
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
QSize rightActionSize() const override;
QMargins rightActionMargins() const override;
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
bool rightActionDisabled() const override {
return true;
}
void paintStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected) override;
private:
void updateStatus(const ChatLinkData &data);
const not_null<RowDelegate*> _delegate;
ChatLinkData _data;
Ui::Text::String _status;
Ui::Text::String _clicks;
};
[[nodiscard]] uint64 ComputeRowId(const ChatLinkData &data) {
return UniqueRowIdFromString(data.link);
}
[[nodiscard]] QString ComputeClicks(const ChatLinkData &link) {
return link.clicks
? tr::lng_chat_links_clicks(tr::now, lt_count, link.clicks)
: tr::lng_chat_links_no_clicks(tr::now);
}
Row::Row(not_null<RowDelegate*> delegate, const ChatLinkData &data)
: PeerListRow(ComputeRowId(data))
, _delegate(delegate)
, _data(data) {
setCustomStatus(QString());
updateStatus(data);
}
void Row::updateStatus(const ChatLinkData &data) {
const auto context = Core::TextContext({
.session = _delegate->rowSession(),
.repaint = [=] { _delegate->rowUpdateRow(this); },
});
_status.setMarkedText(
st::messageTextStyle,
data.message,
kMarkupTextOptions,
context);
_clicks.setText(st::messageTextStyle, ComputeClicks(data));
}
void Row::update(const ChatLinkData &data) {
_data = data;
updateStatus(data);
refreshName(st::inviteLinkList.item);
_delegate->rowUpdateRow(this);
}
ChatLinkData Row::data() const {
return _data;
}
QString Row::generateName() {
if (!_data.title.isEmpty()) {
return _data.title;
}
auto result = _data.link;
return result.replace(
u"https://"_q,
QString()
);
}
QString Row::generateShortName() {
return generateName();
}
PaintRoundImageCallback Row::generatePaintUserpicCallback(bool forceRound) {
return [=](
QPainter &p,
int x,
int y,
int outerWidth,
int size) {
_delegate->rowPaintIcon(p, x, y, size);
};
}
QSize Row::rightActionSize() const {
return QSize(
_clicks.maxWidth(),
st::inviteLinkThreeDotsIcon.height());
}
QMargins Row::rightActionMargins() const {
return QMargins(
0,
(st::inviteLinkList.item.height - rightActionSize().height()) / 2,
st::inviteLinkThreeDotsSkip,
0);
}
void Row::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
p.setPen(selected ? st::windowSubTextFgOver : st::windowSubTextFg);
_clicks.draw(p, x, y, outerWidth);
}
void Row::paintStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected) {
p.setPen(selected ? st.statusFgOver : st.statusFg);
_status.draw(p, {
.position = { x, y },
.outerWidth = outerWidth,
.availableWidth = availableWidth,
.palette = &st::defaultTextPalette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = crl::now(),
.elisionLines = 1,
});
}
class LinksController final
: public PeerListController
, public RowDelegate
, public base::has_weak_ptr {
public:
explicit LinksController(not_null<Window::SessionController*> window);
[[nodiscard]] rpl::producer<int> fullCountValue() const {
return _count.value();
}
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowRightActionClicked(not_null<PeerListRow*> row) override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
Main::Session &session() const override;
not_null<Main::Session*> rowSession() override;
void rowUpdateRow(not_null<Row*> row) override;
void rowPaintIcon(
QPainter &p,
int x,
int y,
int size) override;
private:
void appendRow(const ChatLinkData &data);
void prependRow(const ChatLinkData &data);
void updateRow(const ChatLinkData &data);
bool removeRow(const QString &link);
void showRowMenu(
not_null<PeerListRow*> row,
bool highlightRow);
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> createRowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row);
const not_null<Window::SessionController*> _window;
const not_null<Main::Session*> _session;
rpl::variable<int> _count;
base::unique_qptr<Ui::PopupMenu> _menu;
QImage _icon;
rpl::lifetime _lifetime;
};
struct LinksList {
not_null<Ui::RpWidget*> widget;
not_null<LinksController*> controller;
};
LinksList AddLinksList(
not_null<Window::SessionController*> window,
not_null<Ui::VerticalLayout*> container) {
auto &lifetime = container->lifetime();
const auto delegate = lifetime.make_state<PeerListContentDelegateShow>(
window->uiShow());
const auto controller = lifetime.make_state<LinksController>(window);
controller->setStyleOverrides(&st::inviteLinkList);
const auto content = container->add(object_ptr<PeerListContent>(
container,
controller));
delegate->setContent(content);
controller->setDelegate(delegate);
return { content, controller };
}
void EditChatLinkBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller,
ChatLinkData data,
Fn<void(ChatLinkData, Fn<void()> close)> submit) {
box->setTitle(data.link.isEmpty()
? tr::lng_chat_link_new_title()
: tr::lng_chat_link_edit_title());
box->setWidth(st::boxWideWidth);
Ui::AddDividerText(
box->verticalLayout(),
tr::lng_chat_link_description());
const auto peer = controller->session().user();
const auto outer = box->getDelegate()->outerContainer();
const auto field = box->addRow(
object_ptr<Ui::InputField>(
box.get(),
st::settingsChatLinkField,
Ui::InputField::Mode::MultiLine,
tr::lng_chat_link_placeholder()));
box->setFocusCallback([=] {
field->setFocusFast();
});
Ui::AddDivider(box->verticalLayout());
Ui::AddSkip(box->verticalLayout());
const auto title = box->addRow(object_ptr<Ui::InputField>(
box.get(),
st::defaultInputField,
tr::lng_chat_link_name(),
data.title));
const auto emojiToggle = Ui::CreateChild<Ui::EmojiButton>(
field->parentWidget(),
st::defaultComposeFiles.emoji);
using Selector = ChatHelpers::TabbedSelector;
auto &lifetime = box->lifetime();
const auto emojiPanel = lifetime.make_state<ChatHelpers::TabbedPanel>(
outer,
controller,
object_ptr<Selector>(
nullptr,
controller->uiShow(),
Window::GifPauseReason::Layer,
Selector::Mode::EmojiOnly));
emojiPanel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
emojiPanel->hide();
emojiPanel->selector()->setCurrentPeer(peer);
emojiPanel->selector()->emojiChosen(
) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {
Ui::InsertEmojiAtCursor(field->textCursor(), data.emoji);
}, field->lifetime());
emojiPanel->selector()->customEmojiChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
Data::InsertCustomEmoji(field, data.document);
}, field->lifetime());
emojiToggle->installEventFilter(emojiPanel);
emojiToggle->addClickHandler([=] {
emojiPanel->toggleAnimated();
});
const auto allow = [](not_null<DocumentData*>) { return true; };
InitMessageFieldHandlers(
controller,
field,
Window::GifPauseReason::Layer,
allow);
Ui::Emoji::SuggestionsController::Init(
outer,
field,
&controller->session(),
{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
field->setSubmitSettings(Core::App().settings().sendSubmitWay());
field->setMaxHeight(st::defaultComposeFiles.caption.heightMax);
const auto save = [=] {
auto copy = data;
copy.title = title->getLastText().trimmed();
auto textWithTags = field->getTextWithAppliedMarkdown();
copy.message = TextWithEntities{
textWithTags.text,
TextUtilities::ConvertTextTagsToEntities(textWithTags.tags)
};
submit(copy, crl::guard(box, [=] {
box->closeBox();
}));
};
const auto updateEmojiPanelGeometry = [=] {
const auto parent = emojiPanel->parentWidget();
const auto global = emojiToggle->mapToGlobal({ 0, 0 });
const auto local = parent->mapFromGlobal(global);
emojiPanel->moveBottomRight(
local.y(),
local.x() + emojiToggle->width() * 3);
};
const auto filterCallback = [=](not_null<QEvent*> event) {
const auto type = event->type();
if (type == QEvent::Move || type == QEvent::Resize) {
// updateEmojiPanelGeometry uses not only container geometry, but
// also container children geometries that will be updated later.
crl::on_main(emojiPanel, updateEmojiPanelGeometry);
}
return base::EventFilterResult::Continue;
};
base::install_event_filter(emojiPanel, outer, filterCallback);
field->submits(
) | rpl::on_next([=] {
title->setFocus();
}, field->lifetime());
field->cancelled(
) | rpl::on_next([=] {
box->closeBox();
}, field->lifetime());
title->submits(
) | rpl::on_next(save, title->lifetime());
rpl::combine(
box->sizeValue(),
field->geometryValue()
) | rpl::on_next([=](QSize outer, QRect inner) {
emojiToggle->moveToLeft(
inner.x() + inner.width() - emojiToggle->width(),
inner.y() + st::settingsChatLinkEmojiTop);
emojiToggle->update();
crl::on_main(emojiPanel, updateEmojiPanelGeometry);
}, emojiToggle->lifetime());
const auto initial = TextWithTags{
data.message.text,
TextUtilities::ConvertEntitiesToTextTags(data.message.entities)
};
field->setTextWithTags(initial, Ui::InputField::HistoryAction::Clear);
auto cursor = field->textCursor();
cursor.movePosition(QTextCursor::End);
field->setTextCursor(cursor);
const auto checkChangedTimer = lifetime.make_state<base::Timer>([=] {
if (field->getTextWithAppliedMarkdown() == initial) {
box->setCloseByOutsideClick(true);
}
});
field->changes(
) | rpl::on_next([=] {
checkChangedTimer->callOnce(kChangesDebounceTimeout);
box->setCloseByOutsideClick(false);
}, field->lifetime());
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
void EditChatLink(
not_null<Window::SessionController*> window,
not_null<Main::Session*> session,
ChatLinkData data) {
const auto submitting = std::make_shared<bool>();
const auto submit = [=](ChatLinkData data, Fn<void()> close) {
if (std::exchange(*submitting, true)) {
return;
}
const auto done = crl::guard(window, [=](const auto&) {
window->showToast(tr::lng_chat_link_saved(tr::now));
close();
});
session->api().chatLinks().edit(
data.link,
data.title,
data.message,
done);
};
window->show(Box(
EditChatLinkBox,
window,
data,
crl::guard(window, submit)));
}
LinksController::LinksController(
not_null<Window::SessionController*> window)
: _window(window)
, _session(&window->session()) {
style::PaletteChanged(
) | rpl::on_next([=] {
_icon = QImage();
}, _lifetime);
_session->api().chatLinks().updates(
) | rpl::on_next([=](const Api::ChatLinkUpdate &update) {
if (!update.now) {
if (removeRow(update.was)) {
delegate()->peerListRefreshRows();
}
} else if (update.was.isEmpty()) {
prependRow(*update.now);
delegate()->peerListRefreshRows();
} else {
updateRow(*update.now);
}
}, _lifetime);
}
void LinksController::prepare() {
auto &&list = _session->api().chatLinks().list()
| ranges::views::reverse;
for (const auto &link : list) {
appendRow(link);
}
delegate()->peerListRefreshRows();
}
void LinksController::rowClicked(not_null<PeerListRow*> row) {
showRowMenu(row, true);
}
void LinksController::showRowMenu(
not_null<PeerListRow*> row,
bool highlightRow) {
delegate()->peerListShowRowMenu(row, highlightRow);
}
void LinksController::rowRightActionClicked(not_null<PeerListRow*> row) {
delegate()->peerListShowRowMenu(row, true);
}
base::unique_qptr<Ui::PopupMenu> LinksController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
auto result = createRowContextMenu(parent, row);
if (result) {
// First clear _menu value, so that we don't check row positions yet.
base::take(_menu);
// Here unique_qptr is used like a shared pointer, where
// not the last destroyed pointer destroys the object, but the first.
_menu = base::unique_qptr<Ui::PopupMenu>(result.get());
}
return result;
}
base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
const auto real = static_cast<Row*>(row.get());
const auto data = real->data();
const auto link = data.link;
auto result = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
result->addAction(tr::lng_group_invite_context_copy(tr::now), [=] {
QGuiApplication::clipboard()->setText(link);
delegate()->peerListUiShow()->showToast(
tr::lng_chat_link_copied(tr::now));
}, &st::menuIconCopy);
result->addAction(tr::lng_group_invite_context_share(tr::now), [=] {
delegate()->peerListUiShow()->showBox(ShareInviteLinkBox(
_session,
link,
tr::lng_chat_link_copied(tr::now)));
}, &st::menuIconShare);
result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
nullptr,
link,
tr::lng_chat_link_qr_title(),
tr::lng_chat_link_qr_about()));
}, &st::menuIconQrCode);
result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] {
EditChatLink(_window, _session, data);
}, &st::menuIconEdit);
result->addAction(tr::lng_group_invite_context_delete(tr::now), [=] {
const auto sure = [=](Fn<void()> &&close) {
_window->session().api().chatLinks().destroy(link, close);
};
_window->show(Ui::MakeConfirmBox({
.text = tr::lng_chat_link_delete_sure(tr::now),
.confirmed = sure,
.confirmText = tr::lng_box_delete(tr::now),
}));
}, &st::menuIconDelete);
return result;
}
Main::Session &LinksController::session() const {
return *_session;
}
void LinksController::appendRow(const ChatLinkData &data) {
delegate()->peerListAppendRow(std::make_unique<Row>(this, data));
_count = _count.current() + 1;
}
void LinksController::prependRow(const ChatLinkData &data) {
delegate()->peerListPrependRow(std::make_unique<Row>(this, data));
_count = _count.current() + 1;
}
void LinksController::updateRow(const ChatLinkData &data) {
if (const auto row = delegate()->peerListFindRow(ComputeRowId(data))) {
const auto real = static_cast<Row*>(row);
real->update(data);
delegate()->peerListUpdateRow(row);
}
}
bool LinksController::removeRow(const QString &link) {
const auto id = UniqueRowIdFromString(link);
if (const auto row = delegate()->peerListFindRow(id)) {
delegate()->peerListRemoveRow(row);
_count = std::max(_count.current() - 1, 0);
return true;
}
return false;
}
not_null<Main::Session*> LinksController::rowSession() {
return _session;
}
void LinksController::rowUpdateRow(not_null<Row*> row) {
delegate()->peerListUpdateRow(row);
}
void LinksController::rowPaintIcon(
QPainter &p,
int x,
int y,
int size) {
const auto skip = st::inviteLinkIconSkip;
const auto inner = size - 2 * skip;
const auto bg = &st::msgFile1Bg;
if (_icon.isNull()) {
_icon = QImage(
QSize(inner, inner) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
_icon.fill(Qt::transparent);
_icon.setDevicePixelRatio(style::DevicePixelRatio());
auto p = QPainter(&_icon);
p.setPen(Qt::NoPen);
p.setBrush(*bg);
{
auto hq = PainterHighQualityEnabler(p);
auto rect = QRect(0, 0, inner, inner);
p.drawEllipse(rect);
}
st::inviteLinkIcon.paintInCenter(p, Rect(Size(inner)));
}
p.drawImage(x + skip, y + skip, _icon);
}
ChatLinks::ChatLinks(
QWidget *parent,
not_null<Window::SessionController*> controller)
: BusinessSection(parent, controller)
, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
setupContent(controller);
}
ChatLinks::~ChatLinks() = default;
rpl::producer<QString> ChatLinks::title() {
return tr::lng_chat_links_title();
}
void ChatLinks::setupContent(
not_null<Window::SessionController*> controller) {
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
AddDividerTextWithLottie(content, {
.lottie = u"chat_link"_q,
.lottieSize = st::settingsCloudPasswordIconSize,
.lottieMargins = st::peerAppearanceIconPadding,
.showFinished = showFinishes() | rpl::take(1),
.about = tr::lng_chat_links_about(tr::marked),
.aboutMargins = st::peerAppearanceCoverLabelMargin,
});
Ui::AddSkip(content);
const auto limit = controller->session().appConfig().get<int>(
u"business_chat_links_limit"_q,
100);
const auto add = content->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
content,
MakeCreateLinkButton(
content,
tr::lng_chat_links_create_link()))
)->setDuration(0);
const auto list = AddLinksList(controller, content);
add->toggleOn(list.controller->fullCountValue() | rpl::map(_1 < limit));
add->finishAnimating();
add->entity()->setClickedCallback([=] {
if (!controller->session().premium()) {
ShowPremiumPreviewToBuy(
controller,
PremiumFeature::ChatLinks);
return;
}
const auto submitting = std::make_shared<bool>();
const auto submit = [=](ChatLinkData data, Fn<void()> close) {
if (std::exchange(*submitting, true)) {
return;
}
const auto done = [=](const auto&) {
controller->showToast(tr::lng_chat_link_saved(tr::now));
close();
};
controller->session().api().chatLinks().create(
data.title,
data.message,
done);
};
controller->show(Box(
EditChatLinkBox,
controller,
ChatLinkData(),
crl::guard(this, submit)));
});
Ui::AddSkip(content);
const auto self = controller->session().user();
const auto username = self->username();
const auto make = [&](std::vector<QString> links) {
Expects(!links.empty());
for (auto &link : links) {
link = controller->session().createInternalLink(link);
}
return (links.size() > 1)
? tr::lng_chat_links_footer_both(
tr::now,
lt_username,
tr::link(links[0], "https://" + links[0]),
lt_link,
tr::link(links[1], "https://" + links[1]),
tr::marked)
: tr::link(links[0], "https://" + links[0]);
};
auto links = !username.isEmpty()
? make({ username, '+' + self->phone() })
: make({ '+' + self->phone() });
auto label = object_ptr<Ui::FlatLabel>(
content,
tr::lng_chat_links_footer(
lt_links,
rpl::single(std::move(links)),
tr::marked),
st::boxDividerLabel);
label->setClickHandlerFilter([=](ClickHandlerPtr handler, auto) {
QGuiApplication::clipboard()->setText(handler->url());
controller->showToast(tr::lng_chat_link_copied(tr::now));
return false;
});
content->add(object_ptr<Ui::DividerLabel>(
content,
std::move(label),
st::settingsChatbotsBottomTextMargin,
st::defaultDividerBar,
RectPart::Top));
Ui::ResizeFitChild(this, content);
}
} // namespace
Type ChatLinksId() {
return ChatLinks::Id();
}
} // namespace Settings

View File

@@ -0,0 +1,16 @@
/*
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 "settings/settings_type.h"
namespace Settings {
[[nodiscard]] Type ChatLinksId();
} // namespace Settings

View File

@@ -0,0 +1,578 @@
/*
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 "settings/business/settings_chatbots.h"
#include "apiwrap.h"
#include "boxes/peers/edit_peer_permissions_box.h"
#include "boxes/peers/prepare_short_info_box.h"
#include "boxes/peer_list_box.h"
#include "core/application.h"
#include "data/business/data_business_chatbots.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/business/settings_recipients_helper.h"
#include "ui/boxes/confirm_box.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace Settings {
namespace {
constexpr auto kDebounceTimeout = crl::time(400);
enum class LookupState {
Empty,
Loading,
Unsupported,
Ready,
};
struct BotState {
UserData *bot = nullptr;
LookupState state = LookupState::Empty;
};
[[nodiscard]] constexpr Data::ChatbotsPermissions Defaults() {
return Data::ChatbotsPermission::ViewMessages;
}
class Chatbots final : public BusinessSection<Chatbots> {
public:
Chatbots(
QWidget *parent,
not_null<Window::SessionController*> controller);
~Chatbots();
[[nodiscard]] bool closeByOutsideClick() const override;
[[nodiscard]] rpl::producer<QString> title() override;
const Ui::RoundRect *bottomSkipRounding() const override {
return _detailsWrap->count() ? nullptr : &_bottomSkipRounding;
}
private:
void setupContent();
void refreshDetails();
void save();
Ui::RoundRect _bottomSkipRounding;
Ui::VerticalLayout *_detailsWrap = nullptr;
rpl::variable<Data::BusinessRecipients> _recipients;
rpl::variable<QString> _usernameValue;
rpl::variable<BotState> _botValue;
rpl::variable<Data::ChatbotsPermissions> _permissions = Defaults();
Fn<Data::ChatbotsPermissions()> _resolvePermissions;
};
class PreviewController final : public PeerListController {
public:
PreviewController(not_null<PeerData*> peer, Fn<void()> resetBot);
void prepare() override;
void loadMoreRows() override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowRightActionClicked(not_null<PeerListRow*> row) override;
Main::Session &session() const override;
private:
const not_null<PeerData*> _peer;
const Fn<void()> _resetBot;
rpl::lifetime _lifetime;
};
class PreviewRow final : public PeerListRow {
public:
using PeerListRow::PeerListRow;
QSize rightActionSize() const override;
QMargins rightActionMargins() const override;
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
void rightActionAddRipple(
QPoint point,
Fn<void()> updateCallback) override;
void rightActionStopLastRipple() override;
private:
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
};
QSize PreviewRow::rightActionSize() const {
return QSize(
st::settingsChatbotsDeleteIcon.width(),
st::settingsChatbotsDeleteIcon.height()) * 2;
}
QMargins PreviewRow::rightActionMargins() const {
const auto itemHeight = st::peerListSingleRow.item.height;
const auto skip = (itemHeight - rightActionSize().height()) / 2;
return QMargins(0, skip, skip, 0);
}
void PreviewRow::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
if (_actionRipple) {
_actionRipple->paint(p, x, y, outerWidth);
if (_actionRipple->empty()) {
_actionRipple.reset();
}
}
const auto rect = QRect(QPoint(x, y), PreviewRow::rightActionSize());
(actionSelected
? st::settingsChatbotsDeleteIconOver
: st::settingsChatbotsDeleteIcon).paintInCenter(p, rect);
}
void PreviewRow::rightActionAddRipple(
QPoint point,
Fn<void()> updateCallback) {
if (!_actionRipple) {
auto mask = Ui::RippleAnimation::EllipseMask(rightActionSize());
_actionRipple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
std::move(updateCallback));
}
_actionRipple->add(point);
}
void PreviewRow::rightActionStopLastRipple() {
if (_actionRipple) {
_actionRipple->lastStop();
}
}
PreviewController::PreviewController(
not_null<PeerData*> peer,
Fn<void()> resetBot)
: _peer(peer)
, _resetBot(std::move(resetBot)) {
}
void PreviewController::prepare() {
delegate()->peerListAppendRow(std::make_unique<PreviewRow>(_peer));
delegate()->peerListRefreshRows();
}
void PreviewController::loadMoreRows() {
}
void PreviewController::rowClicked(not_null<PeerListRow*> row) {
}
void PreviewController::rowRightActionClicked(not_null<PeerListRow*> row) {
_resetBot();
}
Main::Session &PreviewController::session() const {
return _peer->session();
}
[[nodiscard]] rpl::producer<QString> DebouncedValue(
not_null<Ui::InputField*> field) {
return [=](auto consumer) {
auto result = rpl::lifetime();
struct State {
base::Timer timer;
QString lastText;
};
const auto state = result.make_state<State>();
const auto push = [=] {
state->timer.cancel();
consumer.put_next_copy(state->lastText);
};
state->timer.setCallback(push);
state->lastText = field->getLastText();
consumer.put_next_copy(field->getLastText());
field->changes() | rpl::on_next([=] {
const auto &text = field->getLastText();
const auto was = std::exchange(state->lastText, text);
if (std::abs(int(text.size()) - int(was.size())) == 1) {
state->timer.callOnce(kDebounceTimeout);
} else {
push();
}
}, result);
return result;
};
}
[[nodiscard]] QString ExtractUsername(QString text) {
text = text.trimmed();
if (text.startsWith(QChar('@'))) {
return text.mid(1);
}
static const auto expression = QRegularExpression(
"^(https://)?([a-zA-Z0-9\\.]+/)?([a-zA-Z0-9_\\.]+)");
const auto match = expression.match(text);
return match.hasMatch() ? match.captured(3) : text;
}
[[nodiscard]] rpl::producer<BotState> LookupBot(
not_null<Main::Session*> session,
rpl::producer<QString> usernameChanges) {
using Cache = base::flat_map<QString, UserData*>;
const auto cache = std::make_shared<Cache>();
return std::move(
usernameChanges
) | rpl::map([=](const QString &username) -> rpl::producer<BotState> {
const auto extracted = ExtractUsername(username);
const auto owner = &session->data();
static const auto expression = QRegularExpression(
"^[a-zA-Z0-9_\\.]+$");
if (!expression.match(extracted).hasMatch()) {
return rpl::single(BotState());
} else if (const auto peer = owner->peerByUsername(extracted)) {
if (const auto user = peer->asUser(); user && user->isBot()) {
if (user->botInfo->supportsBusiness) {
return rpl::single(BotState{
.bot = user,
.state = LookupState::Ready,
});
}
return rpl::single(BotState{
.state = LookupState::Unsupported,
});
}
return rpl::single(BotState{
.state = LookupState::Ready,
});
} else if (const auto i = cache->find(extracted); i != end(*cache)) {
return rpl::single(BotState{
.bot = i->second,
.state = LookupState::Ready,
});
}
return [=](auto consumer) {
auto result = rpl::lifetime();
const auto requestId = result.make_state<mtpRequestId>();
*requestId = session->api().request(MTPcontacts_ResolveUsername(
MTP_flags(0),
MTP_string(extracted),
MTP_string()
)).done([=](const MTPcontacts_ResolvedPeer &result) {
const auto &data = result.data();
session->data().processUsers(data.vusers());
session->data().processChats(data.vchats());
const auto peerId = peerFromMTP(data.vpeer());
const auto peer = session->data().peer(peerId);
if (const auto user = peer->asUser()) {
if (user->isBot()) {
cache->emplace(extracted, user);
consumer.put_next(BotState{
.bot = user,
.state = LookupState::Ready,
});
return;
}
}
cache->emplace(extracted, nullptr);
consumer.put_next(BotState{ .state = LookupState::Ready });
}).fail([=] {
cache->emplace(extracted, nullptr);
consumer.put_next(BotState{ .state = LookupState::Ready });
}).send();
result.add([=] {
session->api().request(*requestId).cancel();
});
return result;
};
}) | rpl::flatten_latest();
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeBotPreview(
not_null<Ui::RpWidget*> parent,
rpl::producer<BotState> state,
Fn<void()> resetBot) {
auto result = object_ptr<Ui::SlideWrap<>>(
parent.get(),
object_ptr<Ui::RpWidget>(parent.get()));
const auto raw = result.data();
const auto inner = raw->entity();
raw->hide(anim::type::instant);
const auto child = inner->lifetime().make_state<Ui::RpWidget*>(nullptr);
std::move(state) | rpl::filter([=](BotState state) {
return state.state != LookupState::Loading;
}) | rpl::on_next([=](BotState state) {
raw->toggle(
(state.state == LookupState::Ready
|| state.state == LookupState::Unsupported),
anim::type::normal);
if (state.bot) {
const auto delegate = parent->lifetime().make_state<
PeerListContentDelegateSimple
>();
const auto controller = parent->lifetime().make_state<
PreviewController
>(state.bot, resetBot);
controller->setStyleOverrides(&st::peerListSingleRow);
const auto content = Ui::CreateChild<PeerListContent>(
inner,
controller);
delegate->setContent(content);
controller->setDelegate(delegate);
delete base::take(*child);
*child = content;
} else if (state.state == LookupState::Ready
|| state.state == LookupState::Unsupported) {
const auto content = Ui::CreateChild<Ui::RpWidget>(inner);
const auto label = Ui::CreateChild<Ui::FlatLabel>(
content,
(state.state == LookupState::Unsupported
? tr::lng_chatbots_not_supported()
: tr::lng_chatbots_not_found()),
st::settingsChatbotsNotFound);
content->resize(
inner->width(),
st::peerListSingleRow.item.height);
rpl::combine(
content->sizeValue(),
label->sizeValue()
) | rpl::on_next([=](QSize size, QSize inner) {
label->move(
(size.width() - inner.width()) / 2,
(size.height() - inner.height()) / 2);
}, label->lifetime());
delete base::take(*child);
*child = content;
} else {
return;
}
(*child)->show();
inner->widthValue() | rpl::on_next([=](int width) {
(*child)->resizeToWidth(width);
}, (*child)->lifetime());
(*child)->heightValue() | rpl::on_next([=](int height) {
inner->resize(inner->width(), height + st::contactSkip);
}, inner->lifetime());
}, inner->lifetime());
raw->finishAnimating();
return result;
}
Chatbots::Chatbots(
QWidget *parent,
not_null<Window::SessionController*> controller)
: BusinessSection(parent, controller)
, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
setupContent();
}
Chatbots::~Chatbots() {
if (!Core::Quitting()) {
save();
}
}
bool Chatbots::closeByOutsideClick() const {
return false;
}
rpl::producer<QString> Chatbots::title() {
return tr::lng_chatbots_title();
}
void Chatbots::setupContent() {
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
const auto current = controller()->session().data().chatbots().current();
_recipients = Data::BusinessRecipients::MakeValid(current.recipients);
_permissions = current.permissions;
AddDividerTextWithLottie(content, {
.lottie = u"robot"_q,
.lottieSize = st::settingsCloudPasswordIconSize,
.lottieMargins = st::peerAppearanceIconPadding,
.showFinished = showFinishes(),
.about = tr::lng_chatbots_about(
lt_link,
tr::lng_chatbots_about_link(
tr::url(tr::lng_chatbots_info_url(tr::now))),
tr::marked),
.aboutMargins = st::peerAppearanceCoverLabelMargin,
});
const auto username = content->add(
object_ptr<Ui::InputField>(
content,
st::settingsChatbotsUsername,
tr::lng_chatbots_placeholder(),
(current.bot
? current.bot->session().createInternalLink(
current.bot->username())
: QString())),
st::settingsChatbotsUsernameMargins);
_usernameValue = DebouncedValue(username);
_botValue = rpl::single(BotState{
current.bot,
current.bot ? LookupState::Ready : LookupState::Empty
}) | rpl::then(
LookupBot(&controller()->session(), _usernameValue.changes())
);
const auto resetBot = [=] {
username->setText(QString());
username->setFocus();
_permissions = Defaults();
refreshDetails();
};
content->add(object_ptr<Ui::SlideWrap<Ui::RpWidget>>(
content,
MakeBotPreview(content, _botValue.value(), resetBot)));
Ui::AddDividerText(
content,
tr::lng_chatbots_add_about(),
st::peerAppearanceDividerTextMargin,
st::defaultDividerLabel,
RectPart::Top);
_detailsWrap = content->add(object_ptr<Ui::VerticalLayout>(content));
refreshDetails();
_botValue.changes() | rpl::on_next([=](const BotState &value) {
_permissions = Defaults();
refreshDetails();
}, lifetime());
Ui::ResizeFitChild(this, content);
}
void Chatbots::refreshDetails() {
_resolvePermissions = [=] {
return Data::ChatbotsPermissions();
};
while (_detailsWrap->count()) {
delete _detailsWrap->widgetAt(0);
}
const auto bot = _botValue.current().bot;
if (!bot) {
return;
}
const auto content = _detailsWrap;
AddBusinessRecipientsSelector(content, {
.controller = controller(),
.title = tr::lng_chatbots_access_title(),
.data = &_recipients,
.type = Data::BusinessRecipientsType::Bots,
});
Ui::AddSkip(content, st::settingsChatbotsAccessSkip);
Ui::AddDividerText(
content,
tr::lng_chatbots_exclude_about(),
st::peerAppearanceDividerTextMargin);
Ui::AddSkip(content);
Ui::AddSubsectionTitle(content, tr::lng_chatbots_permissions_title());
auto permissions = CreateEditChatbotPermissions(
content,
_permissions.current());
content->add(std::move(permissions.widget));
_resolvePermissions = permissions.value;
std::move(
permissions.changes
) | rpl::on_next([=](Data::ChatbotsPermissions now) {
const auto warn = [&](tr::phrase<lngtag_bot> text) {
controller()->show(Ui::MakeInformBox({
.text = text(tr::now, lt_bot, tr::bold(bot->name()), tr::rich),
.title = tr::lng_chatbots_warning_title(),
}));
};
const auto was = _permissions.current();
const auto diff = now ^ was;
const auto enabled = diff & now;
using Flag = Data::ChatbotsPermission;
if (enabled & (Flag::TransferGifts | Flag::SellGifts)) {
if (enabled & Flag::TransferStars) {
warn(tr::lng_chatbots_warning_both_text);
} else {
warn(tr::lng_chatbots_warning_gifts_text);
}
} else if (enabled & Flag::TransferStars) {
warn(tr::lng_chatbots_warning_stars_text);
} else if (enabled & Flag::EditUsername) {
warn(tr::lng_chatbots_warning_username_text);
}
_permissions = now;
}, lifetime());
Ui::AddSkip(content);
_detailsWrap->resizeToWidth(width());
}
void Chatbots::save() {
const auto show = controller()->uiShow();
const auto fail = [=](QString error) {
if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) {
show->showToast(tr::lng_greeting_recipients_empty(tr::now));
} else if (error == u"BOT_BUSINESS_MISSING"_q) {
show->showToast(tr::lng_chatbots_not_supported(tr::now));
}
};
controller()->session().data().chatbots().save({
.bot = _botValue.current().bot,
.recipients = _recipients.current(),
.permissions = _resolvePermissions(),
}, [=] {
}, fail);
}
} // namespace
Type ChatbotsId() {
return Chatbots::Id();
}
} // namespace Settings

View File

@@ -0,0 +1,16 @@
/*
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 "settings/settings_type.h"
namespace Settings {
[[nodiscard]] Type ChatbotsId();
} // namespace Settings

View File

@@ -0,0 +1,293 @@
/*
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 "settings/business/settings_greeting.h"
#include "base/event_filter.h"
#include "core/application.h"
#include "data/business/data_business_info.h"
#include "data/business/data_shortcut_messages.h"
#include "data/data_session.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/business/settings_shortcut_messages.h"
#include "settings/business/settings_recipients_helper.h"
#include "ui/boxes/time_picker_box.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/box_content_divider.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/vertical_drum_picker.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace Settings {
namespace {
constexpr auto kDefaultNoActivityDays = 7;
class Greeting : public BusinessSection<Greeting> {
public:
Greeting(
QWidget *parent,
not_null<Window::SessionController*> controller);
~Greeting();
[[nodiscard]] bool closeByOutsideClick() const override;
[[nodiscard]] rpl::producer<QString> title() override;
const Ui::RoundRect *bottomSkipRounding() const override {
return &_bottomSkipRounding;
}
private:
void setupContent(not_null<Window::SessionController*> controller);
void save();
Ui::RoundRect _bottomSkipRounding;
rpl::variable<Data::BusinessRecipients> _recipients;
rpl::variable<bool> _canHave;
rpl::event_stream<> _deactivateOnAttempt;
rpl::variable<int> _noActivityDays;
rpl::variable<bool> _enabled;
};
Greeting::Greeting(
QWidget *parent,
not_null<Window::SessionController*> controller)
: BusinessSection(parent, controller)
, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
setupContent(controller);
}
void EditPeriodBox(
not_null<Ui::GenericBox*> box,
int days,
Fn<void(int)> save) {
auto values = std::vector{ 7, 14, 21, 28 };
if (!ranges::contains(values, days)) {
values.push_back(days);
ranges::sort(values);
}
const auto phrases = ranges::views::all(
values
) | ranges::views::transform([](int days) {
return tr::lng_days(tr::now, lt_count, days);
}) | ranges::to_vector;
const auto take = TimePickerBox(box, values, phrases, days);
box->addButton(tr::lng_settings_save(), [=] {
const auto weak = base::make_weak(box);
save(take());
if (const auto strong = weak.get()) {
strong->closeBox();
}
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
Greeting::~Greeting() {
if (!Core::Quitting()) {
save();
}
}
bool Greeting::closeByOutsideClick() const {
return false;
}
rpl::producer<QString> Greeting::title() {
return tr::lng_greeting_title();
}
void Greeting::setupContent(
not_null<Window::SessionController*> controller) {
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
const auto info = &controller->session().data().businessInfo();
const auto current = info->greetingSettings();
const auto disabled = !current.noActivityDays;
_recipients = disabled
? Data::BusinessRecipients{ .allButExcluded = true }
: Data::BusinessRecipients::MakeValid(current.recipients);
_noActivityDays = disabled
? kDefaultNoActivityDays
: current.noActivityDays;
AddDividerTextWithLottie(content, {
.lottie = u"greeting"_q,
.lottieSize = st::settingsCloudPasswordIconSize,
.lottieMargins = st::peerAppearanceIconPadding,
.showFinished = showFinishes(),
.about = tr::lng_greeting_about(tr::marked),
.aboutMargins = st::peerAppearanceCoverLabelMargin,
});
const auto session = &controller->session();
_canHave = rpl::combine(
ShortcutsCountValue(session),
ShortcutsLimitValue(session),
ShortcutExistsValue(session, u"hello"_q),
(_1 < _2) || _3);
Ui::AddSkip(content);
const auto enabled = content->add(object_ptr<Ui::SettingsButton>(
content,
tr::lng_greeting_enable(),
st::settingsButtonNoIcon
))->toggleOn(rpl::single(
!disabled
) | rpl::then(rpl::merge(
_canHave.value() | rpl::filter(!_1),
_deactivateOnAttempt.events() | rpl::map_to(false)
)));
_enabled = enabled->toggledValue();
_enabled.value() | rpl::filter(_1) | rpl::on_next([=] {
if (!_canHave.current()) {
controller->showToast({
.text = { tr::lng_greeting_limit_reached(tr::now) },
.adaptive = true,
});
_deactivateOnAttempt.fire({});
}
}, lifetime());
Ui::AddSkip(content);
content->add(
object_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(
content,
object_ptr<Ui::BoxContentDivider>(
content,
st::boxDividerHeight,
st::defaultDividerBar,
RectPart::Top))
)->setDuration(0)->toggleOn(enabled->toggledValue() | rpl::map(!_1));
content->add(
object_ptr<Ui::SlideWrap<Ui::BoxContentDivider>>(
content,
object_ptr<Ui::BoxContentDivider>(
content))
)->setDuration(0)->toggleOn(enabled->toggledValue());
const auto wrap = content->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
content,
object_ptr<Ui::VerticalLayout>(content)));
const auto inner = wrap->entity();
const auto createWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto createInner = createWrap->entity();
Ui::AddSkip(createInner);
const auto create = AddButtonWithLabel(
createInner,
rpl::conditional(
ShortcutExistsValue(session, u"hello"_q),
tr::lng_business_edit_messages(),
tr::lng_greeting_create()),
ShortcutMessagesCountValue(
session,
u"hello"_q
) | rpl::map([=](int count) {
return count
? tr::lng_forum_messages(tr::now, lt_count, count)
: QString();
}),
st::settingsButtonLightNoIcon);
create->setClickedCallback([=] {
const auto owner = &controller->session().data();
const auto id = owner->shortcutMessages().emplaceShortcut("hello");
showOther(ShortcutMessagesId(id));
});
Ui::AddSkip(createInner);
Ui::AddDivider(createInner);
createWrap->toggleOn(rpl::single(true));
Ui::AddSkip(inner);
AddBusinessRecipientsSelector(inner, {
.controller = controller,
.title = tr::lng_greeting_recipients(),
.data = &_recipients,
.type = Data::BusinessRecipientsType::Messages,
});
Ui::AddSkip(inner);
Ui::AddDivider(inner);
Ui::AddSkip(inner);
AddButtonWithLabel(
inner,
tr::lng_greeting_period_title(),
_noActivityDays.value(
) | rpl::map(
[](int days) { return tr::lng_days(tr::now, lt_count, days); }
),
st::settingsButtonNoIcon
)->setClickedCallback([=] {
controller->show(Box(
EditPeriodBox,
_noActivityDays.current(),
[=](int days) { _noActivityDays = days; }));
});
Ui::AddSkip(inner);
Ui::AddDividerText(
inner,
tr::lng_greeting_period_about(),
st::settingsChatbotsBottomTextMargin,
st::defaultDividerLabel,
RectPart::Top);
wrap->toggleOn(enabled->toggledValue());
wrap->finishAnimating();
Ui::ResizeFitChild(this, content);
}
void Greeting::save() {
const auto show = controller()->uiShow();
const auto session = &controller()->session();
const auto fail = [=](QString error) {
if (error == u"BUSINESS_RECIPIENTS_EMPTY"_q) {
show->showToast(tr::lng_greeting_recipients_empty(tr::now));
} else if (error != u"SHORTCUT_INVALID"_q) {
show->showToast(error);
}
};
session->data().businessInfo().saveGreetingSettings(
_enabled.current() ? Data::GreetingSettings{
.recipients = _recipients.current(),
.noActivityDays = _noActivityDays.current(),
.shortcutId = LookupShortcutId(session, u"hello"_q),
} : Data::GreetingSettings(),
fail);
}
} // namespace
Type GreetingId() {
return Greeting::Id();
}
} // namespace Settings

View File

@@ -0,0 +1,16 @@
/*
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 "settings/settings_type.h"
namespace Settings {
[[nodiscard]] Type GreetingId();
} // namespace Settings

View File

@@ -0,0 +1,300 @@
/*
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 "settings/business/settings_location.h"
#include "core/application.h"
#include "core/shortcuts.h"
#include "data/business/data_business_info.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "settings/business/settings_recipients_helper.h"
#include "settings/settings_common.h"
#include "storage/storage_account.h"
#include "ui/controls/location_picker.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
namespace Settings {
namespace {
class Location : public BusinessSection<Location> {
public:
Location(
QWidget *parent,
not_null<Window::SessionController*> controller);
~Location();
[[nodiscard]] rpl::producer<QString> title() override;
const Ui::RoundRect *bottomSkipRounding() const override {
return mapSupported() ? nullptr : &_bottomSkipRounding;
}
private:
void setupContent(not_null<Window::SessionController*> controller);
void save();
void setupPicker(not_null<Ui::VerticalLayout*> content);
void setupUnsupported(not_null<Ui::VerticalLayout*> content);
[[nodiscard]] bool mapSupported() const;
void chooseOnMap();
const Ui::LocationPickerConfig _config;
rpl::variable<Data::BusinessLocation> _data;
rpl::variable<Data::CloudImage*> _map = nullptr;
base::weak_ptr<Ui::LocationPicker> _picker;
std::shared_ptr<QImage> _view;
Ui::RoundRect _bottomSkipRounding;
};
[[nodiscard]] Ui::LocationPickerConfig ResolveBusinessMapsConfig(
not_null<Main::Session*> session) {
const auto &appConfig = session->appConfig();
auto map = appConfig.get<base::flat_map<QString, QString>>(
u"tdesktop_config_map"_q,
base::flat_map<QString, QString>());
return {
.mapsToken = map[u"bmaps"_q],
.geoToken = map[u"bgeo"_q],
};
}
Location::Location(
QWidget *parent,
not_null<Window::SessionController*> controller)
: BusinessSection(parent, controller)
, _config(ResolveBusinessMapsConfig(&controller->session()))
, _bottomSkipRounding(st::boxRadius, st::boxDividerBg) {
setupContent(controller);
}
Location::~Location() {
if (!Core::Quitting()) {
save();
}
}
rpl::producer<QString> Location::title() {
return tr::lng_location_title();
}
void Location::setupContent(
not_null<Window::SessionController*> controller) {
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
if (mapSupported()) {
setupPicker(content);
} else {
setupUnsupported(content);
}
Ui::ResizeFitChild(this, content);
}
void Location::setupPicker(not_null<Ui::VerticalLayout*> content) {
_data = controller()->session().user()->businessDetails().location;
AddDividerTextWithLottie(content, {
.lottie = u"location"_q,
.lottieSize = st::settingsCloudPasswordIconSize,
.lottieMargins = st::peerAppearanceIconPadding,
.showFinished = showFinishes(),
.about = tr::lng_location_about(tr::marked),
.aboutMargins = st::peerAppearanceCoverLabelMargin,
});
const auto address = content->add(
object_ptr<Ui::InputField>(
content,
st::settingsLocationAddress,
Ui::InputField::Mode::MultiLine,
tr::lng_location_address(),
_data.current().address),
st::settingsChatbotsUsernameMargins);
_data.value(
) | rpl::on_next([=](const Data::BusinessLocation &location) {
address->setText(location.address);
}, address->lifetime());
address->changes() | rpl::on_next([=] {
auto copy = _data.current();
copy.address = address->getLastText();
_data = std::move(copy);
}, address->lifetime());
AddDivider(content);
AddSkip(content);
const auto maptoggle = AddButtonWithIcon(
content,
tr::lng_location_set_map(),
st::settingsButton,
{ &st::menuIconAddress }
)->toggleOn(_data.value(
) | rpl::map([](const Data::BusinessLocation &location) {
return location.point.has_value();
}));
maptoggle->toggledValue() | rpl::on_next([=](bool toggled) {
if (!toggled) {
auto copy = _data.current();
if (copy.point.has_value()) {
copy.point = std::nullopt;
_data = std::move(copy);
}
} else if (!_data.current().point.has_value()) {
_data.force_assign(_data.current());
chooseOnMap();
}
}, maptoggle->lifetime());
const auto mapSkip = st::defaultVerticalListSkip;
const auto mapWrap = content->add(
object_ptr<Ui::SlideWrap<Ui::AbstractButton>>(
content,
object_ptr<Ui::AbstractButton>(content),
st::boxRowPadding + QMargins(0, mapSkip, 0, mapSkip)));
mapWrap->toggle(_data.current().point.has_value(), anim::type::instant);
const auto map = mapWrap->entity();
map->resize(map->width(), st::locationSize.height());
_data.value(
) | rpl::on_next([=](const Data::BusinessLocation &location) {
const auto image = location.point.has_value()
? controller()->session().data().location(*location.point).get()
: nullptr;
if (image) {
image->load(&controller()->session(), {});
_view = image->createView();
}
mapWrap->toggle(image != nullptr, anim::type::normal);
_map = image;
}, mapWrap->lifetime());
map->paintRequest() | rpl::on_next([=] {
auto p = QPainter(map);
const auto left = (map->width() - st::locationSize.width()) / 2;
const auto rect = QRect(QPoint(left, 0), st::locationSize);
const auto &image = _view ? *_view : QImage();
if (!image.isNull()) {
p.drawImage(rect, image);
}
const auto paintMarker = [&](const style::icon &icon) {
icon.paint(
p,
rect.x() + ((rect.width() - icon.width()) / 2),
rect.y() + (rect.height() / 2) - icon.height(),
width());
};
paintMarker(st::historyMapPoint);
paintMarker(st::historyMapPointInner);
}, map->lifetime());
controller()->session().downloaderTaskFinished(
) | rpl::on_next([=] {
map->update();
}, map->lifetime());
map->setClickedCallback([=] {
chooseOnMap();
});
showFinishes() | rpl::on_next([=] {
address->setFocus();
}, address->lifetime());
}
void Location::chooseOnMap() {
if (const auto strong = _picker.get()) {
strong->activate();
return;
}
const auto callback = [=](Data::InputVenue venue) {
auto copy = _data.current();
copy.point = Data::LocationPoint(
venue.lat,
venue.lon,
Data::LocationPoint::NoAccessHash);
copy.address = venue.address;
_data = std::move(copy);
};
const auto session = &controller()->session();
const auto current = _data.current().point;
const auto initial = current
? Core::GeoLocation{
.point = { current->lat(), current->lon() },
.accuracy = Core::GeoLocationAccuracy::Exact,
}
: Core::GeoLocation();
_picker = Ui::LocationPicker::Show({
.parent = controller()->widget(),
.config = _config,
.chooseLabel = tr::lng_maps_point_set(),
.session = session,
.initial = initial,
.callback = crl::guard(this, callback),
.quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); },
.storageId = session->local().resolveStorageIdBots(),
.closeRequests = death(),
});
}
void Location::setupUnsupported(not_null<Ui::VerticalLayout*> content) {
AddDividerTextWithLottie(content, {
.lottie = u"phone"_q,
.lottieSize = st::settingsCloudPasswordIconSize,
.lottieMargins = st::peerAppearanceIconPadding,
.showFinished = showFinishes(),
.about = tr::lng_location_fallback(tr::marked),
.aboutMargins = st::peerAppearanceCoverLabelMargin,
.parts = RectPart::Top,
});
}
void Location::save() {
const auto fail = [=](QString error) {
};
auto value = _data.current();
value.address = value.address.trimmed();
controller()->session().data().businessInfo().saveLocation(value, fail);
}
bool Location::mapSupported() const {
return Ui::LocationPicker::Available(_config);
}
} // namespace
Type LocationId() {
return Location::Id();
}
} // namespace Settings

View File

@@ -0,0 +1,16 @@
/*
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 "settings/settings_type.h"
namespace Settings {
[[nodiscard]] Type LocationId();
} // namespace Settings

View File

@@ -0,0 +1,238 @@
/*
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 "settings/business/settings_quick_replies.h"
#include "boxes/premium_preview_box.h"
#include "core/application.h"
#include "data/business/data_shortcut_messages.h"
#include "data/data_session.h"
#include "lang/lang_keys.h"
#include "main/main_account.h"
#include "main/main_session.h"
#include "settings/business/settings_recipients_helper.h"
#include "settings/business/settings_shortcut_messages.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace Settings {
namespace {
constexpr auto kShortcutLimit = 32;
class QuickReplies : public BusinessSection<QuickReplies> {
public:
QuickReplies(
QWidget *parent,
not_null<Window::SessionController*> controller);
~QuickReplies();
[[nodiscard]] rpl::producer<QString> title() override;
private:
void setupContent(not_null<Window::SessionController*> controller);
rpl::variable<int> _count;
};
QuickReplies::QuickReplies(
QWidget *parent,
not_null<Window::SessionController*> controller)
: BusinessSection(parent, controller) {
setupContent(controller);
}
QuickReplies::~QuickReplies() = default;
rpl::producer<QString> QuickReplies::title() {
return tr::lng_replies_title();
}
void QuickReplies::setupContent(
not_null<Window::SessionController*> controller) {
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
AddDividerTextWithLottie(content, {
.lottie = u"writing"_q,
.lottieSize = st::settingsCloudPasswordIconSize,
.lottieMargins = st::peerAppearanceIconPadding,
.showFinished = showFinishes(),
.about = tr::lng_replies_about(tr::marked),
.aboutMargins = st::peerAppearanceCoverLabelMargin,
});
Ui::AddSkip(content);
const auto addWrap = content->add(
object_ptr<Ui::VerticalLayout>(content));
const auto owner = &controller->session().data();
const auto messages = &owner->shortcutMessages();
rpl::combine(
_count.value(),
ShortcutsLimitValue(&controller->session())
) | rpl::on_next([=](int count, int limit) {
while (addWrap->count()) {
delete addWrap->widgetAt(0);
}
if (count < limit) {
const auto add = addWrap->add(object_ptr<Ui::SettingsButton>(
addWrap,
tr::lng_replies_add(),
st::settingsButtonNoIcon
));
add->setClickedCallback([=] {
if (!controller->session().premium()) {
ShowPremiumPreviewToBuy(
controller,
PremiumFeature::QuickReplies);
return;
}
const auto submit = [=](QString name, Fn<void()> close) {
const auto id = messages->emplaceShortcut(name);
showOther(ShortcutMessagesId(id));
close();
};
controller->show(Box(
EditShortcutNameBox,
QString(),
crl::guard(this, submit)));
});
if (count > 0) {
AddSkip(addWrap);
AddDivider(addWrap);
AddSkip(addWrap);
}
}
if (const auto width = content->width()) {
content->resizeToWidth(width);
}
}, lifetime());
const auto inner = content->add(
object_ptr<Ui::VerticalLayout>(content));
rpl::single(rpl::empty) | rpl::then(
messages->shortcutsChanged()
) | rpl::on_next([=] {
auto old = inner->count();
const auto &shortcuts = messages->shortcuts();
for (const auto &[_, shortcut]
: shortcuts.list | ranges::views::reverse) {
if (!shortcut.count) {
continue;
}
const auto name = shortcut.name;
AddButtonWithLabel(
inner,
rpl::single('/' + name),
tr::lng_forum_messages(
lt_count,
rpl::single(1. * shortcut.count)),
st::settingsButtonNoIcon
)->setClickedCallback([=] {
const auto id = messages->emplaceShortcut(name);
showOther(ShortcutMessagesId(id));
});
if (old) {
delete inner->widgetAt(0);
--old;
}
}
while (old--) {
delete inner->widgetAt(0);
}
_count = inner->count();
}, content->lifetime());
Ui::ResizeFitChild(this, content);
}
[[nodiscard]] bool ValidShortcutName(const QString &name) {
if (name.isEmpty() || name.size() > kShortcutLimit) {
return false;
}
for (const auto &ch : name) {
if (!ch.isLetterOrNumber()
&& (ch != QChar('_'))
&& (ch.unicode() != 0x200c)
&& (ch.unicode() != 0x00b7)
&& (ch.unicode() < 0x0d80 || ch.unicode() > 0x0dff)) {
return false;
}
}
return true;
}
} // namespace
Type QuickRepliesId() {
return QuickReplies::Id();
}
void EditShortcutNameBox(
not_null<Ui::GenericBox*> box,
QString name,
Fn<void(QString, Fn<void()>)> submit) {
name = name.trimmed();
const auto editing = !name.isEmpty();
box->setTitle(editing
? tr::lng_replies_edit_title()
: tr::lng_replies_add_title());
box->addRow(object_ptr<Ui::FlatLabel>(
box,
(editing
? tr::lng_replies_edit_about()
: tr::lng_replies_add_shortcut()),
st::settingsAddReplyLabel));
const auto field = box->addRow(object_ptr<Ui::InputField>(
box,
st::settingsAddReplyField,
tr::lng_replies_add_placeholder(),
name));
box->setFocusCallback([=] {
field->setFocusFast();
});
field->selectAll();
field->setMaxLength(kShortcutLimit * 2);
Ui::AddLengthLimitLabel(field, kShortcutLimit);
const auto callback = [=] {
const auto name = field->getLastText().trimmed();
if (!ValidShortcutName(name)) {
field->showError();
} else {
submit(name, [weak = base::make_weak(box)] {
if (const auto strong = weak.get()) {
strong->closeBox();
}
});
}
};
field->submits(
) | rpl::on_next(callback, field->lifetime());
box->addButton(tr::lng_settings_save(), callback);
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
} // namespace Settings

View File

@@ -0,0 +1,25 @@
/*
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 "settings/settings_type.h"
namespace Ui {
class GenericBox;
} // namespace Ui
namespace Settings {
[[nodiscard]] Type QuickRepliesId();
void EditShortcutNameBox(
not_null<Ui::GenericBox*> box,
QString name,
Fn<void(QString, Fn<void()>)> submit);
} // namespace Settings

View File

@@ -0,0 +1,433 @@
/*
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 "settings/business/settings_recipients_helper.h"
#include "boxes/filters/edit_filter_chats_list.h"
#include "boxes/filters/edit_filter_chats_preview.h"
#include "data/business/data_shortcut_messages.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "settings/settings_common.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_settings.h"
namespace Settings {
namespace {
constexpr auto kAllExcept = 0;
constexpr auto kSelectedOnly = 1;
using Flag = Data::ChatFilter::Flag;
using Flags = Data::ChatFilter::Flags;
[[nodiscard]] Flags TypesToFlags(Data::BusinessChatTypes types) {
using Type = Data::BusinessChatType;
return ((types & Type::Contacts) ? Flag::Contacts : Flag())
| ((types & Type::NonContacts) ? Flag::NonContacts : Flag())
| ((types & Type::NewChats) ? Flag::NewChats : Flag())
| ((types & Type::ExistingChats) ? Flag::ExistingChats : Flag());
}
[[nodiscard]] Data::BusinessChatTypes FlagsToTypes(Flags flags) {
using Type = Data::BusinessChatType;
return ((flags & Flag::Contacts) ? Type::Contacts : Type())
| ((flags & Flag::NonContacts) ? Type::NonContacts : Type())
| ((flags & Flag::NewChats) ? Type::NewChats : Type())
| ((flags & Flag::ExistingChats) ? Type::ExistingChats : Type());
}
} // namespace
void EditBusinessChats(
not_null<Window::SessionController*> window,
BusinessChatsDescriptor &&descriptor) {
const auto session = &window->session();
const auto options = Flag::ExistingChats
| Flag::NewChats
| Flag::Contacts
| Flag::NonContacts;
auto &&peers = descriptor.current.list | ranges::views::transform([=](
not_null<UserData*> user) {
return user->owner().history(user);
});
auto controller = std::make_unique<EditFilterChatsListController>(
session,
(descriptor.include
? tr::lng_filters_include_title()
: tr::lng_filters_exclude_title()),
(descriptor.usersOnly ? Flag() : options),
TypesToFlags(descriptor.current.types) & options,
base::flat_set<not_null<History*>>(begin(peers), end(peers)),
100,
nullptr);
const auto rawController = controller.get();
const auto save = descriptor.save;
auto initBox = [=](not_null<PeerListBox*> box) {
box->setCloseByOutsideClick(false);
box->addButton(tr::lng_settings_save(), crl::guard(box, [=] {
const auto peers = box->collectSelectedRows();
auto &&users = ranges::views::all(
peers
) | ranges::views::transform([=](not_null<PeerData*> peer) {
return not_null(peer->asUser());
}) | ranges::to_vector;
save(Data::BusinessChats{
.types = FlagsToTypes(rawController->chosenOptions()),
.list = std::move(users),
});
box->closeBox();
}));
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
};
window->show(
Box<PeerListBox>(std::move(controller), std::move(initBox)));
}
not_null<FilterChatsPreview*> SetupBusinessChatsPreview(
not_null<Ui::VerticalLayout*> container,
not_null<rpl::variable<Data::BusinessChats>*> data) {
const auto rules = data->current();
const auto locked = std::make_shared<bool>();
auto &&peers = data->current().list | ranges::views::transform([=](
not_null<UserData*> user) {
return user->owner().history(user);
});
const auto preview = container->add(object_ptr<FilterChatsPreview>(
container,
TypesToFlags(data->current().types),
base::flat_set<not_null<History*>>(begin(peers), end(peers))));
preview->flagRemoved(
) | rpl::on_next([=](Flag flag) {
*locked = true;
*data = Data::BusinessChats{
data->current().types & ~FlagsToTypes(flag),
data->current().list
};
*locked = false;
}, preview->lifetime());
preview->peerRemoved(
) | rpl::on_next([=](not_null<History*> history) {
auto list = data->current().list;
list.erase(
ranges::remove(list, not_null(history->peer->asUser())),
end(list));
*locked = true;
*data = Data::BusinessChats{
data->current().types,
std::move(list)
};
*locked = false;
}, preview->lifetime());
data->changes(
) | rpl::filter([=] {
return !*locked;
}) | rpl::on_next([=](const Data::BusinessChats &rules) {
auto &&peers = rules.list | ranges::views::transform([=](
not_null<UserData*> user) {
return user->owner().history(user);
});
preview->updateData(
TypesToFlags(rules.types),
base::flat_set<not_null<History*>>(begin(peers), end(peers)));
}, preview->lifetime());
return preview;
}
void AddBusinessRecipientsSelector(
not_null<Ui::VerticalLayout*> container,
BusinessRecipientsSelectorDescriptor &&descriptor) {
Ui::AddSkip(container);
Ui::AddSubsectionTitle(container, std::move(descriptor.title));
auto &lifetime = container->lifetime();
const auto controller = descriptor.controller;
const auto data = descriptor.data;
const auto includeWithExcluded = (descriptor.type
== Data::BusinessRecipientsType::Bots);
const auto change = [=](Fn<void(Data::BusinessRecipients&)> modify) {
auto now = data->current();
modify(now);
*data = std::move(now);
};
const auto &current = data->current();
const auto all = current.allButExcluded || current.included.empty();
const auto group = std::make_shared<Ui::RadiobuttonGroup>(
all ? kAllExcept : kSelectedOnly);
container->add(
object_ptr<Ui::Radiobutton>(
container,
group,
kAllExcept,
tr::lng_chatbots_all_except(tr::now),
st::settingsChatbotsAccess),
st::settingsChatbotsAccessMargins);
container->add(
object_ptr<Ui::Radiobutton>(
container,
group,
kSelectedOnly,
tr::lng_chatbots_selected(tr::now),
st::settingsChatbotsAccess),
st::settingsChatbotsAccessMargins);
Ui::AddSkip(container, st::settingsChatbotsAccessSkip);
Ui::AddDivider(container);
const auto includeWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container))
)->setDuration(0);
const auto excludeWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container))
)->setDuration(0);
const auto excludeInner = excludeWrap->entity();
Ui::AddSkip(excludeInner);
Ui::AddSubsectionTitle(excludeInner, tr::lng_chatbots_excluded_title());
const auto excludeAdd = AddButtonWithIcon(
excludeInner,
tr::lng_chatbots_exclude_button(),
st::settingsChatbotsAdd,
{ &st::settingsIconRemove, IconType::Round, &st::windowBgActive });
const auto addExcluded = [=] {
const auto save = [=](Data::BusinessChats value) {
change([&](Data::BusinessRecipients &data) {
if (includeWithExcluded) {
if (!data.allButExcluded) {
value.types = {};
}
for (const auto &user : value.list) {
data.included.list.erase(
ranges::remove(data.included.list, user),
end(data.included.list));
}
}
if (!value.empty()) {
data.included = {};
}
data.excluded = std::move(value);
});
};
EditBusinessChats(controller, {
.current = data->current().excluded,
.save = crl::guard(excludeAdd, save),
.usersOnly = (includeWithExcluded
&& !data->current().allButExcluded),
.include = false,
});
};
excludeAdd->setClickedCallback(addExcluded);
const auto excluded = lifetime.make_state<
rpl::variable<Data::BusinessChats>
>(data->current().excluded);
data->changes(
) | rpl::on_next([=](const Data::BusinessRecipients &value) {
*excluded = value.excluded;
}, lifetime);
excluded->changes(
) | rpl::on_next([=](Data::BusinessChats &&value) {
change([&](Data::BusinessRecipients &data) {
data.excluded = std::move(value);
});
}, lifetime);
SetupBusinessChatsPreview(excludeInner, excluded);
excludeWrap->toggleOn(data->value(
) | rpl::map([=](const Data::BusinessRecipients &value) {
return value.allButExcluded || includeWithExcluded;
}));
excludeWrap->finishAnimating();
const auto includeInner = includeWrap->entity();
Ui::AddSkip(includeInner);
Ui::AddSubsectionTitle(includeInner, tr::lng_chatbots_included_title());
const auto includeAdd = AddButtonWithIcon(
includeInner,
tr::lng_chatbots_include_button(),
st::settingsChatbotsAdd,
{ &st::settingsIconAdd, IconType::Round, &st::windowBgActive });
const auto addIncluded = [=] {
const auto save = [=](Data::BusinessChats value) {
change([&](Data::BusinessRecipients &data) {
if (includeWithExcluded) {
for (const auto &user : value.list) {
data.excluded.list.erase(
ranges::remove(data.excluded.list, user),
end(data.excluded.list));
}
}
if (!value.empty()) {
data.excluded.types = {};
}
data.included = std::move(value);
});
if (!data->current().included.empty()) {
group->setValue(kSelectedOnly);
}
};
EditBusinessChats(controller, {
.current = data->current().included,
.save = crl::guard(includeAdd, save),
.include = true,
});
};
includeAdd->setClickedCallback(addIncluded);
const auto included = lifetime.make_state<
rpl::variable<Data::BusinessChats>
>(data->current().included);
data->changes(
) | rpl::on_next([=](const Data::BusinessRecipients &value) {
*included = value.included;
}, lifetime);
included->changes(
) | rpl::on_next([=](Data::BusinessChats &&value) {
change([&](Data::BusinessRecipients &data) {
data.included = std::move(value);
});
}, lifetime);
SetupBusinessChatsPreview(includeInner, included);
included->value(
) | rpl::on_next([=](const Data::BusinessChats &value) {
if (value.empty() && group->current() == kSelectedOnly) {
group->setValue(kAllExcept);
}
}, lifetime);
includeWrap->toggleOn(data->value(
) | rpl::map([](const Data::BusinessRecipients &value) {
return !value.allButExcluded;
}));
includeWrap->finishAnimating();
group->setChangedCallback([=](int value) {
if (value == kSelectedOnly && data->current().included.empty()) {
group->setValue(kAllExcept);
addIncluded();
return;
}
change([&](Data::BusinessRecipients &data) {
data.allButExcluded = (value == kAllExcept);
});
});
}
int ShortcutsCount(not_null<Main::Session*> session) {
const auto &shortcuts = session->data().shortcutMessages().shortcuts();
auto result = 0;
for (const auto &[_, shortcut] : shortcuts.list) {
if (shortcut.count > 0) {
++result;
}
}
return result;
}
rpl::producer<int> ShortcutsCountValue(not_null<Main::Session*> session) {
const auto messages = &session->data().shortcutMessages();
return rpl::single(rpl::empty) | rpl::then(
messages->shortcutsChanged()
) | rpl::map([=] {
return ShortcutsCount(session);
});
}
int ShortcutMessagesCount(
not_null<Main::Session*> session,
const QString &name) {
const auto &shortcuts = session->data().shortcutMessages().shortcuts();
for (const auto &[_, shortcut] : shortcuts.list) {
if (shortcut.name == name) {
return shortcut.count;
}
}
return 0;
}
rpl::producer<int> ShortcutMessagesCountValue(
not_null<Main::Session*> session,
const QString &name) {
const auto messages = &session->data().shortcutMessages();
return rpl::single(rpl::empty) | rpl::then(
messages->shortcutsChanged()
) | rpl::map([=] {
return ShortcutMessagesCount(session, name);
});
}
bool ShortcutExists(not_null<Main::Session*> session, const QString &name) {
return ShortcutMessagesCount(session, name) > 0;
}
rpl::producer<bool> ShortcutExistsValue(
not_null<Main::Session*> session,
const QString &name) {
return ShortcutMessagesCountValue(session, name)
| rpl::map(rpl::mappers::_1 > 0);
}
int ShortcutsLimit(not_null<Main::Session*> session) {
const auto appConfig = &session->appConfig();
return appConfig->get<int>("quick_replies_limit", 100);
}
rpl::producer<int> ShortcutsLimitValue(not_null<Main::Session*> session) {
const auto appConfig = &session->appConfig();
return appConfig->value() | rpl::map([=] {
return ShortcutsLimit(session);
});
}
int ShortcutMessagesLimit(not_null<Main::Session*> session) {
const auto appConfig = &session->appConfig();
return appConfig->get<int>("quick_reply_messages_limit", 20);
}
rpl::producer<int> ShortcutMessagesLimitValue(
not_null<Main::Session*> session) {
const auto appConfig = &session->appConfig();
return appConfig->value() | rpl::map([=] {
return ShortcutMessagesLimit(session);
});
}
BusinessShortcutId LookupShortcutId(
not_null<Main::Session*> session,
const QString &name) {
const auto messages = &session->data().shortcutMessages();
for (const auto &[id, shortcut] : messages->shortcuts().list) {
if (shortcut.name == name) {
return id;
}
}
return {};
}
} // namespace Settings

View File

@@ -0,0 +1,103 @@
/*
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/required.h"
#include "data/business/data_business_common.h"
#include "settings/settings_common_session.h"
class FilterChatsPreview;
namespace Ui {
class VerticalLayout;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
namespace Settings {
template <typename SectionType>
class BusinessSection : public Section<SectionType> {
public:
BusinessSection(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section<SectionType>(parent)
, _controller(controller) {
}
[[nodiscard]] not_null<Window::SessionController*> controller() const {
return _controller;
}
[[nodiscard]] rpl::producer<> showFinishes() const {
return _showFinished.events();
}
private:
void showFinished() override {
_showFinished.fire({});
}
const not_null<Window::SessionController*> _controller;
rpl::event_stream<> _showFinished;
};
struct BusinessChatsDescriptor {
Data::BusinessChats current;
Fn<void(const Data::BusinessChats&)> save;
bool usersOnly = false;
bool include = false;
};
void EditBusinessChats(
not_null<Window::SessionController*> window,
BusinessChatsDescriptor &&descriptor);
not_null<FilterChatsPreview*> SetupBusinessChatsPreview(
not_null<Ui::VerticalLayout*> container,
not_null<rpl::variable<Data::BusinessChats>*> data);
struct BusinessRecipientsSelectorDescriptor {
not_null<Window::SessionController*> controller;
rpl::producer<QString> title;
not_null<rpl::variable<Data::BusinessRecipients>*> data;
base::required<Data::BusinessRecipientsType> type;
};
void AddBusinessRecipientsSelector(
not_null<Ui::VerticalLayout*> container,
BusinessRecipientsSelectorDescriptor &&descriptor);
[[nodiscard]] int ShortcutsCount(not_null<::Main::Session*> session);
[[nodiscard]] rpl::producer<int> ShortcutsCountValue(
not_null<::Main::Session*> session);
[[nodiscard]] int ShortcutMessagesCount(
not_null<::Main::Session*> session,
const QString &name);
[[nodiscard]] rpl::producer<int> ShortcutMessagesCountValue(
not_null<::Main::Session*> session,
const QString &name);
[[nodiscard]] bool ShortcutExists(
not_null<::Main::Session*> session,
const QString &name);
[[nodiscard]] rpl::producer<bool> ShortcutExistsValue(
not_null<::Main::Session*> session,
const QString &name);
[[nodiscard]] int ShortcutsLimit(not_null<::Main::Session*> session);
[[nodiscard]] rpl::producer<int> ShortcutsLimitValue(
not_null<::Main::Session*> session);
[[nodiscard]] int ShortcutMessagesLimit(not_null<::Main::Session*> session);
[[nodiscard]] rpl::producer<int> ShortcutMessagesLimitValue(
not_null<::Main::Session*> session);
[[nodiscard]] BusinessShortcutId LookupShortcutId(
not_null<::Main::Session*> session,
const QString &name);
} // namespace Settings

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
/*
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 "settings/settings_type.h"
namespace Settings {
[[nodiscard]] Type ShortcutMessagesId(int shortcutId);
} // namespace Settings

View File

@@ -0,0 +1,727 @@
/*
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 "settings/business/settings_working_hours.h"
#include "base/event_filter.h"
#include "base/unixtime.h"
#include "core/application.h"
#include "data/business/data_business_info.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/business/settings_recipients_helper.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/vertical_drum_picker.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/vertical_list.h"
#include "ui/ui_utility.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace Settings {
namespace {
constexpr auto kDay = Data::WorkingInterval::kDay;
constexpr auto kWeek = Data::WorkingInterval::kWeek;
constexpr auto kInNextDayMax = Data::WorkingInterval::kInNextDayMax;
class WorkingHours : public BusinessSection<WorkingHours> {
public:
WorkingHours(
QWidget *parent,
not_null<Window::SessionController*> controller);
~WorkingHours();
[[nodiscard]] bool closeByOutsideClick() const override;
[[nodiscard]] rpl::producer<QString> title() override;
private:
void setupContent(not_null<Window::SessionController*> controller);
void save();
rpl::variable<Data::WorkingHours> _hours;
rpl::variable<bool> _enabled;
};
[[nodiscard]] QString TimezoneFullName(const Data::Timezone &data) {
const auto abs = std::abs(data.utcOffset);
const auto hours = abs / 3600;
const auto minutes = (abs % 3600) / 60;
const auto sign = (data.utcOffset < 0) ? '-' : '+';
const auto prefix = u"(UTC"_q
+ sign
+ QString::number(hours)
+ u":"_q
+ QString::number(minutes).rightJustified(2, u'0')
+ u")"_q;
return prefix + ' ' + data.name;
}
[[nodiscard]] QString FormatDayTime(
TimeId time,
bool showEndAsNextDay = false) {
const auto wrap = [](TimeId value) {
const auto hours = value / 3600;
const auto minutes = (value % 3600) / 60;
return QString::number(hours).rightJustified(2, u'0')
+ ':'
+ QString::number(minutes).rightJustified(2, u'0');
};
return (time > kDay || (showEndAsNextDay && time == kDay))
? tr::lng_hours_next_day(tr::now, lt_time, wrap(time - kDay))
: wrap(time == kDay ? 0 : time);
}
[[nodiscard]] QString FormatTimeHour(TimeId time) {
const auto wrap = [](TimeId value) {
return QString::number(value / 3600).rightJustified(2, u'0');
};
if (time < kDay) {
return wrap(time);
}
const auto wrapped = wrap(time - kDay);
const auto result = tr::lng_hours_on_next_day(tr::now, lt_time, wrapped);
const auto i = result.indexOf(wrapped);
return (i >= 0) ? (result.left(i) + wrapped) : result;
}
[[nodiscard]] QString FormatTimeMinute(TimeId time) {
const auto wrap = [](TimeId value) {
return QString::number((value / 60) % 60).rightJustified(2, u'0');
};
if (time < kDay) {
return wrap(time);
}
const auto wrapped = wrap(time - kDay);
const auto result = tr::lng_hours_on_next_day(tr::now, lt_time, wrapped);
const auto i = result.indexOf(wrapped);
return (i >= 0)
? (wrapped + result.right(result.size() - i - wrapped.size()))
: result;
}
[[nodiscard]] QString JoinIntervals(const Data::WorkingIntervals &data) {
auto result = QStringList();
result.reserve(data.list.size());
for (const auto &interval : data.list) {
const auto start = FormatDayTime(interval.start);
const auto end = FormatDayTime(interval.end);
result.push_back(start + u" - "_q + end);
}
return result.join(u", "_q);
}
void EditTimeBox(
not_null<Ui::GenericBox*> box,
TimeId low,
TimeId high,
TimeId value,
Fn<void(TimeId)> save) {
Expects(low <= high);
const auto 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 hoursCount = (high - low + 3600) / 3600;
const auto hoursStartIndex = (value / 3600) - (low / 3600);
const auto hoursPaint = [=](QPainter &p, QRectF rect, int index) {
p.drawText(
rect,
FormatTimeHour(((low / 3600) + index) * 3600),
style::al_right);
};
const auto hours = picker(hoursCount, hoursStartIndex, hoursPaint);
const auto minutes = content->lifetime().make_state<
rpl::variable<Ui::VerticalDrumPicker*>
>(nullptr);
// hours->value() is valid only after size is set.
const auto separator = u":"_q;
const auto separatorWidth = st::boxTextFont->width(separator);
rpl::combine(
content->sizeValue(),
minutes->value()
) | rpl::on_next([=](QSize s, Ui::VerticalDrumPicker *minutes) {
const auto half = (s.width() - separatorWidth) / 2;
hours->setGeometry(0, 0, half, s.height());
if (minutes) {
minutes->setGeometry(half + separatorWidth, 0, half, s.height());
}
}, content->lifetime());
Ui::SendPendingMoveResizeEvents(hours);
const auto minutesStart = content->lifetime().make_state<TimeId>();
hours->value() | rpl::on_next([=](int hoursIndex) {
const auto start = std::max(low, (hoursIndex + (low / 3600)) * 3600);
const auto end = std::min(high, ((start / 3600) * 60 + 59) * 60);
const auto minutesCount = (end - start + 60) / 60;
const auto minutesStartIndex = minutes->current()
? std::clamp(
((((*minutesStart) / 60 + minutes->current()->index()) % 60)
- ((start / 60) % 60)),
0,
(minutesCount - 1))
: std::clamp((value / 60) - (start / 60), 0, minutesCount - 1);
*minutesStart = start;
const auto minutesPaint = [=](QPainter &p, QRectF rect, int index) {
p.drawText(
rect,
FormatTimeMinute(((start / 60) + index) * 60),
style::al_left);
};
const auto updated = picker(
minutesCount,
minutesStartIndex,
minutesPaint);
delete minutes->current();
*minutes = updated;
minutes->current()->show();
}, hours->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);
p.drawText(QRectF(content->rect()), separator, style::al_center);
}, content->lifetime());
base::install_event_filter(box, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
hours->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));
}
return base::EventFilterResult::Continue;
});
box->addButton(tr::lng_settings_save(), [=] {
const auto weak = base::make_weak(box);
save(std::clamp(
((*minutesStart) / 60 + minutes->current()->index()) * 60,
low,
high));
if (const auto strong = weak.get()) {
strong->closeBox();
}
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
void EditDayBox(
not_null<Ui::GenericBox*> box,
rpl::producer<QString> title,
Data::WorkingIntervals intervals,
Fn<void(Data::WorkingIntervals)> save) {
box->setTitle(std::move(title));
box->setWidth(st::boxWideWidth);
struct State {
rpl::variable<Data::WorkingIntervals> data;
};
const auto state = box->lifetime().make_state<State>(State{
.data = std::move(intervals),
});
const auto container = box->verticalLayout();
const auto rows = container->add(
object_ptr<Ui::VerticalLayout>(container));
const auto makeRow = [=](
Data::WorkingInterval interval,
TimeId min,
TimeId max) {
auto result = object_ptr<Ui::VerticalLayout>(rows);
const auto raw = result.data();
AddDivider(raw);
AddSkip(raw);
AddButtonWithLabel(
raw,
tr::lng_hours_opening(),
rpl::single(FormatDayTime(interval.start, true)),
st::settingsButtonNoIcon
)->setClickedCallback([=] {
const auto max = std::max(min, interval.end - 60);
const auto now = std::clamp(interval.start, min, max);
const auto save = crl::guard(box, [=](TimeId value) {
auto now = state->data.current();
const auto i = ranges::find(now.list, interval);
if (i != end(now.list)) {
i->start = value;
state->data = now.normalized();
}
});
box->getDelegate()->show(Box(EditTimeBox, min, max, now, save));
});
AddButtonWithLabel(
raw,
tr::lng_hours_closing(),
rpl::single(FormatDayTime(interval.end, true)),
st::settingsButtonNoIcon
)->setClickedCallback([=] {
const auto min = std::min(max, interval.start + 60);
const auto now = std::clamp(interval.end, min, max);
const auto save = crl::guard(box, [=](TimeId value) {
auto now = state->data.current();
const auto i = ranges::find(now.list, interval);
if (i != end(now.list)) {
i->end = value;
state->data = now.normalized();
}
});
box->getDelegate()->show(Box(EditTimeBox, min, max, now, save));
});
raw->add(object_ptr<Ui::SettingsButton>(
raw,
tr::lng_hours_remove(),
st::settingsAttentionButton
))->setClickedCallback([=] {
auto now = state->data.current();
const auto i = ranges::find(now.list, interval);
if (i != end(now.list)) {
now.list.erase(i);
state->data = std::move(now);
}
});
AddSkip(raw);
return result;
};
const auto addWrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
AddDivider(addWrap->entity());
AddSkip(addWrap->entity());
const auto add = addWrap->entity()->add(
object_ptr<Ui::SettingsButton>(
container,
tr::lng_hours_add_button(),
st::settingsButtonLightNoIcon));
add->setClickedCallback([=] {
auto now = state->data.current();
if (now.list.empty()) {
now.list.push_back({ 8 * 3600, 20 * 3600 });
} else if (const auto last = now.list.back().end; last + 60 < kDay) {
const auto from = std::max(
std::min(last + 30 * 60, kDay - 30 * 60),
last + 60);
now.list.push_back({ from, from + 4 * 3600 });
}
state->data = std::move(now);
});
state->data.value(
) | rpl::on_next([=](const Data::WorkingIntervals &data) {
const auto count = int(data.list.size());
for (auto i = 0; i != count; ++i) {
const auto min = (i == 0) ? 0 : (data.list[i - 1].end + 60);
const auto max = (i == count - 1)
? (kDay + kInNextDayMax)
: (data.list[i + 1].start - 60);
rows->insert(i, makeRow(data.list[i], min, max));
if (rows->count() > i + 1) {
delete rows->widgetAt(i + 1);
}
}
while (rows->count() > count) {
delete rows->widgetAt(count);
}
rows->resizeToWidth(st::boxWideWidth);
addWrap->toggle(data.list.empty()
|| data.list.back().end + 60 < kDay, anim::type::instant);
add->clearState();
}, add->lifetime());
addWrap->finishAnimating();
AddSkip(container);
AddDividerText(container, tr::lng_hours_about_day());
box->addButton(tr::lng_settings_save(), [=] {
const auto weak = base::make_weak(box);
save(state->data.current());
if (const auto strong = weak.get()) {
strong->closeBox();
}
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
void ChooseTimezoneBox(
not_null<Ui::GenericBox*> box,
std::vector<Data::Timezone> list,
QString id,
Fn<void(QString)> save) {
Expects(!list.empty());
box->setWidth(st::boxWideWidth);
box->setTitle(tr::lng_hours_time_zone_title());
const auto height = st::boxWideWidth;
box->setMaxHeight(height);
ranges::sort(list, ranges::less(), [](const Data::Timezone &value) {
return std::pair(value.utcOffset, value.name);
});
if (!ranges::contains(list, id, &Data::Timezone::id)) {
id = Data::FindClosestTimezoneId(list);
}
const auto i = ranges::find(list, id, &Data::Timezone::id);
const auto value = int(i - begin(list));
const auto group = std::make_shared<Ui::RadiobuttonGroup>(value);
const auto radioPadding = st::defaultCheckbox.margin;
const auto max = std::max(radioPadding.top(), radioPadding.bottom());
auto index = 0;
auto padding = st::boxRowPadding + QMargins(0, max, 0, max);
auto selected = (Ui::Radiobutton*)nullptr;
for (const auto &entry : list) {
const auto button = box->addRow(
object_ptr<Ui::Radiobutton>(
box,
group,
index++,
TimezoneFullName(entry)),
padding);
if (index == value + 1) {
selected = button;
}
padding = st::boxRowPadding + QMargins(0, 0, 0, max);
}
if (selected) {
box->verticalLayout()->resizeToWidth(st::boxWideWidth);
const auto y = selected->y() - (height - selected->height()) / 2;
box->setInitScrollCallback([=] {
box->scrollToY(y);
});
}
group->setChangedCallback([=](int index) {
const auto weak = base::make_weak(box);
save(list[index].id);
if (const auto strong = weak.get()) {
strong->closeBox();
}
});
box->addButton(tr::lng_close(), [=] {
box->closeBox();
});
}
void AddWeekButton(
not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
int index,
not_null<rpl::variable<Data::WorkingHours>*> data) {
auto label = [&] {
switch (index) {
case 0: return tr::lng_hours_monday();
case 1: return tr::lng_hours_tuesday();
case 2: return tr::lng_hours_wednesday();
case 3: return tr::lng_hours_thursday();
case 4: return tr::lng_hours_friday();
case 5: return tr::lng_hours_saturday();
case 6: return tr::lng_hours_sunday();
}
Unexpected("Index in AddWeekButton.");
}();
const auto &st = st::settingsWorkingHoursWeek;
const auto button = AddButtonWithIcon(
container,
rpl::duplicate(label),
st);
button->setClickedCallback([=] {
const auto done = [=](Data::WorkingIntervals intervals) {
auto now = data->current();
now.intervals = ReplaceDayIntervals(
now.intervals,
index,
std::move(intervals));
*data = now.normalized();
};
controller->show(Box(
EditDayBox,
rpl::duplicate(label),
ExtractDayIntervals(data->current().intervals, index),
crl::guard(button, done)));
});
const auto toggleButton = Ui::CreateChild<Ui::SettingsButton>(
container.get(),
nullptr,
st);
const auto checkView = button->lifetime().make_state<Ui::ToggleView>(
st.toggle,
false,
[=] { toggleButton->update(); });
auto status = data->value(
) | rpl::map([=](const Data::WorkingHours &data) -> rpl::producer<QString> {
using namespace Data;
const auto intervals = ExtractDayIntervals(data.intervals, index);
const auto empty = intervals.list.empty();
if (checkView->checked() == empty) {
checkView->setChecked(!empty, anim::type::instant);
}
if (!intervals) {
return tr::lng_hours_closed();
} else if (IsFullOpen(intervals)) {
return tr::lng_hours_open_full();
}
return rpl::single(JoinIntervals(intervals));
}) | rpl::flatten_latest();
const auto details = Ui::CreateChild<Ui::FlatLabel>(
button.get(),
std::move(status),
st::settingsWorkingHoursDetails);
details->show();
details->moveToLeft(
st.padding.left(),
st.padding.top() + st.height - details->height());
details->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto separator = Ui::CreateChild<Ui::RpWidget>(container.get());
separator->paintRequest(
) | rpl::on_next([=, bg = st.textBgOver] {
auto p = QPainter(separator);
p.fillRect(separator->rect(), bg);
}, separator->lifetime());
const auto separatorHeight = st.height - 2 * st.toggle.border;
button->geometryValue(
) | rpl::on_next([=](const QRect &r) {
const auto w = st::rightsButtonToggleWidth;
toggleButton->setGeometry(
r.x() + r.width() - w,
r.y(),
w,
r.height());
separator->setGeometry(
toggleButton->x() - st::lineWidth,
r.y() + (r.height() - separatorHeight) / 2,
st::lineWidth,
separatorHeight);
}, toggleButton->lifetime());
const auto checkWidget = Ui::CreateChild<Ui::RpWidget>(toggleButton);
checkWidget->resize(checkView->getSize());
checkWidget->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(checkWidget);
checkView->paint(p, 0, 0, checkWidget->width());
}, checkWidget->lifetime());
toggleButton->sizeValue(
) | rpl::on_next([=](const QSize &s) {
checkWidget->moveToRight(
st.toggleSkip,
(s.height() - checkWidget->height()) / 2);
}, toggleButton->lifetime());
toggleButton->setClickedCallback([=] {
const auto enabled = !checkView->checked();
checkView->setChecked(enabled, anim::type::normal);
auto now = data->current();
now.intervals = ReplaceDayIntervals(
now.intervals,
index,
(enabled
? Data::WorkingIntervals{ { { 0, kDay } } }
: Data::WorkingIntervals()));
*data = now.normalized();
});
}
WorkingHours::WorkingHours(
QWidget *parent,
not_null<Window::SessionController*> controller)
: BusinessSection(parent, controller) {
setupContent(controller);
}
WorkingHours::~WorkingHours() {
if (!Core::Quitting()) {
save();
}
}
bool WorkingHours::closeByOutsideClick() const {
return false;
}
rpl::producer<QString> WorkingHours::title() {
return tr::lng_hours_title();
}
void WorkingHours::setupContent(
not_null<Window::SessionController*> controller) {
using namespace rpl::mappers;
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
struct State {
rpl::variable<Data::Timezones> timezones;
bool timezoneEditPending = false;
};
const auto info = &controller->session().data().businessInfo();
const auto state = content->lifetime().make_state<State>(State{
.timezones = info->timezonesValue(),
});
_hours = controller->session().user()->businessDetails().hours;
AddDividerTextWithLottie(content, {
.lottie = u"hours"_q,
.lottieSize = st::settingsCloudPasswordIconSize,
.lottieMargins = st::peerAppearanceIconPadding,
.showFinished = showFinishes(),
.about = tr::lng_hours_about(tr::marked),
.aboutMargins = st::peerAppearanceCoverLabelMargin,
});
Ui::AddSkip(content);
const auto enabled = content->add(object_ptr<Ui::SettingsButton>(
content,
tr::lng_hours_show(),
st::settingsButtonNoIcon
))->toggleOn(rpl::single(bool(_hours.current())));
_enabled = enabled->toggledValue();
const auto wrap = content->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
content,
object_ptr<Ui::VerticalLayout>(content)));
const auto inner = wrap->entity();
Ui::AddSkip(inner);
Ui::AddDivider(inner);
Ui::AddSkip(inner);
for (auto i = 0; i != 7; ++i) {
AddWeekButton(inner, controller, i, &_hours);
}
Ui::AddSkip(inner);
Ui::AddDivider(inner);
Ui::AddSkip(inner);
state->timezones.value(
) | rpl::filter([=](const Data::Timezones &value) {
return !value.list.empty();
}) | rpl::on_next([=](const Data::Timezones &value) {
const auto now = _hours.current().timezoneId;
if (!ranges::contains(value.list, now, &Data::Timezone::id)) {
auto copy = _hours.current();
copy.timezoneId = Data::FindClosestTimezoneId(value.list);
_hours = std::move(copy);
}
}, inner->lifetime());
auto timezoneLabel = rpl::combine(
_hours.value(),
state->timezones.value()
) | rpl::map([](
const Data::WorkingHours &hours,
const Data::Timezones &timezones) {
const auto i = ranges::find(
timezones.list,
hours.timezoneId,
&Data::Timezone::id);
return (i != end(timezones.list)) ? TimezoneFullName(*i) : QString();
});
const auto editTimezone = [=](const std::vector<Data::Timezone> &list) {
const auto was = _hours.current().timezoneId;
controller->show(Box(ChooseTimezoneBox, list, was, [=](QString id) {
if (id != was) {
auto copy = _hours.current();
copy.timezoneId = id;
_hours = std::move(copy);
}
}));
};
AddButtonWithLabel(
inner,
tr::lng_hours_time_zone(),
std::move(timezoneLabel),
st::settingsButtonNoIcon
)->setClickedCallback([=] {
const auto &list = state->timezones.current().list;
if (!list.empty()) {
editTimezone(list);
} else {
state->timezoneEditPending = true;
}
});
if (state->timezones.current().list.empty()) {
state->timezones.value(
) | rpl::filter([](const Data::Timezones &value) {
return !value.list.empty();
}) | rpl::on_next([=](const Data::Timezones &value) {
if (state->timezoneEditPending) {
state->timezoneEditPending = false;
editTimezone(value.list);
}
}, inner->lifetime());
}
wrap->toggleOn(enabled->toggledValue());
wrap->finishAnimating();
Ui::ResizeFitChild(this, content);
}
void WorkingHours::save() {
const auto show = controller()->uiShow();
controller()->session().data().businessInfo().saveWorkingHours(
_enabled.current() ? _hours.current() : Data::WorkingHours(),
[=](QString error) { show->showToast(error); });
}
} // namespace
Type WorkingHoursId() {
return WorkingHours::Id();
}
} // namespace Settings

View File

@@ -0,0 +1,16 @@
/*
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 "settings/settings_type.h"
namespace Settings {
[[nodiscard]] Type WorkingHoursId();
} // namespace Settings