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
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:
376
Telegram/SourceFiles/settings/business/settings_away_message.cpp
Normal file
376
Telegram/SourceFiles/settings/business/settings_away_message.cpp
Normal 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
|
||||
@@ -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
|
||||
646
Telegram/SourceFiles/settings/business/settings_chat_intro.cpp
Normal file
646
Telegram/SourceFiles/settings/business/settings_chat_intro.cpp
Normal 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
|
||||
16
Telegram/SourceFiles/settings/business/settings_chat_intro.h
Normal file
16
Telegram/SourceFiles/settings/business/settings_chat_intro.h
Normal 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
|
||||
823
Telegram/SourceFiles/settings/business/settings_chat_links.cpp
Normal file
823
Telegram/SourceFiles/settings/business/settings_chat_links.cpp
Normal 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
|
||||
16
Telegram/SourceFiles/settings/business/settings_chat_links.h
Normal file
16
Telegram/SourceFiles/settings/business/settings_chat_links.h
Normal 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
|
||||
578
Telegram/SourceFiles/settings/business/settings_chatbots.cpp
Normal file
578
Telegram/SourceFiles/settings/business/settings_chatbots.cpp
Normal 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
|
||||
16
Telegram/SourceFiles/settings/business/settings_chatbots.h
Normal file
16
Telegram/SourceFiles/settings/business/settings_chatbots.h
Normal 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
|
||||
293
Telegram/SourceFiles/settings/business/settings_greeting.cpp
Normal file
293
Telegram/SourceFiles/settings/business/settings_greeting.cpp
Normal 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
|
||||
16
Telegram/SourceFiles/settings/business/settings_greeting.h
Normal file
16
Telegram/SourceFiles/settings/business/settings_greeting.h
Normal 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
|
||||
300
Telegram/SourceFiles/settings/business/settings_location.cpp
Normal file
300
Telegram/SourceFiles/settings/business/settings_location.cpp
Normal 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
|
||||
16
Telegram/SourceFiles/settings/business/settings_location.h
Normal file
16
Telegram/SourceFiles/settings/business/settings_location.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ¤t = 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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user