diff --git a/Telegram/Resources/icons/menu/craft_chance.svg b/Telegram/Resources/icons/menu/craft_chance.svg
new file mode 100644
index 0000000000..c3ea4c40db
--- /dev/null
+++ b/Telegram/Resources/icons/menu/craft_chance.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index eab0525678..dfdb5f8e48 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -4100,17 +4100,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_collection_remove_from" = "Remove from Collection";
"lng_gift_locked_title" = "Gift Locked";
"lng_gift_craft_info_title" = "Gift Crafting";
-"lng_gift_craft_info_about" = "Use your existing gifts to craft new ones.";
-"lng_gift_craft_combine_title" = "New Models";
-"lng_gift_craft_combine_about" = "Add up to 4 NFT Gifts to attempt crafting a new upgraded model.";
-"lng_gift_craft_input_title" = "Input Matters";
-"lng_gift_craft_input_about" = "Combine more gifts to improve your success chance.";
-"lng_gift_craft_exclusive_title" = "Exclusive Look";
-"lng_gift_craft_exclusive_about" = "Reforge gifts into a rarer collectibles with a new look.";
+"lng_gift_craft_info_about" = "Turn your gifts into rare, epic\nand legendary versions.";
+"lng_gift_craft_rare_title" = "Get Rare Models";
+"lng_gift_craft_rare_about" = "Select up to 4 gifts to craft a new exclusive model.";
+"lng_gift_craft_chance_title" = "Maximize Chances";
+"lng_gift_craft_chance_about" = "Combine more gifts to increase your odds of success.";
+"lng_gift_craft_affect_title" = "Affect the Result";
+"lng_gift_craft_affect_about" = "Use gifts with the same attribute to boost its chance.";
"lng_gift_craft_start_button" = "Start Crafting";
"lng_gift_craft_title" = "Craft Gift";
-"lng_gift_craft_about1" = "Add up to **4** gifts to craft new {gift}.";
-"lng_gift_craft_about2" = "If crafting fails, all selected gifts will be **consumed**.";
+"lng_gift_craft_about1" = "Add up to **4** gifts to craft new\n{gift}.";
+"lng_gift_craft_about2" = "If crafting fails, all used gifts\nwill be lost.";
"lng_gift_craft_view_all" = "{emoji} View all new variants {arrow}";
"lng_gift_craft_button" = "Craft {gift}";
"lng_gift_craft_button_chance" = "{percent} Success Chance";
@@ -4121,9 +4121,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_craft_select_market#one" = "{count} suitable gift on sale";
"lng_gift_craft_select_market#other" = "{count} suitable gifts on sale";
"lng_gift_craft_progress" = "Crafting";
+"lng_gift_craft_progress_about" = "Crafting **{gift}**";
"lng_gift_craft_progress_chance" = "{percent} success chance";
-"lng_gift_craft_progress_fails" = "If crafting fails, all selected gifts will be consumed.";
+"lng_gift_craft_progress_fails" = "If crafting fails, all used\ngifts will be lost.";
"lng_gift_craft_failed" = "Crafting failed. Gifts consumed :(";
+"lng_gift_craft_failed_title" = "Crafting Failed";
+"lng_gift_craft_failed_about#one" = "This crafting attempt was unsuccessful.\n**{count}** gift was lost.";
+"lng_gift_craft_failed_about#other" = "This crafting attempt was unsuccessful.\n**{count}** gifts were lost.";
+"lng_gift_craft_new_button" = "Craft New Gift";
"lng_auction_about_title" = "Auction";
"lng_auction_about_subtitle" = "Join the battle for exclusive gifts.";
diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp
index a10fffcd15..f3af5312fb 100644
--- a/Telegram/SourceFiles/api/api_who_reacted.cpp
+++ b/Telegram/SourceFiles/api/api_who_reacted.cpp
@@ -666,12 +666,7 @@ QString FormatReadDate(TimeId date, const QDateTime &now) {
return tr::lng_mediaview_date_time(
tr::now,
lt_date,
- tr::lng_month_day(
- tr::now,
- lt_month,
- Lang::MonthDay(readDate.month())(tr::now),
- lt_day,
- QString::number(readDate.day())),
+ langDayOfMonthShort(readDate),
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
}
diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp b/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp
index 1811dbbe29..53318cad65 100644
--- a/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp
+++ b/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp
@@ -18,12 +18,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "history/view/media/history_view_sticker_player.h"
#include "info/peer_gifts/info_peer_gifts_common.h"
+#include "lang/lang_keys.h"
#include "lottie/lottie_common.h"
#include "main/main_session.h"
#include "settings/settings_credits_graphics.h"
#include "ui/boxes/boost_box.h"
#include "ui/image/image_prepare.h"
#include "ui/layers/generic_box.h"
+#include "ui/widgets/gradient_round_button.h"
+#include "ui/widgets/labels.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/top_background_gradient.h"
#include "ui/painter.h"
@@ -46,6 +49,13 @@ constexpr auto kFailureFadeDuration = crl::time(400);
constexpr auto kSuccessFadeInDuration = crl::time(300);
constexpr auto kSuccessExpandDuration = crl::time(400);
constexpr auto kSuccessExpandStart = crl::time(100);
+constexpr auto kProgressFadeInDuration = crl::time(300);
+constexpr auto kFailureFadeInDuration = crl::time(300);
+
+[[nodiscard]] QString FormatPercent(int permille) {
+ const auto rounded = (permille + 5) / 10;
+ return QString::number(rounded) + '%';
+}
struct Rotation {
float64 x = 0.;
@@ -424,8 +434,7 @@ void PaintCubeFirstFlight(
sideBg.setAlphaF(progress);
auto hq = PainterHighQualityEnabler(p);
- const auto offset = shared->craftingOffsetY;
- const auto forge = shared->forgeRect.translated(0, offset);
+ const auto forge = shared->forgeRect;
const auto radius = (1. - progress) * st::boxRadius;
p.setPen(Qt::NoPen);
p.setBrush(overlayBg);
@@ -450,35 +459,37 @@ void FailureAnimationCheck(
not_null canvas,
crl::time now) {
const auto animation = state->failureAnimation.get();
- if (!animation) {
+ if (!animation || animation->started) {
return;
}
const auto elapsed = (now - state->animationStartTime);
const auto left = (state->animationDuration * Slowing()) - elapsed;
- if (!animation->started
- && left > 0
- && left <= kFailureFadeDuration * Slowing()) {
- animation->started = true;
- auto &covers = state->shared->covers;
- for (auto i = 0; i != 5; ++i) {
- auto &cover = covers[i];
- if (!cover.shown) {
- animation->finalCoverIndex = i;
- if (i != 4) {
- auto &last = covers.back();
- cover.backdrop = last.backdrop;
- cover.pattern = std::move(last.pattern);
- cover.button1 = last.button1;
- cover.button2 = last.button2;
- }
- cover.shown = true;
- cover.shownAnimation.start([=] {
- canvas->update();
- }, 0., 1., left);
- break;
+ if (left <= 0 || left > kFailureFadeDuration * Slowing()) {
+ return;
+ }
+ animation->started = true;
+ auto &covers = state->shared->covers;
+ for (auto i = 0; i != 5; ++i) {
+ auto &cover = covers[i];
+ if (!cover.shown) {
+ animation->finalCoverIndex = i;
+ if (i != 4) {
+ auto &last = covers.back();
+ cover.backdrop = last.backdrop;
+ cover.pattern = std::move(last.pattern);
+ cover.button1 = last.button1;
+ cover.button2 = last.button2;
}
+ cover.shown = true;
+ cover.shownAnimation.start([=] {
+ canvas->update();
+ }, 0., 1., left);
+ break;
}
}
+
+ state->failureShown = true;
+ state->progressShown = false;
}
void FailureAnimationPrepareFrame(
@@ -521,6 +532,11 @@ void FailureAnimationPrepareFrame(
p.fillRect(rect, bg);
}
st::craftFail.paintInCenter(p, rect, st::white->c);
+ p.setOpacity(progress);
+ p.drawImage(
+ st::craftForgePadding,
+ st::craftForgePadding,
+ state->shared->lostRadial);
}
[[nodiscard]] QRect PrepareCraftFrame(
@@ -559,11 +575,11 @@ void FailureAnimationPrepareFrame(
}
void PaintCube(
- QPainter &p,
- not_null state,
- not_null canvas,
- QPointF center,
- float64 size) {
+ QPainter &p,
+ not_null state,
+ not_null canvas,
+ QPointF center,
+ float64 size) {
const auto shared = state->shared.get();
const auto now = crl::now();
@@ -824,15 +840,13 @@ void PaintFlyingGift(
void PaintSlideOutCorner(
QPainter &p,
const CraftState::CornerSnapshot &corner,
- float64 offsetY,
float64 progress) {
if (corner.originalRect.isEmpty()) {
return;
}
const auto ratio = style::DevicePixelRatio();
- const auto originalTopLeft = QPointF(corner.originalRect.topLeft());
- const auto currentTopLeft = originalTopLeft + QPointF(0, offsetY);
+ const auto currentTopLeft = QPointF(corner.originalRect.topLeft());
if (corner.giftButton) {
const auto frame = corner.gift(0.);
const auto giftSize = QSizeF(frame.size()) / ratio;
@@ -887,19 +901,6 @@ void PaintSlideOutPhase(
float64 progress) {
const auto ratio = style::DevicePixelRatio();
- if (!shared->topPart.isNull()) {
- const auto topSize = QSizeF(shared->topPart.size()) / ratio;
- const auto slideDistance = topSize.height();
- const auto offsetY = slideDistance * progress;
- const auto opacity = 1. - progress;
-
- p.setOpacity(opacity);
- p.drawImage(
- shared->topPartRect.topLeft() - QPointF(0, offsetY),
- shared->topPart);
- p.setOpacity(1.);
- }
-
if (!shared->bottomPart.isNull()) {
const auto slideDistance = QSizeF(shared->bottomPart.size()).height()
/ ratio;
@@ -913,13 +914,12 @@ void PaintSlideOutPhase(
p.setOpacity(1.);
}
- const auto offset = shared->craftingOffsetY;
for (const auto &corner : shared->corners) {
- PaintSlideOutCorner(p, corner, offset, progress);
+ PaintSlideOutCorner(p, corner, progress);
}
auto hq = PainterHighQualityEnabler(p);
- const auto forge = shared->forgeRect.translated(0, offset);
+ const auto forge = shared->forgeRect;
const auto radius = st::boxRadius;
p.setPen(Qt::NoPen);
p.setBrush(shared->forgeBgOverlay);
@@ -932,6 +932,46 @@ void PaintSlideOutPhase(
shared->forgePercent);
}
+void PaintFailureThumbnails(
+ QPainter &p,
+ not_null shared,
+ QSize canvasSize,
+ float64 fadeProgress) {
+ if (fadeProgress <= 0.) {
+ return;
+ }
+
+ p.setOpacity(fadeProgress);
+
+ const auto width = canvasSize.width();
+ const auto giftsCount = int(shared->lostGifts.size());
+ const auto thumbSize = shared->lostGifts.front().thumbnail.size()
+ / style::DevicePixelRatio();
+ const auto thumbSpacing = st::boxRowPadding.left() / 2;
+ const auto totalThumbWidth = giftsCount * thumbSize.width()
+ + (giftsCount - 1) * thumbSpacing;
+ const auto available = width
+ - st::boxRowPadding.left()
+ - st::boxRowPadding.right();
+ const auto skip = (totalThumbWidth > available)
+ ? (available - giftsCount * thumbSize.width()) / (giftsCount - 1)
+ : thumbSpacing;
+ const auto full = giftsCount * thumbSize.width()
+ + (giftsCount - 1) * skip;
+ auto x = (width - full) / 2;
+ const auto y = shared->forgeRect.bottom() + st::craftFailureThumbsTop;
+
+ for (const auto &gift : shared->lostGifts) {
+ if (!gift.thumbnail.isNull()) {
+ const auto targetRect = QRect(QPoint(x, y), thumbSize);
+ p.drawImage(targetRect, gift.thumbnail);
+ }
+ x += thumbSize.width() + skip;
+ }
+
+ p.setOpacity(1.);
+}
+
[[nodiscard]] std::array RotateCornersForFace(
std::array corners,
int rotation) {
@@ -1018,8 +1058,7 @@ void StartGiftFlightToFace(
const auto cubeSize = float64(shared->forgeRect.width());
const auto cubeCenter = QPointF(shared->forgeRect.topLeft())
- + QPointF(cubeSize, cubeSize) / 2.
- + QPointF(0, shared->craftingOffsetY);
+ + QPointF(cubeSize, cubeSize) / 2.;
const auto targetCorners = ComputeCubeFaceCorners(
cubeCenter,
@@ -1090,8 +1129,7 @@ void PaintLoadingAnimation(
}
const auto shared = state->shared.get();
- const auto forge = shared->forgeRect.translated(
- { 0, shared->craftingOffsetY });
+ const auto forge = shared->forgeRect;
const auto inner = forge.marginsRemoved({
st::craftForgePadding,
st::craftForgePadding,
@@ -1150,12 +1188,6 @@ void CraftState::paint(
return gradient;
};
auto patternOffsetY = 0.;
- if (slideProgress > 0.
- && containerHeight > 0
- && craftingAreaCenterY > 0) {
- patternOffsetY = (containerHeight / 2. - craftingAreaCenterY)
- * slideProgress;
- }
const auto paintPattern = [&](
QPainter &q,
PatternView &pattern,
@@ -1279,28 +1311,202 @@ CraftState::EmptySide CraftState::prepareEmptySide(
return { .bg = bg, .frame = result };
}
+void SetupProgressControls(
+ not_null state,
+ not_null canvas) {
+ const auto add = [&](not_null label) {
+ state->progressOpacity.value() | rpl::on_next([=](float64 opacity) {
+ label->setOpacity(opacity);
+ }, label->lifetime());
+ };
+
+ const auto controls = CreateChild(canvas);
+ controls->resizeToWidth(canvas->width());
+ controls->move(0, state->shared->craftingBottom);
+
+ const auto title = controls->add(
+ object_ptr(
+ controls,
+ tr::lng_gift_craft_progress(),
+ st::uniqueGiftTitle),
+ st::boxRowPadding + st::craftProgressTitleMargin,
+ style::al_top);
+ add(title);
+ title->setTextColorOverride(st::white->c);
+
+ const auto subColor = QColor(255, 255, 255, 178);
+ const auto about = controls->add(
+ object_ptr(
+ controls,
+ tr::lng_gift_craft_progress_about(
+ lt_gift,
+ rpl::single(tr::marked(state->shared->giftName)),
+ tr::rich),
+ st::uniqueGiftSubtitle),
+ st::boxRowPadding + st::craftProgressAboutMargin,
+ style::al_top);
+ add(about);
+ about->setTextColorOverride(subColor);
+
+ const auto warning = controls->add(
+ object_ptr(
+ controls,
+ tr::lng_gift_craft_progress_fails(),
+ st::craftAbout),
+ st::boxRowPadding + st::craftProgressWarningMargin,
+ style::al_top);
+ add(warning);
+ warning->setTextColorOverride(subColor);
+
+ const auto chance = controls->add(
+ object_ptr(
+ controls,
+ tr::lng_gift_craft_progress_chance(
+ tr::now,
+ lt_percent,
+ FormatPercent(state->shared->successPermille)),
+ st::craftChance),
+ st::boxRowPadding + st::craftProgressChanceMargin,
+ style::al_top);
+ add(chance);
+ chance->setTextColorOverride(st::white->c);
+ chance->paintOn([=](QPainter &p) {
+ if (const auto opacity = state->progressOpacity.current()) {
+ p.setPen(Qt::NoPen);
+ p.setOpacity(opacity);
+ p.setBrush(state->shared->forgeBgOverlay);
+ const auto radius = chance->height() / 2.;
+ p.drawRoundedRect(chance->rect(), radius, radius);
+ }
+ });
+
+ controls->showOn(state->progressOpacity.value(
+ ) | rpl::map(
+ rpl::mappers::_1 > 0.
+ ) | rpl::distinct_until_changed());
+}
+
+void SetupFailureControls(
+ not_null state,
+ not_null canvas) {
+ const auto add = [&](not_null label) {
+ state->failureOpacity.value() | rpl::on_next([=](float64 opacity) {
+ label->setOpacity(opacity);
+ }, label->lifetime());
+ };
+
+ const auto controls = CreateChild(canvas);
+ controls->resizeToWidth(canvas->width());
+ controls->move(0, state->shared->craftingBottom);
+
+ const auto title = controls->add(
+ object_ptr(
+ controls,
+ tr::lng_gift_craft_failed_title(),
+ st::uniqueGiftTitle),
+ st::boxRowPadding + st::craftFailureTitleMargin,
+ style::al_top);
+ add(title);
+ title->setTextColorOverride(QColor(0xF8, 0x4A, 0x4A));
+
+ const auto about = controls->add(
+ object_ptr(
+ controls,
+ tr::lng_gift_craft_failed_about(
+ lt_count,
+ rpl::single(state->shared->lostGifts.size() * 1.),
+ tr::rich),
+ st::uniqueGiftSubtitle),
+ st::boxRowPadding + st::craftFailureAboutMargin,
+ style::al_top);
+ add(about);
+ about->setTextColorOverride(QColor(0xFF, 0xBC, 0x9B));
+
+ controls->showOn(state->failureOpacity.value(
+ ) | rpl::map(
+ rpl::mappers::_1 > 0.
+ ) | rpl::distinct_until_changed());
+}
+
+void SetupCraftNewButton(
+ not_null state,
+ not_null canvas) {
+ const auto button = CreateChild(
+ canvas,
+ QGradientStops{
+ { 0., QColor(0xE2, 0x75, 0x19) },
+ { 1., QColor(0xDD, 0x48, 0x19) },
+ });
+ button->setFullRadius(true);
+ button->setClickedCallback([=] {
+ state->retryWithNewGift();
+ });
+
+ const auto buttonLabel = CreateChild(
+ button,
+ tr::lng_gift_craft_new_button(),
+ st::creditsBoxButtonLabel);
+ buttonLabel->setTextColorOverride(st::white->c);
+ buttonLabel->setAttribute(
+ Qt::WA_TransparentForMouseEvents);
+ button->sizeValue() | rpl::on_next([=](QSize size) {
+ buttonLabel->moveToLeft(
+ (size.width() - buttonLabel->width()) / 2,
+ st::giftBox.button.textTop);
+ }, buttonLabel->lifetime());
+
+ const auto buttonWidth = canvas->width()
+ - st::craftBoxButtonPadding.left()
+ - st::craftBoxButtonPadding.right();
+ const auto buttonHeight = st::giftBox.button.height;
+ button->setNaturalWidth(buttonWidth);
+ button->setGeometry(
+ st::craftBoxButtonPadding.left(),
+ state->shared->originalButtonY,
+ buttonWidth,
+ buttonHeight);
+ button->show();
+
+ button->shownValue() | rpl::filter(
+ rpl::mappers::_1
+ ) | rpl::take(1) | rpl::on_next([=] {
+ button->startGlareAnimation();
+ }, button->lifetime());
+}
+
void StartCraftAnimation(
not_null box,
std::shared_ptr show,
std::shared_ptr shared,
Fn startRequest,
- Fn closeParent) {
+ Fn closeParent,
+ Fn closeCurrent)> retryWithNewGift) {
const auto container = box->verticalLayout();
- const auto closeOnFail = crl::guard(box, [=] {
- if (const auto onstack = closeParent) {
- onstack();
- }
- box->closeBox();
- show->showToast("failed");// tr::lng_gift_craft_failed(tr::now));
- });
-
while (container->count() > 0) {
delete container->widgetAt(0);
}
+
auto canvas = object_ptr(container);
const auto raw = canvas.data();
const auto state = raw->lifetime().make_state();
+ state->retryWithNewGift = [=] {
+ retryWithNewGift(crl::guard(box, [=] { box->closeBox(); }));
+ };
+
+ const auto title = CreateChild(
+ raw,
+ tr::lng_gift_craft_title(),
+ st::uniqueGiftTitle);
+ title->naturalWidthValue() | rpl::on_next([=](int titleWidth) {
+ const auto width = st::boxWideWidth;
+ title->moveToLeft(
+ (width - titleWidth) / 2,
+ st::craftTitleMargin.top(),
+ width);
+ }, title->lifetime());
+ title->setTextColorOverride(st::white->c);
+
const auto height = shared->containerHeight;
const auto craftingHeight = shared->craftingBottom - shared->craftingTop;
state->shared = std::move(shared);
@@ -1319,6 +1525,38 @@ void StartCraftAnimation(
[=] { raw->update(); },
st::craftForgeLoading);
+ SetupProgressControls(state, raw);
+ SetupFailureControls(state, raw);
+
+ state->progressShown.value(
+ ) | rpl::combine_previous(
+ ) | rpl::on_next([=](bool wasShown, bool nowShown) {
+ if (wasShown == nowShown) {
+ return;
+ }
+ const auto from = wasShown ? 1. : 0.;
+ const auto to = nowShown ? 1. : 0.;
+ state->progressFadeIn.start([=] {
+ raw->update();
+ }, from, to, kProgressFadeInDuration, anim::easeOutCubic);
+ }, raw->lifetime());
+
+ state->failureShown.value(
+ ) | rpl::combine_previous(
+ ) | rpl::on_next([=](bool wasShown, bool nowShown) {
+ if (wasShown == nowShown) {
+ return;
+ }
+ const auto from = wasShown ? 1. : 0.;
+ const auto to = nowShown ? 1. : 0.;
+ state->failureFadeIn.start([=] {
+ raw->update();
+ if (!state->failureFadeIn.animating()) {
+ SetupCraftNewButton(state, raw);
+ }
+ }, from, to, kFailureFadeDuration, anim::easeOutCubic);
+ }, raw->lifetime());
+
raw->paintOn([=](QPainter &p) {
const auto shared = state->shared.get();
const auto success = state->successAnimation.get();
@@ -1347,27 +1585,31 @@ void StartCraftAnimation(
}
}
- shared->craftingOffsetY = (shared->containerHeight / 2.)
- - shared->craftingAreaCenterY;
if (slideProgress < 1.) {
- shared->craftingOffsetY *= slideProgress;
PaintSlideOutPhase(p, shared, craftingSize, slideProgress);
} else {
- if (success) {
- const auto target = success->coverShift;
- shared->craftingOffsetY -= target * success->expanded;
+ const auto craftingOffsetY = success
+ ? (-success->coverShift * success->expanded)
+ : 0.;
+ if (success && success->expanded > 0.) {
+ const auto width = st::boxWideWidth;
+ const auto top = st::craftTitleMargin.top();
+ title->moveToLeft(
+ (width - title->naturalWidth()) / 2,
+ top - (success->expanded * (top + title->height())),
+ width);
}
const auto cubeSize = float64(shared->forgeRect.width());
const auto cubeCenter = QPointF(shared->forgeRect.topLeft())
+ QPointF(cubeSize, cubeSize) / 2.
- + QPointF(0, shared->craftingOffsetY);
+ + QPointF(0, craftingOffsetY);
for (auto i = 0; i < 4; ++i) {
const auto &corner = shared->corners[i];
if (corner.giftButton && state->giftToSide[i].face < 0) {
const auto giftTopLeft = QPointF()
+ QPointF(corner.originalRect.topLeft())
- + QPointF(0, shared->craftingOffsetY);
+ + QPointF(0, craftingOffsetY);
p.drawImage(giftTopLeft, corner.gift(0));
}
}
@@ -1399,7 +1641,7 @@ void StartCraftAnimation(
if (state->flightTargetCorners) {
const auto sourceRect = QRectF(corner.originalRect)
- .translated(0, shared->craftingOffsetY);
+ .translated(0, craftingOffsetY);
const auto sourceCorners = RectToCorners(sourceRect);
const auto interpolatedCorners = InterpolateQuadCorners(
sourceCorners,
@@ -1416,13 +1658,26 @@ void StartCraftAnimation(
cubeCenter,
cubeSize,
progress,
- shared->craftingOffsetY);
+ craftingOffsetY);
PaintFlyingGift(p, corner, position, progress);
}
}
PaintLoadingAnimation(p, state);
+
+ if (const auto opacity = state->failureOpacity.current()) {
+ PaintFailureThumbnails(
+ p,
+ shared,
+ craftingSize,
+ opacity);
+ }
}
+
+ state->progressOpacity = state->progressFadeIn.value(
+ state->progressShown.current() ? 1. : 0.);
+ state->failureOpacity = state->failureFadeIn.value(
+ state->failureShown.current() ? 1. : 0.);
});
const auto startFlying = [=] {
@@ -1472,6 +1727,8 @@ void StartCraftAnimation(
}
};
+ state->progressShown = true;
+
state->slideOutAnimation.start([=] {
raw->update();
@@ -1597,9 +1854,10 @@ void StartCraftAnimation(
raw->resizeToWidth(st::boxWideWidth);
SendPendingMoveResizeEvents(raw);
- const auto fullHeight = state->shared->containerHeight;
success->coverHeight = raw->height();
- success->coverShift = (fullHeight - raw->height()) / 2;
+ success->coverShift = state->shared->forgeRect.y()
+ + (state->shared->forgeRect.height() / 2)
+ - (raw->height() / 2);
}
tryStartFlying();
});
diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_animation.h b/Telegram/SourceFiles/boxes/star_gift_craft_animation.h
index 82a675e0cc..dc68b56e11 100644
--- a/Telegram/SourceFiles/boxes/star_gift_craft_animation.h
+++ b/Telegram/SourceFiles/boxes/star_gift_craft_animation.h
@@ -32,7 +32,9 @@ class GiftButton;
namespace Ui {
+class FlatLabel;
class GenericBox;
+class RpWidget;
class VerticalLayout;
class UniqueGiftCoverWidget;
@@ -104,7 +106,16 @@ struct CraftState {
int craftingTop = 0;
int craftingBottom = 0;
int craftingAreaCenterY = 0;
- int craftingOffsetY = 0;
+ int originalButtonY = 0;
+
+ QString giftName;
+ int successPermille = 0;
+ struct LostGift {
+ QImage thumbnail;
+ QString number;
+ };
+ std::vector lostGifts;
+ QImage lostRadial;
void paint(
QPainter &p,
@@ -178,6 +189,16 @@ struct CraftAnimationState {
std::unique_ptr successAnimation;
std::unique_ptr failureAnimation;
+
+ rpl::variable progressOpacity;
+ Animations::Simple progressFadeIn;
+ rpl::variable progressShown;
+
+ rpl::variable failureOpacity;
+ Animations::Simple failureFadeIn;
+ rpl::variable failureShown;
+
+ Fn retryWithNewGift;
};
void StartCraftAnimation(
@@ -185,6 +206,7 @@ void StartCraftAnimation(
std::shared_ptr show,
std::shared_ptr state,
Fn startRequest,
- Fn closeParent);
+ Fn closeParent,
+ Fn closeCurrent)> retryWithNewGift);
} // namespace Ui
diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp
index dba07751a8..16ee851c2d 100644
--- a/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp
+++ b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp
@@ -9,11 +9,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/call_delayed.h"
#include "base/random.h"
+#include "boxes/star_gift_auction_box.h"
#include "boxes/star_gift_craft_animation.h"
+#include "boxes/star_gift_preview_box.h"
#include "apiwrap.h"
#include "api/api_credits.h"
#include "api/api_premium.h"
#include "boxes/star_gift_box.h"
+#include "data/components/gift_auctions.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_document.h"
@@ -36,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/ui_utility.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
+#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h" // stickerPanDeleteIconFg
#include "styles/style_credits.h"
#include "styles/style_layers.h"
@@ -104,6 +108,12 @@ struct GiftForCraft {
const GiftForCraft &) = default;
};
+void ShowGiftCraftBoxInternal(
+ not_null controller,
+ std::vector gifts,
+ Fn closeParent,
+ bool autoStartCraft);
+
struct CraftingView {
object_ptr widget;
rpl::producer editRequests;
@@ -119,7 +129,8 @@ struct CraftingView {
[[nodiscard]] not_null MakeRadialPercent(
not_null parent,
const style::CraftRadialPercent &st,
- rpl::producer permille) {
+ rpl::producer permille,
+ Fn format = FormatPercent) {
auto raw = CreateChild(parent);
struct State {
@@ -130,7 +141,7 @@ struct CraftingView {
Animations::Simple animation;
NumbersAnimation numbers;
- int permille = 0;
+ int permille = -1;
};
const auto state = raw->lifetime().make_state(
st,
@@ -144,7 +155,7 @@ struct CraftingView {
raw->update();
}, state->permille, value, st::slideWrapDuration);
state->permille = value;
- state->numbers.setText(FormatPercent(value), value);
+ state->numbers.setText(format(value), value);
}, raw->lifetime());
state->animation.stop();
state->numbers.finishAnimating();
@@ -476,6 +487,7 @@ AbstractButton *MakeRemoveButton(
craftState->forgePercent = GrabWidgetToImage(state->forgeRadial);
auto giftsTotal = 0;
+ auto lostIndex = 0;
for (auto i = 0; i != 4; ++i) {
auto &entry = state->entries[i];
auto &corner = craftState->corners[i];
@@ -488,6 +500,11 @@ AbstractButton *MakeRemoveButton(
if (entry.remove) {
corner.removeButton = GrabWidgetToImage(entry.remove);
}
+ if (lostIndex < craftState->lostGifts.size()) {
+ craftState->lostGifts[lostIndex].thumbnail
+ = GrabWidgetToImage(entry.button);
+ ++lostIndex;
+ }
corner.giftButton.reset(entry.button);
entry.button->setParent(parent);
base::take(entry.button)->hide();
@@ -499,6 +516,15 @@ AbstractButton *MakeRemoveButton(
}
}
+ const auto failedCount = MakeRadialPercent(
+ raw,
+ st::craftForgePercent,
+ rpl::single(0),
+ [=](int) { return QString::number(giftsTotal); });
+ failedCount->setGeometry(state->forgeRadial->geometry());
+ craftState->lostRadial = GrabWidgetToImage(failedCount);
+ delete failedCount;
+
const auto overlayBg = craftState->forgeBgOverlay = ForgeBgOverlay();
const auto backdrop = CraftBackdrops()[giftsTotal - 1].backdrop;
craftState->forgeBg1 = anim::color(
@@ -815,6 +841,7 @@ void ShowSelectGiftBox(
void Craft(
not_null box,
+ not_null controller,
std::shared_ptr show,
std::shared_ptr state,
const std::vector &gifts,
@@ -827,7 +854,8 @@ void Craft(
const auto giftsCopy = gifts;
base::call_delayed(delay, box, [=] {
- const auto shouldSucceed = false;// (base::RandomIndex(5) != 0);
+ //static auto testing = 0;
+ const auto shouldSucceed = false;// (((++testing) / 4) % 2 == 0);
const auto count = int(giftsCopy.size());
if (shouldSucceed && count > 0) {
const auto &chosen = giftsCopy[base::RandomIndex(count)];
@@ -847,18 +875,135 @@ void Craft(
}
});
};
+ const auto session = &controller->session();
+ const auto initialGiftId = gifts.front().unique->initialGiftId;
+ auto retryWithNewGift = [=](Fn closeCurrent) {
+ session->api().request(MTPpayments_GetCraftStarGifts(
+ MTP_long(initialGiftId),
+ MTP_string(),
+ MTP_int(30)
+ )).done(crl::guard(box, [=](const MTPpayments_SavedStarGifts &result) {
+ const auto user = session->user();
+ const auto owner = &session->data();
+ const auto &data = result.data();
+ owner->processUsers(data.vusers());
+ owner->processChats(data.vchats());
+
+ auto list = std::vector();
+ list.reserve(data.vgifts().v.size());
+ for (const auto &gift : data.vgifts().v) {
+ if (auto parsed = Api::FromTL(user, gift)) {
+ list.push_back(std::move(*parsed));
+ }
+ }
+
+ ShowSelectGiftBox(controller, list, [=](GiftForCraft chosen) {
+ ShowGiftCraftBoxInternal(
+ controller,
+ { chosen },
+ closeParent,
+ false);
+ closeCurrent();
+ }, {});
+ })).send();
+ };
StartCraftAnimation(
box,
std::move(show),
std::move(state),
std::move(startRequest),
- closeParent);
+ closeParent,
+ std::move(retryWithNewGift));
+}
+
+void AddPreviewNewModels(
+ not_null container,
+ std::shared_ptr show,
+ const QString &giftName,
+ Data::UniqueGiftAttributes attributes) {
+ auto exceptional = std::vector();
+ for (const auto &model : attributes.models) {
+ if (Data::UniqueGiftAttributeHasSpecialRarity(model)) {
+ exceptional.push_back(model);
+ }
+ }
+ attributes.models = std::move(exceptional);
+
+ auto emoji = TextWithEntities();
+ const auto indices = RandomIndicesSubset(
+ int(attributes.models.size()),
+ std::min(3, int(attributes.models.size())));
+ for (const auto index : indices) {
+ emoji.append(Data::SingleCustomEmoji(
+ attributes.models[index].document));
+ }
+
+ auto badge = object_ptr(container);
+ const auto badgeRaw = badge.data();
+
+ const auto label = CreateChild(
+ badgeRaw,
+ tr::lng_gift_craft_view_all(
+ lt_emoji,
+ rpl::single(emoji),
+ lt_arrow,
+ rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
+ Text::WithEntities),
+ st::uniqueGiftResalePrice,
+ st::defaultPopupMenu,
+ Core::TextContext({ .session = &show->session() }));
+ label->setTextColorOverride(st::white->c);
+
+ label->sizeValue() | rpl::on_next([=](QSize size) {
+ const auto padding = st::craftPreviewAllPadding;
+ badgeRaw->setNaturalWidth(
+ padding.left() + size.width() + padding.right());
+ badgeRaw->resize(
+ badgeRaw->naturalWidth(),
+ padding.top() + size.height() + padding.bottom());
+ }, label->lifetime());
+
+ badgeRaw->widthValue() | rpl::on_next([=](int width) {
+ const auto padding = st::craftPreviewAllPadding;
+ label->move(padding.left(), padding.top());
+ }, badgeRaw->lifetime());
+
+ badgeRaw->paintOn([=](QPainter &p) {
+ auto hq = PainterHighQualityEnabler(p);
+ const auto rect = badgeRaw->rect();
+ const auto radius = rect.height() / 2.;
+ p.setPen(Qt::NoPen);
+ p.setBrush(ForgeBgOverlay());
+ p.drawRoundedRect(rect, radius, radius);
+ });
+
+ label->setAttribute(Qt::WA_TransparentForMouseEvents);
+ badgeRaw->setClickedCallback([=] {
+ auto previewAttrs = attributes;
+ previewAttrs.models.erase(
+ ranges::remove_if(previewAttrs.models, [](const auto &m) {
+ return !Data::UniqueGiftAttributeHasSpecialRarity(m);
+ }),
+ end(previewAttrs.models));
+ show->show(Box(
+ StarGiftPreviewBox,
+ giftName,
+ previewAttrs,
+ Data::GiftAttributeIdType::Model,
+ nullptr));
+ });
+
+ container->add(
+ std::move(badge),
+ st::craftPreviewAllMargin,
+ style::al_top);
}
void MakeCraftContent(
not_null box,
not_null controller,
std::vector gifts,
+ Data::UniqueGiftAttributes attributes,
Fn closeParent,
bool autoStartCraft) {
Expects(!gifts.empty());
@@ -902,6 +1047,7 @@ void MakeCraftContent(
}
}
+ const auto giftName = gifts.front().unique->title;
state->chosen = std::move(gifts);
for (auto i = 0; i != int(state->chosen.current().size()); ++i) {
state->craftState->covers[i].shown = true;
@@ -1030,8 +1176,15 @@ void MakeCraftContent(
state->attributes = raw->add(
MakeRarityExpectancyPreview(raw, session, state->chosen.value()),
+ st::craftAttributesMargin,
style::al_top);
+ AddPreviewNewModels(
+ raw,
+ controller->uiShow(),
+ giftName,
+ std::move(attributes));
+
raw->paintOn([=](QPainter &p) {
const auto &cs = state->craftState;
if (cs->craftingStarted) {
@@ -1050,7 +1203,7 @@ void MakeCraftContent(
const auto button = state->button = raw->add(
object_ptr(raw, QGradientStops()),
- st::giftBox.buttonPadding);
+ st::craftBoxButtonPadding);
button->setFullRadius(true);
button->startGlareAnimation();
@@ -1061,9 +1214,17 @@ void MakeCraftContent(
state->crafting = true;
const auto &cs = state->craftState;
+ const auto &gifts = state->chosen.current();
+ cs->giftName = state->name.current();
+ cs->successPermille = state->successPercentPermille.current();
- cs->topPart = GrabWidgetToImage(state->title);
- cs->topPartRect = state->title->geometry();
+ for (const auto &gift : gifts) {
+ if (gift.unique) {
+ cs->lostGifts.push_back({
+ .number = u"#%1"_q.arg(gift.unique->number),
+ });
+ }
+ }
if (state->grabCraftingView) {
state->grabCraftingView(cs);
@@ -1080,8 +1241,9 @@ void MakeCraftContent(
cs->craftingAreaCenterY = cs->forgeRect.center().y();
const auto aboutPos = state->about->pos();
- const auto buttonBottom = state->button->pos().y()
- + state->button->height();
+ const auto buttonY = state->button->pos().y();
+ const auto buttonBottom = buttonY + state->button->height();
+ cs->originalButtonY = buttonY;
const auto bottomRect = QRect(
0,
aboutPos.y(),
@@ -1109,6 +1271,7 @@ void MakeCraftContent(
Craft(
box,
+ controller,
controller->uiShow(),
state->craftState,
state->chosen.current(),
@@ -1131,19 +1294,20 @@ void MakeCraftContent(
st::resaleButtonSubtitle);
raw->widthValue() | rpl::on_next([=](int width) {
- const auto padding = st::giftBox.buttonPadding;
+ const auto padding = st::craftBoxButtonPadding;
button->setNaturalWidth(width - padding.left() - padding.right());
button->resize(button->naturalWidth(), st::giftBox.button.height);
}, raw->lifetime());
if (autoStartCraft) {
- base::call_delayed(crl::time(1000), raw, startCrafting);
+// base::call_delayed(crl::time(1000), raw, startCrafting);
}
}
-void ShowGiftCraftBoxInternal(
+void ShowGiftCraftBoxWithAttributes(
not_null controller,
std::vector gifts,
+ Data::UniqueGiftAttributes attributes,
Fn closeParent,
bool autoStartCraft) {
controller->show(Box([=](not_null box) {
@@ -1154,6 +1318,7 @@ void ShowGiftCraftBoxInternal(
box,
controller,
gifts,
+ attributes,
closeParent,
autoStartCraft);
AddUniqueCloseButton(box);
@@ -1177,6 +1342,33 @@ void ShowGiftCraftBoxInternal(
}));
}
+void ShowGiftCraftBoxInternal(
+ not_null controller,
+ std::vector gifts,
+ Fn closeParent,
+ bool autoStartCraft) {
+ const auto giftId = gifts.front().unique->initialGiftId;
+ const auto auctions = &controller->session().giftAuctions();
+ if (auto attrs = auctions->attributes(giftId)) {
+ ShowGiftCraftBoxWithAttributes(
+ controller,
+ std::move(gifts),
+ std::move(*attrs),
+ std::move(closeParent),
+ autoStartCraft);
+ } else {
+ auctions->requestAttributes(giftId, crl::guard(controller, [=] {
+ auto attrs = auctions->attributes(giftId);
+ ShowGiftCraftBoxWithAttributes(
+ controller,
+ gifts,
+ attrs ? std::move(*attrs) : Data::UniqueGiftAttributes{},
+ closeParent,
+ autoStartCraft);
+ }));
+ }
+}
+
} // namespace
void ShowGiftCraftInfoBox(
@@ -1201,19 +1393,19 @@ void ShowGiftCraftInfoBox(
const auto features = std::vector{
{
- st::menuIconTools,
- tr::lng_gift_craft_combine_title(tr::now),
- tr::lng_gift_craft_combine_about(tr::now, tr::rich),
+ st::menuIconUnique,
+ tr::lng_gift_craft_rare_title(tr::now),
+ tr::lng_gift_craft_rare_about(tr::now, tr::rich),
},
{
st::menuIconCraftTraits,
- tr::lng_gift_craft_input_title(tr::now),
- tr::lng_gift_craft_input_about(tr::now, tr::marked),
+ tr::lng_gift_craft_chance_title(tr::now),
+ tr::lng_gift_craft_chance_about(tr::now, tr::marked),
},
{
- st::menuIconUnique,
- tr::lng_gift_craft_exclusive_title(tr::now),
- tr::lng_gift_craft_exclusive_about(tr::now, tr::marked),
+ st::menuIconCraftChance,
+ tr::lng_gift_craft_affect_title(tr::now),
+ tr::lng_gift_craft_affect_about(tr::now, tr::marked),
},
};
for (const auto &feature : features) {
diff --git a/Telegram/SourceFiles/lang/lang_keys.cpp b/Telegram/SourceFiles/lang/lang_keys.cpp
index aa65ed81d7..8df01ea7e9 100644
--- a/Telegram/SourceFiles/lang/lang_keys.cpp
+++ b/Telegram/SourceFiles/lang/lang_keys.cpp
@@ -110,6 +110,20 @@ QString langDayOfMonthFull(const QDate &date) {
});
}
+QString langDayOfMonthShort(const QDate &date) {
+ auto day = date.day();
+ return langDateMaybeWithYear(date, [&](int month, int year) {
+ return QLocale().toString(date, QLocale::ShortFormat);
+ }, [day](int month, int year) {
+ return tr::lng_month_day(
+ tr::now,
+ lt_month,
+ MonthSmall(month)(tr::now),
+ lt_day,
+ QString::number(day));
+ });
+}
+
QString langMonthOfYear(int month, int year) {
return (month > 0 && month <= 12)
? tr::lng_month_year(
diff --git a/Telegram/SourceFiles/lang/lang_keys.h b/Telegram/SourceFiles/lang/lang_keys.h
index f49f4fd32c..0a2a9b9f1a 100644
--- a/Telegram/SourceFiles/lang/lang_keys.h
+++ b/Telegram/SourceFiles/lang/lang_keys.h
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
[[nodiscard]] QString langDayOfMonth(const QDate &date);
[[nodiscard]] QString langDayOfMonthFull(const QDate &date);
+[[nodiscard]] QString langDayOfMonthShort(const QDate &date);
[[nodiscard]] QString langMonthOfYear(int month, int year);
[[nodiscard]] QString langMonth(const QDate &date);
[[nodiscard]] QString langMonthOfYearFull(int month, int year);
diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style
index 4706535385..b74c4f8931 100644
--- a/Telegram/SourceFiles/ui/effects/credits.style
+++ b/Telegram/SourceFiles/ui/effects/credits.style
@@ -598,15 +598,32 @@ craftAbout: FlatLabel(defaultFlatLabel) {
align: align(top);
minWidth: 128px;
}
-craftAboutMargin: margins(24px, 16px, 24px, 4px);
+craftAboutMargin: margins(24px, 12px, 24px, 4px);
+craftChance: FlatLabel(defaultFlatLabel) {
+ style: semiboldTextStyle;
+ textFg: attentionButtonFg;
+ margin: margins(8px, 4px, 8px, 4px);
+}
craftPattern: IconEmoji {
icon: icon{{ "chat/filled_forge-36x36", windowFg }};
padding: margins(0px, 0px, 0px, 0px);
}
craftAddIcon: icon {{ "settings/plus", windowFgActive }};
-craftPreviewPadding: margins(24px, 12px, 40px, 8px);
+craftPreviewPadding: margins(24px, 8px, 40px, 8px);
+craftBoxButtonPadding: margins(22px, 12px, 22px, 22px);
+craftAttributesMargin: margins(22px, 12px, 22px, 16px);
+craftPreviewAllPadding: margins(6px, 4px, 6px, 4px);
+craftPreviewAllMargin: margins(22px, 0px, 22px, 8px);
+craftProgressTitleMargin: margins(0px, 12px, 0px, 16px);
+craftProgressAboutMargin: margins(0px, 4px, 0px, 16px);
+craftProgressWarningMargin: margins(0px, 4px, 0px, 16px);
+craftProgressChanceMargin: margins(0px, 40px, 0px, 16px);
+craftFailureThumbsTop: 36px;
+craftFailureTitleMargin: margins(0px, 88px, 0px, 12px);
+craftFailureAboutMargin: margins(0px, 0px, 0px, 16px);
+
craftForge: icon{{ "chat/filled_forge-48x48", windowFg }};
-craftFail: icon{{ "chat/filled_forge_fail-48x48", windowFg }};
+craftFail: icon{{ "chat/filled_forge_fail-36x36", windowFg }};
craftForgePadding: 12px;
craftForgePercent: CraftRadialPercent {
stroke: 4px;
diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style
index 6c365d3343..3c48f7d4fd 100644
--- a/Telegram/SourceFiles/ui/menu_icons.style
+++ b/Telegram/SourceFiles/ui/menu_icons.style
@@ -200,6 +200,7 @@ menuIconTon: icon{{ "payments/ton_emoji-18x18", menuIconColor, point(3px, 2px) }
menuIconReorder: icon{{ "menu/reorder-24x24", menuIconColor }};
menuIconTools: icon{{ "menu/craft_tools-24x24", menuIconColor }};
menuIconCraftTraits: icon{{ "menu/craft_random-24x24", menuIconColor }};
+menuIconCraftChance: icon{{ "menu/craft_chance-24x24", menuIconColor }};
menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }};
menuIconTTLAnyTextPosition: point(11px, 22px);
diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp
index ab65f6dccd..fe070ff6ec 100644
--- a/Telegram/SourceFiles/window/window_session_controller.cpp
+++ b/Telegram/SourceFiles/window/window_session_controller.cpp
@@ -1685,7 +1685,7 @@ SessionController::SessionController(
#if _DEBUG // TEST: Auto-open craft box on startup
constexpr auto kGiftsCount = 4;
crl::on_main(this, [=] {
- if (rand() % 2 < 0) {
+ if (rand() % 2 >= 0) {
return;
}
const auto user = session->user();