Arquivos
tdesktop/Telegram/SourceFiles/boxes/preview_ai_tone_box.cpp
T
2026-05-07 17:49:56 +04:00

582 linhas
16 KiB
C++

/*
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 "boxes/preview_ai_tone_box.h"
#include "boxes/create_ai_tone_box.h"
#include "core/ui_integration.h"
#include "data/data_ai_compose_tones.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 "ui/controls/custom_emoji_toast_icon.h"
#include "ui/effects/animation_value.h"
#include "ui/effects/animations.h"
#include "ui/effects/skeleton_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/layers/show.h"
#include "ui/painter.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/text/text_entity.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include <optional>
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
namespace {
constexpr auto kToastDuration = crl::time(4000);
constexpr auto kSpinDuration = crl::time(600);
constexpr auto kWaitDuration = crl::time(1000);
class RefreshSpinEmoji final : public Ui::Text::CustomEmoji {
public:
RefreshSpinEmoji(
std::shared_ptr<rpl::variable<bool>> loading,
Fn<void()> repaint);
int width() override;
QString entityData() override;
void paint(QPainter &p, const Context &context) override;
void unload() override;
bool ready() override;
bool readyInDefaultState() override;
private:
enum class Phase : uchar { Idle, Spinning, Waiting };
void handleLoading(bool now);
void tick(crl::time now);
[[nodiscard]] float64 angleDegrees(crl::time now) const;
rpl::variable<bool> _loading;
const Fn<void()> _repaint;
Ui::Animations::Basic _animation;
crl::time _phaseStarted = 0;
Phase _phase = Phase::Idle;
rpl::lifetime _lifetime;
};
RefreshSpinEmoji::RefreshSpinEmoji(
std::shared_ptr<rpl::variable<bool>> loading,
Fn<void()> repaint)
: _loading(loading->value())
, _repaint(std::move(repaint))
, _animation([=](crl::time now) { tick(now); }) {
_loading.value(
) | rpl::on_next([=](bool value) {
handleLoading(value);
}, _lifetime);
}
int RefreshSpinEmoji::width() {
const auto &e = st::aiTonePreviewAnotherExampleIcon;
return e.padding.left() + e.icon.width() + e.padding.right();
}
QString RefreshSpinEmoji::entityData() {
return u"ai-tone-refresh"_q;
}
float64 RefreshSpinEmoji::angleDegrees(crl::time now) const {
if (_phase != Phase::Spinning) {
return 0.;
}
const auto elapsed = now - _phaseStarted;
const auto dt = std::clamp(
elapsed / float64(kSpinDuration),
0.,
1.);
return anim::easeOutBack(360., dt);
}
void RefreshSpinEmoji::paint(QPainter &p, const Context &context) {
const auto &e = st::aiTonePreviewAnotherExampleIcon;
const auto size = e.icon.size();
const auto pos = context.position
+ QPoint(e.padding.left(), e.padding.top());
const auto angle = angleDegrees(context.now);
auto hq = PainterHighQualityEnabler(p);
if (angle != 0.) {
const auto center = QPointF(pos)
+ QPointF(size.width() / 2.0, size.height() / 2.0);
p.save();
p.translate(center);
p.rotate(angle);
p.translate(-center);
e.icon.paint(p, pos, 0, context.textColor);
p.restore();
} else {
e.icon.paint(p, pos, 0, context.textColor);
}
}
void RefreshSpinEmoji::unload() {
}
bool RefreshSpinEmoji::ready() {
return true;
}
bool RefreshSpinEmoji::readyInDefaultState() {
return _phase == Phase::Idle;
}
void RefreshSpinEmoji::handleLoading(bool now) {
if (now) {
if (_phase == Phase::Idle) {
_phase = Phase::Spinning;
_phaseStarted = crl::now();
_animation.start();
if (_repaint) {
_repaint();
}
}
} else if (_phase == Phase::Waiting) {
_phase = Phase::Idle;
_animation.stop();
if (_repaint) {
_repaint();
}
}
}
void RefreshSpinEmoji::tick(crl::time now) {
const auto elapsed = now - _phaseStarted;
if (_phase == Phase::Spinning && elapsed >= kSpinDuration) {
if (_loading.current()) {
_phase = Phase::Waiting;
_phaseStarted = now;
} else {
_phase = Phase::Idle;
_animation.stop();
}
} else if (_phase == Phase::Waiting && elapsed >= kWaitDuration) {
if (_loading.current()) {
_phase = Phase::Spinning;
_phaseStarted = now;
} else {
_phase = Phase::Idle;
_animation.stop();
}
}
if (_repaint) {
_repaint();
}
}
class PreviewAiToneExampleCard final : public Ui::RpWidget {
public:
PreviewAiToneExampleCard(
QWidget *parent,
not_null<Main::Session*> session,
std::shared_ptr<rpl::variable<bool>> loading);
void showExample(Data::AiComposeToneExample example);
void showSkeleton(bool shown);
void setAnotherVisible(bool visible);
[[nodiscard]] rpl::producer<> anotherExampleRequested() const;
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
private:
const not_null<Main::Session*> _session;
const not_null<Ui::VerticalLayout*> _layout;
const not_null<Ui::FlatLabel*> _beforeTitle;
const not_null<Ui::FlatLabel*> _beforeBody;
const style::complex_color _shadowColor;
const not_null<Ui::PlainShadow*> _shadow;
const not_null<Ui::FlatLabel*> _afterTitle;
const not_null<Ui::FlatLabel*> _afterBody;
const not_null<Ui::RoundButton*> _another;
Ui::SkeletonAnimation _beforeSkeleton;
Ui::SkeletonAnimation _afterSkeleton;
rpl::event_stream<> _anotherExampleRequested;
};
PreviewAiToneExampleCard::PreviewAiToneExampleCard(
QWidget *parent,
not_null<Main::Session*> session,
std::shared_ptr<rpl::variable<bool>> loading)
: RpWidget(parent)
, _session(session)
, _layout(Ui::CreateChild<Ui::VerticalLayout>(this))
, _beforeTitle(_layout->add(
object_ptr<Ui::FlatLabel>(
_layout,
tr::lng_ai_compose_before(tr::now),
st::aiComposeCardTitle),
QMargins(
st::aiTonePreviewExampleCardPadding.left(),
st::aiTonePreviewExampleCardPadding.top(),
st::aiTonePreviewExampleCardPadding.right(),
0)))
, _beforeBody(_layout->add(
object_ptr<Ui::FlatLabel>(_layout, st::aiComposeBodyLabel),
QMargins(
st::aiTonePreviewExampleCardPadding.left(),
st::aiTonePreviewExampleCardTitleSkip,
st::aiTonePreviewExampleCardPadding.right(),
0)))
, _shadowColor([] {
auto color = st::windowSubTextFg->c;
color.setAlphaF(st::aiComposeShadowOpacity);
return color;
})
, _shadow(_layout->add(
object_ptr<Ui::PlainShadow>(_layout, _shadowColor.color()),
QMargins(
st::aiTonePreviewExampleCardPadding.left(),
st::aiTonePreviewExampleCardSectionSkip / 2,
st::aiTonePreviewExampleCardPadding.right(),
0)))
, _afterTitle(_layout->add(
object_ptr<Ui::FlatLabel>(
_layout,
tr::lng_ai_compose_after(tr::now),
st::aiComposeCardTitle),
QMargins(
st::aiTonePreviewExampleCardPadding.left(),
st::aiTonePreviewExampleCardSectionSkip / 2,
st::aiTonePreviewExampleCardPadding.right(),
0)))
, _afterBody(_layout->add(
object_ptr<Ui::FlatLabel>(_layout, st::aiComposeBodyLabel),
QMargins(
st::aiTonePreviewExampleCardPadding.left(),
st::aiTonePreviewExampleCardTitleSkip,
st::aiTonePreviewExampleCardPadding.right(),
st::aiTonePreviewExampleCardPadding.bottom())))
, _another(Ui::CreateChild<Ui::RoundButton>(
this,
rpl::single(QString()),
st::aiTonePreviewAnotherExampleButton))
, _beforeSkeleton(_beforeBody)
, _afterSkeleton(_afterBody) {
_beforeBody->setSelectable(true);
_afterBody->setSelectable(true);
_another->raise();
rpl::combine(
widthValue(),
_beforeTitle->geometryValue(),
_another->widthValue()
) | rpl::on_next([=](int width, QRect titleGeometry, int) {
const auto right = st::aiTonePreviewExampleCardPadding.left();
const auto &button = st::aiTonePreviewAnotherExampleButton;
const auto &title = st::aiComposeCardTitle;
const auto shift = title.style.font->ascent
- button.style.font->ascent
- button.textTop
- button.padding.top();
_another->moveToRight(
right,
titleGeometry.top() + shift,
width);
}, lifetime());
auto context = Ui::Text::MarkedContext();
context.repaint = [raw = _another.get()] { raw->update(); };
context.customEmojiFactory = [loading = std::move(loading)](
QStringView data,
const Ui::Text::MarkedContext &context
) -> std::unique_ptr<Ui::Text::CustomEmoji> {
if (data != u"ai-tone-refresh"_q) {
return nullptr;
}
return std::make_unique<RefreshSpinEmoji>(loading, context.repaint);
};
_another->setContext(context);
_another->setText(rpl::single(
Ui::Text::SingleCustomEmoji(u"ai-tone-refresh"_q)
.append(tr::lng_ai_compose_tone_preview_add_example(tr::now))));
_another->setClickedCallback([=] {
_anotherExampleRequested.fire({});
});
}
void PreviewAiToneExampleCard::showExample(
Data::AiComposeToneExample example) {
const auto context = Core::TextContext({ .session = _session });
_beforeBody->setMarkedText(example.from, context);
_afterBody->setMarkedText(example.to, context);
_beforeSkeleton.stop();
_afterSkeleton.stop();
if (width() > 0) {
resizeToWidth(width());
}
}
void PreviewAiToneExampleCard::showSkeleton(bool shown) {
if (shown) {
_beforeSkeleton.start();
_afterSkeleton.start();
} else {
_beforeSkeleton.stop();
_afterSkeleton.stop();
}
}
void PreviewAiToneExampleCard::setAnotherVisible(bool visible) {
_another->setVisible(visible);
}
rpl::producer<> PreviewAiToneExampleCard::anotherExampleRequested() const {
return _anotherExampleRequested.events();
}
int PreviewAiToneExampleCard::resizeGetHeight(int newWidth) {
_layout->resizeToWidth(newWidth);
_layout->moveToLeft(0, 0, newWidth);
return _layout->heightNoMargins();
}
void PreviewAiToneExampleCard::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::aiTonePreviewExampleCardBg);
p.drawRoundedRect(
rect(),
st::aiTonePreviewExampleCardRadius,
st::aiTonePreviewExampleCardRadius);
}
void ShowToneAddedToast(
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session,
const Data::AiComposeTone &tone) {
const auto size = QSize(
st::aiComposeToneToastIconSize.width(),
st::aiComposeToneToastIconSize.height());
show->showToast(Ui::Toast::Config{
.title = tr::lng_ai_compose_tone_added(tr::now),
.text = tr::lng_ai_compose_tone_added_description(
tr::now,
lt_name,
tr::marked(tone.title),
tr::marked),
.iconContent = Ui::MakeCustomEmojiToastIcon(
session,
tone.emojiId,
size),
.iconPadding = st::aiComposeToneToastIconPadding,
.duration = kToastDuration,
});
}
void ShowToneRemovedToast(std::shared_ptr<Ui::Show> show, bool deleted) {
show->showToast(Ui::Toast::Config{
.text = { (deleted
? tr::lng_ai_compose_tone_deleted
: tr::lng_ai_compose_tone_removed)(tr::now) },
.icon = &st::aiComposeToneRemovedToastIcon,
.duration = kToastDuration,
});
}
[[nodiscard]] auto FindInstalledCustomTone(
not_null<Main::Session*> session,
const Data::AiComposeTone &tone)
-> std::optional<Data::AiComposeTone> {
if (tone.isDefault) {
return std::nullopt;
}
for (const auto &installedTone : session->data().aiComposeTones().list()) {
if (!installedTone.isDefault && (installedTone.id == tone.id)) {
return installedTone;
}
}
return std::nullopt;
}
} // namespace
void PreviewAiToneBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
Data::AiComposeTone tone) {
box->setStyle(st::aiComposeBox);
box->setNoContentMargin(true);
box->setWidth(st::boxWideWidth);
box->addTopButton(st::aiComposeBoxClose, [=] { box->closeBox(); });
const auto top = box->setPinnedToTopContent(
object_ptr<Ui::VerticalLayout>(box));
Ui::AddSkip(top, st::defaultVerticalListSkip * 4);
AddAiToneIconPreview(top, session, rpl::single(tone.emojiId), nullptr);
top->add(
object_ptr<Ui::FlatLabel>(
top,
rpl::single(tone.title),
st::aiTonePreviewTitleLabel),
st::aiTonePreviewTitleMargin,
style::al_top);
top->add(
object_ptr<Ui::FlatLabel>(
top,
tr::lng_ai_compose_tone_preview_about(),
st::aiTonePreviewAboutLabel),
st::aiTonePreviewAboutMargin,
style::al_top
)->setTryMakeSimilarLines(true);
const auto body = box->verticalLayout();
struct State {
int examplesCount = 0;
std::shared_ptr<rpl::variable<bool>> requesting
= std::make_shared<rpl::variable<bool>>(false);
};
const auto state = box->lifetime().make_state<State>();
state->examplesCount = tone.firstExample ? 1 : 0;
const auto card = body->add(
object_ptr<PreviewAiToneExampleCard>(
body,
session,
state->requesting),
st::aiTonePreviewExampleCardMargin);
const auto maxExamples = session->appConfig().get<int>(
u"aicompose_tone_examples_num"_q,
3);
const auto updateAnother = [=] {
card->setAnotherVisible(state->examplesCount < maxExamples);
};
updateAnother();
const auto loadAnother = [=] {
if (state->requesting->current()) {
return;
}
*state->requesting = true;
card->showSkeleton(true);
const auto num = state->examplesCount;
session->data().aiComposeTones().getToneExample(
tone,
num,
crl::guard(box, [=](Data::AiComposeToneExample example) {
*state->requesting = false;
++state->examplesCount;
card->showExample(std::move(example));
updateAnother();
}),
crl::guard(box, [=](const MTP::Error &error) {
*state->requesting = false;
card->showSkeleton(false);
if (!MTP::IgnoreError(error)) {
box->showToast(error.type());
}
}));
};
card->anotherExampleRequested(
) | rpl::on_next(loadAnother, card->lifetime());
if (tone.firstExample) {
card->showExample(*tone.firstExample);
} else {
loadAnother();
}
auto text = tr::marked();
if (tone.installsCount > 0) {
text = tr::lng_ai_compose_tone_preview_used_by(
tr::now,
lt_count,
tone.installsCount,
tr::marked);
}
if (const auto user = session->data().userLoaded(tone.authorId)) {
const auto name = user->shortName();
auto mention = tr::marked(name);
mention.entities.push_back(EntityInText(
EntityType::MentionName,
0,
name.size(),
TextUtilities::MentionNameDataFromFields({
.selfId = session->userId().bare,
.userId = tone.authorId.bare,
.accessHash = user->accessHash(),
})));
auto createdBy = tr::lng_ai_compose_tone_preview_created_by(
tr::now,
lt_user,
std::move(mention),
tr::marked);
if (!text.empty()) {
text.append(' ').append(std::move(createdBy));
} else {
text = std::move(createdBy);
}
}
if (!text.empty()) {
const auto attribution = body->add(
object_ptr<Ui::FlatLabel>(body, st::aiTonePreviewAttributionLabel),
st::aiTonePreviewAttributionMargin,
style::al_top);
attribution->setMarkedText(
std::move(text),
Core::TextContext({ .session = session }));
}
Ui::AddSkip(body, st::aiTonePreviewBottomSkip);
const auto installedTone = FindInstalledCustomTone(session, tone);
if (installedTone) {
const auto remove = box->addButton(
installedTone->creator
? tr::lng_ai_compose_tone_delete()
: tr::lng_ai_compose_tone_remove(),
[=] {
const auto show = box->uiShow();
ConfirmDeleteAiTone(
show,
session,
*installedTone,
crl::guard(box, [=] {
box->closeBox();
ShowToneRemovedToast(show, installedTone->creator);
}));
},
st::aiToneDeleteButton);
remove->setFullRadius(true);
} else {
const auto add = box->addButton(
tr::lng_ai_compose_tone_preview_add(),
[=] {
const auto show = box->uiShow();
session->data().aiComposeTones().save(
tone,
false,
crl::guard(box, [=] {
box->closeBox();
ShowToneAddedToast(show, session, tone);
}),
crl::guard(box, [=](const MTP::Error &error) {
if (error.type() == u"TONES_SAVED_TOO_MANY"_q) {
ShowAiComposeToneLimitError(show, session);
} else if (!MTP::IgnoreError(error)) {
box->showToast(error.type());
}
}));
});
add->setFullRadius(true);
}
}