Implement nice Cocoon about box.

This commit is contained in:
John Preston
2025-12-26 12:24:02 +04:00
parent 57754be6f0
commit 093eb28e75
12 changed files with 415 additions and 11 deletions
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

+17
View File
@@ -4971,6 +4971,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_translate_settings" = "Settings"; "lng_translate_settings" = "Settings";
"lng_translate_undo" = "Undo"; "lng_translate_undo" = "Undo";
"lng_translate_cocoon_menu" = "Translations are powered by\n**{emoji} Cocoon**. {link}";
"lng_translate_cocoon_link" = "How does it work?";
"lng_translate_cocoon_title" = "Cocoon";
"lng_translate_cocoon_subtitle" = "**Cocoon** ({text}) handles AI tasks **safely** and **efficiently**.";
"lng_translate_cocoon_explain" = "**Co**nfidential **Co**mpute **O**pen **N**etwork";
"lng_translate_cocoon_private_title" = "Private";
"lng_translate_cocoon_private_text" = "No third party can access any data, such as translations, inside {mention}.";
"lng_translate_cocoon_private_mention" = "@cocoon";
"lng_translate_cocoon_efficient_title" = "Efficient";
"lng_translate_cocoon_efficient_text" = "Cocoon has allowed Telegram to reduce translation costs by 6x.";
"lng_translate_cocoon_everyone_title" = "For Everyone";
"lng_translate_cocoon_everyone_text" = "Any developer can use Cocoon for AI features. Learn more at {domain}.";
"lng_translate_cocoon_everyone_domain" = "cocoon.org";
"lng_translate_cocoon_text" = "Want to integrate Cocoon into your projects?\nReach out at {link}";
"lng_translate_cocoon_text_link" = "t.me/cocoon?direct";
"lng_translate_cocoon_done" = "Understood";
"lng_downloads_section" = "Downloads"; "lng_downloads_section" = "Downloads";
"lng_downloads_view_in_chat" = "View in chat"; "lng_downloads_view_in_chat" = "View in chat";
"lng_downloads_view_in_section" = "View in downloads"; "lng_downloads_view_in_section" = "View in downloads";
@@ -52,6 +52,7 @@
<file alias="show_or_premium_readtime.tgs">../../animations/show_or_premium_readtime.tgs</file> <file alias="show_or_premium_readtime.tgs">../../animations/show_or_premium_readtime.tgs</file>
<file alias="passkeys.tgs">../../animations/passkeys.tgs</file> <file alias="passkeys.tgs">../../animations/passkeys.tgs</file>
<file alias="ban.tgs">../../animations/ban.tgs</file> <file alias="ban.tgs">../../animations/ban.tgs</file>
<file alias="cocoon.tgs">../../animations/cocoon.tgs</file>
<file alias="profile_muting.tgs">../../animations/profile/profile_muting.tgs</file> <file alias="profile_muting.tgs">../../animations/profile/profile_muting.tgs</file>
<file alias="profile_unmuting.tgs">../../animations/profile/profile_unmuting.tgs</file> <file alias="profile_unmuting.tgs">../../animations/profile/profile_unmuting.tgs</file>
@@ -9,6 +9,7 @@
<file alias="art/logo_256_no_margin.png">../../art/logo_256_no_margin.png</file> <file alias="art/logo_256_no_margin.png">../../art/logo_256_no_margin.png</file>
<file alias="art/themeimage.jpg">../../art/themeimage.jpg</file> <file alias="art/themeimage.jpg">../../art/themeimage.jpg</file>
<file alias="art/round_placeholder.jpg">../../art/round_placeholder.jpg</file> <file alias="art/round_placeholder.jpg">../../art/round_placeholder.jpg</file>
<file alias="art/cocoon.webp">../../art/cocoon.webp</file>
<file alias="day-blue.tdesktop-theme">../../day-blue.tdesktop-theme</file> <file alias="day-blue.tdesktop-theme">../../day-blue.tdesktop-theme</file>
<file alias="night.tdesktop-theme">../../night.tdesktop-theme</file> <file alias="night.tdesktop-theme">../../night.tdesktop-theme</file>
<file alias="night-green.tdesktop-theme">../../night-green.tdesktop-theme</file> <file alias="night-green.tdesktop-theme">../../night-green.tdesktop-theme</file>
@@ -354,10 +354,37 @@ not_null<DocumentData*> GenerateLocalSticker(
not_null<DocumentData*> GenerateLocalTgsSticker( not_null<DocumentData*> GenerateLocalTgsSticker(
not_null<Main::Session*> session, not_null<Main::Session*> session,
const QString &name) { const QString &name) {
const auto cache = [&] {
struct Session {
base::weak_ptr<Main::Session> session;
base::flat_map<QString, not_null<DocumentData*>> cache;
};
static auto Map = std::vector<Session>();
for (auto i = begin(Map); i != end(Map);) {
if (const auto strong = i->session.get()) {
if (strong == session) {
return &i->cache;
}
++i;
} else {
i = Map.erase(i);
}
}
Map.push_back({ .session = session });
return &Map.back().cache;
}();
const auto i = cache->find(name);
if (i != end(*cache)) {
return i->second;
}
const auto result = GenerateLocalSticker( const auto result = GenerateLocalSticker(
session, session,
u":/animations/"_q + name + u".tgs"_q); u":/animations/"_q + name + u".tgs"_q);
cache->emplace(name, result);
Ensures(result->sticker()->isLottie()); Ensures(result->sticker()->isLottie());
return result; return result;
} }
@@ -7,19 +7,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "history/view/history_view_translate_bar.h" #include "history/view/history_view_translate_bar.h"
#include "boxes/star_gift_box.h" // AddUniqueCloseButton
#include "boxes/translate_box.h" #include "boxes/translate_box.h"
#include "chat_helpers/stickers_lottie.h"
#include "core/application.h" #include "core/application.h"
#include "core/core_settings.h" #include "core/core_settings.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_document.h"
#include "history/history.h" #include "history/history.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "settings/settings_credits_graphics.h" // CreditsEntryBoxStyleOverrides
#include "ui/controls/feature_list.h"
#include "ui/effects/ministar_particles.h"
#include "ui/effects/ripple_animation.h" #include "ui/effects/ripple_animation.h"
#include "ui/boxes/choose_language_box.h" // EditSkipTranslationLanguages. #include "ui/boxes/choose_language_box.h" // EditSkipTranslationLanguages.
#include "ui/layers/box_content.h" #include "ui/layers/box_content.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/menu/menu_item_base.h" #include "ui/widgets/menu/menu_item_base.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/toast/toast.h" #include "ui/toast/toast.h"
#include "ui/top_background_gradient.h"
#include "ui/widgets/menu/menu_multiline_action.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h" #include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h" #include "ui/widgets/popup_menu.h"
@@ -27,6 +38,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h" #include "ui/painter.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/style_info.h" // infoStarsUnderstood
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
#include <QtGui/QtEvents> #include <QtGui/QtEvents>
@@ -80,6 +93,247 @@ TextParseOptions MenuTextOptions = {
Qt::LayoutDirectionAuto, // dir Qt::LayoutDirectionAuto, // dir
}; };
void AddCocoonBoxCover(not_null<Ui::VerticalLayout*> container) {
const auto cover = container->add(object_ptr<Ui::RpWidget>(container));
static const auto gradientEdge = QColor(0x06, 0x11, 0x29);
static const auto gradientCenter = QColor(0x09, 0x16, 0x43);
static const auto gradientLeft = QColor(0x68, 0xc9, 0xff);
static const auto gradientRight = QColor(0xcb, 0x57, 0xff);
static const auto textColor = QColor(0xb8, 0xc9, 0xef);
static const auto boldColor = QColor(0xff, 0xff, 0xff);
const auto logoTop = st::cocoonLogoTop;
const auto logoSize = st::cocoonLogoSize;
const auto ratio = style::DevicePixelRatio();
auto logo = QImage(u":/gui/art/cocoon.webp"_q).scaled(
QSize(logoSize, logoSize) * ratio,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
logo.setDevicePixelRatio(ratio);
const auto titleTop = logoTop + logoSize + st::cocoonTitleTop;
const auto subtitleTop = titleTop
+ st::cocoonTitleFont->height
+ st::cocoonSubtitleTop;
const auto boldToColorized = [](QString text) {
auto result = tr::rich(text);
auto &entities = result.entities;
for (auto i = entities.begin(); i != entities.end(); ++i) {
if (i->type() == EntityType::Bold) {
i = entities.insert(
i,
EntityInText(
EntityType::Colorized,
i->offset(),
i->length()));
++i;
}
}
return result;
};
struct State {
QImage gradient;
Ui::Animations::Basic animation;
std::optional<Ui::StarParticles> particles;
style::owned_color subtitleFg = style::owned_color{ textColor };
style::owned_color subtitleBoldFg = style::owned_color{ boldColor };
style::FlatLabel subtitleSt = st::cocoonSubtitle;
};
const auto state = cover->lifetime().make_state<State>();
state->subtitleSt.textFg = state->subtitleFg.color();
state->subtitleSt.palette.linkFg = state->subtitleBoldFg.color();
state->animation.init([=] {
cover->update();
if (anim::Disabled()) {
state->animation.stop();
}
});
constexpr auto kParticlesCount = 50;
state->particles.emplace(
Ui::StarParticles::Type::RadialInside,
kParticlesCount,
st::cocoonLogoSize / 12);
state->particles->setSpeed(0.05);
auto particleColors = std::vector<QColor>();
constexpr auto kParticleColors = 12;
particleColors.reserve(kParticleColors);
for (auto i = 0; i != kParticleColors; ++i) {
particleColors.push_back(anim::color(
gradientLeft,
gradientRight,
float64(i) / (kParticleColors - 1)));
}
state->particles->setColors(std::move(particleColors));
const auto subtitle = CreateChild<Ui::FlatLabel>(
cover,
tr::lng_translate_cocoon_subtitle(
lt_text,
tr::lng_translate_cocoon_explain(boldToColorized),
tr::rich),
state->subtitleSt);
subtitle->setTryMakeSimilarLines(true);
cover->widthValue() | rpl::on_next([=](int width) {
const auto available = width
- st::boxRowPadding.left()
- st::boxRowPadding.right();
subtitle->resizeToWidth(available);
subtitle->moveToLeft(st::boxRowPadding.left(), subtitleTop);
const auto bottom = st::cocoonSubtitleBottom;
cover->resize(width, subtitle->y() + subtitle->height() + bottom);
}, cover->lifetime());
cover->paintRequest() | rpl::on_next([=] {
auto p = Painter(cover);
const auto width = cover->width();
const auto ratio = style::DevicePixelRatio();
if (state->gradient.size() != cover->size() * ratio) {
state->gradient = Ui::CreateTopBgGradient(
cover->size(),
gradientCenter,
gradientEdge);
auto font = st::cocoonTitleFont->f;
font.setWeight(QFont::Bold);
auto metrics = QFontMetrics(font);
const auto text = tr::lng_translate_cocoon_title(tr::now);
const auto textw = metrics.width(text);
const auto left = (width - textw) / 2;
auto q = QPainter(&state->gradient);
auto hq = PainterHighQualityEnabler(q);
auto gradient = QLinearGradient(left, 0, left + textw, 0);
gradient.setStops({
{ 0., gradientLeft },
{ 1., gradientRight },
});
auto pen = QPen(QBrush(gradient), 1.);
q.setPen(pen);
q.setFont(font);
q.setBrush(Qt::NoBrush);
q.drawText(left, titleTop + metrics.ascent(), text);
}
p.drawImage(0, 0, state->gradient);
const auto logoRect = QRect(
(width - logoSize) / 2,
logoTop,
logoSize,
logoSize);
const auto paddingAdd = int(base::SafeRound(logoTop * 1.2));
const auto particlesRect = logoRect.marginsAdded(
{ paddingAdd, paddingAdd, paddingAdd, paddingAdd });
state->particles->paint(p, particlesRect, crl::now());
if (!anim::Disabled() && !state->animation.animating()) {
state->animation.start();
}
p.drawImage(logoRect, logo);
}, cover->lifetime());
}
struct CocoonLinkInfo {
QString text;
QString link;
};
[[nodiscard]] CocoonLinkInfo CocoonMention() {
const auto mention = tr::lng_translate_cocoon_private_mention(tr::now);
const auto username = QString(mention).replace('@', QString());
const auto link = u"tg://resolve?domain="_q + username;
return { mention, link };
}
[[nodiscard]] CocoonLinkInfo CocoonDomain() {
const auto domain = tr::lng_translate_cocoon_everyone_domain(tr::now);
const auto link = u"https://"_q + domain;
return { domain, link };
}
[[nodiscard]] CocoonLinkInfo CocoonDirect() {
const auto text = tr::lng_translate_cocoon_text_link(tr::now);
const auto link = u"https://"_q + text;
return { text, link };
}
void AboutCocoonBox(not_null<Ui::GenericBox*> box) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::stakeBox);
box->setNoContentMargin(true);
const auto container = box->verticalLayout();
AddCocoonBoxCover(container);
AddUniqueCloseButton(box, {});
const auto mention = CocoonMention();
const auto domain = CocoonDomain();
const auto direct = CocoonDirect();
const auto features = std::vector<Ui::FeatureListEntry>{
{
st::menuIconLock,
tr::lng_translate_cocoon_private_title(tr::now),
tr::lng_translate_cocoon_private_text(
tr::now,
lt_mention,
tr::link(mention.text, mention.link),
tr::rich),
},
{
st::menuIconStats,
tr::lng_translate_cocoon_efficient_title(tr::now),
tr::lng_translate_cocoon_efficient_text(tr::now, tr::rich),
},
{
st::menuIconGiftPremium,
tr::lng_translate_cocoon_everyone_title(tr::now),
tr::lng_translate_cocoon_everyone_text(
tr::now,
lt_domain,
tr::link(domain.text, domain.link),
tr::rich),
},
};
auto margin = QMargins(0, st::defaultVerticalListSkip, 0, 0);
for (const auto &feature : features) {
box->addRow(
MakeFeatureListEntry(box, feature),
st::boxRowPadding + margin);
margin = {};
}
box->addRow(
object_ptr<Ui::PlainShadow>(box),
st::cocoonJoinSeparatorPadding);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_translate_cocoon_text(
lt_link,
rpl::single(tr::link(direct.text, direct.link)),
tr::rich),
st::cocoonJoinLabel),
style::al_top);
box->addButton(rpl::single(QString()), [=] {
box->closeBox();
})->setText(rpl::single(Ui::Text::IconEmoji(
&st::infoStarsUnderstood
).append(' ').append(tr::lng_translate_cocoon_done(tr::now))));
}
TwoTextAction::TwoTextAction( TwoTextAction::TwoTextAction(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
const style::Menu &st, const style::Menu &st,
@@ -476,6 +730,36 @@ void TranslateBar::showMenu(base::unique_qptr<Ui::PopupMenu> menu) {
tr::lng_translate_menu_hide(tr::now), tr::lng_translate_menu_hide(tr::now),
hideBar, hideBar,
&st::menuIconCancel); &st::menuIconCancel);
_menu->addSeparator();
const auto cocoon = ChatHelpers::GenerateLocalTgsSticker(
&_history->session(),
u"cocoon"_q);
cocoon->overrideEmojiUsesTextColor(true);
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
_menu,
st::defaultMenu,
st::historyTranslateCocoonLabel,
QPoint(
st::defaultMenu.itemPadding.left(),
st::defaultMenu.itemPadding.top()),
tr::lng_translate_cocoon_menu(
tr::now,
lt_emoji,
Data::SingleCustomEmoji(cocoon),
lt_link,
tr::link(tr::lng_translate_cocoon_link(tr::now, tr::bold)),
tr::rich),
Core::TextContext({
.session = &_history->session(),
.customEmojiLoopLimit = -1,
}));
item->clicks(
) | rpl::on_next([controller = _controller] {
controller->show(Box(AboutCocoonBox));
}, item->lifetime());
_menu->addAction(std::move(item));
_menu->popup(_wrap.mapToGlobal( _menu->popup(_wrap.mapToGlobal(
QPoint(_wrap.width(), 0) + st::historyTranslateMenuPosition)); QPoint(_wrap.width(), 0) + st::historyTranslateMenuPosition));
} }
+26
View File
@@ -971,6 +971,32 @@ historyTranslateSettings: IconButton(defaultIconButton) {
} }
} }
historyTranslateMenuPosition: point(-6px, 30px); historyTranslateMenuPosition: point(-6px, 30px);
historyTranslateCocoonLabel: FlatLabel(historyHasCustomEmoji) {
style: TextStyle(defaultTextStyle) {
font: font(12px);
}
margin: margins(4px, 8px, 4px, 8px);
}
cocoonLogoTop: 32px;
cocoonLogoSize: 96px;
cocoonTitleFont: font(36px semibold);
cocoonTitleTop: 12px;
cocoonSubtitle: FlatLabel(defaultFlatLabel) {
minWidth: 120px;
align: align(top);
}
cocoonSubtitleTop: 8px;
cocoonSubtitleBottom: 16px;
cocoonJoinSeparatorPadding: margins(24px, 4px, 24px, 12px);
cocoonJoinLabel: FlatLabel(defaultFlatLabel) {
style: TextStyle(defaultTextStyle) {
font: font(12px);
}
textFg: windowSubTextFg;
minWidth: 120px;
align: align(top);
}
historySponsoredAboutMenuLabelPosition: point(54px, 4px); historySponsoredAboutMenuLabelPosition: point(54px, 4px);
@@ -42,6 +42,7 @@ object_ptr<RpWidget> MakeFeatureListEntry(
icon->show(); icon->show();
title->show(); title->show();
about->show(); about->show();
about->setLinksTrusted();
widget->widthValue( widget->widthValue(
) | rpl::on_next([=](int width) { ) | rpl::on_next([=](int width) {
const auto left = st::infoStarsFeatureLabelLeft; const auto left = st::infoStarsFeatureLabelLeft;
@@ -32,7 +32,31 @@ void StarParticles::setVisible(float visible) {
} }
void StarParticles::setColor(const QColor &color) { void StarParticles::setColor(const QColor &color) {
_color = color; setColors({ color });
}
void StarParticles::setColors(std::vector<QColor> colors) {
Expects(!colors.empty());
const auto was = int(_colors.size());
_colors = std::move(colors);
const auto now = int(_colors.size());
_paintedCaches.clear();
if (now != was) {
if (now > 1) {
auto buffered = base::BufferedRandom<uint32>(_particles.size());
for (auto &p : _particles) {
p.colorIndex = base::RandomIndex(now, buffered);
}
} else {
for (auto &p : _particles) {
p.colorIndex = 0;
}
}
}
} }
QImage StarParticles::generateStarCache(int size, QColor color) { QImage StarParticles::generateStarCache(int size, QColor color) {
@@ -76,12 +100,14 @@ void StarParticles::generate() {
_particles.clear(); _particles.clear();
_particles.reserve(_count); _particles.reserve(_count);
constexpr auto kRandomPerParticle = 12; constexpr auto kRandomPerParticle = 13;
auto random = bytes::vector(_count * kRandomPerParticle); auto random = bytes::vector(_count * kRandomPerParticle);
base::RandomFill(random.data(), random.size()); base::RandomFill(random.data(), random.size());
auto idx = 0; auto idx = 0;
const auto next = [&] { return uchar(random[idx++]) / 255.; }; const auto next = [&] { return uchar(random[idx++]) / 255.; };
const auto colors = int(_colors.size());
for (auto i = 0; i < _count; ++i) { for (auto i = 0; i < _count; ++i) {
auto p = Particle(); auto p = Particle();
p.x = next(); p.x = next();
@@ -106,6 +132,10 @@ void StarParticles::generate() {
p.vy = -std::sin(angle) * speed; p.vy = -std::sin(angle) * speed;
} }
if (colors > 1) {
p.colorIndex = int(base::SafeRound(next() * (colors - 1)));
}
_particles.push_back(p); _particles.push_back(p);
} }
} }
@@ -116,13 +146,26 @@ void StarParticles::paint(QPainter &p, const QRect &rect, crl::time now) {
return; return;
} }
const auto colorKey = _color.rgba(); const auto validate = [&](int colorIndex) {
const auto i = _starCache.find(colorKey); Expects(colorIndex >= 0 && colorIndex < _colors.size());
const auto &cache = (i != end(_starCache))
? i->second if (_paintedCaches.empty()) {
: _starCache.emplace( _paintedCaches.resize(_colors.size());
colorKey, } else if (const auto result = _paintedCaches[colorIndex]
generateStarCache(_starSize, _color)).first->second; ; !result.isNull()) {
return result;
}
const auto &color = _colors[colorIndex];
const auto colorKey = color.rgba();
const auto i = _starCache.find(colorKey);
const auto &cache = (i != end(_starCache))
? i->second
: _starCache.emplace(
colorKey,
generateStarCache(_starSize, color)).first->second;
_paintedCaches[colorIndex] = cache;
return cache;
};
const auto dt = (now - _lastTime) / 1000.; const auto dt = (now - _lastTime) / 1000.;
_lastTime = now; _lastTime = now;
@@ -147,6 +190,7 @@ void StarParticles::paint(QPainter &p, const QRect &rect, crl::time now) {
const auto x = rect.x() + particle.x * rect.width(); const auto x = rect.x() + particle.x * rect.width();
const auto y = rect.y() + particle.y * rect.height(); const auto y = rect.y() + particle.y * rect.height();
const auto cache = validate(particle.colorIndex);
const auto size = cache.width() const auto size = cache.width()
/ cache.devicePixelRatio() * particle.size; / cache.devicePixelRatio() * particle.size;
@@ -24,10 +24,12 @@ public:
void setSpeed(float speed); void setSpeed(float speed);
void setVisible(float visible); void setVisible(float visible);
void setColor(const QColor &color); void setColor(const QColor &color);
void setColors(std::vector<QColor> colors);
void paint(QPainter &p, const QRect &rect, crl::time now); void paint(QPainter &p, const QRect &rect, crl::time now);
private: private:
struct Particle { struct Particle {
int colorIndex = 0;
float x = 0.; float x = 0.;
float y = 0.; float y = 0.;
float vx = 0.; float vx = 0.;
@@ -47,7 +49,8 @@ private:
int _starSize = 0; int _starSize = 0;
float _speed = 1.f; float _speed = 1.f;
float _visible = 1.f; float _visible = 1.f;
QColor _color = QColor(255, 200, 70); std::vector<QColor> _colors = { QColor(255, 200, 70) };
std::vector<QImage> _paintedCaches;
std::vector<Particle> _particles; std::vector<Particle> _particles;
base::flat_map<QRgb, QImage> _starCache; base::flat_map<QRgb, QImage> _starCache;
crl::time _lastTime = 0; crl::time _lastTime = 0;