ZaStoGram_desktop/Telegram/SourceFiles/wallet/wallet_content.cpp
loop-uh 0fd5d76a67 Merge upstream v7.3.1
Слияние с tdesktop от 64ca5475f2 до тега v7.3.1 (7.2.10 beta, 7.3, 7.3.1).

- Версия 7.3.1 (7003001), BetaChannel 0, комментарий ZaStoGram сохранён.
- Обновлятор: оригинал убрал пакеты v1 (RSA), а наш Packer и Forgejo
  выпускают именно их. v1 оставлен как UnpackUpdateV1 рядом с v2 и
  использует новые проверенные распаковку и извлечение файлов.
- mtproto: исправления импорта авторизации из оригинала перенесены в нашу
  структуру RequestRegistry; пути include новых файлов кошелька
  приведены к нашей раскладке mtproto/.
- prepare.py: стадии оригинала (ucrt64, ffmpeg 8, openssl 3.5, libheif,
  Rust, wallet-engine) с нашими skip-debug и patches_extra.
- lib_ui, lib_lottie: форки слиты с версиями из v7.3.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:43:47 +03:00

16830 lines
477 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 "wallet/wallet_content.h"
#include "api/api_cloud_password.h"
#include "api/api_common.h"
#include "apiwrap.h"
#include "base/call_delayed.h"
#include "base/debug_log.h"
#include "base/event_filter.h"
#include "base/invoke_queued.h"
#include "base/qthelp_regex.h"
#include "base/qthelp_url.h"
#include "base/random.h"
#include "base/timer.h"
#include "base/unique_qptr.h"
#include "base/unixtime.h"
#include "boxes/passcode_box.h"
#include "boxes/peer_list_box.h"
#include "boxes/peer_list_controllers.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/credits_amount.h"
#include "core/local_url_handlers.h"
#include "core/ton_explorer_url.h"
#include "core/ui_integration.h"
#include "data/components/credits.h"
#include "data/components/promo_suggestions.h"
#include "data/components/recent_money_recipients.h"
#include "data/components/recent_peers.h"
#include "data/components/top_peers.h"
#include "data/data_changes.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "dialogs/ui/dialogs_pill.h"
#include "history/history.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
#include "info/channel_statistics/earn/earn_icons.h"
#include "info/profile/info_profile_values.h"
#include "inline_bots/bot_attach_web_view.h"
#include "lang/lang_hardcoded.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "main/session/session_show.h"
#include "main/main_app_config.h"
#include "main/main_domain.h"
#include "main/main_session.h"
#include "mtproto/protocol/mtproto_response.h"
#include "qr/qr_generate.h"
#include "settings/cloud_password/settings_cloud_password_common.h"
#include "settings/sections/settings_credits.h"
#include "settings/settings_common.h"
#include "storage/storage_account.h"
#include "storage/storage_domain.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/feature_list.h"
#include "ui/controls/table_rows.h"
#include "ui/controls/ton_common.h"
#include "ui/effects/ripple_animation.h"
#include "ui/effects/star_burst.h"
#include "ui/effects/unique_gift_message_bubble.h"
#include "ui/image/image_prepare.h"
#include "ui/layers/generic_box.h"
#include "ui/text/custom_emoji_helper.h"
#include "ui/text/format_values.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/discrete_sliders.h"
#include "ui/widgets/glare_tooltip.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/multi_select.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/separate_panel.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/table_layout.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/basic_click_handlers.h"
#include "ui/empty_userpic.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/round_rect.h"
#include "ui/ui_utility.h"
#include "ui/vertical_list.h"
#include "wallet/wallet_address.h"
#include "wallet/wallet_amount_field.h"
#include "wallet/wallet_amount_painter.h"
#include "wallet/wallet_card_angle.h"
#include "wallet/wallet_card_gradient.h"
#include "wallet/wallet_chat_show.h"
#include "wallet/wallet_collectible_media.h"
#include "wallet/wallet_collectibles.h"
#include "wallet/wallet_comment.h"
#include "wallet/wallet_custody.h"
#include "wallet/wallet_diamond_flight.h"
#include "wallet/wallet_fiat.h"
#include "wallet/wallet_key_protection.h"
#include "wallet/wallet_onramp.h"
#include "wallet/wallet_palette.h"
#include "wallet/wallet_rates.h"
#include "wallet/wallet_sending_effects.h"
#include "wallet/wallet_session.h"
#include "wallet/wallet_ton_connect.h"
#include "wallet/wallet_ton_connect_box.h"
#include "wallet/wallet_unlock.h"
#include "wallet/wallet_user_addresses.h"
#include "window/themes/window_theme.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include <QtCore/QLocale>
#include <QtCore/QUrl>
#include <QtCore/QtMath>
#include <QtGui/QClipboard>
#include <QtGui/QGuiApplication>
#include <QtGui/QKeyEvent>
#include <QtGui/QPainterPath>
#include <QtSvg/QSvgRenderer>
#include <QtWidgets/QTextEdit>
#include <array>
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_giveaway.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include "styles/style_wallet.h"
#include "styles/style_widgets.h"
#include "styles/style_window.h"
namespace Wallet {
namespace {
constexpr auto kAddressLength = 48;
constexpr auto kAddressGroup = 4;
constexpr auto kAddressGroupsPerLine = 6;
constexpr auto kReceiveGroupsPerLine = 4;
constexpr auto kReceiveLines = kAddressLength
/ kAddressGroup
/ kReceiveGroupsPerLine;
constexpr auto kSendUserCardGroupsPerLine = 6;
constexpr auto kQrQuietZoneModules = 4;
constexpr auto kShortAddressChars = 4;
constexpr auto kGaslessDailyTransfersDefault = 5;
constexpr auto kMinus = QChar(0x2212);
constexpr auto kImportWordCountShort = 12;
constexpr auto kImportWordCountLong = 24;
constexpr auto kImportSuggestionsLimit = 3;
constexpr auto kBackupWriteDownDelay = 30 * crl::time(1000);
constexpr auto kBackupQuizWordCount = 3;
constexpr auto kCoverBodyPart = 0.90;
constexpr auto kCoverTitleScale = 0.05;
constexpr auto kCardFoldMinHeight = 1.;
constexpr auto kIntroTooltipShownPref = "wallet_intro_tooltip_shown"_cs;
constexpr auto kWalletIntroGlares = 2;
constexpr auto kUndatedRowDate = std::numeric_limits<TimeId>::max();
constexpr auto kTransactionLookupInterval = crl::time(1000);
constexpr auto kTransactionLookupAttempts = 10;
constexpr auto kMaxFiatUnits = 999'999'999LL;
constexpr auto kMaxAmountNano = 999'999'999'999'999'999LL;
constexpr auto kSendUserLoadTimeout = 30 * crl::time(1000);
constexpr auto kRecipientSearchLimit = 64;
constexpr auto kNameBusyRetryDelay = crl::time(500);
constexpr auto kSendOwnerLookupDelay = crl::time(500);
constexpr auto kSendRefusalRetries = 3;
constexpr auto kCommentPasswordStateTimeout = 30 * crl::time(1000);
constexpr auto kSigningReadyTimeout = 30 * crl::time(1000);
constexpr auto kHeldSendKeyTimeout = 60 * crl::time(1000);
constexpr auto kCustodyResolveTimeout = 20 * crl::time(1000);
constexpr auto kGramDigits = 9;
constexpr auto kSendingRowGlareDuration = crl::time(1450);
constexpr auto kSendingRowGlarePause = crl::time(1040);
constexpr auto kSendingRowGlareLag = 0.4;
constexpr auto kSendingRowGlareBorder = 0.5;
constexpr auto kSendingRowGlareBackground = 0.12;
constexpr auto kSendingRowEntranceDuration = crl::time(370);
constexpr auto kSendingRowEntrancePeak = crl::time(180);
constexpr auto kSendingRowEntranceDamping = 0.57;
constexpr auto kSendingRowEntranceScale = 0.93;
constexpr auto kSendingRowFlightDuration = crl::time(480);
constexpr auto kSendingRowBumpPress = crl::time(115);
constexpr auto kSendingRowBumpRelease = crl::time(220);
constexpr auto kSendingRowBumpRebound = 0.35;
constexpr auto kSendingRowBumpTaper = crl::time(460);
constexpr auto kSendingRowBumpDuration = crl::time(560);
constexpr auto kSendingRowBumpDrop = 0.04;
constexpr auto kSendingRowBumpShrink = 0.012;
constexpr auto kSendingRowBumpSwell = 0.15;
constexpr auto kSendingRowSettlePill = crl::time(110);
constexpr auto kSendingRowSettleBadgeFrom = crl::time(65);
constexpr auto kSendingRowSettleBadgeTill = crl::time(230);
constexpr auto kSendingRowSettleLabelFrom = crl::time(35);
constexpr auto kSendingRowSettleLabelTill = crl::time(230);
constexpr auto kSendingRowSettleAmount = crl::time(170);
constexpr auto kSendingRowSettleAmountFadeFrom = crl::time(110);
constexpr auto kSendingRowSettleDiamondMove = crl::time(130);
constexpr auto kSendingRowSettleDiamondSwellTill = crl::time(100);
constexpr auto kSendingRowSettleDiamondSwell = 1.1;
constexpr auto kSendingRowSettleDiamond = crl::time(370);
constexpr auto kSendingRowSettleDiamondFadeFrom = crl::time(230);
constexpr auto kSendingRowSettlePaintWait = crl::time(1000);
constexpr auto kSendingRowBurstDelay = crl::time(50);
constexpr auto kSendingRowBurstSpread = crl::time(160);
constexpr auto kSendingRowBurstLifeMin = crl::time(450);
constexpr auto kSendingRowBurstLifeMax = crl::time(650);
constexpr auto kRowEmojiDiamondLeft = 7. / 72.;
constexpr auto kRowEmojiDiamondTop = 12. / 72.;
constexpr auto kRowEmojiDiamondRight = 65. / 72.;
constexpr auto kRowEmojiDiamondBottom = 62. / 72.;
class BalanceInk;
class Card;
struct CardFold;
class InfoIsland;
class InfoIslandEntry;
class SendingHistoryRow;
struct SendingRow;
class KeyContext final
: public Main::SessionShow
, public std::enable_shared_from_this<KeyContext> {
public:
KeyContext(
std::shared_ptr<Main::SessionShow> show,
std::shared_ptr<CommentScope> scope,
Fn<bool()> current,
Fn<void(KeyAuthorization)> done);
void showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<Ui::BoxContent>,
std::unique_ptr<Ui::LayerWidget>> &&layer,
Ui::LayerOptions options,
anim::type animated) const override;
not_null<QWidget*> toastParent() const override;
bool valid() const override;
operator bool() const override;
Main::Session &session() const override;
[[nodiscard]] std::shared_ptr<CommentScope> scope() const;
// The show this context wraps, for a box that must outlive a prompt
// closing under it instead of being read as the end of the attempt.
[[nodiscard]] std::shared_ptr<Main::SessionShow> plain() const;
[[nodiscard]] CustodyInstaller installer();
void acceptClosed();
void allowPromptRetry(base::weak_qptr<Ui::BoxContent> box);
void cancelOnClose(
base::weak_qptr<Ui::BoxContent> box,
bool allowSuccessor = false);
void closePrompt(base::weak_qptr<Ui::BoxContent> box);
void ready(KeyAuthorization auth);
void cancel();
[[nodiscard]] rpl::lifetime &lifetime();
private:
struct Prompt {
base::weak_qptr<Ui::BoxContent> box;
bool closing = false;
bool accepted = false;
bool cancelOnClose = false;
bool allowSuccessor = true;
};
void promptClosed(const std::shared_ptr<Prompt> &prompt);
void finish(KeyAuthorization auth);
const std::shared_ptr<Main::SessionShow> _show;
const base::weak_ptr<Main::Session> _session;
const std::shared_ptr<CommentScope> _scope;
const Fn<bool()> _current;
Fn<void(KeyAuthorization)> _done;
mutable std::vector<std::shared_ptr<Prompt>> _prompts;
bool _finished = false;
bool _installing = false;
rpl::lifetime _lifetime;
};
class EncryptedCommentLabel final : public Ui::FlatLabel {
public:
EncryptedCommentLabel(
QWidget *parent,
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
TransferItem item,
Fn<bool()> originCurrent);
QString accessibilityName() override;
private:
const TextWithEntities _cover;
const bool _revealable = false;
bool _closed = false;
bool _revealed = false;
TransferComment _comment;
};
class Content final : public Ui::RpWidget {
public:
Content(
not_null<Ui::SeparatePanel*> panel,
std::shared_ptr<Main::SessionShow> show);
~Content();
void flySendDiamond(
const std::string &operationId,
not_null<Ui::TonAmountInput*> amount);
protected:
void focusInEvent(QFocusEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
private:
struct Regions {
int reserve = 0;
int scrollTop = 0;
int scrollHeight = 0;
};
void setupContent();
void setupPinned();
void setupInfoIsland();
void setupWaltEntry(not_null<InfoIsland*> island);
void setupEarningsEntry(not_null<InfoIsland*> island);
void setupOldWalletEntry(not_null<InfoIsland*> island);
void setupProtectRow();
void setupBalance();
void setupTabs(rpl::producer<bool> collectiblesShown);
void setupStrip();
void setupListsLoading();
void setupCustodyEntry(not_null<InfoIsland*> island);
void paintTitle(QPainter &p, float64 fold);
void updateRegions();
void updatePinned();
void updateVisibleArea();
void checkLoadMore();
bool revealSendingRow();
[[nodiscard]] int pinnedMax() const;
[[nodiscard]] int pinnedMin() const;
[[nodiscard]] Regions countRegions(int columnHeight) const;
[[nodiscard]] QRect cardRest() const;
[[nodiscard]] float64 foldProgress() const;
[[nodiscard]] const CardFold &cardFold() const;
[[nodiscard]] QRegion cardOutline() const;
const std::shared_ptr<Main::SessionShow> _show;
Ui::SeparatePanel *_panel = nullptr;
object_ptr<Ui::ScrollArea> _scroll;
SingleQueuedInvokation _loadMoreCheck;
std::unique_ptr<BalanceInk> _ink;
Ui::Text::String _title;
base::unique_qptr<Ui::RpWidget> _titleBalance;
Ui::RpWidget *_container = nullptr;
Ui::PaddingWrap<Ui::VerticalLayout> *_column = nullptr;
Ui::RpWidget *_pinnedBackground = nullptr;
Ui::RpWidget *_pinned = nullptr;
Ui::VerticalLayout *_pinnedInner = nullptr;
Ui::RpWidget *_pinnedBalance = nullptr;
Ui::PlainShadow *_headerShadow = nullptr;
Ui::SlideWrap<Ui::SettingsSlider> *_tabsWrap = nullptr;
Ui::PlainShadow *_tabsShadow = nullptr;
Ui::PlainShadow *_stripShadow = nullptr;
Ui::RpWidget *_strip = nullptr;
Ui::RpWidget *_listsLoading = nullptr;
Ui::FlatLabel *_custodyBarLabel = nullptr;
Ui::FixedHeightWidget *_cardPlaceholder = nullptr;
Card *_card = nullptr;
Ui::AbstractButton *_cardButton = nullptr;
std::unique_ptr<SendingRow> _sendingRow;
std::unique_ptr<DiamondFlight> _diamondFlight;
QRect _paintedInk;
int _reserve = 0;
int _paintedHeight = -1;
int _paintedMin = -1;
bool _tabsShown = false;
bool _stripShown = false;
};
struct CardFold {
QRect rest;
QPolygonF quad;
QTransform transform;
float64 topY = 0.;
float64 bottomY = 0.;
float64 opacity = 1.;
float64 fold = 0.;
bool valid = false;
};
// CardFold is expressed entirely in Content coordinates: `rest` is the
// card's rest rect there, `quad` the folded quadrilateral it is painted
// as, and `transform` maps the first onto the second. A Card paints with
// a widget-local painter, so it reconciles with p.translate(-x(), -y())
// before applying `transform` and draws into `rest`; paintedQuad() and
// paintedOutline() return Content coordinates for the same reason.
[[nodiscard]] CardFold ComputeCardFold(QRect cardRest, float64 fold);
class Card final : public Ui::RpWidget {
public:
Card(
QWidget *parent,
std::shared_ptr<Main::SessionShow> show,
rpl::producer<TextWithEntities> name);
void setFold(const CardFold &fold);
[[nodiscard]] const CardFold &fold() const;
[[nodiscard]] QPolygonF paintedQuad() const;
[[nodiscard]] QPolygonF paintedOutline() const;
void invalidateCache();
void followCursor();
protected:
void paintEvent(QPaintEvent *e) override;
private:
[[nodiscard]] QRect restRect() const;
[[nodiscard]] QRectF paintedRect() const;
[[nodiscard]] float64 paintAngle();
void paintContent(Painter &p, float64 angle);
void validateCache(float64 angle);
void refreshAddress();
const std::shared_ptr<Main::SessionShow> _show;
style::TextStyle _nameStyle;
Ui::Text::String _name;
QString _addressLine1;
QString _addressLine2;
CardFold _fold;
CardBackground _background;
QImage _cache;
float64 _cacheAngle = 0.;
std::unique_ptr<CardAngle> _angle;
};
struct BalancePalette {
QColor mark;
QColor amount;
QColor secondary;
};
enum class BalanceStyle : uchar {
Balance,
Minus,
Plus,
Exact,
};
class BalanceInk final {
public:
BalanceInk();
void setContent(
CreditsAmount amount,
const QString &fiat,
BalanceStyle style = BalanceStyle::Balance);
void setOuterWidth(int outerWidth);
void playMark(Fn<void()> repaint);
void refresh();
void paint(
QPainter &p,
const CardFold &fold,
const QRegion &cardOutline,
QRect clip) const;
[[nodiscard]] QRect boundingRect(const CardFold &fold) const;
[[nodiscard]] QRect markRect(QRect cardRest) const;
[[nodiscard]] QRect markPaintRect(const CardFold &fold) const;
private:
[[nodiscard]] QRectF amountRect(const CardFold &fold) const;
[[nodiscard]] QRectF fiatRect(const CardFold &fold) const;
[[nodiscard]] QRectF markVisible(float64 fold) const;
[[nodiscard]] QRectF markDrawRect(
float64 fold,
const QRectF &box,
const QRectF &visible) const;
void paintMark(
QPainter &p,
float64 fold,
const QImage &mono,
bool card) const;
void paintPass(
QPainter &p,
const CardFold &fold,
const BalancePalette &palette,
const QImage &mark,
bool card,
float64 secondaryOpacity) const;
[[nodiscard]] QTransform groupTransform(const CardFold &fold) const;
std::unique_ptr<Lottie::Icon> _markLottie;
Fn<void()> _markRepaint;
Wallet::AmountPainter _painter;
QPainterPath _fiat;
QImage _markCard;
QImage _markSettled;
QRectF _markFrame;
QRectF _markLottieVisible;
QRectF _markMonoVisible;
CreditsAmount _balance;
QString _fiatText;
float64 _markTop = 0.;
float64 _fiatWidth = 0.;
int _outerWidth = 0;
BalanceStyle _style = BalanceStyle::Balance;
};
class InfoIslandEntry final : public Ui::SettingsButton {
public:
InfoIslandEntry(
QWidget *parent,
rpl::producer<QString> text,
const style::SettingsButton &st);
InfoIslandEntry(
QWidget *parent,
std::nullptr_t,
const style::SettingsButton &st);
void setRounding(RectParts corners, int radius);
void setMinimalHeight(int height);
protected:
int resizeGetHeight(int newWidth) override;
QImage prepareRippleMask() const override;
private:
RectParts _corners;
int _radius = 0;
int _minimalHeight = 0;
};
class InfoIsland final : public Ui::VerticalLayout {
public:
explicit InfoIsland(QWidget *parent);
not_null<Ui::SlideWrap<InfoIslandEntry>*> add(
object_ptr<InfoIslandEntry> entry);
[[nodiscard]] rpl::producer<bool> anyShownValue() const;
protected:
int resizeGetHeight(int newWidth) override;
private:
void refreshRounding();
void paintPill(QPainter &p);
[[nodiscard]] QRect pillRect() const;
std::vector<Ui::SlideWrap<InfoIslandEntry>*> _entries;
Ui::MultiSlideTracker _tracker;
Ui::BoxShadow _shadow;
QMargins _extend;
};
[[nodiscard]] int PillRadius(QRect pill) {
return std::min({
st::walletCardRadius,
pill.width() / 2,
pill.height() / 2,
});
}
InfoIslandEntry::InfoIslandEntry(
QWidget *parent,
rpl::producer<QString> text,
const style::SettingsButton &st)
: Ui::SettingsButton(parent, std::move(text), st) {
}
InfoIslandEntry::InfoIslandEntry(
QWidget *parent,
std::nullptr_t,
const style::SettingsButton &st)
: Ui::SettingsButton(parent, nullptr, st) {
}
void InfoIslandEntry::setRounding(RectParts corners, int radius) {
if (_corners == corners && _radius == radius) {
return;
}
_corners = corners;
_radius = radius;
finishAnimating();
update();
}
void InfoIslandEntry::setMinimalHeight(int height) {
if (_minimalHeight == height) {
return;
}
_minimalHeight = height;
resizeToWidth(width());
}
int InfoIslandEntry::resizeGetHeight(int newWidth) {
return std::max(
Ui::SettingsButton::resizeGetHeight(newWidth),
_minimalHeight);
}
QImage InfoIslandEntry::prepareRippleMask() const {
if (_radius <= 0 || !_corners) {
return Ui::RippleAnimation::RectMask(size());
}
// The filled RoundRectMask overload starts from a fully opaque mask and
// only cuts out the corners it is handed, so a null one stays square.
// Images::CornersMaskRef holds bare pointers into the array it is built
// from, which is why that array must outlive the call reading them.
const auto masks = Images::CornersMask(_radius);
auto corners = Images::CornersMaskRef();
const auto fill = [&](RectPart corner, int index) {
if (_corners & corner) {
corners.p[index] = &masks[index];
}
};
fill(RectPart::TopLeft, Images::kTopLeft);
fill(RectPart::TopRight, Images::kTopRight);
fill(RectPart::BottomLeft, Images::kBottomLeft);
fill(RectPart::BottomRight, Images::kBottomRight);
return Ui::RippleAnimation::RoundRectMask(size(), corners);
}
InfoIsland::InfoIsland(QWidget *parent)
: Ui::VerticalLayout(parent)
, _shadow(st::walletInfoIslandShadow)
, _extend(_shadow.extend()) {
Ui::AddSkip(this, _extend.top());
Ui::AddSkip(this, _extend.bottom());
paintOn([=](QPainter &p) {
paintPill(p);
});
}
not_null<Ui::SlideWrap<InfoIslandEntry>*> InfoIsland::add(
object_ptr<InfoIslandEntry> entry) {
// The row margins are vertically zero on purpose: VerticalLayout counts
// a row margin even for a zero-height child, so any non-zero one would
// leak height from a hidden entry and grow the island where it must
// contribute nothing at all.
const auto wrap = insert(
count() - 1,
object_ptr<Ui::SlideWrap<InfoIslandEntry>>(this, std::move(entry)),
style::margins(
st::walletIslandMargin.left(),
0,
st::walletIslandMargin.right(),
0));
_entries.push_back(wrap);
_tracker.track(wrap);
// VerticalLayout registers its own heightValue handler inside insert,
// so that one runs first and has already repositioned the rows and
// resized the island by the time this one does, which is what makes
// the wrap's height and y current here. heightValue also emits once
// on subscription, and refreshing the rounding is idempotent.
wrap->heightValue(
) | rpl::on_next([=] {
refreshRounding();
}, wrap->lifetime());
return wrap;
}
rpl::producer<bool> InfoIsland::anyShownValue() const {
return _tracker.atLeastOneShownValue();
}
int InfoIsland::resizeGetHeight(int newWidth) {
const auto result = Ui::VerticalLayout::resizeGetHeight(newWidth);
refreshRounding();
return result;
}
void InfoIsland::refreshRounding() {
auto visible = std::vector<not_null<InfoIslandEntry*>>();
visible.reserve(_entries.size());
for (const auto &wrap : _entries) {
if (wrap->height() > 0) {
visible.push_back(wrap->entity());
}
}
const auto shown = int(visible.size());
const auto radius = PillRadius(pillRect());
for (auto i = 0; i != shown; ++i) {
const auto first = !i;
const auto last = (i == shown - 1);
visible[i]->setRounding((first && last)
? RectParts(RectPart::AllCorners)
: first
? (RectPart::TopLeft | RectPart::TopRight)
: last
? (RectPart::BottomLeft | RectPart::BottomRight)
: RectParts(), radius);
}
update();
}
QRect InfoIsland::pillRect() const {
const auto &margin = st::walletIslandMargin;
return QRect(
margin.left(),
_extend.top(),
width() - margin.left() - margin.right(),
height() - _extend.top() - _extend.bottom());
}
void InfoIsland::paintPill(QPainter &p) {
const auto pill = pillRect();
if (pill.isEmpty()) {
return;
}
Dialogs::PaintPillBackground(p, _shadow, pill, PillRadius(pill));
auto first = true;
for (const auto &wrap : _entries) {
if (wrap->height() <= 0) {
continue;
} else if (first) {
first = false;
continue;
}
p.fillRect(
pill.x(),
wrap->y(),
pill.width(),
st::lineWidth,
st::shadowFg);
}
}
KeyContext::KeyContext(
std::shared_ptr<Main::SessionShow> show,
std::shared_ptr<CommentScope> scope,
Fn<bool()> current,
Fn<void(KeyAuthorization)> done)
: _show(std::move(show))
, _session(base::make_weak(&_show->session()))
, _scope(std::move(scope))
, _current(std::move(current))
, _done(std::move(done)) {
}
void KeyContext::showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<Ui::BoxContent>,
std::unique_ptr<Ui::LayerWidget>> &&layer,
Ui::LayerOptions,
anim::type animated) const {
const auto self = std::const_pointer_cast<KeyContext>(
shared_from_this());
if (!valid()) {
self->cancel();
return;
}
const auto content = std::get_if<object_ptr<Ui::BoxContent>>(&layer);
if (!content || !*content) {
self->cancel();
return;
}
const auto prompt = std::make_shared<Prompt>();
self->acceptClosed();
prompt->box = content->data();
_prompts.push_back(prompt);
const auto weak = std::weak_ptr(self);
// Native gates report success either from boxClosing() or immediately
// after closeBox() returns. Register before their deferred preparation,
// then let the native continuation accept its closed prompts before
// deciding whether dismissal was terminal. Cloud gates may also show
// their successor before closing, while an uncontinued dismissal retires
// the entire comment attempt and closes its remaining owned prompts.
const auto closed = [weak, prompt] {
prompt->closing = true;
if (prompt->cancelOnClose) {
if (const auto strong = weak.lock()) {
strong->promptClosed(prompt);
}
return;
}
crl::on_main([weak, prompt] {
if (const auto strong = weak.lock()) {
strong->promptClosed(prompt);
}
});
};
prompt->box->boxClosing() | rpl::on_next(
closed,
prompt->box->lifetime());
prompt->box->lifetime().add(closed);
_show->showOrHideBoxOrLayer(
std::move(layer),
Ui::LayerOption::KeepOther,
animated);
}
not_null<QWidget*> KeyContext::toastParent() const {
return _show->toastParent();
}
bool KeyContext::valid() const {
return !_finished
&& _session
&& _show->valid()
&& _current()
&& (!_scope || _session->wallet().commentScopeCurrent(_scope));
}
KeyContext::operator bool() const {
return valid();
}
Main::Session &KeyContext::session() const {
Expects(_session != nullptr);
return *_session;
}
std::shared_ptr<CommentScope> KeyContext::scope() const {
return _scope;
}
std::shared_ptr<Main::SessionShow> KeyContext::plain() const {
return _show;
}
CustodyInstaller KeyContext::installer() {
const auto self = shared_from_this();
// WHY: the ladder stores the key on this device, which is right to
// finish even once the action that asked for it has expired, so it is
// judged by the window it lives in and not by that action. Handing it
// this context instead made every unrelated wallet event close the
// chooser with nothing stored and nothing said, and the next press
// started the whole restore again.
const auto native = MakeCustodyInstaller(_show);
return [=](CustodyInstallRequest request) {
if (!self->valid()) {
request.ready({});
self->cancel();
return;
}
self->_installing = true;
request.passcodeCreated = [
self,
created = std::move(request.passcodeCreated)
](quint32 previousEpoch, quint32 epoch) {
// Judged like the rest of the ladder: the vault transition has
// to be sound, and whether the action that asked for the key is
// still there decides nothing about storing it.
if (!created || !created(previousEpoch, epoch)) {
return false;
}
self->acceptClosed();
return true;
};
request.ready = [=, ready = std::move(request.ready)](
CustodyInstall result) {
self->_installing = false;
self->acceptClosed();
const auto installed = result.grant != nullptr;
ready(std::move(result));
if (!installed) {
self->cancel();
}
};
native(std::move(request));
};
}
void KeyContext::acceptClosed() {
for (const auto &prompt : _prompts) {
if (prompt->closing) {
prompt->accepted = true;
}
}
}
void KeyContext::allowPromptRetry(
base::weak_qptr<Ui::BoxContent> box) {
for (const auto &prompt : _prompts) {
if (prompt->box == box) {
prompt->accepted = true;
return;
}
}
}
void KeyContext::cancelOnClose(
base::weak_qptr<Ui::BoxContent> box,
bool allowSuccessor) {
if (!box) {
cancel();
return;
}
for (const auto &prompt : _prompts) {
if (prompt->box == box) {
prompt->cancelOnClose = true;
prompt->allowSuccessor = allowSuccessor;
if (prompt->closing) {
promptClosed(prompt);
}
return;
}
}
}
void KeyContext::closePrompt(base::weak_qptr<Ui::BoxContent> box) {
for (const auto &prompt : _prompts) {
if (prompt->box == box) {
prompt->accepted = true;
if (box && !prompt->closing && box->hasDelegate()) {
box->closeBox();
}
return;
}
}
}
void KeyContext::ready(KeyAuthorization auth) {
if (!valid() || !auth.grant) {
cancel();
return;
}
finish(std::move(auth));
}
void KeyContext::cancel() {
finish({});
}
void KeyContext::finish(KeyAuthorization auth) {
if (_finished) {
return;
}
_finished = true;
const auto scope = _scope;
auto lifetime = base::take(_lifetime);
const auto done = base::take(_done);
const auto prompts = base::take(_prompts);
if (!auth.grant && scope) {
scope->cancel();
}
lifetime.destroy();
for (const auto &prompt : ranges::views::reverse(prompts)) {
prompt->accepted = true;
if (const auto box = prompt->box.get()) {
if (!prompt->closing && box->hasDelegate()) {
box->closeBox();
}
}
}
if (done) {
done(std::move(auth));
}
}
rpl::lifetime &KeyContext::lifetime() {
return _lifetime;
}
void KeyContext::promptClosed(const std::shared_ptr<Prompt> &prompt) {
if (_finished || prompt->accepted) {
return;
} else if (_installing) {
// The install ladder is this press continuing, not the user
// abandoning it, and it owns no prompt of this context any more.
prompt->accepted = true;
return;
}
auto later = false;
for (const auto &other : _prompts) {
if (prompt->allowSuccessor
&& later
&& other->box
&& !other->closing) {
prompt->accepted = true;
return;
}
later = later || (other == prompt);
}
cancel();
}
EncryptedCommentLabel::EncryptedCommentLabel(
QWidget *parent,
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
TransferItem item,
Fn<bool()> originCurrent)
: FlatLabel(parent, st::walletCommentLabel)
, _cover(TransferCommentCover(item))
, _revealable(EncryptedCommentRevealable(item))
, _comment(&show->session(), std::move(item), [
this,
originCurrent = std::move(originCurrent)] {
return !_closed && (!originCurrent || originCurrent());
}) {
setContextCopyText(QString());
setSelectable(!_revealable);
setMarkedText(_cover);
setContextMenuHook([weak = base::make_weak(this)](ContextMenuRequest request) {
if (!weak || !weak->_comment.plaintext()) {
return;
}
request.menu->addAction(tr::lng_context_copy_text(tr::now), [weak] {
if (weak) {
if (const auto &text = weak->_comment.plaintext()) {
TextUtilities::SetClipboardText(TextForMimeData::Simple(*text));
}
}
});
});
setClickHandlerFilter([=](const ClickHandlerPtr &, Qt::MouseButton button) {
if (button != Qt::LeftButton) {
return false;
} else if (!_revealable) {
return true;
} else if (!_comment.plaintext()) {
_comment.activate(show);
}
return false;
});
setAnimationsPausedCallback([] {
return On(PowerSaving::kChatSpoiler)
? WhichAnimationsPaused::Spoiler
: WhichAnimationsPaused::None;
});
_comment.changes() | rpl::on_next([=] {
if (_closed) {
return; // the closing reset must not flash the cover as it fades
}
const auto revealed = _comment.plaintext().has_value();
if (revealed == _revealed) {
return;
}
_revealed = revealed;
if (const auto &text = _comment.plaintext()) {
setText(*text);
setSelectable(true);
} else {
setSelectable(false);
setContextCopyText(QString());
setMarkedText(_cover);
}
}, lifetime());
box->boxClosing() | rpl::on_next([=] {
_closed = true;
_comment.reset();
}, lifetime());
}
QString EncryptedCommentLabel::accessibilityName() {
const auto &text = _comment.plaintext();
return text
? *text
: _revealable
? tr::lng_action_gram_transfer_encrypted_comment(tr::now)
: _cover.text;
}
[[nodiscard]] int PanelCardWidth() {
return st::walletPanelSize.width()
- st::walletCardMargin.left()
- st::walletCardMargin.right();
}
[[nodiscard]] QRect CardQrRect(int cardWidth) {
return QRect(
cardWidth - st::walletCardQrRight - st::walletCardQrSize.width(),
st::walletCardQrTop,
st::walletCardQrSize.width(),
st::walletCardQrSize.height());
}
[[nodiscard]] QRect TransferCardInfoRect(int cardWidth) {
return QRect(
cardWidth - st::walletCardContentLeft - st::walletCardQrSize.width(),
st::walletCardQrTop,
st::walletCardQrSize.width(),
st::walletCardQrSize.height());
}
// WHY: the plate under this glyph is fixed brand appearance no theme can
// move, and windowSubTextFg over it measured 1.90:1 in the day theme;
// this grey clears 2.0:1 over every point of the plate the glyph covers.
[[nodiscard]] QColor CardQrIconFg() {
return QColor(0x73, 0x73, 0x73);
}
[[nodiscard]] QString GroupedAddressLine(
const QString &address,
int offset) {
auto groups = QStringList();
for (auto i = 0; i != kAddressGroupsPerLine; ++i) {
groups.append(address.mid(
offset + i * kAddressGroup,
kAddressGroup));
}
return groups.join(QChar(' '));
}
[[nodiscard]] QStringList TransferCardLines(
const QString &destination,
int recipients) {
if (recipients >= 2) {
const auto summary = tr::lng_wallet_connect_request_recipients(
tr::now,
lt_count,
recipients);
return { summary };
}
auto result = QStringList();
const auto perLine = kAddressGroup * kAddressGroupsPerLine;
for (auto offset = 0; offset < destination.size(); offset += perLine) {
result.append(GroupedAddressLine(destination, offset).trimmed());
}
return result;
}
// The sheet's address presentation is the friendly form, so a raw address
// the engine cannot convert is a reading this sheet does not have and
// builds no row. Substituting the raw form would show a different kind of
// address without saying so.
[[nodiscard]] std::optional<QString> DetailsFriendlyAddress(
const TransferItem &item) {
const auto friendly = FormatFriendly(
item.counterparty,
item.counterpartyBounceable);
if (friendly.isEmpty()) {
return std::nullopt;
}
return friendly;
}
[[nodiscard]] TextWithEntities DetailsAddressValue(
const QString &address) {
auto groups = QStringList();
for (auto offset = 0; offset < address.size(); offset += kAddressGroup) {
groups.append(address.mid(offset, kAddressGroup));
}
return Ui::Text::Wrapped(
{ groups.join(QChar(' ')) },
EntityType::Code,
{});
}
[[nodiscard]] Fn<void()> CopyAddressCallback(
std::shared_ptr<Ui::Show> show,
const QString &address) {
return CopyTextCallback(
std::move(show),
address,
tr::lng_gift_unique_address_copied(tr::now));
}
} // namespace
Fn<void()> CopyTextCallback(
std::shared_ptr<Ui::Show> show,
QString text,
QString toast) {
return [=] {
TextUtilities::SetClipboardText(TextForMimeData::Simple(text));
show->showToast({
.text = { toast },
.iconLottie = u"toast/copy"_q,
.iconLottieSize = st::toastLottieIconSize,
});
};
}
object_ptr<Ui::FlatLabel> AddressValueLabel(
not_null<QWidget*> parent,
std::shared_ptr<Ui::Show> show,
const QString &address) {
auto result = object_ptr<Ui::FlatLabel>(
parent,
rpl::single(DetailsAddressValue(address)),
st::walletDetailsAddressLabel);
result->setTryMakeSimilarLines(true);
const auto copy = CopyAddressCallback(std::move(show), address);
result->setClickHandlerFilter([=](const auto &...) {
copy();
return false;
});
return result;
}
namespace {
[[nodiscard]] object_ptr<Ui::FlatLabel> NameValueLabel(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<Ui::Show> show,
const QString &name,
const QString &address) {
auto result = object_ptr<Ui::FlatLabel>(
parent,
rpl::single(tr::link(name)),
st::defaultTableValue);
const auto copy = CopyAddressCallback(std::move(show), address);
result->setClickHandlerFilter([=](const auto &...) {
copy();
return false;
});
return result;
}
[[nodiscard]] QString OnrampProvider(const TransferItem &item) {
return (item.kind == TransferItem::Kind::Onramp)
? item.provider
: QString();
}
enum class RowAvatar {
Peer,
In,
Out,
KeyChange,
Gear,
};
struct HistoryRowContent {
QString title;
QString subtitle;
QString date;
int64 amountNano = 0;
bool incoming = false;
bool pending = false;
bool failed = false;
RowAvatar avatar = RowAvatar::Out;
PeerData *peer = nullptr;
bool itemAmount = false;
QString collectible;
friend bool operator==(
const HistoryRowContent &,
const HistoryRowContent &) = default;
};
struct SendingRow {
std::string operationId;
Ui::VerticalLayout *slot = nullptr;
TransferItem item;
HistoryRowContent content;
SendingHistoryRow *look = nullptr;
// Owed by the hand-over or the creation's postponed call, whichever runs first
bool revealPending = false;
};
[[nodiscard]] QString ShortAddressForm(
const QString &full,
int chars = kShortAddressChars) {
return full.left(chars) + QChar(0x2026) + full.right(chars);
}
[[nodiscard]] QString ShortAddress(const QString &address) {
if (address.isEmpty()) {
return QString();
}
const auto full = FormatFriendly(address, true);
return ShortAddressForm(full);
}
[[nodiscard]] QString CounterpartyAddress(const TransferItem &item) {
return item.counterparty.isEmpty()
? QString()
: FormatFriendly(item.counterparty, item.counterpartyBounceable);
}
void SetAmountColor(
not_null<Ui::FlatLabel*> major,
not_null<Ui::FlatLabel*> minor,
const style::color &color) {
rpl::single(rpl::empty) | rpl::then(
style::PaletteChanged()
) | rpl::on_next([=] {
const auto scope = WindowPaletteScope(major);
major->setTextColorOverride(color->c);
minor->setTextColorOverride(color->c);
}, major->lifetime());
}
[[nodiscard]] QString GramMajorPart(int64 amountNano) {
return QString::number(amountNano / Ui::kNanosInOne);
}
[[nodiscard]] QString GramMinorPart(int64 amountNano) {
const auto tiny = TinyAmountFraction(amountNano, kGramDigits);
if (!tiny.isEmpty()) {
return QString(QLocale().decimalPoint()) + tiny;
}
const auto cents = std::abs(amountNano % Ui::kNanosInOne)
/ (Ui::kNanosInOne / 100);
return QString(QLocale().decimalPoint())
+ u"%1"_q.arg(cents, 2, 10, QChar('0'));
}
struct RowAmountText {
QString major;
TextWithEntities minor;
Ui::Text::MarkedContext context;
};
[[nodiscard]] QString RowAmountSign(bool incoming) {
return incoming ? u"+"_q : QString(kMinus);
}
[[nodiscard]] QString RowAmountWhole(int64 amountNano, const QString &sign) {
return (amountNano ? sign : QString()) + GramMajorPart(amountNano);
}
[[nodiscard]] RowAmountText PrepareRowAmountText(
int64 amountNano,
const QString &sign) {
auto helper = Ui::Text::CustomEmojiHelper();
auto minor = tr::marked(GramMinorPart(amountNano));
minor.append(helper.paletteDependent({
.factory = [] {
return Ui::Earn::IconCurrencyTwoTone(
st::walletRowMarkSize,
st::windowActiveTextFg->c);
},
.margin = st::walletRowIconMargin,
}));
return {
.major = RowAmountWhole(amountNano, sign),
.minor = std::move(minor),
.context = helper.context(),
};
}
[[nodiscard]] RowAmountText PrepareRowItemAmountText(bool incoming) {
return {
.major = ((incoming ? QChar('+') : kMinus)
+ tr::lng_wallet_row_items(tr::now, lt_count, 1)),
.minor = Ui::Text::IconEmoji(incoming
? &st::walletRowItemMarkIn
: &st::walletRowItemMarkOut),
};
}
[[nodiscard]] const style::color &RowAmountColor(
bool incoming,
bool pending,
bool failed) {
return (pending || failed)
? st::windowSubTextFg
: incoming
? st::boxTextFgGood
: st::windowBoldFg;
}
void SetRowAmountText(
not_null<Ui::FlatLabel*> major,
not_null<Ui::FlatLabel*> minor,
int64 amountNano,
const QString &sign) {
auto text = PrepareRowAmountText(amountNano, sign);
major->setText(text.major);
minor->setMarkedText(std::move(text.minor), std::move(text.context));
}
void SetRowAmount(
not_null<Ui::FlatLabel*> major,
not_null<Ui::FlatLabel*> minor,
int64 amountNano,
bool incoming,
bool pending,
bool failed) {
SetRowAmountText(
major,
minor,
amountNano,
RowAmountSign(incoming));
SetAmountColor(major, minor, RowAmountColor(incoming, pending, failed));
}
void SetRowItemAmount(
not_null<Ui::FlatLabel*> major,
not_null<Ui::FlatLabel*> minor,
bool incoming,
bool failed) {
auto text = PrepareRowItemAmountText(incoming);
major->setText(text.major);
minor->setMarkedText(std::move(text.minor), std::move(text.context));
SetAmountColor(major, minor, RowAmountColor(incoming, false, failed));
}
void PaintRowAvatar(Painter &p, QRect rect, RowAvatar avatar) {
auto hq = PainterHighQualityEnabler(p);
const auto in = (avatar == RowAvatar::In);
const auto flat = (avatar == RowAvatar::KeyChange)
|| (avatar == RowAvatar::Gear);
if (flat) {
p.setBrush(st::historyPeerArchiveUserpicBg);
} else {
const auto &top = in
? st::historyPeer2UserpicBg
: st::historyPeer4UserpicBg;
const auto &bottom = in
? st::historyPeer2UserpicBg2
: st::historyPeer4UserpicBg2;
auto gradient = QLinearGradient(
rect.topLeft(),
rect.bottomLeft());
gradient.setStops({ { 0., top->c }, { 1., bottom->c } });
p.setBrush(gradient);
}
p.setPen(Qt::NoPen);
p.drawEllipse(rect);
const auto icon = in
? &st::walletRowArrowIn
: (avatar == RowAvatar::KeyChange)
? &st::walletRowKeyIcon
: (avatar == RowAvatar::Gear)
? &st::walletRowGearIcon
: &st::walletRowArrowOut;
icon->paintInCenter(p, rect);
}
[[nodiscard]] int LabelLineHeight(const style::FlatLabel &st) {
return std::max(st.style.font->height, st.style.lineHeight);
}
struct HistoryRowHeights {
int title = 0;
std::optional<int> subtitle;
int date = 0;
int amount = 0;
};
struct HistoryRowLine {
int top = 0;
int lines = 0;
};
struct HistoryRowLayout {
HistoryRowLine title;
HistoryRowLine subtitle;
HistoryRowLine date;
int height = 0;
int avatarCenter = 0;
int amountTop = 0;
int titleWidth = 0;
int textWidth = 0;
int chipTop = 0;
};
[[nodiscard]] int HistoryRowTitleSkip(int amountWidth) {
return amountWidth + st::walletRowSkip;
}
[[nodiscard]] HistoryRowLayout ComputeHistoryRowLayout(
const HistoryRowHeights &heights) {
const auto &padding = st::walletRowPadding;
auto result = HistoryRowLayout();
auto top = padding.top();
result.title = {
.top = top,
.lines = heights.title / LabelLineHeight(st::walletRowTitleLabel),
};
top += heights.title;
if (heights.subtitle) {
top += st::walletRowSkip;
result.subtitle = {
.top = top,
.lines = (*heights.subtitle
/ LabelLineHeight(st::walletRowSubtitleLabel)),
};
top += *heights.subtitle;
}
top += st::walletRowSkip;
result.date = {
.top = top,
.lines = heights.date / LabelLineHeight(st::walletRowDateLabel),
};
result.height = top + heights.date + padding.bottom();
result.avatarCenter = heights.subtitle
? (padding.top()
+ (heights.title + st::walletRowSkip + *heights.subtitle) / 2)
: (result.height / 2);
result.amountTop = padding.top() + (heights.title - heights.amount) / 2;
return result;
}
struct HistoryRowText {
Ui::Text::String title;
Ui::Text::String subtitle;
Ui::Text::String date;
Ui::Text::String major;
Ui::Text::String minor;
bool subtitleShown = false;
bool chip = false;
};
// Built as Ui::FlatLabel builds its text, so it measures as the label does.
[[nodiscard]] Ui::Text::String HistoryRowLabelText(
const style::FlatLabel &st,
const QString &text) {
return Ui::Text::String(
st.style,
text,
kPlainTextOptions,
st.minWidth ? st.minWidth : Ui::kQFixedMax);
}
[[nodiscard]] int HistoryRowLabelHeight(
const style::FlatLabel &st,
const Ui::Text::String &text,
int width,
bool breakEverywhere) {
const auto full = text.countHeight(width, breakEverywhere);
return st.maxHeight ? std::min(full, st.maxHeight) : full;
}
[[nodiscard]] HistoryRowText PrepareHistoryRowText(
const HistoryRowContent &content,
Fn<void()> repaint = nullptr) {
auto amount = content.itemAmount
? PrepareRowItemAmountText(content.incoming)
: PrepareRowAmountText(
content.amountNano,
RowAmountSign(content.incoming));
if (repaint) {
amount.context.repaint = std::move(repaint);
}
auto result = HistoryRowText{
.title = HistoryRowLabelText(st::walletRowTitleLabel, content.title),
.subtitle = HistoryRowLabelText(
st::walletRowSubtitleLabel,
content.subtitle),
.date = HistoryRowLabelText(st::walletRowDateLabel, content.date),
.major = HistoryRowLabelText(
st::walletRowAmountMajorLabel,
amount.major),
.subtitleShown = !content.subtitle.isEmpty(),
.chip = content.itemAmount,
};
result.minor.setMarkedText(
st::walletRowAmountMinorLabel.style,
amount.minor,
kMarkupTextOptions,
amount.context);
return result;
}
[[nodiscard]] HistoryRowLayout MeasureHistoryRow(
const HistoryRowText &text,
int width) {
const auto &padding = st::walletRowPadding;
const auto textWidth = std::max(
width - padding.left() - padding.right(),
0);
const auto titleWidth = std::max(
textWidth - HistoryRowTitleSkip(
text.major.maxWidth() + text.minor.maxWidth()),
0);
auto result = ComputeHistoryRowLayout({
.title = HistoryRowLabelHeight(
st::walletRowTitleLabel,
text.title,
titleWidth,
true),
.subtitle = (text.subtitleShown
? std::make_optional(HistoryRowLabelHeight(
st::walletRowSubtitleLabel,
text.subtitle,
textWidth,
true))
: std::nullopt),
.date = HistoryRowLabelHeight(
st::walletRowDateLabel,
text.date,
textWidth,
false),
.amount = HistoryRowLabelHeight(
st::walletRowAmountMajorLabel,
text.major,
text.major.maxWidth(),
false),
});
result.titleWidth = titleWidth;
result.textWidth = textWidth;
if (text.chip) {
result.chipTop = result.height
- padding.bottom()
+ st::walletChipTopSkip;
result.height += st::walletChipTopSkip + st::walletRowIconSize;
}
return result;
}
struct RowAmountPlacement {
QPoint major;
QPoint minor;
};
[[nodiscard]] RowAmountPlacement PlaceRowAmount(
const HistoryRowText &text,
int amountTop,
int width) {
const auto rtl = style::RightToLeft();
const auto mirror = [&](int left, int size) {
return rtl ? (width - left - size) : left;
};
const auto majorWidth = text.major.maxWidth();
const auto minorWidth = text.minor.maxWidth();
const auto minorLeft = width - st::walletRowPadding.right() - minorWidth;
return {
.major = QPoint(
mirror(minorLeft - majorWidth, majorWidth),
amountTop),
.minor = QPoint(
mirror(minorLeft, minorWidth),
amountTop + st::walletRowAmountMinorSkip),
};
}
struct HistoryRowChipState {
Gram::NftKind kind = Gram::NftKind::Generic;
Ui::Text::String title;
Ui::Text::String subtitle;
int natural = 0;
};
class HistoryRowButton final : public Ui::SettingsButton {
public:
using Ui::SettingsButton::SettingsButton;
void setPaintUnderRipple(Fn<void(Painter&)> paint);
protected:
void paintEvent(QPaintEvent *e) override;
private:
Fn<void(Painter&)> _paintUnderRipple;
};
void HistoryRowButton::setPaintUnderRipple(Fn<void(Painter&)> paint) {
_paintUnderRipple = std::move(paint);
update();
}
void HistoryRowButton::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto over = (isOver() || isDown()) && !isDisabled();
paintBg(p, e->rect(), over);
if (_paintUnderRipple) {
_paintUnderRipple(p);
}
paintRipple(p, 0, 0);
const auto outerw = width();
paintText(p, over, outerw);
paintToggle(p, outerw);
}
void PaintHistoryRowChipPlate(
Painter &p,
int outerWidth,
const HistoryRowChipState &state) {
const auto side = st::walletRowIconSize;
const auto radius = st::walletCollectibleThumbRadius;
const auto plate = std::min(state.natural, outerWidth);
const auto plateLeft = style::RightToLeft() ? (outerWidth - plate) : 0;
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgOver);
p.drawRoundedRect(
QRect(plateLeft, 0, plate, side),
radius,
radius);
}
void PaintHistoryRowChipArtwork(
Painter &p,
int outerWidth,
const HistoryRowChipState &state,
const std::shared_ptr<CollectibleMedia> &media,
const QString &address) {
const auto side = st::walletRowIconSize;
const auto radius = st::walletCollectibleThumbRadius;
const auto rtl = style::RightToLeft();
const auto square = QRect(rtl ? (outerWidth - side) : 0, 0, side, side);
const auto dark = (state.kind == Gram::NftKind::TelegramUsername)
|| (state.kind == Gram::NftKind::TelegramNumber);
if (dark) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::callBgOpaque);
p.drawRoundedRect(square, radius, radius);
}
if (state.kind == Gram::NftKind::TelegramUsername) {
st::walletChipUsernameIcon.paintInCenter(p, square);
} else if (state.kind == Gram::NftKind::TelegramNumber) {
st::walletChipNumberIcon.paintInCenter(p, square);
} else {
media->paintArtwork(p, address, square, outerWidth, radius);
}
}
void PaintHistoryRowChipText(
Painter &p,
int outerWidth,
const HistoryRowChipState &state) {
const auto side = st::walletRowIconSize;
const auto plate = std::min(state.natural, outerWidth);
const auto rtl = style::RightToLeft();
const auto plateLeft = rtl ? (outerWidth - plate) : 0;
const auto available = plate
- side
- st::walletChipTextSkip
- st::walletChipPadding.right();
if (available <= 0) {
return;
}
const auto textLeft = rtl
? (plateLeft + st::walletChipPadding.right())
: (side + st::walletChipTextSkip);
const auto titleHeight = st::walletCollectibleTitleStyle.font->height;
const auto subtitleHeight = st::walletRowDateLabel.style.font->height;
const auto top = (side
- titleHeight
- st::walletRowSkip
- subtitleHeight) / 2;
p.setPen(st::windowBoldFg);
state.title.draw(p, {
.position = { textLeft, top },
.outerWidth = outerWidth,
.availableWidth = available,
.palette = &st::walletCollectibleTitlePalette,
.elisionLines = 1,
});
p.setPen(st::windowSubTextFg);
state.subtitle.draw(p, {
.position = { textLeft, top + titleHeight + st::walletRowSkip },
.outerWidth = outerWidth,
.availableWidth = available,
.elisionLines = 1,
});
}
void TrackHistoryRowChip(
not_null<HistoryRowChipState*> state,
std::shared_ptr<CollectibleMedia> media,
QString address,
Fn<void()> repaint,
rpl::lifetime &lifetime) {
const auto refresh = [=] {
const auto view = media->view(address);
state->kind = view.kind;
using Kind = Gram::NftKind;
state->title.setMarkedText(
st::walletCollectibleTitleStyle,
((view.kind == Kind::TelegramUsername)
? Ui::Text::Semibold('@' + view.key)
: (view.kind == Kind::TelegramNumber)
? Ui::Text::Semibold(Ui::FormatPhone(view.key))
: CollectibleTitleText(view)));
state->subtitle.setText(
st::walletRowDateLabel.style,
CollectibleKindText(view));
state->natural = st::walletRowIconSize
+ st::walletChipTextSkip
+ std::max(state->title.maxWidth(), state->subtitle.maxWidth())
+ st::walletChipPadding.right();
repaint();
};
const auto mine = [=](const QString &changed) {
return (changed == address);
};
media->changed(
) | rpl::filter(mine) | rpl::on_next(refresh, lifetime);
media->repaint(
) | rpl::filter(mine) | rpl::on_next(repaint, lifetime);
media->resolveBackground(address);
refresh();
}
void AddHistoryRowChip(
not_null<Ui::VerticalLayout*> inner,
not_null<HistoryRowButton*> button,
std::shared_ptr<CollectibleMedia> media,
QString address) {
Ui::AddSkip(inner, st::walletChipTopSkip);
const auto chip = inner->add(object_ptr<Ui::FixedHeightWidget>(
inner,
st::walletRowIconSize));
chip->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto state = chip->lifetime().make_state<HistoryRowChipState>();
const auto repaint = [=] {
chip->update();
button->update(Ui::MapFrom(button, chip, chip->rect()));
};
chip->paintRequest(
) | rpl::on_next([=] {
auto p = Painter(chip);
PaintHistoryRowChipArtwork(p, chip->width(), *state, media, address);
PaintHistoryRowChipText(p, chip->width(), *state);
}, chip->lifetime());
button->setPaintUnderRipple([=](Painter &p) {
const auto origin = Ui::MapFrom(button, chip, QPoint());
p.translate(origin);
PaintHistoryRowChipPlate(p, chip->width(), *state);
p.translate(-origin);
});
TrackHistoryRowChip(state, media, address, repaint, chip->lifetime());
}
not_null<Ui::RpWidget*> AddHistoryRow(
not_null<Ui::VerticalLayout*> list,
const HistoryRowContent &content,
Fn<void()> clicked,
std::shared_ptr<CollectibleMedia> media = nullptr) {
const auto wrap = list->add(
object_ptr<Ui::PaddingWrap<Ui::VerticalLayout>>(
list,
object_ptr<Ui::VerticalLayout>(list),
st::walletRowPadding));
const auto inner = wrap->entity();
inner->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto button = Ui::CreateChild<HistoryRowButton>(
wrap,
rpl::single(QString()));
button->setClickedCallback(std::move(clicked));
const auto major = Ui::CreateChild<Ui::FlatLabel>(
wrap,
st::walletRowAmountMajorLabel);
major->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto minor = Ui::CreateChild<Ui::FlatLabel>(
wrap,
st::walletRowAmountMinorLabel);
minor->setAttribute(Qt::WA_TransparentForMouseEvents);
if (content.itemAmount) {
SetRowItemAmount(major, minor, content.incoming, content.failed);
} else {
SetRowAmount(
major,
minor,
content.amountNano,
content.incoming,
content.pending,
content.failed);
}
const auto title = inner->add(
object_ptr<Ui::FlatLabel>(
inner,
content.title,
st::walletRowTitleLabel),
{ 0, 0, HistoryRowTitleSkip(major->width() + minor->width()), 0 });
title->setBreakEverywhere(true);
auto subtitle = (Ui::FlatLabel*)nullptr;
if (!content.subtitle.isEmpty()) {
Ui::AddSkip(inner, st::walletRowSkip);
subtitle = inner->add(object_ptr<Ui::FlatLabel>(
inner,
content.subtitle,
st::walletRowSubtitleLabel));
subtitle->setBreakEverywhere(true);
}
Ui::AddSkip(inner, st::walletRowSkip);
const auto date = inner->add(object_ptr<Ui::FlatLabel>(
inner,
content.date,
st::walletRowDateLabel));
const auto hasChip = content.itemAmount && (media != nullptr);
if (hasChip) {
AddHistoryRowChip(inner, button, media, content.collectible);
}
const auto circle = Ui::CreateChild<Ui::RpWidget>(wrap);
circle->resize(st::walletRowIconSize, st::walletRowIconSize);
circle->setAttribute(Qt::WA_TransparentForMouseEvents);
if (const auto peer = content.peer) {
const auto userpic = circle->lifetime().make_state<
Ui::PeerUserpicView>(peer->createUserpicView());
peer->session().downloaderTaskFinished(
) | rpl::on_next([=] {
circle->update();
}, circle->lifetime());
circle->paintRequest(
) | rpl::on_next([=] {
auto p = Painter(circle);
peer->paintUserpicLeft(
p,
*userpic,
0,
0,
circle->width(),
circle->width());
}, circle->lifetime());
} else {
const auto avatar = content.avatar;
circle->paintRequest(
) | rpl::on_next([=] {
auto p = Painter(circle);
PaintRowAvatar(p, circle->rect(), avatar);
}, circle->lifetime());
}
Ui::ToggleChildrenVisibility(wrap, true);
wrap->geometryValue(
) | rpl::on_next([=](const QRect &g) {
const auto layout = ComputeHistoryRowLayout({
.title = title->height(),
.subtitle = (subtitle
? std::make_optional(subtitle->height())
: std::nullopt),
.date = date->height(),
.amount = major->height(),
});
circle->moveToLeft(
st::walletRowIconLeft,
layout.avatarCenter - circle->height() / 2);
minor->moveToRight(
st::walletRowPadding.right(),
layout.amountTop + st::walletRowAmountMinorSkip);
major->moveToRight(
st::walletRowPadding.right() + minor->width(),
layout.amountTop);
button->resize(g.size());
button->lower();
}, wrap->lifetime());
return wrap;
}
struct SendingRowLayout {
QRect pill;
int radius = 0;
QRect avatar;
QPointF badge;
QPoint diamond;
QPointF amount;
int titleWidth = 0;
int textWidth = 0;
};
struct SendingRowSettleTarget {
QPoint major;
QPoint minor;
int boundary = 0;
QPointF anchor;
QRectF diamond;
int titleWidth = 0;
int textWidth = 0;
};
struct SendingRowSettleProgress {
crl::time elapsed = 0;
float64 pill = 0.;
float64 badge = 1.;
float64 label = 0.;
float64 amount = 0.;
float64 amountFade = 0.;
float64 emoji = 0.;
};
struct SendingRowSettleRequest {
HistoryRowContent content;
Fn<void()> done;
bool burst = false;
};
class SendingHistoryRow final : public Ui::AbstractButton {
public:
SendingHistoryRow(
not_null<Ui::RpWidget*> parent,
not_null<Ui::RpWidget*> layer,
not_null<Ui::RpWidget*> bounds,
HistoryRowContent content,
std::shared_ptr<CollectibleMedia> media);
void setContent(HistoryRowContent content);
void awaitDiamond();
void landDiamond(std::unique_ptr<Lottie::Icon> icon, crl::time loopStarted);
void cancelDiamondAwait();
void scheduleBump(crl::time at);
void settle(HistoryRowContent content, bool burst, Fn<void()> done);
[[nodiscard]] bool surfaceShown() const;
[[nodiscard]] bool inView() const;
[[nodiscard]] bool settling() const;
[[nodiscard]] bool settled() const;
[[nodiscard]] QRectF diamondTarget(crl::time now) const;
QString accessibilityName() override;
protected:
int resizeGetHeight(int newWidth) override;
void visibleTopBottomUpdated(int visibleTop, int visibleBottom) override;
private:
struct Settle;
[[nodiscard]] const HistoryRowLayout &rowLayout() const;
void updateRowLayout();
[[nodiscard]] SendingRowLayout layout() const;
[[nodiscard]] SendingRowSettleTarget settleTarget() const;
[[nodiscard]] crl::time settleDuration() const;
[[nodiscard]] crl::time settleElapsed(crl::time now) const;
[[nodiscard]] SendingRowSettleProgress settleProgress(
crl::time now) const;
[[nodiscard]] QRectF diamondCanvas(
const SendingRowLayout &layout,
const SendingRowSettleTarget &target,
crl::time elapsed) const;
[[nodiscard]] QTransform surfaceTransform(crl::time now) const;
[[nodiscard]] int surfaceSkip() const;
void updateSurfaceGeometry();
void paintSurface();
void paintAvatar(
Painter &p,
const SendingRowLayout &layout,
float64 cutout,
float64 swap);
void paintTexts(
Painter &p,
const SendingRowLayout &layout,
const SendingRowSettleTarget &target,
const SendingRowSettleProgress &progress);
void paintSettleAmount(
Painter &p,
const SendingRowLayout &layout,
const SendingRowSettleTarget &target,
const SendingRowSettleProgress &progress,
crl::time now);
void paintItemAmount(
Painter &p,
const SendingRowSettleProgress &progress,
crl::time now);
void paintChip(Painter &p);
void paintDiamond(QPainter &p, QRectF canvas, crl::time now);
void startAnimation();
void startSettle(HistoryRowContent content, bool burst, Fn<void()> done);
[[nodiscard]] bool glareOwed(crl::time now) const;
void tickGlare(crl::time now);
void releaseSettleWait();
const not_null<Ui::RpWidget*> _layer;
const not_null<Ui::RpWidget*> _bounds;
const std::shared_ptr<CollectibleMedia> _media;
HistoryRowContent _content;
HistoryRowText _text;
HistoryRowLayout _layout;
HistoryRowChipState _chip;
AmountPainter _amount;
std::unique_ptr<Lottie::Icon> _diamond;
std::unique_ptr<Ui::PeerUserpicView> _userpic;
rpl::lifetime _userpicLifetime;
Ui::BoxShadow _shadow;
GlareCycle _glare;
Ui::Animations::Basic _animation;
base::unique_qptr<Ui::RpWidget> _surface;
rpl::lifetime _chipLifetime;
std::unique_ptr<Settle> _settle;
std::optional<SendingRowSettleRequest> _waiting;
crl::time _started = 0;
crl::time _diamondStarted = 0;
crl::time _bumpAt = 0;
int _digitsHeight = 0;
bool _inView = true;
bool _diamondAway = false;
bool _glareSeen = false;
};
[[nodiscard]] int DigitsHeight(const style::font &font) {
return int(base::SafeRound(
-font->metrics().tightBoundingRect(u"0123456789"_q).top()));
}
[[nodiscard]] float64 SettleRamp(crl::time t, crl::time from, crl::time till) {
return std::clamp(float64(t - from) / (till - from), 0., 1.);
}
[[nodiscard]] int SendingRowDiamondCanvas() {
return GramDiamondCanvas(st::walletSendingRowAmountFont);
}
[[nodiscard]] int SendingRowDiamondLeft(int width) {
const auto canvas = SendingRowDiamondCanvas();
const auto drawnRight = width - st::walletRowPadding.right();
return int(base::SafeRound(drawnRight - canvas * kGramDiamondRight));
}
[[nodiscard]] float64 SendingRowAmountRight(int width) {
const auto canvas = SendingRowDiamondCanvas();
return SendingRowDiamondLeft(width)
+ canvas * kGramDiamondLeft
- st::walletSendingRowDiamondSkip;
}
[[nodiscard]] float64 SendingRowEntrance(crl::time elapsed) {
const auto z = kSendingRowEntranceDamping;
const auto wd = M_PI / kSendingRowEntrancePeak;
const auto wn = wd / std::sqrt(1. - z * z);
const auto t = float64(elapsed);
return 1. - std::exp(-z * wn * t)
* (std::cos(wd * t) + (z * wn / wd) * std::sin(wd * t));
}
[[nodiscard]] float64 SendingRowBump(crl::time elapsed) {
if (elapsed < 0 || elapsed >= kSendingRowBumpDuration) {
return 0.;
}
const auto t = float64(elapsed);
if (elapsed <= kSendingRowBumpPress) {
return std::sin(M_PI / 2. * t / kSendingRowBumpPress);
}
const auto u = t - kSendingRowBumpPress;
const auto w = M_PI / kSendingRowBumpRelease;
const auto sigma = -std::log(kSendingRowBumpRebound)
/ kSendingRowBumpRelease;
const auto bump = std::exp(-sigma * u)
* (std::cos(w * u) + (sigma / w) * std::sin(w * u));
if (elapsed <= kSendingRowBumpTaper) {
return bump;
}
const auto x = (t - kSendingRowBumpTaper)
/ (kSendingRowBumpDuration - kSendingRowBumpTaper);
return bump * (1. - x * x * (3. - 2. * x));
}
[[nodiscard]] ClockStyle SendingRowClockStyle() {
return {
.size = st::walletSendingRowClockSize,
.stroke = st::walletSendingRowClockStroke,
.minuteHand = st::walletSendingRowClockMinuteHand,
.hourHand = st::walletSendingRowClockHourHand,
};
}
[[nodiscard]] Ui::StarBurstDescriptor SendingRowBurstDescriptor(
QColor color) {
const auto mirror = style::RightToLeft() ? -1. : 1.;
return {
.sides = {
{
.sign = -mirror,
.count = 30,
.angle = { -8., 18. },
.reach = { 0.15, 0.55 },
},
{
.sign = mirror,
.count = 6,
.angle = { -35., 5. },
.reach = { 0.03, 0.10 },
},
},
.delay = kSendingRowBurstDelay,
.spread = kSendingRowBurstSpread,
.lifeMin = kSendingRowBurstLifeMin,
.lifeMax = kSendingRowBurstLifeMax,
.fall = { 0., 0.04 },
.startX = { 0., 0.5 },
.startY = { -0.3, 0.5 },
.size = { 0.008, 0.027 },
.alpha = { 0.45, 0.95 },
.twinkle = { 0.1, 1.9 },
.appearTill = 0.2,
.fadeAfter = 0.55,
.deformation = 0.1,
.color = color,
};
}
struct SendingHistoryRow::Settle {
HistoryRowContent content;
HistoryRowText text;
HistoryRowLayout layout;
std::unique_ptr<Ui::StarBurst> burst;
std::unique_ptr<Ui::PeerUserpicView> userpic;
rpl::lifetime userpicLifetime;
crl::time started = 0;
base::Timer finish;
Fn<void()> done;
float64 scale = 1.;
int fraction = 0;
bool finished = false;
};
// WHY: the surface paints outside the row, so it lives in `layer`, an
// unclipped ancestor of `bounds` (the list's wrap), and follows `bounds`:
// hidden with it and cut at its bottom edge, so a collapsing list hides it.
SendingHistoryRow::SendingHistoryRow(
not_null<Ui::RpWidget*> parent,
not_null<Ui::RpWidget*> layer,
not_null<Ui::RpWidget*> bounds,
HistoryRowContent content,
std::shared_ptr<CollectibleMedia> media)
: Ui::AbstractButton(parent.get())
, _layer(layer)
, _bounds(bounds)
, _media(std::move(media))
, _shadow(st::walletInfoIslandShadow)
, _surface(Ui::CreateChild<Ui::RpWidget>(layer.get())) {
Expects(_media || !content.itemAmount);
_digitsHeight = DigitsHeight(st::walletSendingRowAmountFont);
if (!content.itemAmount) {
const auto canvas = SendingRowDiamondCanvas();
_diamond = Lottie::MakeIcon({
.name = u"gram"_q,
.sizeOverride = { canvas, canvas },
.frame = -1,
.limitFps = true,
});
}
_animation.init([=](crl::time now) {
if (!_inView || !isVisible() || anim::Disabled()) {
releaseSettleWait();
return false;
}
tickGlare(now);
if (_waiting && !glareOwed(now)) {
auto waiting = *base::take(_waiting);
startSettle(
std::move(waiting.content),
waiting.burst,
std::move(waiting.done));
}
_surface->update();
return true;
});
_surface->setAttribute(Qt::WA_TransparentForMouseEvents);
_surface->raise();
_surface->show();
_surface->paintRequest(
) | rpl::on_next([=] {
paintSurface();
}, _surface->lifetime());
rpl::combine(
geometryValue(),
parent->geometryValue(),
bounds->geometryValue()
) | rpl::on_next([=] {
updateSurfaceGeometry();
}, lifetime());
rpl::combine(
shownValue(),
bounds->shownValue()
) | rpl::on_next([=](bool shown, bool boundsShown) {
_surface->setVisible(shown && boundsShown);
}, lifetime());
setContent(std::move(content));
}
int SendingHistoryRow::surfaceSkip() const {
return (_settle && _settle->burst) ? st::walletSendingRowBurstOutset : 0;
}
void SendingHistoryRow::updateSurfaceGeometry() {
const auto extend = _shadow.extend();
const auto shift = st::walletSendingRowEntranceShift;
const auto skip = surfaceSkip();
auto geometry = Ui::MapFrom(_layer, this, rect()).marginsAdded({
0,
shift + extend.top() + skip,
0,
shift + extend.bottom() + skip,
});
const auto limit = Ui::MapFrom(_layer, _bounds, _bounds->rect());
geometry.setHeight(std::clamp(
limit.y() + limit.height() - geometry.y(),
0,
geometry.height()));
_surface->setGeometry(geometry);
}
void SendingHistoryRow::setContent(HistoryRowContent content) {
const auto peerChanged = !_userpic || (_content.peer != content.peer);
const auto chipChanged = (_content.collectible != content.collectible);
_content = std::move(content);
_text = PrepareHistoryRowText(_content);
if (!_content.itemAmount) {
const auto amountNano = _content.amountNano;
const auto &font = st::walletSendingRowAmountFont;
_amount.setContent({ .big = font, .small = font }, {
.whole = RowAmountWhole(
amountNano,
RowAmountSign(_content.incoming)),
.fraction = GramMinorPart(amountNano),
});
}
if (chipChanged) {
_chipLifetime.destroy();
_chip = HistoryRowChipState();
if (_content.itemAmount) {
TrackHistoryRowChip(
&_chip,
_media,
_content.collectible,
[=] { _surface->update(); },
_chipLifetime);
}
}
if (peerChanged) {
_userpicLifetime.destroy();
if (const auto peer = _content.peer) {
_userpic = std::make_unique<Ui::PeerUserpicView>(
peer->createUserpicView());
peer->session().downloaderTaskFinished(
) | rpl::on_next([=] {
_surface->update();
}, _userpicLifetime);
} else {
_userpic = nullptr;
}
}
accessibilityNameChanged();
updateRowLayout();
_surface->update();
}
void SendingHistoryRow::awaitDiamond() {
_diamondAway = true;
_surface->update();
}
void SendingHistoryRow::landDiamond(
std::unique_ptr<Lottie::Icon> icon,
crl::time loopStarted) {
if (icon) {
_diamond = std::move(icon);
_diamondStarted = loopStarted;
}
_diamondAway = false;
_bumpAt = (anim::Disabled() || _settle) ? 0 : crl::now();
startAnimation();
_surface->update();
}
void SendingHistoryRow::cancelDiamondAwait() {
_diamondAway = false;
_surface->update();
}
void SendingHistoryRow::scheduleBump(crl::time at) {
_bumpAt = anim::Disabled() ? 0 : at;
startAnimation();
}
void SendingHistoryRow::settle(
HistoryRowContent content,
bool burst,
Fn<void()> done) {
if (!_settle && (_waiting || glareOwed(crl::now()))) {
_waiting = SendingRowSettleRequest{
.content = std::move(content),
.done = std::move(done),
.burst = burst,
};
startAnimation();
return;
}
startSettle(std::move(content), burst, std::move(done));
}
void SendingHistoryRow::startSettle(
HistoryRowContent content,
bool burst,
Fn<void()> done) {
if (!_settle) {
_settle = std::make_unique<Settle>();
_settle->finish.setCallback([=] {
_settle->finished = true;
crl::on_main(this, [=] {
if (const auto done = _settle ? _settle->done : nullptr) {
done();
}
});
});
if (burst && !anim::Disabled()) {
const auto scope = WindowPaletteScope(this);
_settle->burst = Ui::StarBurst::Make(
SendingRowBurstDescriptor(st::windowActiveTextFg->c));
}
_settle->finish.callOnce(settleDuration() + kSendingRowSettlePaintWait);
updateSurfaceGeometry();
}
const auto peer = content.peer;
if (!peer || peer == _content.peer) {
_settle->userpicLifetime.destroy();
_settle->userpic = nullptr;
} else if (!_settle->userpic || _settle->content.peer != peer) {
_settle->userpicLifetime.destroy();
_settle->userpic = std::make_unique<Ui::PeerUserpicView>(
peer->createUserpicView());
peer->session().downloaderTaskFinished(
) | rpl::on_next([=] {
_surface->update();
}, _settle->userpicLifetime);
}
_settle->done = std::move(done);
_settle->content = std::move(content);
const auto &entry = _settle->content;
_settle->text = PrepareHistoryRowText(entry, [=] {
_surface->update();
});
const auto &font = st::walletRowAmountMinorLabel.style.font;
_settle->fraction = font->width(GramMinorPart(entry.amountNano));
_settle->scale = DigitsHeight(st::walletRowAmountMajorLabel.style.font)
/ float64(_digitsHeight);
accessibilityNameChanged();
updateRowLayout();
startAnimation();
_surface->update();
}
bool SendingHistoryRow::glareOwed(crl::time now) const {
return _started
&& !anim::Disabled()
&& _inView
&& isVisible()
&& (!_glareSeen || _glare.progress(now).has_value());
}
// WHY: a pass counts only when this run of the frame callback, which stops
// out of view, in a hidden window and with animations off, saw it whole after
// the first paint; none is born under a settle, so no pass is cut by one.
void SendingHistoryRow::tickGlare(crl::time now) {
if (_settle) {
return;
}
if (_started
&& (_glare.birth > _started)
&& (_glare.birth >= _animation.started())
&& (now > _glare.death)) {
_glareSeen = true;
}
_glare.tick(now, kSendingRowGlareDuration, kSendingRowGlarePause);
}
// The list decides again, as when a result finds the row in this state.
void SendingHistoryRow::releaseSettleWait() {
if (auto waiting = base::take(_waiting)) {
crl::on_main(this, [done = std::move(waiting->done)] {
if (done) {
done();
}
});
}
}
bool SendingHistoryRow::surfaceShown() const {
return _surface && !_surface->isHidden();
}
bool SendingHistoryRow::inView() const {
return _inView;
}
bool SendingHistoryRow::settling() const {
return (_settle && !_settle->finished) || _waiting.has_value();
}
bool SendingHistoryRow::settled() const {
return _settle && _settle->finished;
}
QRectF SendingHistoryRow::diamondTarget(crl::time now) const {
return surfaceTransform(now).mapRect(diamondCanvas(
layout(),
settleTarget(),
settleProgress(now).elapsed));
}
int SendingHistoryRow::resizeGetHeight(int newWidth) {
_amount.setAvailableWidth(int(SendingRowAmountRight(newWidth))
- st::walletRowPadding.left()
- st::walletSendingRowTextMinWidth);
_layout = MeasureHistoryRow(_text, newWidth);
if (_settle) {
_settle->layout = MeasureHistoryRow(_settle->text, newWidth);
}
return rowLayout().height;
}
const HistoryRowLayout &SendingHistoryRow::rowLayout() const {
return _settle ? _settle->layout : _layout;
}
void SendingHistoryRow::updateRowLayout() {
if (const auto w = width()) {
resizeToWidth(w);
}
}
void SendingHistoryRow::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
const auto inView = (visibleBottom > visibleTop);
if (_inView == inView) {
return;
}
_inView = inView;
if (_inView && !_animation.animating()) {
_surface->update();
}
}
QString SendingHistoryRow::accessibilityName() {
const auto &amount = _amount.parts();
const auto &content = _settle ? _settle->content : _content;
auto parts = QStringList();
for (const auto &text : {
content.title,
content.subtitle,
content.date,
(_content.itemAmount
? (_settle ? _settle->text : _text).major.toString()
: _settle
? (_settle->text.major.toString()
+ GramMinorPart(content.amountNano))
: QString(amount.whole + amount.fraction)),
}) {
if (!text.isEmpty()) {
parts.push_back(text);
}
}
return parts.join(u", "_q);
}
SendingRowLayout SendingHistoryRow::layout() const {
const auto w = width();
const auto h = height();
const auto rtl = style::RightToLeft();
const auto mirror = [&](float64 left, float64 width) {
return rtl ? (w - left - width) : left;
};
const auto &padding = st::walletRowPadding;
const auto outset = st::walletSendingRowOutset;
const auto pillLeft = st::walletRowIconLeft - outset;
const auto pillRight = w - padding.right() + outset;
auto result = SendingRowLayout();
result.pill = QRect(
int(mirror(pillLeft, pillRight - pillLeft)),
0,
pillRight - pillLeft,
h);
result.radius = PillRadius(result.pill);
const auto size = st::walletRowIconSize;
const auto center = rowLayout().avatarCenter;
result.avatar = QRect(
int(mirror(st::walletRowIconLeft, size)),
center - size / 2,
size,
size);
const auto shift = st::walletSendingRowClockShift;
result.badge = QRectF(result.avatar).center()
+ QPointF(rtl ? -shift : shift, shift);
if (_content.itemAmount) {
result.titleWidth = _layout.titleWidth;
result.textWidth = _layout.textWidth;
return result;
}
const auto canvas = SendingRowDiamondCanvas();
const auto drawnLeft = SendingRowDiamondLeft(w)
+ canvas * kGramDiamondLeft;
const auto drawnWidth = canvas * (kGramDiamondRight - kGramDiamondLeft);
result.diamond = QPoint(
int(base::SafeRound(mirror(drawnLeft, drawnWidth)
- canvas * kGramDiamondLeft)),
int(base::SafeRound(h / 2.
- canvas * (kGramDiamondTop + kGramDiamondBottom) / 2.)));
const auto amountWidth = _amount.size().width();
const auto amountLeft = SendingRowAmountRight(w) - amountWidth;
const auto digitsCenter = (_amount.baseline() - _digitsHeight / 2.)
* _amount.scale();
result.amount = QPointF(
mirror(amountLeft, amountWidth),
h / 2. - digitsCenter);
result.textWidth = std::max(
int(amountLeft) - st::walletRowSkip - padding.left(),
0);
result.titleWidth = result.textWidth;
return result;
}
SendingRowSettleTarget SendingHistoryRow::settleTarget() const {
auto result = SendingRowSettleTarget();
if (!_settle) {
return result;
}
const auto place = PlaceRowAmount(
_settle->text,
_settle->layout.amountTop,
width());
result.major = place.major;
result.minor = place.minor;
result.boundary = result.minor.x() + _settle->fraction;
const auto &font = st::walletRowAmountMinorLabel.style.font;
result.anchor = QPointF(result.boundary, result.minor.y() + font->ascent);
const auto emoji = st::emojiSize;
const auto skip = (emoji - Ui::Text::AdjustCustomEmojiSize(emoji)) / 2;
const auto &margin = st::walletRowIconMargin;
const auto size = float64(st::walletRowMarkSize);
const auto image = QPointF(
result.boundary + margin.left() + skip,
result.minor.y() + (font->height - emoji) / 2 + skip + margin.top());
result.diamond = QRectF(
image.x() + size * kRowEmojiDiamondLeft,
image.y() + size * kRowEmojiDiamondTop,
size * (kRowEmojiDiamondRight - kRowEmojiDiamondLeft),
size * (kRowEmojiDiamondBottom - kRowEmojiDiamondTop));
result.textWidth = _settle->layout.textWidth;
result.titleWidth = _settle->layout.titleWidth;
return result;
}
crl::time SendingHistoryRow::settleDuration() const {
return (_settle && _settle->burst)
? _settle->burst->duration()
: kSendingRowSettleDiamond;
}
crl::time SendingHistoryRow::settleElapsed(crl::time now) const {
return !_settle
? crl::time(0)
: _settle->finished
? settleDuration()
: _settle->started
? (now - _settle->started)
: crl::time(0);
}
SendingRowSettleProgress SendingHistoryRow::settleProgress(
crl::time now) const {
auto result = SendingRowSettleProgress();
if (!_settle) {
return result;
}
const auto t = anim::Disabled()
? kSendingRowSettleDiamond
: settleElapsed(now);
const auto out = [](float64 x) {
return anim::easeOutCubic(1., x);
};
result.elapsed = t;
result.pill = out(SettleRamp(t, 0, kSendingRowSettlePill));
result.badge = 1. - SettleRamp(
t,
kSendingRowSettleBadgeFrom,
kSendingRowSettleBadgeTill);
result.label = SettleRamp(
t,
kSendingRowSettleLabelFrom,
kSendingRowSettleLabelTill);
result.amount = out(SettleRamp(t, 0, kSendingRowSettleAmount));
result.amountFade = SettleRamp(
t,
kSendingRowSettleAmountFadeFrom,
kSendingRowSettleAmount);
result.emoji = SettleRamp(
t,
kSendingRowSettleDiamondFadeFrom,
kSendingRowSettleDiamond);
return result;
}
QRectF SendingHistoryRow::diamondCanvas(
const SendingRowLayout &layout,
const SendingRowSettleTarget &target,
crl::time elapsed) const {
const auto canvas = float64(SendingRowDiamondCanvas());
const auto middle = QPointF(
(kGramDiamondLeft + kGramDiamondRight) / 2.,
(kGramDiamondTop + kGramDiamondBottom) / 2.);
auto centre = QPointF(layout.diamond) + middle * canvas;
auto scale = 1.;
if (_settle) {
const auto t = elapsed;
const auto swell = kSendingRowSettleDiamondSwell;
centre += (target.diamond.center() - centre) * anim::easeOutCubic(
1.,
SettleRamp(t, 0, kSendingRowSettleDiamondMove));
if (t < kSendingRowSettleDiamondSwellTill) {
scale = 1. + (swell - 1.) * anim::easeOutCubic(
1.,
SettleRamp(t, 0, kSendingRowSettleDiamondSwellTill));
} else {
const auto x = SettleRamp(
t,
kSendingRowSettleDiamondSwellTill,
kSendingRowSettleDiamond);
const auto end = target.diamond.width()
/ (canvas * (kGramDiamondRight - kGramDiamondLeft));
scale = swell + (end - swell) * x * x * (3. - 2. * x);
}
}
const auto side = canvas * scale;
return QRectF(centre - middle * side, QSizeF(side, side));
}
QTransform SendingHistoryRow::surfaceTransform(crl::time now) const {
auto result = QTransform();
if (anim::Disabled()) {
return result;
}
const auto pill = QRectF(layout().pill);
const auto center = pill.center();
const auto fade = 1. - settleProgress(now).pill;
const auto elapsed = _started ? (now - _started) : crl::time(0);
if (elapsed < kSendingRowEntranceDuration) {
const auto progress = 1. - (1. - SendingRowEntrance(elapsed)) * fade;
const auto scale = kSendingRowEntranceScale
+ (1. - kSendingRowEntranceScale) * progress;
const auto offset = st::walletSendingRowEntranceShift
* (1. - progress);
result.translate(center.x(), center.y() + offset);
result.scale(scale, scale);
result.translate(-center.x(), -center.y());
}
const auto bump = (_bumpAt ? SendingRowBump(now - _bumpAt) : 0.) * fade;
if (bump != 0.) {
const auto scale = 1. - kSendingRowBumpShrink * bump;
const auto drop = (kSendingRowBumpDrop - kSendingRowBumpShrink / 2.)
* pill.height()
* bump;
result.translate(center.x(), center.y() + drop);
result.scale(scale, scale);
result.translate(-center.x(), -center.y());
}
return result;
}
void SendingHistoryRow::paintSurface() {
auto p = Painter(_surface.get());
const auto now = crl::now();
if (!_started) {
_started = now;
_glare.death = now + kSendingRowEntranceDuration;
}
if (!_diamondStarted) {
_diamondStarted = _started;
}
if (_settle && !_settle->started && !_settle->finished) {
_settle->started = now;
_settle->finish.callOnce(settleDuration());
}
startAnimation();
auto hq = PainterHighQualityEnabler(p);
const auto layout = this->layout();
const auto target = settleTarget();
const auto progress = settleProgress(now);
p.translate(
0,
(st::walletSendingRowEntranceShift
+ _shadow.extend().top()
+ surfaceSkip()));
p.setTransform(surfaceTransform(now), true);
const auto disabled = anim::Disabled();
const auto elapsed = now - _started;
const auto fade = 1. - progress.pill;
const auto inset = int(base::SafeRound(
st::walletSendingRowOutset * progress.pill));
const auto pill = layout.pill.marginsRemoved({ inset, 0, inset, 0 });
const auto radius = PillRadius(pill);
const auto accent = st::windowActiveTextFg->c;
p.setOpacity(fade);
if (fade > 0.) {
Dialogs::PaintPillBackground(p, _shadow, pill, radius);
}
if (const auto glare = (disabled || fade <= 0.)
? std::optional<float64>()
: _glare.progress(now)) {
const auto stroke = float64(st::walletSendingRowGlareStroke);
const auto half = stroke / 2.;
const auto pillWidth = float64(pill.width());
const auto pillHeight = float64(pill.height());
const auto slope = kSendingRowGlareLag * pillWidth / pillHeight;
PaintGlare(
p,
QRectF(pill).marginsAdded({ half, half, half, half }),
radius + half,
ComputeGlareBand(
*glare,
pillWidth + slope * pillHeight,
st::walletSendingRowGlareWidth),
{
.stroke = stroke,
.slope = slope,
.border = kSendingRowGlareBorder,
.background = kSendingRowGlareBackground,
},
accent);
}
p.setOpacity(1.);
paintAvatar(
p,
layout,
(st::walletSendingRowClockSize / 2. + st::walletSendingRowClockCutout)
* progress.badge,
progress.label);
if (progress.badge > 0.) {
p.setOpacity(progress.badge);
PaintClock(
p,
SendingRowClockStyle(),
layout.badge,
SendingClockPose(disabled ? 0 : elapsed),
accent);
p.setOpacity(1.);
}
paintTexts(p, layout, target, progress);
if (_content.itemAmount) {
paintItemAmount(p, progress, now);
paintChip(p);
return;
}
if (_settle) {
paintSettleAmount(p, layout, target, progress, now);
} else {
_amount.paint(p, layout.amount, { .digits = st::windowBoldFg->c });
}
if (const auto burst = (_settle && !disabled)
? _settle->burst.get()
: nullptr) {
const auto canvas = SendingRowDiamondCanvas();
burst->paint(p, {
.origin = target.diamond.center(),
.emitter = canvas * (kGramDiamondRight - kGramDiamondLeft),
.extent = float64(width()),
.elapsed = progress.elapsed,
});
}
if (progress.emoji < 1.) {
p.setOpacity(1. - progress.emoji);
paintDiamond(
p,
diamondCanvas(layout, target, progress.elapsed),
now);
p.setOpacity(1.);
}
}
void SendingHistoryRow::paintAvatar(
Painter &p,
const SendingRowLayout &layout,
float64 cutout,
float64 swap) {
p.save();
if (cutout > 0.) {
auto clip = QPainterPath();
clip.addRect(QRectF(layout.avatar));
auto cut = QPainterPath();
cut.addEllipse(layout.badge, cutout, cutout);
p.setClipPath(clip.subtracted(cut));
}
const auto paint = [&](
const HistoryRowContent &content,
Ui::PeerUserpicView *userpic) {
if (content.peer && userpic) {
content.peer->paintUserpic(
p,
*userpic,
layout.avatar.x(),
layout.avatar.y(),
layout.avatar.width());
} else {
PaintRowAvatar(p, layout.avatar, content.avatar);
}
};
const auto next = _settle ? &_settle->content : nullptr;
const auto same = !next
|| (next->peer
? (next->peer == _content.peer)
: (!_content.peer && next->avatar == _content.avatar));
if (same) {
paint(_content, _userpic.get());
} else {
if (swap < 1.) {
paint(_content, _userpic.get());
}
if (swap > 0.) {
p.setOpacity(swap);
paint(*next, _settle->userpic.get());
}
}
p.restore();
}
void SendingHistoryRow::paintTexts(
Painter &p,
const SendingRowLayout &layout,
const SendingRowSettleTarget &target,
const SendingRowSettleProgress &progress) {
const auto left = st::walletRowPadding.left();
const auto draw = [&](
const Ui::Text::String &text,
const style::FlatLabel &st,
HistoryRowLine line,
bool breakEverywhere,
int available,
float64 opacity) {
if (opacity <= 0. || !line.lines) {
return;
}
p.setOpacity(opacity);
p.setPen(st.textFg);
text.draw(p, {
.position = { left, line.top },
.outerWidth = width(),
.availableWidth = available,
.elisionLines = line.lines,
.elisionBreakEverywhere = breakEverywhere,
});
};
const auto settle = _settle.get();
const auto line = [&](
const Ui::Text::String &text,
HistoryRowLine textLine,
const Ui::Text::String *next,
HistoryRowLine nextLine,
bool same,
const style::FlatLabel &st,
bool breakEverywhere,
int available) {
if (!next) {
draw(text, st, textLine, breakEverywhere, available, 1.);
} else if (same) {
draw(*next, st, nextLine, breakEverywhere, available, 1.);
} else {
draw(
text,
st,
textLine,
breakEverywhere,
available,
1. - progress.label);
draw(
*next,
st,
nextLine,
breakEverywhere,
available,
progress.label);
}
};
const auto titleWidth = settle
? int(base::SafeRound(layout.titleWidth
+ (target.titleWidth - layout.titleWidth) * progress.amount))
: layout.titleWidth;
const auto textWidth = settle
? int(base::SafeRound(layout.textWidth
+ (target.textWidth - layout.textWidth) * progress.amount))
: layout.textWidth;
line(
_text.title,
_layout.title,
settle ? &settle->text.title : nullptr,
settle ? settle->layout.title : HistoryRowLine(),
settle && (settle->content.title == _content.title),
st::walletRowTitleLabel,
true,
titleWidth);
line(
_text.subtitle,
_layout.subtitle,
settle ? &settle->text.subtitle : nullptr,
settle ? settle->layout.subtitle : HistoryRowLine(),
settle && (settle->content.subtitle == _content.subtitle),
st::walletRowSubtitleLabel,
true,
textWidth);
line(
_text.date,
_layout.date,
settle ? &settle->text.date : nullptr,
settle ? settle->layout.date : HistoryRowLine(),
settle && (settle->content.date == _content.date),
st::walletRowDateLabel,
false,
textWidth);
p.setOpacity(1.);
}
void SendingHistoryRow::paintSettleAmount(
Painter &p,
const SendingRowLayout &layout,
const SendingRowSettleTarget &target,
const SendingRowSettleProgress &progress,
crl::time now) {
const auto &content = _settle->content;
const auto &color = RowAmountColor(
content.incoming,
content.pending,
content.failed);
if (progress.amountFade < 1.) {
const auto from = _amount.scale();
const auto natural = QPointF(
_amount.naturalWidth(),
_amount.baseline());
const auto start = layout.amount + natural * from;
const auto anchor = start + (target.anchor - start) * progress.amount;
const auto scale = from + (_settle->scale - from) * progress.amount;
p.save();
p.setOpacity(1. - progress.amountFade);
p.translate(anchor);
p.scale(scale, scale);
p.translate(-natural);
_amount.paint(p, {
.digits = anim::color(st::windowBoldFg, color, progress.amount),
});
p.restore();
}
const auto draw = [&](
const Ui::Text::String &text,
QPoint position,
const style::FlatLabel &st,
QRect clip,
float64 opacity) {
if (opacity <= 0.) {
return;
}
p.save();
p.setOpacity(opacity);
p.setClipRect(clip, Qt::IntersectClip);
p.setPen(color);
text.draw(p, {
.position = position,
.availableWidth = text.maxWidth(),
.palette = &st.palette,
.now = now,
});
p.restore();
};
const auto h = height();
draw(
_settle->text.major,
target.major,
st::walletRowAmountMajorLabel,
rect(),
progress.amountFade);
draw(
_settle->text.minor,
target.minor,
st::walletRowAmountMinorLabel,
QRect(0, 0, target.boundary, h),
progress.amountFade);
draw(
_settle->text.minor,
target.minor,
st::walletRowAmountMinorLabel,
QRect(target.boundary, 0, width() - target.boundary, h),
progress.emoji);
}
void SendingHistoryRow::paintItemAmount(
Painter &p,
const SendingRowSettleProgress &progress,
crl::time now) {
const auto &text = _settle ? _settle->text : _text;
const auto &next = _settle ? _settle->content : _content;
const auto place = PlaceRowAmount(text, rowLayout().amountTop, width());
p.setPen(anim::color(
RowAmountColor(_content.incoming, false, _content.failed),
RowAmountColor(next.incoming, false, next.failed),
progress.label));
const auto draw = [&](
const Ui::Text::String &string,
QPoint position,
const style::FlatLabel &st) {
string.draw(p, {
.position = position,
.availableWidth = string.maxWidth(),
.palette = &st.palette,
.now = now,
});
};
draw(text.major, place.major, st::walletRowAmountMajorLabel);
draw(text.minor, place.minor, st::walletRowAmountMinorLabel);
}
void SendingHistoryRow::paintChip(Painter &p) {
const auto &padding = st::walletRowPadding;
const auto outer = std::max(width() - padding.left() - padding.right(), 0);
const auto origin = QPoint(
style::RightToLeft() ? padding.right() : padding.left(),
rowLayout().chipTop);
p.translate(origin);
PaintHistoryRowChipPlate(p, outer, _chip);
PaintHistoryRowChipArtwork(p, outer, _chip, _media, _content.collectible);
PaintHistoryRowChipText(p, outer, _chip);
p.translate(-origin);
}
void SendingHistoryRow::paintDiamond(
QPainter &p,
QRectF canvas,
crl::time now) {
if (_diamondAway || !_diamond || !_diamond->valid()) {
return;
}
AdvanceSendingDiamond(_diamond.get(), _diamondStarted, now);
const auto size = SendingRowDiamondCanvas();
const auto fade = 1. - settleProgress(now).pill;
const auto bump = (_bumpAt && !anim::Disabled())
? (SendingRowBump(now - _bumpAt) * fade)
: 0.;
const auto swell = 1. + kSendingRowBumpSwell * std::max(bump, 0.);
const auto centre = canvas.topLeft() + QPointF(
canvas.width() * (kGramDiamondLeft + kGramDiamondRight) / 2.,
canvas.height() * (kGramDiamondTop + kGramDiamondBottom) / 2.);
const auto target = QRectF(
centre + (canvas.topLeft() - centre) * swell,
canvas.size() * swell);
p.drawImage(target, _diamond->frame(QSize(size, size), nullptr).image);
}
void SendingHistoryRow::startAnimation() {
if (anim::Disabled() || _animation.animating()) {
return;
}
_animation.start();
}
not_null<SendingHistoryRow*> AddSendingHistoryRow(
not_null<Ui::VerticalLayout*> slot,
not_null<Ui::RpWidget*> layer,
not_null<Ui::RpWidget*> bounds,
const HistoryRowContent &content,
std::shared_ptr<CollectibleMedia> media,
Fn<void()> clicked) {
const auto result = slot->add(object_ptr<SendingHistoryRow>(
slot,
layer,
bounds,
content,
std::move(media)));
result->setClickedCallback(std::move(clicked));
return result;
}
[[nodiscard]] bool ShowsCollectible(const TransferItem &item) {
return (item.kind == TransferItem::Kind::Collectible)
&& !item.collectible.isEmpty();
}
[[nodiscard]] QString RowStatusSubtitle(TransferItem::Status status) {
using Status = TransferItem::Status;
switch (status) {
case Status::Pending:
return tr::lng_channel_earn_history_pending(tr::now);
case Status::Failure:
return tr::lng_channel_earn_history_failed(tr::now);
case Status::Success:
return QString();
}
Unexpected("Status in RowStatusSubtitle.");
}
[[nodiscard]] HistoryRowContent RowContentFromItem(
const TransferItem &item,
not_null<Main::Session*> session) {
using Kind = TransferItem::Kind;
const auto date = item.date
? langDateTime(base::unixtime::parse(*item.date))
: QString();
if (ShowsCollectible(item)) {
const auto peer = item.counterpartyPeer
? session->data().peerLoaded(PeerId(item.counterpartyPeer))
: nullptr;
const auto hasCounterparty = !item.counterparty.isEmpty();
const auto domain = hasCounterparty
? item.counterpartyName.trimmed()
: QString();
const auto kindText = item.incoming
? tr::lng_wallet_row_collectible_in(tr::now)
: tr::lng_wallet_row_collectible_out(tr::now);
const auto titleIsKind = !peer && !hasCounterparty;
const auto statusText = RowStatusSubtitle(item.status);
return {
.title = (peer
? peer->name()
: !domain.isEmpty()
? domain
: hasCounterparty
? ShortAddressForm(CounterpartyAddress(item))
: kindText),
.subtitle = (!statusText.isEmpty()
? statusText
: titleIsKind
? QString()
: kindText),
.date = date,
.incoming = item.incoming,
.failed = (item.status == TransferItem::Status::Failure),
.avatar = (peer
? RowAvatar::Peer
: item.incoming
? RowAvatar::In
: RowAvatar::Out),
.peer = peer,
.itemAmount = true,
.collectible = item.collectible,
};
}
const auto pending
= (item.status == TransferItem::Status::Pending);
const auto failed
= (item.status == TransferItem::Status::Failure);
const auto statusText = RowStatusSubtitle(item.status);
const auto transfer = (item.kind == Kind::Transfer)
|| (item.kind == Kind::PeerTransfer)
|| (item.kind == Kind::Onramp);
const auto address = transfer
? CounterpartyAddress(item)
: QString();
const auto domain = !address.isEmpty()
? item.counterpartyName.trimmed()
: QString();
const auto provider = !address.isEmpty()
? OnrampProvider(item)
: QString();
if (!provider.isEmpty()) {
return {
.title = provider,
.subtitle = (!statusText.isEmpty()
? statusText
: item.incoming
? tr::lng_wallet_row_topup(tr::now)
: tr::lng_wallet_row_withdrawal(tr::now)),
.date = date,
.amountNano = item.amountNano,
.incoming = item.incoming,
.pending = pending,
.failed = failed,
.avatar = (item.incoming ? RowAvatar::In : RowAvatar::Out),
};
}
if (item.kind == Kind::PeerTransfer && item.counterpartyPeer) {
const auto peer = session->data().peerLoaded(
PeerId(item.counterpartyPeer));
if (peer) {
return {
.title = peer->name(),
.subtitle = (!statusText.isEmpty()
? statusText
: !domain.isEmpty()
? domain
: item.incoming
? tr::lng_wallet_row_incoming(tr::now)
: tr::lng_wallet_row_outgoing(tr::now)),
.date = date,
.amountNano = item.amountNano,
.incoming = item.incoming,
.pending = pending,
.failed = failed,
.avatar = RowAvatar::Peer,
.peer = peer,
};
}
}
if (item.kind == Kind::KeyChange) {
// A zero served amount leaves the paid fee as the wallet's outflow.
const auto outflow = (!item.amountNano
&& item.feeNano
&& *item.feeNano > 0)
? *item.feeNano
: item.amountNano;
return {
.title = tr::lng_wallet_row_key_change(tr::now),
.subtitle = statusText,
.date = date,
.amountNano = outflow,
.incoming = item.incoming,
.pending = pending,
.failed = failed,
.avatar = RowAvatar::KeyChange,
};
}
const auto collectible = (item.kind == Kind::Collectible);
const auto hasCounterparty = transfer
? !address.isEmpty()
: !item.counterparty.isEmpty();
const auto kindText = collectible
? (item.incoming
? tr::lng_wallet_row_collectible_in(tr::now)
: tr::lng_wallet_row_collectible_out(tr::now))
: item.incoming
? tr::lng_wallet_row_deposit(tr::now)
: tr::lng_wallet_row_withdrawal(tr::now);
return {
.title = (hasCounterparty
? (!domain.isEmpty()
? domain
: ShortAddressForm(transfer
? address
: CounterpartyAddress(item)))
: kindText),
.subtitle = (!statusText.isEmpty()
? statusText
: hasCounterparty
? kindText
: QString()),
.date = date,
.amountNano = item.amountNano,
.incoming = item.incoming,
.pending = pending,
.failed = failed,
.avatar = (item.incoming ? RowAvatar::In : RowAvatar::Out),
};
}
// WHY: it plays once when the box finishes showing and a click replays it
// once it has stopped, with no pointer cursor or anything else saying so,
// because finding that out is the whole of it.
void AddWalletLottie(
not_null<Ui::GenericBox*> box,
const style::margins &margin = style::margins(),
Ui::VerticalLayout *container = nullptr) {
const auto into = container ? container : box->verticalLayout().get();
const auto size = st::walletDetailsLottieSize;
auto icon = Settings::CreateLottieIcon(
into,
{
.name = u"gram"_q,
.sizeOverride = { size, size },
},
margin);
const auto raw = icon.widget.data();
const auto animate = icon.animate;
const auto animating = icon.animating;
into->add(std::move(icon.widget));
const auto replay = Ui::CreateChild<Ui::AbstractButton>(raw);
replay->setPointerCursor(false);
replay->setClickedCallback([=] {
if (!animating()) {
animate(anim::repeat::once);
}
});
// The rect the icon paints into: centered in the row, under the top margin.
raw->sizeValue() | rpl::on_next([=](QSize outer) {
replay->setGeometry(
(outer.width() - size) / 2,
margin.top(),
size,
size);
}, replay->lifetime());
box->showFinishes() | rpl::on_next([=] {
animate(anim::repeat::once);
}, raw->lifetime());
}
void AddDetailsAmountHeader(
not_null<Ui::VerticalLayout*> layout,
const TransferItem &item,
int topSkip,
int bottomSkip,
rpl::producer<FiatRate> rate = nullptr) {
const auto container = layout->add(
object_ptr<Ui::RpWidget>(layout),
style::margins(0, topSkip, 0, bottomSkip),
style::al_top);
auto formatted = Ui::FormatTonAmount(item.amountNano);
const auto negativeSign = QString(QLocale::system().negativeSign());
if (item.amountNano < 0 && formatted.wholeString.startsWith(negativeSign)) {
formatted.wholeString.remove(0, negativeSign.size());
}
const auto major = Ui::CreateChild<Ui::FlatLabel>(
container,
(!item.amountNano
? QString()
: item.incoming
? u"+"_q
: QString(kMinus)) + formatted.wholeString,
st::walletDetailsAmountMajorLabel);
const auto minor = Ui::CreateChild<Ui::FlatLabel>(
container,
formatted.nanoString.isEmpty()
? QString()
: (formatted.separator + formatted.nanoString),
st::walletDetailsAmountMinorLabel);
// The unit is spelled out instead of drawn as the currency mark: the
// mark is the animation above the amount now, and saying it twice in
// one header only makes the line harder to read.
const auto ticker = Ui::CreateChild<Ui::FlatLabel>(
container,
tr::lng_action_gram_transfer_ticker(
tr::now,
lt_count,
std::abs(item.amountNano / float64(Ui::kNanosInOne))).toUpper(),
st::walletDetailsTickerLabel);
const auto subdued = (item.status == TransferItem::Status::Pending)
|| (item.status == TransferItem::Status::Failure);
const auto &color = subdued
? st::windowSubTextFg
: item.incoming
? st::boxTextFgGood
: st::windowFg;
rpl::single(rpl::empty) | rpl::then(
style::PaletteChanged()
) | rpl::on_next([=] {
const auto scope = WindowPaletteScope(container);
major->setTextColorOverride(color->c);
minor->setTextColorOverride(color->c);
}, container->lifetime());
const auto amountNano = item.amountNano;
const auto fiat = rate
? Ui::CreateChild<Ui::FlatLabel>(
container,
st::walletDetailsFiatLabel)
: nullptr;
const auto relayout = [=] {
const auto majorSize = major->size();
const auto minorSize = minor->size();
const auto tickerSize = ticker->size();
const auto tickerSkip = st::walletDetailsTickerSkip;
const auto amountWidth = majorSize.width()
+ minorSize.width()
+ tickerSkip
+ tickerSize.width();
// The smaller labels are dropped by the difference in font size so
// that all three sit on one baseline with the whole amount.
const auto minorSkip = st::walletDetailsAmountMinorSkip;
const auto amountHeight = std::max({
majorSize.height(),
minorSkip + minorSize.height(),
minorSkip + tickerSize.height(),
});
const auto withFiat = (fiat != nullptr);
const auto width = std::max(
amountWidth,
withFiat ? fiat->width() : 0);
const auto height = amountHeight + (withFiat
? st::walletDetailsFiatSkip + fiat->height()
: 0);
container->resize(width, height);
container->setNaturalWidth(width);
const auto left = (width - amountWidth) / 2;
major->moveToLeft(left, 0, width);
minor->moveToLeft(left + majorSize.width(), minorSkip, width);
ticker->moveToLeft(
left + majorSize.width() + minorSize.width() + tickerSkip,
minorSkip,
width);
if (withFiat) {
fiat->moveToLeft(
(width - fiat->width()) / 2,
amountHeight + st::walletDetailsFiatSkip,
width);
}
};
if (fiat) {
std::move(rate) | rpl::on_next([=](const FiatRate &value) {
fiat->setText(FormatFiat(amountNano, value));
relayout();
}, fiat->lifetime());
}
rpl::combine(
major->sizeValue(),
minor->sizeValue(),
ticker->sizeValue()
) | rpl::on_next(relayout, container->lifetime());
}
void AddDetailsCollectibleHeader(
not_null<Ui::VerticalLayout*> layout,
not_null<Main::Session*> session,
std::shared_ptr<CollectibleMedia> media,
const TransferItem &item,
int bottomSkip,
bool withCollection = true) {
const auto container = layout->add(
object_ptr<Ui::RpWidget>(layout),
style::margins(0, st::walletDetailsAmountTopSkip, 0, bottomSkip),
style::al_top);
const auto address = item.collectible;
const auto available = st::boxWideWidth
- st::giveawayGiftCodeTableMargin.left()
- st::giveawayGiftCodeTableMargin.right();
const auto arrowWidth = st::walletDetailsCollectionArrowSkip
+ st::walletDetailsCollectionArrow.width();
const auto artwork = Ui::CreateChild<Ui::RpWidget>(container);
artwork->resize(
st::walletDetailsCollectibleSize,
st::walletDetailsCollectibleSize);
artwork->setAttribute(Qt::WA_TransparentForMouseEvents);
artwork->paintRequest(
) | rpl::on_next([=] {
auto p = Painter(artwork);
media->paint(
p,
address,
artwork->rect(),
artwork->width(),
st::walletDetailsCollectibleRadius);
}, artwork->lifetime());
const auto name = Ui::CreateChild<Ui::FlatLabel>(
container,
st::walletCollectibleTitleLabel);
name->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto collection = Ui::CreateChild<Ui::AbstractButton>(container);
const auto collectionLabel = Ui::CreateChild<Ui::FlatLabel>(
collection,
st::walletDetailsCollectionLabel);
collectionLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
collection->hide();
collection->setClickedCallback([=] {
const auto contract = media->collection(address);
if (!contract.isEmpty()) {
UrlClickHandler::Open(Core::TonExplorerUrl(
session,
FormatFriendly(contract, true)));
}
});
collection->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(collection);
const auto &arrow = st::walletDetailsCollectionArrow;
arrow.paint(
p,
collectionLabel->width() + st::walletDetailsCollectionArrowSkip,
(collection->height() - arrow.height()) / 2,
collection->width());
}, collection->lifetime());
collectionLabel->sizeValue(
) | rpl::on_next([=](QSize size) {
collection->resize(size.width() + arrowWidth, size.height());
collectionLabel->moveToLeft(0, 0, collection->width());
}, collection->lifetime());
const auto relayout = [=] {
const auto hasCollection = withCollection
&& !media->collection(address).isEmpty();
collection->setVisible(hasCollection);
const auto nameTop = st::walletDetailsCollectibleSize
+ st::walletDetailsCollectibleNameSkip;
const auto collectionTop = nameTop
+ name->height()
+ st::walletDetailsCollectionSkip;
const auto height = hasCollection
? (collectionTop + collection->height())
: (nameTop + name->height());
container->resize(available, height);
container->setNaturalWidth(available);
artwork->moveToLeft((available - artwork->width()) / 2, 0, available);
name->moveToLeft((available - name->width()) / 2, nameTop, available);
if (hasCollection) {
collection->moveToLeft(
(available - collection->width()) / 2,
collectionTop,
available);
}
};
const auto apply = [=] {
const auto view = media->view(address);
name->setMarkedText(CollectibleTitleText(view));
name->resizeToNaturalWidth(available);
const auto contract = media->collection(address);
if (!contract.isEmpty()) {
collectionLabel->setText(view.collectionName.isEmpty()
? ShortAddress(contract)
: view.collectionName);
collectionLabel->resizeToNaturalWidth(available - arrowWidth);
}
relayout();
};
const auto mine = [=](const QString &changed) {
return (changed == address);
};
media->changed(
) | rpl::filter(mine) | rpl::on_next(apply, container->lifetime());
media->repaint(
) | rpl::filter(mine) | rpl::on_next([=] {
artwork->update();
}, container->lifetime());
rpl::combine(
name->sizeValue(),
collection->sizeValue()
) | rpl::on_next(relayout, container->lifetime());
apply();
}
class ActionRow final : public Ui::RpWidget {
public:
ActionRow(QWidget *parent, ActionRowArgs &&args);
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
private:
const RowAvatar _avatar = RowAvatar::Gear;
const not_null<Ui::FlatLabel*> _title;
Ui::FlatLabel *_subtitle = nullptr;
Ui::FlatLabel *_major = nullptr;
Ui::FlatLabel *_minor = nullptr;
int _circleTop = 0;
};
[[nodiscard]] RowAvatar ActionRowAvatar(ActionRowIcon icon) {
switch (icon) {
case ActionRowIcon::Incoming: return RowAvatar::In;
case ActionRowIcon::Outgoing: return RowAvatar::Out;
case ActionRowIcon::Gear: return RowAvatar::Gear;
}
Unexpected("Icon in ActionRowAvatar.");
}
ActionRow::ActionRow(QWidget *parent, ActionRowArgs &&args)
: RpWidget(parent)
, _avatar(ActionRowAvatar(args.icon))
, _title(Ui::CreateChild<Ui::FlatLabel>(
this,
args.address.isEmpty() ? args.kind : ShortAddressForm(args.address),
st::walletRowTitleLabel)) {
_title->setBreakEverywhere(true);
_title->setAttribute(Qt::WA_TransparentForMouseEvents);
if (!args.address.isEmpty()) {
_subtitle = Ui::CreateChild<Ui::FlatLabel>(
this,
args.kind,
st::walletRowSubtitleLabel);
_subtitle->setBreakEverywhere(true);
_subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
}
if (const auto amount = args.amountNano) {
_major = Ui::CreateChild<Ui::FlatLabel>(
this,
st::walletRowAmountMajorLabel);
_major->setAttribute(Qt::WA_TransparentForMouseEvents);
_minor = Ui::CreateChild<Ui::FlatLabel>(
this,
st::walletRowAmountMinorLabel);
_minor->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto plus = (args.sign == ActionRowSign::Plus);
const auto minus = (args.sign == ActionRowSign::Minus);
SetRowAmountText(
_major,
_minor,
*amount,
plus ? u"+"_q : minus ? QString(kMinus) : QString());
SetAmountColor(
_major,
_minor,
(plus
? st::boxTextFgGood
: minus
? st::windowBoldFg
: st::windowSubTextFg));
}
}
int ActionRow::resizeGetHeight(int newWidth) {
const auto &padding = st::walletConnectActionPadding;
const auto amountWidth = _major
? (_major->width() + _minor->width() + st::walletRowSkip)
: 0;
const auto textWidth = newWidth - padding.left() - padding.right();
_title->resizeToWidth(std::max(textWidth - amountWidth, 1));
if (_subtitle) {
_subtitle->resizeToWidth(std::max(textWidth, 1));
}
const auto textHeight = _title->height()
+ (_subtitle ? (st::walletRowSkip + _subtitle->height()) : 0);
const auto result = padding.top()
+ std::max(st::walletRowIconSize, textHeight)
+ padding.bottom();
const auto textTop = (result - textHeight) / 2;
_title->moveToLeft(padding.left(), textTop, newWidth);
if (_subtitle) {
_subtitle->moveToLeft(
padding.left(),
textTop + _title->height() + st::walletRowSkip,
newWidth);
}
if (_major) {
const auto lineHeight = st::walletRowTitleLabel.style.font->height;
const auto majorTop = textTop + (lineHeight - _major->height()) / 2;
_minor->moveToRight(
padding.right(),
majorTop + st::walletRowAmountMinorSkip,
newWidth);
_major->moveToRight(
padding.right() + _minor->width(),
majorTop,
newWidth);
}
_circleTop = (result - st::walletRowIconSize) / 2;
return result;
}
void ActionRow::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto size = st::walletRowIconSize;
PaintRowAvatar(
p,
style::rtlrect(
st::walletConnectActionIconLeft,
_circleTop,
size,
size,
width()),
_avatar);
}
} // namespace
object_ptr<Ui::RpWidget> MakeActionRow(
not_null<QWidget*> parent,
ActionRowArgs args) {
return object_ptr<ActionRow>(parent, std::move(args));
}
object_ptr<Ui::RpWidget> MakeCommentBubble(
not_null<QWidget*> parent,
object_ptr<Ui::RpWidget> content,
const style::color &bg) {
auto result = object_ptr<Ui::PaddingWrap<Ui::RpWidget>>(
parent,
std::move(content),
st::giveawayGiftCodeValueMargin);
const auto raw = result.data();
const auto background = raw->lifetime().make_state<Ui::RoundRect>(
st::boxRadius,
bg);
raw->paintRequest() | rpl::on_next([=] {
auto p = QPainter(raw);
background->paint(p, raw->rect());
}, raw->lifetime());
return result;
}
namespace {
[[nodiscard]] bool HasDetailsComment(const TransferItem &item) {
return item.commentEncrypted || !item.comment.trimmed().isEmpty();
}
[[nodiscard]] bool SameDetailsHeader(
const TransferItem &a,
const TransferItem &b) {
return (ShowsCollectible(a) == ShowsCollectible(b))
&& (a.collectible == b.collectible)
&& (a.amountNano == b.amountNano)
&& (a.incoming == b.incoming)
&& (a.status == b.status)
&& (a.comment == b.comment)
&& (a.commentEncrypted == b.commentEncrypted)
&& (a.encryptedFormat == b.encryptedFormat)
&& (a.encryptedPayload == b.encryptedPayload);
}
void AddDetailsComment(
not_null<Ui::GenericBox*> box,
not_null<Ui::VerticalLayout*> layout,
std::shared_ptr<Main::SessionShow> show,
const TransferItem &item,
Fn<bool()> originCurrent) {
if (!HasDetailsComment(item)) {
return;
}
const auto comment = item.comment.trimmed();
auto label = item.commentEncrypted
? object_ptr<Ui::FlatLabel>(object_ptr<EncryptedCommentLabel>(
layout,
box,
std::move(show),
item,
std::move(originCurrent)))
: object_ptr<Ui::FlatLabel>(layout, comment, st::walletCommentLabel);
// The bubble stands inside the gap between the amount and the table
// rather than under the amount: the header leaves half of that gap and
// the bubble takes the other half, so it reads as its own line.
layout->add(
MakeCommentBubble(layout, std::move(label), st::windowBg),
style::margins(
st::giveawayGiftCodeTableMargin.left(),
0,
st::giveawayGiftCodeTableMargin.right(),
st::walletDetailsAmountBottomSkip / 2),
style::al_top);
}
[[nodiscard]] int GaslessDailyTransfers(not_null<Main::Session*> session) {
return session->appConfig().get<int>(
u"wallet_gasless_daily_transfers"_q,
kGaslessDailyTransfersDefault);
}
[[nodiscard]] rpl::producer<int> GaslessDailyTransfersValue(
not_null<Main::Session*> session) {
return session->appConfig().value() | rpl::map([=] {
return GaslessDailyTransfers(session);
}) | rpl::distinct_until_changed();
}
// The fee itself is shown in the row that opens this box.
void ShowNetworkFeesAbout(
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session) {
show->showBox(Ui::MakeInformBox({
.text = tr::lng_wallet_fees_text(
tr::now,
lt_count,
GaslessDailyTransfers(session)),
.title = tr::lng_wallet_fees_title(),
}));
}
[[nodiscard]] TextWithEntities GramMark(
Ui::Text::CustomEmojiHelper &helper,
const style::font &font) {
auto descriptor = Ui::Text::PaletteDependentEmoji{
.factory = [=] {
return Ui::Earn::IconCurrencyTwoTone(
font,
st::windowActiveTextFg->c);
},
};
const auto image = descriptor.factory();
const auto alignedTop = Ui::Earn::AlignedMarkTop(font, image);
const auto emojiY = (font->height - st::emojiSize) / 2;
const auto lineShift = Ui::Fixed(font->ascent) - font->fascent;
const auto naturalTop = (lineShift + emojiY).toInt()
+ Ui::Emoji::GetCustomSkipNormal();
const auto marginTop = int(base::SafeRound(alignedTop - naturalTop));
descriptor.margin = QMargins(0, marginTop, 0, 0);
return helper.paletteDependent(std::move(descriptor));
}
// A transaction a message named is served after the box is already open, so
// the fee row exists from the first frame and says what it is waiting for.
enum class DetailsFee {
Known,
Loading,
Failed,
};
void AddPendingFeeTableRow(
not_null<Ui::TableLayout*> table,
DetailsFee state) {
Expects(state != DetailsFee::Known);
if (state == DetailsFee::Failed) {
Ui::AddTableRow(
table,
tr::lng_wallet_details_fee(),
tr::lng_wallet_details_fee_unknown(tr::marked));
return;
}
const auto &font = table->st().defaultValue.style.font;
auto helper = Ui::Text::CustomEmojiHelper();
const auto diamond = GramMark(helper, font);
Ui::AddTableRow(
table,
tr::lng_wallet_details_fee(),
tr::lng_contacts_loading(
tr::italic
) | rpl::map([=](TextWithEntities text) {
auto result = diamond;
result.append(QChar(' '));
result.append(std::move(text));
return result;
}),
helper.context());
}
void AddFeeTableRow(
not_null<Ui::TableLayout*> table,
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session,
const TransferItem &item) {
const auto &font = table->st().defaultValue.style.font;
auto helper = Ui::Text::CustomEmojiHelper();
const auto diamond = GramMark(helper, font);
const auto feeNano = item.feeNano.value_or(0);
auto value = rpl::producer<TextWithEntities>();
if (item.gasless) {
value = tr::lng_wallet_details_fee_free(
) | rpl::map([=](const QString &text) {
auto result = diamond;
result.append(QChar(' '));
result.append(text);
return result;
});
} else {
value = FiatRateValue(session) | rpl::map([=](const FiatRate &rate) {
auto result = diamond;
result.append(QChar(' '));
result.append(Ui::FormatTonAmount(feeNano).full);
result.append(QChar(' '));
result.append(Ui::Text::Colorized(
FormatFiat(feeNano, rate, kFeeFiatDecimals, true)));
return result;
});
}
const auto label = Ui::CreateChild<Ui::FlatLabel>(
table,
std::move(value),
st::walletDetailsFeeLabel,
st::defaultPopupMenu,
helper.context());
Ui::AddTableRow(
table,
tr::lng_wallet_details_fee(),
Ui::MakeMultilineValueWithSmallButton(
table,
label,
rpl::single(u"?"_q),
[=](not_null<Ui::RpWidget*>) {
ShowNetworkFeesAbout(show, session);
}).widget);
}
// Userpic and name leading to the chat, and a Send pill for any user.
[[nodiscard]] object_ptr<Ui::RpWidget> PeerCounterpartyValue(
not_null<Ui::GenericBox*> box,
not_null<Ui::TableLayout*> table,
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> peer) {
const auto chatShow = MakeChatShow(show, true);
const auto user = peer->asUser();
const auto offer = (user != nullptr);
const auto weak = base::make_weak(box);
return Ui::MakePeerTableValue(
table,
chatShow,
peer->id,
offer ? tr::lng_send_button() : nullptr,
offer ? Fn<void()>([=] { ShowSendToUser(show, user); }) : nullptr,
[=] {
const auto window = chatShow->resolveWindow();
if (!window) {
return;
} else if (weak && weak->hasDelegate()) {
weak->closeBox();
}
window->showPeerHistory(peer);
window->window().activate();
});
}
void AddPeerCounterpartyRows(
not_null<Ui::GenericBox*> box,
not_null<Ui::TableLayout*> table,
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> peer,
const TransferItem &item) {
const auto address = !item.counterparty.isEmpty()
? DetailsFriendlyAddress(item)
: std::nullopt;
const auto domain = item.counterpartyName.trimmed();
auto label = (item.incoming
? tr::lng_wallet_details_sender()
: tr::lng_wallet_details_recipient());
auto value = PeerCounterpartyValue(box, table, show, peer);
if (address && !domain.isEmpty()) {
auto wrap = object_ptr<Ui::VerticalLayout>(table);
wrap->add(std::move(value));
wrap->add(NameValueLabel(wrap.data(), show, domain, *address));
value = std::move(wrap);
}
Ui::AddTableRow(
table,
std::move(label),
std::move(value),
st::giveawayGiftCodePeerMargin);
if (address) {
Ui::AddTableRow(
table,
tr::lng_wallet_details_address(),
AddressValueLabel(table, show, *address));
}
}
} // namespace
not_null<Ui::TableLayout*> AddDetailsTableFrame(
not_null<Ui::VerticalLayout*> container) {
const auto wrap = container->add(
object_ptr<Ui::PaddingWrap<Ui::TableLayout>>(
container,
object_ptr<Ui::TableLayout>(container, st::walletDetailsTable),
style::margins()),
st::giveawayGiftCodeTableMargin);
const auto bg = wrap->lifetime().make_state<Ui::RoundRect>(
st::walletDetailsTable.radius,
st::windowBg);
wrap->paintRequest() | rpl::on_next([=] {
auto p = QPainter(wrap);
bg->paint(p, wrap->rect());
}, wrap->lifetime());
return wrap->entity();
}
namespace {
void AddDetailsTable(
not_null<Ui::GenericBox*> box,
not_null<Ui::VerticalLayout*> container,
std::shared_ptr<Main::SessionShow> show,
const TransferItem &item,
DetailsFee fee) {
const auto session = &show->session();
const auto table = AddDetailsTableFrame(container);
if (item.status == TransferItem::Status::Failure) {
const auto reason = item.failureReason.trimmed();
Ui::AddTableRow(
table,
tr::lng_wallet_details_status(),
reason.isEmpty()
? tr::lng_channel_earn_history_failed(tr::marked)
: tr::lng_wallet_details_failed_reason(
lt_reason,
rpl::single(TextWithEntities{ reason }),
tr::marked));
}
const auto peerKind = (item.kind == TransferItem::Kind::PeerTransfer)
|| (item.kind == TransferItem::Kind::Collectible);
const auto peer = (peerKind && item.counterpartyPeer)
? session->data().peerLoaded(PeerId(item.counterpartyPeer))
: nullptr;
if (item.kind == TransferItem::Kind::KeyChange) {
Ui::AddTableRow(
table,
tr::lng_wallet_details_operation(),
tr::lng_wallet_row_key_change(tr::marked));
} else if (peer) {
AddPeerCounterpartyRows(box, table, show, peer, item);
} else if (!item.counterparty.isEmpty()) {
if (const auto address = DetailsFriendlyAddress(item)) {
auto label = (item.incoming
? tr::lng_wallet_details_sender()
: tr::lng_wallet_details_recipient());
const auto provider = OnrampProvider(item);
const auto name = !provider.isEmpty()
? provider
: item.counterpartyName.trimmed();
if (name.isEmpty()) {
Ui::AddTableRow(
table,
std::move(label),
AddressValueLabel(table, box->uiShow(), *address));
} else {
Ui::AddTableRow(
table,
std::move(label),
NameValueLabel(
table,
box->uiShow(),
name,
*address));
Ui::AddTableRow(
table,
tr::lng_wallet_details_address(),
AddressValueLabel(table, box->uiShow(), *address));
}
}
}
const auto pending
= (item.status == TransferItem::Status::Pending);
// An incoming transfer was paid for by whoever sent it, and what the
// wallet spends to receive one is a few nanograms, so the row is left
// out entirely and nothing is waited for on its behalf.
if (!item.incoming) {
if (fee != DetailsFee::Known) {
AddPendingFeeTableRow(table, fee);
} else if (!pending
&& (item.gasless || (item.feeNano && *item.feeNano > 0))) {
AddFeeTableRow(table, box->uiShow(), session, item);
}
}
if (item.date) {
Ui::AddTableRow(
table,
tr::lng_wallet_details_date(),
rpl::single(tr::marked(
langDateTime(base::unixtime::parse(*item.date)))));
}
}
void AddBoxCloseButton(
not_null<Ui::GenericBox*> box,
Fn<void()> close = nullptr) {
box->addTopButton(st::boxTitleClose, [=] {
if (close) {
close();
} else {
box->closeBox();
}
});
}
// The wallet's box footers share one "working" appearance: the label goes
// blank and an infinite spinner appears centred on the button. It stays two
// halves because they attach at two different moments - the label is a
// producer handed to addButton, the spinner is a child added once the button
// exists - so every box keeps its own label choice, its own guards and its
// own position for the spinner. The size and color follow the button's own
// style.
[[nodiscard]] rpl::producer<QString> BusyFooterLabel(
rpl::producer<QString> text,
rpl::producer<bool> busy) {
return rpl::combine(
std::move(text),
std::move(busy)
) | rpl::map([](const QString &text, bool busy) {
return busy ? QString() : text;
});
}
void AddBusyFooterSpinner(
not_null<Ui::RoundButton*> button,
rpl::producer<bool> shown) {
using Radial = style::InfiniteRadialAnimation;
const auto &buttonSt = button->st();
const auto radialSt = button->lifetime().make_state<Radial>(
st::startGiveawayButtonLoading);
radialSt->color = buttonSt.textFg;
const auto loading = Info::Statistics::InfiniteRadialAnimationWidget(
button,
buttonSt.height / 2,
radialSt);
Info::Statistics::AddChildToWidgetCenter(button, loading);
loading->showOn(std::move(shown));
}
// The row counterpart of the footer spinner: the same animation in the
// row's subtext colour at its right edge, so a settings row keeps its label
// while the action it started is still in flight.
void AddRowSpinner(
not_null<Ui::SettingsButton*> button,
rpl::producer<bool> shown) {
const auto &st = button->st();
const auto size = st.style.font->height;
const auto loading = Info::Statistics::InfiniteRadialAnimationWidget(
button,
size,
&st::walletKeysRowLoading);
loading->setAttribute(Qt::WA_TransparentForMouseEvents);
button->sizeValue() | rpl::on_next([=](QSize outer) {
loading->moveToRight(
st.padding.right(),
(outer.height() - size) / 2,
outer.width());
}, loading->lifetime());
loading->showOn(std::move(shown));
}
// An inform box whose one button is Cancel: the label says what is being
// waited for, the spinner under it that the wait is on, and closing it by
// any means reports the same dismissal.
void WalletBusyBox(
not_null<Ui::GenericBox*> box,
rpl::producer<QString> text) {
Ui::InformBox(box, {
.text = std::move(text),
.confirmText = tr::lng_cancel(),
});
const auto &loading = st::walletBusyBoxLoading;
const auto side = loading.size.height() + 2 * loading.thickness;
const auto content = box->addRow(
object_ptr<Ui::FixedHeightWidget>(box, side),
st::walletBusyBoxPadding);
const auto indicator = Info::Statistics::InfiniteRadialAnimationWidget(
content,
side,
&loading);
Info::Statistics::AddChildToWidgetCenter(content, indicator);
indicator->show();
}
[[nodiscard]] QImage ReceiveQrCenter(int side, int markSide) {
auto result = QImage(side, side, QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::white);
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
auto svg = QSvgRenderer(
Ui::Earn::CurrencySvgTwoTone(st::activeButtonBg->c));
const auto skip = (side - markSide) / 2;
svg.render(&p, QRectF(skip, skip, markSide, markSide));
return result;
}
[[nodiscard]] QImage ReceiveQrImage(
const QString &address,
int size,
int ratio,
int quietZoneModules = 0) {
const auto data = Qr::Encode(address, Qr::Redundancy::Quartile);
const auto pixel = std::max(size / std::max(data.size, 1), 1);
auto image = Qr::Generate(data, pixel * ratio, Qt::black, Qt::white);
const auto replaceSide = Qr::ReplaceSize(data, pixel * ratio);
const auto markSide = std::min(
st::walletReceiveMarkSize * ratio,
replaceSide - 2 * pixel * ratio);
image = Qr::ReplaceCenter(
std::move(image),
ReceiveQrCenter(replaceSide, markSide));
if (quietZoneModules > 0) {
const auto skip = quietZoneModules * pixel * ratio;
auto padded = QImage(
image.width() + 2 * skip,
image.height() + 2 * skip,
QImage::Format_ARGB32_Premultiplied);
padded.fill(Qt::white);
auto p = QPainter(&padded);
p.drawImage(skip, skip, image);
p.end();
image = std::move(padded);
}
image.setDevicePixelRatio(ratio);
return image;
}
struct WalletBoxTitleBar {
not_null<Ui::FlatLabel*> title;
not_null<Ui::IconButton*> close;
};
[[nodiscard]] WalletBoxTitleBar AddWalletBoxTitleBar(
not_null<Ui::GenericBox*> box,
rpl::producer<QString> title) {
const auto row = box->addRow(
object_ptr<Ui::FixedHeightWidget>(
box,
st::walletReceiveTitleHeight),
style::margins(),
style::al_justify);
const auto label = Ui::CreateChild<Ui::FlatLabel>(
row,
std::move(title),
st::boxTitle);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto close = Ui::CreateChild<Ui::IconButton>(
row,
st::boxTitleClose);
Ui::ToggleChildrenVisibility(row, true);
row->sizeValue(
) | rpl::on_next([=](QSize size) {
label->moveToLeft(
st::boxTitlePosition.x(),
st::boxTitlePosition.y(),
size.width());
close->moveToRight(st::walletReceiveTitleButtonSkip, 0, size.width());
}, row->lifetime());
return { label, close };
}
struct OldWalletAppLink {
QString appname;
QString startapp;
std::optional<QString> startattach;
bool compact = false;
bool fullscreen = false;
};
[[nodiscard]] bool IsValidOnrampUrl(const QString &url) {
const auto parsed = QUrl(url, QUrl::StrictMode);
return parsed.isValid()
&& parsed.scheme() == u"https"_q
&& !parsed.host().isEmpty();
}
[[nodiscard]] std::optional<OldWalletAppLink> ParseOldWalletAppLink(
not_null<Main::Session*> session,
const QString &url) {
const auto configured = session->appConfig().oldWalletBotUsername();
if (configured.isEmpty()) {
return {};
}
const auto prefix = u"tg://resolve?"_q;
const auto local = Core::TryConvertUrlToLocal(url);
if (!local.startsWith(prefix, Qt::CaseInsensitive)) {
return {};
}
const auto params = qthelp::url_parse_params(
local.mid(prefix.size()),
qthelp::UrlParamNameTransform::ToLower);
if (params.value(u"domain"_q).compare(configured, Qt::CaseInsensitive)) {
return {};
}
const auto mode = params.value(u"mode"_q);
return OldWalletAppLink{
.appname = params.value(u"appname"_q),
.startapp = params.value(u"startapp"_q),
.startattach = (params.contains(u"startattach"_q)
? params.value(u"startattach"_q)
: std::optional<QString>()),
.compact = (mode == u"compact"_q),
.fullscreen = (mode == u"fullscreen"_q),
};
}
void OpenOldWalletApp(
not_null<UserData*> bot,
std::shared_ptr<Ui::Show> show,
const OldWalletAppLink &link) {
const auto startCommand = link.startattach.value_or(link.startapp);
auto source = link.startattach
? InlineBots::WebViewSource(InlineBots::WebViewSourceLinkAttachMenu{
.token = startCommand,
})
: !link.appname.isEmpty()
? InlineBots::WebViewSource(InlineBots::WebViewSourceLinkApp{
.appname = link.appname,
.token = startCommand,
})
: InlineBots::WebViewSource(InlineBots::WebViewSourceLinkBotProfile{
.token = startCommand,
.compact = link.compact,
});
bot->session().attachWebView().open({
.bot = bot,
.parentShow = std::move(show),
.context = {
.action = ::Api::SendAction(bot->owner().history(bot)),
.fullscreen = link.fullscreen,
.maySkipConfirmation = true,
},
.button = { .startCommand = startCommand },
.source = std::move(source),
});
}
void OpenOldWalletAppLink(
not_null<Main::Session*> session,
std::shared_ptr<Ui::Show> show,
const OldWalletAppLink &link,
const QString &url) {
const auto username = session->appConfig().oldWalletBotUsername();
const auto byUsername = session->data().peerByUsername(username);
if (const auto bot = byUsername ? byUsername->asUser() : nullptr) {
if (bot->isOldWalletBot()) {
OpenOldWalletApp(bot, std::move(show), link);
return;
}
}
session->api().request(MTPcontacts_ResolveUsername(
MTP_flags(0),
MTP_string(username),
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 bot = peerId
? session->data().peer(peerId)->asUser()
: nullptr;
if (bot && bot->isOldWalletBot()) {
OpenOldWalletApp(bot, show, link);
} else {
UrlClickHandler::Open(url);
}
}).fail([=] {
UrlClickHandler::Open(url);
}).send();
}
void OpenWalletUrl(
not_null<Main::Session*> session,
std::shared_ptr<Ui::Show> show,
const QString &url) {
if (const auto link = ParseOldWalletAppLink(session, url)) {
OpenOldWalletAppLink(session, std::move(show), *link, url);
} else {
UrlClickHandler::Open(url);
}
}
not_null<Ui::IconButton*> AddRowChevron(not_null<Ui::RpWidget*> button) {
const auto arrow = Ui::CreateChild<Ui::IconButton>(
button,
st::backButton);
arrow->setIconOverride(
&st::settingsPremiumArrow,
&st::settingsPremiumArrowOver);
arrow->setAttribute(Qt::WA_TransparentForMouseEvents);
arrow->show();
button->sizeValue(
) | rpl::on_next([=](QSize size) {
const auto &shift = st::settingsPremiumArrowShift;
arrow->moveToRight(
-shift.x(),
shift.y() + (size.height() - arrow->height()) / 2);
}, arrow->lifetime());
return arrow;
}
void WalletReceiveBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
const QString &address) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::walletReceiveBox);
box->setNoContentMargin(true);
box->setCustomCornersFilling(RectPart::FullTop | RectPart::FullBottom);
struct State {
rpl::variable<bool> resolving = false;
QImage image;
};
const auto state = box->lifetime().make_state<State>();
const auto show = box->uiShow();
box->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(box);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::activeButtonBg);
p.drawRoundedRect(box->rect(), st::boxRadius, st::boxRadius);
}, box->lifetime());
const auto bar = AddWalletBoxTitleBar(box, tr::lng_wallet_add_funds());
rpl::single(
rpl::empty
) | rpl::then(
style::PaletteChanged()
) | rpl::on_next([=] {
const auto scope = WindowPaletteScope(box);
bar.title->setTextColorOverride(st::activeButtonFg->c);
}, box->lifetime());
bar.close->setIconOverride(
&st::walletReceiveCloseIconActive,
&st::walletReceiveCloseIconActiveOver);
bar.close->setRippleColorOverride(&st::activeButtonBgRipple);
bar.close->setClickedCallback([=] {
box->closeBox();
});
const auto inner = box->verticalLayout();
{
const auto scope = WindowPaletteScope(box);
state->image = ReceiveQrImage(
address,
st::walletReceiveQrSize,
style::DevicePixelRatio());
}
const auto qrSide = state->image.width() / style::DevicePixelRatio();
const auto &padding = st::walletReceivePlatePadding;
const auto font = st::walletReceiveAddressFont->monospace();
const auto hintFont = st::walletReceiveHintFont;
const auto groupWidth = font->width(address.left(kAddressGroup));
const auto spaceWidth = font->width(QChar(' '));
const auto lineWidth = kReceiveGroupsPerLine * groupWidth
+ (kReceiveGroupsPerLine - 1) * spaceWidth;
const auto lineHeight = font->height + st::walletReceiveAddressLineSkip;
const auto blockHeight = kReceiveLines * font->height
+ (kReceiveLines - 1) * st::walletReceiveAddressLineSkip;
const auto qrLeft = padding.left();
const auto qrTop = padding.top();
const auto addressTop = qrTop + qrSide + st::walletReceiveAddressTopSkip;
const auto hintTop = addressTop
+ blockHeight
+ st::walletReceiveHintTopSkip;
const auto plateWidth = std::max(qrSide, lineWidth)
+ padding.left()
+ padding.right();
const auto plateHeight = hintTop + hintFont->height + padding.bottom();
auto plateOwned = object_ptr<Ui::FixedHeightWidget>(inner, plateHeight);
plateOwned->setNaturalWidth(plateWidth);
const auto plate = inner->add(
std::move(plateOwned),
st::walletReceivePlateMargin,
style::al_top);
plate->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(plate);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(Qt::white);
p.drawRoundedRect(
plate->rect(),
st::walletReceivePlateRadius,
st::walletReceivePlateRadius);
p.drawImage(qrLeft, qrTop, state->image);
p.setFont(font);
const auto left = (plate->width() - lineWidth) / 2;
for (auto i = 0; i != kAddressLength / kAddressGroup; ++i) {
const auto line = i / kReceiveGroupsPerLine;
const auto column = i % kReceiveGroupsPerLine;
p.setPen((i % 2)
? QColor(0x99, 0x99, 0x99)
: QColor(0x22, 0x22, 0x22));
p.drawText(
left + column * (groupWidth + spaceWidth),
addressTop + line * lineHeight + font->ascent,
address.mid(i * kAddressGroup, kAddressGroup));
}
const auto hint = hintFont->elided(
tr::lng_wallet_receive_copy_hint(tr::now),
plate->width() - padding.left() - padding.right());
p.setFont(hintFont);
p.setPen(QColor(0x99, 0x99, 0x99));
p.drawText(
(plate->width() - hintFont->width(hint)) / 2,
hintTop + hintFont->ascent,
hint);
}, plate->lifetime());
style::PaletteChanged(
) | rpl::on_next([=] {
const auto scope = WindowPaletteScope(plate);
state->image = ReceiveQrImage(
address,
st::walletReceiveQrSize,
style::DevicePixelRatio());
plate->update();
}, plate->lifetime());
const auto qrTarget = Ui::CreateChild<Ui::AbstractButton>(plate);
qrTarget->setClickedCallback([=] {
const auto scope = WindowPaletteScope(plate);
QGuiApplication::clipboard()->setImage(ReceiveQrImage(
address,
st::walletReceiveQrCopySize,
1,
kQrQuietZoneModules));
show->showToast({
.text = { tr::lng_group_invite_qr_copied(tr::now) },
.iconLottie = u"toast/copy"_q,
.iconLottieSize = st::toastLottieIconSize,
});
});
const auto textTarget = Ui::CreateChild<Ui::AbstractButton>(plate);
textTarget->setClickedCallback([=] {
TextUtilities::SetClipboardText(TextForMimeData::Simple(address));
show->showToast({
.text = { tr::lng_gift_unique_address_copied(tr::now) },
.iconLottie = u"toast/copy"_q,
.iconLottieSize = st::toastLottieIconSize,
});
});
Ui::ToggleChildrenVisibility(plate, true);
const auto textTop = addressTop - st::walletReceiveAddressTopSkip / 2;
plate->sizeValue(
) | rpl::on_next([=](QSize size) {
qrTarget->setGeometry(qrLeft, qrTop, qrSide, qrSide);
textTarget->setGeometry(
0,
textTop,
size.width(),
size.height() - textTop);
}, plate->lifetime());
inner->add(
object_ptr<Ui::FlatLabel>(
inner,
tr::lng_wallet_receive_about(),
st::walletReceiveAboutLabel),
st::walletReceiveAboutMargin,
style::al_top
)->setTryMakeSimilarLines(true);
// The margin belongs to the slide wrap's own padding, not to the row:
// VerticalLayout::moveChildGetSkip() adds a row's top and bottom margin
// unconditionally, so a row margin would keep a gap above the Buy button
// while the caveat is collapsed.
const auto caveat = inner->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
inner,
object_ptr<Ui::FlatLabel>(
inner,
tr::lng_wallet_receive_caveat(),
st::walletReceiveAboutLabel),
st::walletReceiveAboutMargin),
style::margins(),
style::al_top);
caveat->entity()->setTryMakeSimilarLines(true);
caveat->toggleOn(session->wallet().deviceCustodyStateValue(
) | rpl::map([](const DeviceCustodyState &custody) {
return (custody.mode == DeviceMode::ReadOnlyNotRestorable);
}));
caveat->finishAnimating();
const auto buy = inner->add(
object_ptr<Ui::RoundButton>(
inner,
BusyFooterLabel(
tr::lng_wallet_buy_button(),
state->resolving.value()),
st::walletReceiveBuyButton),
st::walletReceiveBuyMargin,
style::al_justify);
buy->setTextTransform(Ui::RoundButtonTextTransform::NoTransform);
AddBusyFooterSpinner(buy, state->resolving.value());
buy->setClickedCallback([=] {
if (state->resolving.current()) {
return;
}
state->resolving = true;
session->wallet().onramp().requestSessionUrl(
address,
session->wallet().rates().current().currency,
crl::guard(box, [=](QString url) {
state->resolving = false;
if (!IsValidOnrampUrl(url)) {
show->showToast(tr::lng_wallet_buy_empty(tr::now));
return;
}
OpenWalletUrl(session, show, url);
box->closeBox();
}));
});
buy->widthValue(
) | rpl::on_next([=](int width) {
buy->setFullWidth(width);
}, buy->lifetime());
}
void ShowWalletReceiveBox(
not_null<Main::Session*> session,
std::shared_ptr<Ui::Show> show) {
auto &wallet = session->wallet();
const auto address = wallet.addressFriendly(false);
if (address.size() != kAddressLength) {
return;
}
show->showBox(Box(WalletReceiveBox, session, address));
}
[[nodiscard]] Ui::LayerStackWidget *BoxLayerStack(
not_null<Ui::GenericBox*> box) {
auto stack = (Ui::LayerStackWidget*)nullptr;
for (auto parent = box->parentWidget(); parent && !stack;) {
parent = parent->parentWidget();
stack = dynamic_cast<Ui::LayerStackWidget*>(parent);
}
return stack;
}
void CloseByOutsideClick(not_null<Ui::GenericBox*> box) {
const auto layer = box->parentWidget();
const auto stack = BoxLayerStack(box);
if (!layer || !stack) {
return;
}
// WHY: a background press clears the whole stack, the Transaction box
// below included, so it is eaten while this box is the shown layer, and
// the close is postponed because it destroys this filter synchronously.
base::install_event_filter(box, stack, [=](not_null<QEvent*> e) {
if (e->type() != QEvent::MouseButtonPress || layer->isHidden()) {
return base::EventFilterResult::Continue;
}
Ui::PostponeCall(box, [=] { box->closeBox(); });
return base::EventFilterResult::Cancel;
});
}
void AddWalletFeaturesBody(
not_null<Ui::GenericBox*> box,
rpl::producer<QString> title,
rpl::producer<QString> subtitle,
const std::vector<Ui::FeatureListEntry> &features,
rpl::producer<QString> button) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(title),
st::walletPhraseTitleLabel),
st::boxRowPadding,
style::al_top);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(subtitle),
st::walletHowSubtitleLabel),
st::walletHowSubtitleMargin,
style::al_top
)->setTryMakeSimilarLines(true);
for (const auto &feature : features) {
box->addRow(Ui::MakeFeatureListEntry(box, feature));
}
AddBoxCloseButton(box);
box->addButton(std::move(button), [=] { box->closeBox(); });
box->showFinishes() | rpl::take(1) | rpl::on_next([=] {
CloseByOutsideClick(box);
}, box->lifetime());
}
void WalletHowItWorksBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
AddWalletLottie(box, st::walletHowLottieMargin);
const auto features = std::vector<Ui::FeatureListEntry>{
{
.icon = st::walletAboutInstantIcon,
.title = tr::lng_wallet_about_instant_title(tr::now),
.about = tr::lng_wallet_about_instant_text(tr::now, tr::marked),
.similarLines = true,
},
{
.icon = st::walletAboutFeesIcon,
.title = tr::lng_wallet_about_fees_title(tr::now),
.about = tr::lng_wallet_about_fees_text(
tr::now,
lt_count,
GaslessDailyTransfers(session),
tr::marked),
.similarLines = true,
},
{
.icon = st::walletAboutChainIcon,
.title = tr::lng_wallet_about_chain_title(tr::now),
.about = tr::lng_wallet_about_chain_text(tr::now, tr::marked),
.similarLines = true,
},
};
AddWalletFeaturesBody(
box,
tr::lng_wallet_how_title(),
tr::lng_wallet_how_subtitle(),
features,
tr::lng_archive_hint_button());
}
void WalletFirstGramsBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
AddWalletLottie(box, st::walletHowLottieMargin);
auto subtitle = FiatRateValue(session) | rpl::map([](const FiatRate &rate) {
return rate.available()
? tr::lng_wallet_first_rate(
lt_amount,
rpl::single(FormatFiat(Ui::kNanosInOne, rate)))
: tr::lng_wallet_first_rate_none();
}) | rpl::flatten_latest();
const auto features = std::vector<Ui::FeatureListEntry>{
{
.icon = st::walletFirstSendIcon,
.title = tr::lng_wallet_first_send_title(tr::now),
.about = tr::lng_wallet_first_send_text(
tr::now,
lt_attach,
Ui::Text::IconEmoji(&st::walletFirstAttachEmoji),
lt_money,
tr::marked(tr::lng_wallet_menu(tr::now)),
tr::marked),
.similarLines = true,
},
{
.icon = st::walletFirstTradeIcon,
.title = tr::lng_wallet_first_trade_title(tr::now),
.about = tr::lng_wallet_first_trade_text(tr::now, tr::marked),
.similarLines = true,
},
{
.icon = st::walletFirstStoreIcon,
.title = tr::lng_wallet_first_store_title(tr::now),
.about = tr::lng_wallet_first_store_text(
tr::now,
lt_menu,
Ui::Text::IconEmoji(&st::walletFirstMenuEmoji),
lt_wallet,
tr::marked(tr::lng_wallet_menu(tr::now)),
tr::marked),
.similarLines = true,
},
};
AddWalletFeaturesBody(
box,
tr::lng_wallet_first_title(),
std::move(subtitle),
features,
tr::lng_archive_hint_button());
box->boxClosing() | rpl::on_next([weak = base::make_weak(session)] {
if (const auto strong = weak.get()) {
strong->promoSuggestions().dismiss(
Data::PromoSuggestions::SugWalletFirstIncomingTransfer());
}
}, box->lifetime());
}
void WalletCloudPasswordCreateBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
const auto content = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
st::boxRowPadding);
const auto fields = Settings::CloudPassword::SetupPasswordFields(
content,
Settings::CloudPassword::CreatePasswordDescriptor());
AddBoxCloseButton(box);
struct State {
rpl::lifetime request;
rpl::variable<bool> loading = false;
};
const auto state = box->lifetime().make_state<State>();
const auto submit = [=] {
if (state->request) {
return;
}
const auto password = Settings::CloudPassword::ValidatePasswordFields(
fields);
if (!password) {
return;
}
state->loading = true;
state->request = show->session().api().cloudPassword().set(
QString(),
*password,
QString(),
false,
QString()
) | rpl::on_error_done([=](const QString &type) {
state->request.destroy();
state->loading = false;
fields.error->show();
fields.error->setText(MTP::IsFloodError(type)
? tr::lng_flood_error(tr::now)
: Lang::Hard::ServerError());
}, [=] {
state->request.destroy();
box->closeBox();
show->showToast({
.text = { tr::lng_cloud_password_was_set(tr::now) },
.icon = &st::toastCheckIcon,
});
});
};
const auto button = box->addButton(
BusyFooterLabel(
tr::lng_settings_cloud_password_password_subtitle(),
state->loading.value()),
submit);
AddBusyFooterSpinner(button, state->loading.value());
Settings::CloudPassword::SubmitPasswordFields(fields, submit);
box->setFocusCallback([=] {
Settings::CloudPassword::FocusPasswordFields(fields);
});
}
void WalletCloudPasswordIntroBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
const auto content = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
st::boxRowPadding);
Settings::CloudPassword::SetupIntroHeader(content, box->showFinishes());
Ui::AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);
AddBoxCloseButton(box);
box->addButton(tr::lng_settings_cloud_password_password_subtitle(), [=] {
box->closeBox();
show->showBox(Box(WalletCloudPasswordCreateBox, show));
});
}
void SetupIntroTooltip(
not_null<Ui::RpWidget*> parent,
not_null<Ui::RpWidget*> card,
Fn<QRect()> markRect,
rpl::producer<> moves) {
struct State {
Ui::GlareTooltip *tooltip = nullptr;
base::Timer hide;
bool started = false;
bool finished = false;
};
const auto state = parent->lifetime().make_state<State>();
const auto colors = [=] {
const auto scope = WindowPaletteScope(parent);
return Ui::GlareTooltipColors{
.edge = st::windowActiveTextFg->c,
.center = anim::color(
st::windowActiveTextFg,
st::activeButtonFg,
0.35),
.rim = st::activeButtonFg->c,
.text = st::activeButtonFg->c,
};
};
state->tooltip = Ui::CreateChild<Ui::GlareTooltip>(
parent.get(),
st::walletIntroTooltip,
st::walletIntroTooltipFont,
tr::lng_wallet_intro_text(tr::now),
colors());
state->tooltip->setAttribute(Qt::WA_TransparentForMouseEvents);
state->tooltip->finishAnimating();
style::PaletteChanged() | rpl::on_next([=] {
if (!state->finished) {
state->tooltip->setColors(colors());
}
}, parent->lifetime());
const auto finish = [=] {
if (state->finished) {
return;
}
state->finished = true;
state->hide.cancel();
state->tooltip->stopGlare();
state->tooltip->fade(false);
};
state->hide.setCallback(finish);
rpl::merge(
card->geometryValue() | rpl::to_empty,
parent->widthValue() | rpl::to_empty,
std::move(moves)
) | rpl::on_next([=] {
if (state->finished || !parent->width()) {
return;
}
const auto mark = markRect();
if (mark.isEmpty()) {
if (state->started) {
finish();
}
return;
}
state->tooltip->pointAt(
mark,
Ui::MapFrom(parent, card, card->rect()));
if (!state->started) {
state->started = true;
state->tooltip->fade(true);
state->hide.callOnce(
state->tooltip->glaresDuration(kWalletIntroGlares));
}
}, parent->lifetime());
}
[[nodiscard]] QString ExplorerTransactionUrl(
not_null<Main::Session*> session,
const QByteArray &traceId) {
if (traceId.isEmpty()) {
return QString();
}
return Core::TonExplorerUrl(
session,
u"transaction/"_q + QString::fromLatin1(traceId.toHex()));
}
void WalletTransactionBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
TransferItem item,
bool partial,
std::shared_ptr<CollectibleMedia> media,
Fn<bool()> originCurrent,
rpl::producer<> originInvalidated,
Fn<void()> openWallet,
rpl::producer<TransferItem> updates) {
if (originCurrent && !originCurrent()) {
box->closeBox();
return;
}
const auto session = &show->session();
box->setWidth(st::boxWideWidth);
box->setStyle(st::walletDetailsBox);
box->setNoContentMargin(true);
box->setTitle(tr::lng_wallet_details_title());
struct State {
TransferItem item;
std::shared_ptr<CollectibleMedia> media;
base::Timer retry;
int attempts = 0;
bool looking = false;
};
const auto looking = partial && !item.id.isEmpty();
const auto state = box->lifetime().make_state<State>();
state->item = std::move(item);
state->media = std::move(media);
state->looking = looking;
const auto art = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
style::margins(),
style::al_justify);
const auto header = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
style::margins(),
style::al_justify);
const auto fillArt = [=] {
art->clear();
if (!ShowsCollectible(state->item)) {
// The animation stands where a top skip used to, so the amount and
// everything under it move up by that much under the box title.
AddWalletLottie(box, style::margins(), art);
}
};
const auto fillHeader = [=] {
header->clear();
// The gap between the header and the table is one skip, whether or not
// a comment stands in it - see AddDetailsComment for the other half.
const auto headerBottomSkip = HasDetailsComment(state->item)
? (st::walletDetailsAmountBottomSkip / 2)
: st::walletDetailsAmountBottomSkip;
if (ShowsCollectible(state->item)) {
if (!state->media) {
state->media = std::make_shared<CollectibleMedia>(session);
}
state->media->resolve(state->item.collectible);
AddDetailsCollectibleHeader(
header,
session,
state->media,
state->item,
headerBottomSkip);
} else {
AddDetailsAmountHeader(
header,
state->item,
0,
headerBottomSkip,
FiatRateValue(session));
}
AddDetailsComment(
box,
header,
Main::MakeSessionShow(box->uiShow(), session),
state->item,
originCurrent);
};
fillArt();
fillHeader();
// The amount and the comment are what the message itself said, while the
// rows below are the transaction's own record: who it went to under their
// Telegram name, what it cost and when the chain accepted it. The table
// is built from the message at once and again from the served record, so
// the box shows everything it can immediately and nothing of it waits.
const auto details = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
style::margins());
const auto rebuild = [=](DetailsFee fee) {
details->clear();
AddDetailsTable(box, details, show, state->item, fee);
};
rebuild(state->looking ? DetailsFee::Loading : DetailsFee::Known);
if (updates) {
std::move(updates) | rpl::on_next([=](TransferItem updated) {
if (state->looking || updated == state->item) {
return;
}
const auto was = std::exchange(state->item, std::move(updated));
if (ShowsCollectible(was) != ShowsCollectible(state->item)) {
fillArt();
}
if (!SameDetailsHeader(was, state->item)) {
fillHeader();
}
rebuild(DetailsFee::Known);
}, box->lifetime());
}
if (state->looking) {
// The message can arrive before the transaction it names is served,
// which is what a transfer just sent looks like, so an answer that
// names nothing is asked again for a while before it is read as an
// answer. The timer belongs to the box, so closing it stops asking.
const auto lookup = [=] {
session->wallet().resolveTransaction(
state->item.id,
crl::guard(box, [=](ResolvedTransaction resolved) {
if (originCurrent && !originCurrent()) {
return;
} else if (resolved.item) {
if (resolved.item->traceId.isEmpty()) {
resolved.item->traceId = state->item.traceId;
}
state->item = std::move(*resolved.item);
state->looking = false;
rebuild(DetailsFee::Known);
return;
} else if (resolved.failed
|| ++state->attempts >= kTransactionLookupAttempts) {
rebuild(DetailsFee::Failed);
return;
}
state->retry.callOnce(kTransactionLookupInterval);
}));
};
state->retry.setCallback(lookup);
lookup();
}
AddBoxCloseButton(box);
const auto toggle = box->addTopButton(st::boxTitleMenu);
const auto menu = box->lifetime().make_state<
base::unique_qptr<Ui::PopupMenu>>();
toggle->setClickedCallback([=] {
if (*menu) {
return;
}
*menu = base::make_unique_q<Ui::PopupMenu>(
box,
st::popupMenuWithIcons);
const auto raw = menu->get();
raw->setDestroyedCallback(crl::guard(toggle, [=] {
toggle->setForceRippled(false);
}));
toggle->setForceRippled(true);
// Read when the menu opens, not when the box was built: a served
// transaction can name the trace a message did not carry.
const auto url = ExplorerTransactionUrl(session, state->item.traceId);
if (!url.isEmpty()) {
raw->addAction(
Ui::Text::FixAmpersandInAction(
tr::lng_channel_earn_history_out_button(tr::now)),
[=] { UrlClickHandler::Open(url); },
&st::menuIconSearch);
}
raw->addAction(
Ui::Text::FixAmpersandInAction(
tr::lng_wallet_how_menu(tr::now)),
[=] { show->showBox(Box(WalletHowItWorksBox, session)); },
&st::menuIconFaq);
raw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
const auto scope = WindowPaletteScope(box);
raw->popup(toggle->mapToGlobal(QPoint(
toggle->width(),
toggle->height())));
});
if (openWallet) {
box->addButton(tr::lng_wallet_details_open_wallet(), [=] {
const auto open = openWallet;
box->closeBox();
open();
});
} else {
box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
}
if (originCurrent) {
const auto close = [weak = base::make_weak(box)] {
if (weak && weak->hasDelegate()) {
weak->closeBox();
}
};
std::move(originInvalidated) | rpl::take(1) | rpl::on_next(
close,
box->lifetime());
if (!originCurrent()) {
close();
}
}
}
void ShowWalletTransactionBox(
std::shared_ptr<Main::SessionShow> show,
const TransferItem &item,
std::shared_ptr<CollectibleMedia> media = nullptr,
rpl::producer<TransferItem> updates = nullptr) {
const auto current = [show, identity = item.walletIdentity] {
if (!show || !show->valid()) {
return false;
}
return !identity
|| show->session().wallet().transferWalletIdentityCurrent(*identity);
};
if (!current()) {
return;
}
show->showBox(Box(
WalletTransactionBox,
show,
item,
false,
std::move(media),
current,
show->session().wallet().transferWalletIdentityChanges()
| rpl::filter([=] { return !current(); }),
Fn<void()>(),
std::move(updates)), Ui::LayerOption::KeepOther);
}
[[nodiscard]] int CommentBytes(const QString &text) {
return SendCommentBytes(text);
}
[[nodiscard]] bool CommentFits(const QString &text) {
return SendCommentFits(text);
}
// A comment is limited in UTF-8 bytes, so a Cyrillic letter costs two and an
// emoji four. Saying that in words explains nothing to anyone typing, so the
// field just counts down, and it starts counting late enough that the people
// who never approach the limit never see it. The limit is in bytes while a
// field's own maximum is in UTF-16 units, so the field keeps no maximum of
// its own and the count is allowed to go negative until sending refuses it.
//
// `rightSkip` is what already stands in the field's top right corner, which
// the counter has to stand to the left of.
void ApplyCommentLimit(
not_null<Ui::InputField*> field,
int rightSkip = 0) {
const auto &limitSt = st::defaultInputFieldLimit;
auto options = Ui::LengthLimitLabelOptions{
.customThreshold = kSendCommentWarnBytes,
.customCharactersCount = [=] {
return CommentBytes(field->getLastText());
},
};
if (rightSkip > 0) {
options.customUpdatePosition = [=](QSize parent, QSize label) {
const auto &st = field->st();
// Baseline alignment, the way the default position does it.
const auto top = st.textMargins.top()
+ st.style.font->ascent
- limitSt.style.font->ascent;
return QPoint(parent.width() - rightSkip - label.width(), top);
};
}
Ui::AddLengthLimitLabel(field, kSendCommentMaxBytes, std::move(options));
// A field's own maximum counts UTF-16 units, so it cannot enforce a
// limit that counts bytes. What it can do is keep a paste from running
// the counter off into the thousands. Two units per allowed byte leaves
// every comment that fits typeable - the densest of them, all emoji or
// all Cyrillic, is half that - and no single unit is worth more than
// three bytes, so `deepest` is as far below zero as the counter can go.
field->setMaxLength(kSendCommentMaxLength);
const auto deepest = 3 * kSendCommentMaxLength - kSendCommentMaxBytes;
const auto widest = limitSt.style.font->width(
QChar(0x2212) + QString::number(deepest));
field->setAdditionalMargins({
0,
0,
std::max(rightSkip + widest - field->st().textMargins.right(), 0),
0,
});
}
struct SendQuoteDependencies {
std::optional<TransferWalletIdentity> senderIdentity;
GaslessTerms gaslessTerms;
QString destination;
SendComment comment;
DeviceCustodyState custody;
QByteArray recipientPublicKey;
UserId userId;
int64 amountNano = 0;
int64 balanceNano = 0;
int64 minTransferNano = 0;
bool bounce = false;
bool ready = false;
bool valid = false;
friend bool operator==(
const SendQuoteDependencies &,
const SendQuoteDependencies &) = default;
};
struct SendQuote {
SendArgs args;
SendQuoteDependencies dependencies;
std::shared_ptr<const PreparedSend> prepared;
int64 feeNano = 0;
uint64 revision = 0;
friend bool operator==(const SendQuote &, const SendQuote &) = default;
};
struct SendDraft {
rpl::variable<SendComment> comment;
// False once the recipient is known to take plain comments only. It lives
// here because the comment editor outlives the box that opened it.
rpl::variable<bool> encryptable = true;
rpl::variable<std::optional<SendQuote>> quote;
rpl::variable<bool> preparing = false;
KeyAuthorization authorization;
std::optional<quint32> privateEpoch;
};
struct SendFlow {
QString destination;
bool bounce = true;
QString displayForm;
int64 amountNano = 0;
std::optional<uint64> expiresAt;
std::shared_ptr<SendDraft> draft;
QString tonName;
};
class SendCommentBubble final : public Ui::RpWidget {
public:
SendCommentBubble(
QWidget *parent,
rpl::producer<SendComment> comment,
rpl::producer<bool> clickable,
Fn<void()> clicked);
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
private:
void setText(const QString &text);
const not_null<Ui::AbstractButton*> _button;
Ui::Text::String _text = { 1 };
Ui::UniqueGiftMessageBubble::Layout _layout;
QPainterPath _path;
bool _clickable = false;
};
[[nodiscard]] int AddressGroupsWidth(
const style::font &font,
const QString &address,
int groupsPerLine);
void PaintAddressGroups(
QPainter &p,
const style::font &font,
const QString &address,
QPoint origin,
int groupsPerLine);
class SendRecipientCard final : public Ui::RpWidget {
public:
SendRecipientCard(
QWidget *parent,
std::shared_ptr<Ui::Show> show,
UserData *user,
rpl::producer<QString> address,
Fn<void()> about);
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
private:
[[nodiscard]] int nameHeight() const;
[[nodiscard]] QRect addressRect(int outerWidth) const;
void setAddress(const QString &address);
UserData * const _user = nullptr;
Ui::IconButton * const _about = nullptr;
const not_null<Ui::AbstractButton*> _copy;
style::TextStyle _nameStyle;
style::TextStyle _usernameStyle;
Ui::PeerUserpicView _userpic;
Ui::Text::String _name;
Ui::Text::String _username;
QString _address;
};
SendCommentBubble::SendCommentBubble(
QWidget *parent,
rpl::producer<SendComment> comment,
rpl::producer<bool> clickable,
Fn<void()> clicked)
: RpWidget(parent)
, _button(Ui::CreateChild<Ui::AbstractButton>(this)) {
_button->setClickedCallback(std::move(clicked));
_button->setPointerCursor(false);
std::move(clickable) | rpl::on_next([=](bool value) {
_clickable = value;
_button->setPointerCursor(value);
update();
}, lifetime());
std::move(comment) | rpl::map([](const SendComment &value) {
return value.text;
}) | rpl::distinct_until_changed() | rpl::on_next([this](
const QString &text) {
setText(text);
}, lifetime());
}
int SendCommentBubble::resizeGetHeight(int newWidth) {
if (!newWidth) {
_button->setGeometry(QRect());
return 0;
}
_layout = Ui::UniqueGiftMessageBubble::ResolveLayout(
st::walletSendCommentBubble,
style::margins(
st::walletSendFieldMargin.left(),
0,
st::walletSendFieldMargin.right(),
st::walletSendFieldMargin.bottom()),
newWidth,
_text);
auto mirror = QTransform();
mirror.translate(2 * QRectF(_layout.pathBounds).center().x(), 0.);
mirror.scale(-1., 1.);
_path = mirror.map(Ui::UniqueGiftMessageBubble::Path(
st::walletSendCommentBubble,
_layout));
const auto stroke = st::lineWidth;
_button->setGeometry(_path.boundingRect().toAlignedRect().marginsAdded(
{ stroke, stroke, stroke, stroke }));
const auto shift = -st::walletSendCommentBubble.tailSize.width();
_layout.body.translate(shift, 0);
_layout.text.translate(shift, 0);
return _layout.sectionHeight;
}
void SendCommentBubble::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
{
auto hq = PainterHighQualityEnabler(p);
if (_clickable && _button->isOver()) {
p.fillPath(_path, st::walletSendCommentBgOver);
}
p.setBrush(Qt::NoBrush);
p.setPen(QPen(
st::walletSendCommentOutline,
st::lineWidth,
Qt::SolidLine,
Qt::RoundCap,
Qt::RoundJoin));
p.drawPath(_path);
}
p.setPen(st::walletSendCommentTextFg);
_text.draw(p, {
.position = _layout.text.topLeft(),
.outerWidth = width(),
.availableWidth = _layout.text.width(),
.align = style::al_topleft,
.elisionLines = 0,
});
}
void SendCommentBubble::setText(const QString &text) {
_text.setText(st::walletSendCommentTextStyle, text);
if (width() > 0) {
resizeToWidth(width());
}
update();
}
SendRecipientCard::SendRecipientCard(
QWidget *parent,
std::shared_ptr<Ui::Show> show,
UserData *user,
rpl::producer<QString> address,
Fn<void()> about)
: RpWidget(parent)
, _user(user)
, _about(user
? Ui::CreateChild<Ui::IconButton>(this, st::walletSendUserCardAbout)
: nullptr)
, _copy(Ui::CreateChild<Ui::AbstractButton>(this))
, _nameStyle(st::defaultTextStyle)
, _usernameStyle(st::defaultTextStyle)
, _userpic(user ? user->createUserpicView() : Ui::PeerUserpicView()) {
_nameStyle.font = st::walletSendUserCardNameFont;
_usernameStyle.font = st::boxTextFont;
if (_about) {
_about->setClickedCallback(std::move(about));
_about->hide();
}
_copy->setClickedCallback([this, show = std::move(show)] {
CopyAddressCallback(show, _address)();
});
_copy->hide();
auto name = user
? Info::Profile::NameValue(user)
: tr::lng_wallet_send_gram_wallet();
std::move(name) | rpl::on_next([this](const QString &value) {
_name.setText(_nameStyle, value, Ui::NameTextOptions());
update();
}, lifetime());
if (user) {
Info::Profile::UsernameValue(user) | rpl::on_next([this](
const TextWithEntities &username) {
_username.setText(
_usernameStyle,
username.text,
Ui::NameTextOptions());
update();
}, lifetime());
user->session().downloaderTaskFinished() | rpl::on_next([this] {
update();
}, lifetime());
}
std::move(address) | rpl::on_next([this](const QString &value) {
setAddress(value);
}, lifetime());
}
int SendRecipientCard::resizeGetHeight(int newWidth) {
const auto &padding = st::walletSendUserCardPadding;
const auto font = st::walletSendUserCardAddressFont->monospace();
const auto lines = kAddressLength
/ (kAddressGroup * kSendUserCardGroupsPerLine);
const auto text = nameHeight()
+ st::walletSendUserCardTextSkip
+ lines * font->height;
const auto inner = std::max(st::walletSendUserCardPhoto, text);
if (_about) {
_about->moveToRight(
padding.right(),
(padding.top() + inner + padding.bottom() - _about->height()) / 2,
newWidth);
}
_copy->setGeometry(addressRect(newWidth));
return padding.top() + inner + padding.bottom();
}
void SendRecipientCard::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgOver);
p.drawRoundedRect(
rect(),
st::walletSendUserCardRadius,
st::walletSendUserCardRadius);
const auto &padding = st::walletSendUserCardPadding;
const auto photo = st::walletSendUserCardPhoto;
const auto inner = height() - padding.top() - padding.bottom();
const auto photoTop = padding.top() + (inner - photo) / 2;
if (_user) {
_user->paintUserpicLeft(
p,
_userpic,
padding.left(),
photoTop,
width(),
photo);
} else {
Ui::EmptyUserpic::PaintCurrency(
p,
padding.left(),
photoTop,
width(),
photo);
}
const auto left = padding.left()
+ photo
+ st::walletSendUserCardPhotoSkip;
const auto about = _about
? (_about->width() + st::walletSendUserCardAboutSkip)
: 0;
const auto available = width() - left - padding.right() - about;
const auto skip = st::walletSendUserCardNameSkip;
const auto handle = _username.isEmpty()
? 0
: std::min(_username.maxWidth(), (available - skip) / 2);
const auto nameWidth = std::min(
_name.maxWidth(),
available - (handle ? (handle + skip) : 0));
p.setPen(st::windowBoldFg);
_name.drawLeftElided(p, left, padding.top(), nameWidth, width(), 1);
if (handle) {
p.setPen(st::windowSubTextFg);
_username.drawLeftElided(
p,
left + nameWidth + skip,
padding.top(),
handle,
width(),
1);
}
if (!_address.isEmpty()) {
PaintAddressGroups(
p,
st::walletSendUserCardAddressFont->monospace(),
_address,
addressRect(width()).topLeft(),
kSendUserCardGroupsPerLine);
}
}
int SendRecipientCard::nameHeight() const {
const auto line = [](const style::TextStyle &text) {
return text.lineHeight ? text.lineHeight : text.font->height;
};
return std::max(line(_nameStyle), line(_usernameStyle));
}
QRect SendRecipientCard::addressRect(int outerWidth) const {
const auto &padding = st::walletSendUserCardPadding;
const auto font = st::walletSendUserCardAddressFont->monospace();
const auto left = padding.left()
+ st::walletSendUserCardPhoto
+ st::walletSendUserCardPhotoSkip;
const auto top = padding.top()
+ nameHeight()
+ st::walletSendUserCardTextSkip;
const auto blockWidth = AddressGroupsWidth(
font,
_address,
kSendUserCardGroupsPerLine);
const auto lines = kAddressLength
/ (kAddressGroup * kSendUserCardGroupsPerLine);
const auto x = style::RightToLeft()
? (outerWidth - left - blockWidth)
: left;
return QRect(x, top, blockWidth, lines * font->height);
}
void SendRecipientCard::setAddress(const QString &address) {
_address = address;
if (_about) {
_about->setVisible(!_address.isEmpty());
}
_copy->setGeometry(addressRect(width()));
_copy->setVisible(!_address.isEmpty());
update();
}
enum class KeyActionKind {
Plain,
Reveal,
ResumeAfterRestore,
};
// The one ladder every key-requiring wallet action climbs: the action runs
// at once when this device holds the key, after the backup restore and its
// protection chooser when the key is only in the cloud, after the phrase
// import when there is no backup, and after the conflict is resolved when a
// different wallet is parked here. A context turns the ladder's prompts into
// one attempt that ends when any of them closes without a key.
void RunKeyRequiringAction(
std::shared_ptr<Main::SessionShow> show,
Fn<void()> action,
KeyActionKind kind = KeyActionKind::Plain,
std::shared_ptr<KeyContext> context = nullptr,
rpl::producer<QString> importAbout = nullptr);
void WalletConflictBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
Fn<void()> switched);
[[nodiscard]] std::optional<SendFlow> ParseRecipientFlow(
const QString &text) {
auto address = text;
auto amountNano = int64(0);
auto comment = QString();
auto expiresAt = std::optional<uint64>();
if (const auto link = ParseTransferLink(text)) {
address = link->address;
amountNano = link->amountNano;
comment = link->comment;
expiresAt = link->expiresAt;
}
const auto parsed = ParseAddress(address);
if (!parsed || parsed->testnet) {
return std::nullopt;
}
const auto friendly = FormatFriendly(parsed->raw, parsed->bounceable);
if (friendly.isEmpty()) {
return std::nullopt;
}
const auto draft = std::make_shared<SendDraft>();
draft->comment = SendComment{ .text = std::move(comment) };
return SendFlow{
.destination = parsed->raw,
.bounce = parsed->bounceable,
.displayForm = (parsed->friendly ? address : friendly),
.amountNano = amountNano,
.expiresAt = expiresAt,
.draft = draft,
};
}
[[nodiscard]] QStringList SplitPhraseWords(const QString &text) {
return text.simplified().split(QChar(' '), Qt::SkipEmptyParts);
}
enum class RecipientInputKind : uchar {
Empty,
Address,
Name,
Invalid,
Search,
};
struct RecipientInput {
RecipientInputKind kind = RecipientInputKind::Empty;
std::optional<SendFlow> flow;
};
enum class RecipientError : uchar {
Invalid,
NameNotFound,
NameFailed,
LookupFailed,
OwnWallet,
};
[[nodiscard]] rpl::producer<QString> RecipientErrorText(
RecipientError error) {
switch (error) {
case RecipientError::Invalid:
return tr::lng_wallet_send_invalid_address();
case RecipientError::NameNotFound:
return tr::lng_wallet_send_name_not_found();
case RecipientError::NameFailed:
case RecipientError::LookupFailed:
return tr::lng_wallet_send_user_load_error();
case RecipientError::OwnWallet:
return tr::lng_wallet_collectible_own_wallet();
}
Unexpected("RecipientError in RecipientErrorText.");
}
[[nodiscard]] bool IsTonDnsName(const QString &text) {
if (text.isEmpty() || text.size() > 126) {
return false;
}
const auto parts = text.split(QChar('.'));
if (parts.size() < 2
|| parts.back().compare(u"ton"_q, Qt::CaseInsensitive) != 0) {
return false;
}
for (const auto &part : parts) {
if (part.isEmpty()) {
return false;
}
for (const auto ch : part) {
const auto code = ch.unicode();
const auto good = (code >= 'a' && code <= 'z')
|| (code >= 'A' && code <= 'Z')
|| (code >= '0' && code <= '9')
|| (code == '-');
if (!good) {
return false;
}
}
}
return true;
}
[[nodiscard]] RecipientInput ClassifyRecipientInput(const QString &text) {
using Kind = RecipientInputKind;
if (text.isEmpty()) {
return {};
} else if (auto flow = ParseRecipientFlow(text)) {
return { .kind = Kind::Address, .flow = std::move(flow) };
} else if (IsTonDnsName(text)) {
return { .kind = Kind::Name };
} else if (text.contains(u"://"_q)
|| text.startsWith(u"ton:"_q, Qt::CaseInsensitive)
|| ParseAddress(text)
|| ParseTransferLink(text)
|| (text.size() > kRecipientSearchLimit)
|| (SplitPhraseWords(text).size() >= kImportWordCountShort)) {
// Rejected addresses and pasted secrets never reach contacts.search.
return { .kind = Kind::Invalid };
} else if (TextUtilities::PrepareSearchWords(text).isEmpty()) {
return {};
}
return { .kind = Kind::Search };
}
[[nodiscard]] not_null<Ui::InputField*> AddCommentField(
not_null<Ui::GenericBox*> box,
const QString &comment) {
const auto field = box->addRow(
object_ptr<Ui::InputField>(
box,
st::walletCommentField,
Ui::InputField::Mode::NoNewlines,
tr::lng_wallet_send_comment_optional(),
comment),
st::walletCommentFieldMargin);
ApplyCommentLimit(field);
return field;
}
[[nodiscard]] not_null<Ui::InputField*> AddSendField(
not_null<Ui::VerticalLayout*> container,
const style::InputField &st,
rpl::producer<QString> placeholder,
const QString &value) {
const auto field = container->add(
object_ptr<Ui::InputField>(
container,
st,
Ui::InputField::Mode::NoNewlines,
std::move(placeholder),
value),
st::walletSendFieldMargin);
const auto paste = Ui::CreateChild<Ui::RoundButton>(
field,
tr::lng_mac_menu_paste(),
st::defaultTableSmallButton);
paste->setTextTransform(Ui::RoundButtonTextTransform::NoTransform);
paste->setClickedCallback([=] {
field->setFocusFast();
field->setText(QGuiApplication::clipboard()->text().trimmed());
});
field->widthValue(
) | rpl::on_next([=, &st](int) {
paste->moveToRight(0, st.textMargins.top());
}, paste->lifetime());
const auto updatePaste = [=] {
paste->setVisible(field->getLastText().isEmpty());
};
field->changes() | rpl::on_next(updatePaste, field->lifetime());
updatePaste();
return field;
}
void BindCommentField(
not_null<Ui::InputField*> field,
const std::shared_ptr<SendDraft> &draft) {
field->changes() | rpl::on_next([=] {
auto comment = draft->comment.current();
comment.text = field->getLastText();
draft->comment = std::move(comment);
}, field->lifetime());
draft->comment.value() | rpl::on_next([=](const SendComment &comment) {
if (field->getLastText() != comment.text) {
field->setText(comment.text);
}
if (!CommentFits(comment.text)) {
field->showError();
}
}, field->lifetime());
}
void AddCommentPrivacy(
not_null<Ui::VerticalLayout*> container,
const std::shared_ptr<SendDraft> &draft,
const style::margins &margin,
rpl::producer<bool> encryptable) {
const auto choice = container->add(
object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
container,
object_ptr<Ui::Checkbox>(
container,
tr::lng_wallet_comment_make_public(),
draft->comment.current().isPublic,
st::defaultBoxCheckbox),
margin));
choice->toggleOn(std::move(encryptable));
choice->finishAnimating();
const auto checkbox = choice->entity();
checkbox->setAllowTextLines(0);
checkbox->checkedChanges() | rpl::on_next([=](bool checked) {
auto comment = draft->comment.current();
comment.isPublic = checked;
draft->comment = std::move(comment);
}, checkbox->lifetime());
draft->comment.value() | rpl::on_next([=](const SendComment &comment) {
checkbox->setChecked(
comment.isPublic,
Ui::Checkbox::NotifyAboutChange::DontNotify);
}, checkbox->lifetime());
const auto warning = container->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
container,
object_ptr<Ui::FlatLabel>(
container,
tr::lng_wallet_comment_public(),
st::walletCommentCaptionLabel),
st::walletCommentCaptionMargin));
warning->toggleOn(draft->comment.value() | rpl::map([](
const SendComment &comment) {
return comment.isPublic;
}));
warning->finishAnimating();
}
void WalletSendCommentBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<SendDraft> draft,
Fn<bool()> originValid,
rpl::producer<bool> encryptable) {
box->setWidth(st::boxWideWidth);
box->setTitle(tr::lng_wallet_comment_title());
const auto staged = std::make_shared<SendDraft>();
staged->comment = draft->comment.current();
const auto field = AddCommentField(box, staged->comment.current().text);
BindCommentField(field, staged);
AddCommentPrivacy(
box->verticalLayout(),
staged,
st::walletCommentPrivacyMargin,
std::move(encryptable));
struct State {
bool closed = false;
};
const auto state = box->lifetime().make_state<State>();
const auto weak = base::make_weak(box.get());
box->boxClosing() | rpl::on_next([=] {
state->closed = true;
}, box->lifetime());
const auto save = [=] {
if (state->closed || !originValid()) {
return;
} else if (!CommentFits(staged->comment.current().text)) {
field->showError();
field->setFocusFast();
return;
}
state->closed = true;
auto comment = staged->comment.current();
if (!draft->encryptable.current()) {
comment.isPublic = true;
}
draft->comment = std::move(comment);
if (const auto alive = weak.get()) {
alive->closeBox();
}
};
box->addButton(tr::lng_wallet_comment_add(), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
field->submits() | rpl::on_next(save, field->lifetime());
box->setFocusCallback([=] { field->setFocusFast(); });
AddBoxCloseButton(box);
}
void ShowKeyChangedBox(
std::shared_ptr<Main::SessionShow> show,
v::text::data text) {
show->showBox(Ui::MakeConfirmBox({
.text = std::move(text),
.confirmed = [=](Fn<void()> close) {
close();
RunKeyRequiringAction(show, [] {});
},
.confirmText = tr::lng_wallet_restore_title(),
.title = tr::lng_wallet_send_key_changed_title(),
}));
}
} // namespace
QString SendErrorText(SendError error, int64 minTransferNano) {
switch (error) {
case SendError::None:
case SendError::Silent:
case SendError::SubmissionUnknown:
return QString();
case SendError::AmountTooSmall:
return tr::lng_wallet_send_error_too_small(
tr::now,
lt_amount,
Ui::FormatTonAmount(minTransferNano).full);
case SendError::CommentTooLong:
return tr::lng_wallet_comment_too_long(tr::now);
case SendError::CommentEncryptionUnavailable:
return tr::lng_wallet_comment_encryption_failed(tr::now);
// A balance that covers the amount but not the fee is the same problem
// to the sender as one that covers neither, and one sentence says it.
case SendError::InsufficientBalance:
case SendError::InsufficientFees:
return tr::lng_wallet_send_error_insufficient(tr::now);
case SendError::PreviousUnresolved:
case SendError::AlreadySending:
return tr::lng_wallet_send_error_in_progress(tr::now);
case SendError::SigningUnavailable:
return tr::lng_wallet_readonly_bar(tr::now);
case SendError::Locked:
return tr::lng_wallet_vault_locked(tr::now);
case SendError::InvalidRequest:
case SendError::Failed:
case SendError::KeyMismatch:
case SendError::Rejected:
case SendError::DataInvalid:
case SendError::CollectibleRejected:
return tr::lng_wallet_send_error_failed(tr::now);
case SendError::CollectibleUnavailable:
return tr::lng_wallet_collectible_not_owned(tr::now);
case SendError::KeyChanged:
return tr::lng_wallet_send_key_changed_text(tr::now);
case SendError::QuoteExpired:
return tr::lng_wallet_send_error_quote_expired(tr::now);
case SendError::LinkExpired:
return tr::lng_wallet_send_link_expired(tr::now);
}
Unexpected("Error value in SendErrorText.");
}
void ShowWalletKeyChanged(std::shared_ptr<Main::SessionShow> show) {
ShowKeyChangedBox(show, tr::lng_wallet_send_key_changed_text());
}
QString ErrorWithType(const QString &message, const QString &error) {
return error.isEmpty()
? message
: tr::lng_wallet_error_with_type(
tr::now,
lt_message,
message,
lt_error,
error);
}
namespace {
[[nodiscard]] QString SendUserLoadErrorText(const QString &error) {
if (error == u"WALLET_UNAVAILABLE"_q) {
return tr::lng_wallet_unavailable(tr::now);
} else if (error == u"WALLET_NOT_READY"_q) {
return tr::lng_wallet_state_error(tr::now);
} else if (error == u"WALLET_BALANCE_EMPTY"_q) {
return tr::lng_wallet_send_error_insufficient(tr::now);
} else if (error == u"WALLET_USER_INVALID"_q
|| error == u"WALLET_USER_INELIGIBLE"_q) {
return tr::lng_wallet_send_user_unavailable(tr::now);
} else if (error == u"WALLET_ADDRESS_INVALID"_q) {
return tr::lng_wallet_send_user_load_error(tr::now);
}
return ErrorWithType(tr::lng_wallet_state_error(tr::now), error);
}
// A button that cannot be pressed yet keeps the background its own style
// gives it and fades its label halfway into that background, which is how the
// rest of the app shows a disabled button. The colors come from the button's
// own style, so an attention or light button fades into its own background
// instead of an active button's.
void ApplyButtonDisabledLook(not_null<Ui::RoundButton*> button) {
const auto color = [&]() -> std::optional<QColor> {
if (!button->isDisabled()) {
return std::nullopt;
}
const auto scope = WindowPaletteScope(button);
const auto &buttonStyle = button->st();
return anim::color(buttonStyle.textBg, buttonStyle.textFg, 0.5);
}();
button->setTextFgOverride(color);
}
void SetButtonDisabledLook(
not_null<Ui::RoundButton*> button,
bool disabled) {
if (disabled) {
button->clearState();
}
button->setDisabled(disabled);
button->setAttribute(Qt::WA_TransparentForMouseEvents, disabled);
ApplyButtonDisabledLook(button);
if (!button->property("walletDisabledLook").toBool()) {
button->setProperty("walletDisabledLook", true);
style::PaletteChanged() | rpl::on_next([=] {
ApplyButtonDisabledLook(button);
}, button->lifetime());
}
}
// The send box offers to fund an empty wallet, so the balance never hides one.
[[nodiscard]] bool CanSendToUser(
not_null<Main::Session*> session,
UserId id) {
const auto error = session->wallet().userAddresses().forceResolveError(id);
return error.isEmpty() || (error == u"WALLET_BALANCE_EMPTY"_q);
}
[[nodiscard]] UserData *SendableUser(
not_null<Main::Session*> session,
UserId id) {
const auto user = id ? session->data().userLoaded(id) : nullptr;
if (!user || !CanSendToUser(session, id)) {
return nullptr;
}
return user;
}
[[nodiscard]] bool SendsToOwnWallet(
not_null<Main::Session*> session,
const QString &destination) {
const auto identity = session->wallet().transferWalletIdentity();
return identity
&& !destination.isEmpty()
&& (CanonicalAddress(destination) == identity->address);
}
void ChooseMoneyRecipient(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
not_null<UserData*> user) {
ShowSendToUser(show, user, nullptr, 0, nullptr, box.get());
}
class RecentMoneyRecipientsController final
: public PeerListController
, public base::has_weak_ptr {
public:
RecentMoneyRecipientsController(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
Fn<void(not_null<UserData*>)> choose);
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
Main::Session &session() const override;
void setContent(not_null<PeerListContent*> content);
void clear();
[[nodiscard]] rpl::producer<bool> shownValue() const;
private:
[[nodiscard]] bool active() const;
[[nodiscard]] bool canOffer(not_null<UserData*> user) const;
void fillIfEmpty();
void refresh();
void scheduleRefresh();
void watchUsers();
const base::weak_qptr<Ui::GenericBox> _box;
const std::shared_ptr<Main::SessionShow> _show;
const base::weak_ptr<Main::Session> _session;
const Fn<void(not_null<UserData*>)> _choose;
PeerListContentDelegateSimple _delegate;
std::vector<not_null<UserData*>> _users;
rpl::lifetime _userLifetime;
rpl::variable<bool> _shown = false;
bool _closed = false;
bool _refreshQueued = false;
};
RecentMoneyRecipientsController::RecentMoneyRecipientsController(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
Fn<void(not_null<UserData*>)> choose)
: _box(box)
, _show(std::move(show))
, _session(&_show->session())
, _choose(std::move(choose)) {
}
void RecentMoneyRecipientsController::setContent(
not_null<PeerListContent*> content) {
_delegate.setContent(content);
setDelegate(&_delegate);
}
void RecentMoneyRecipientsController::prepare() {
_box->boxClosing() | rpl::on_next([=] {
_closed = true;
_shown = false;
}, lifetime());
_session->recentMoneyRecipients().updates() | rpl::on_next([=] {
refresh();
}, lifetime());
rpl::merge(
_session->recentPeers().updates(),
_session->topPeers().updates()
) | rpl::on_next([=] {
fillIfEmpty();
}, lifetime());
const auto schedule = [=] { scheduleRefresh(); };
const auto &wallet = _session->wallet();
wallet.stateKnownValue() | rpl::skip(1) | rpl::on_next(
schedule,
lifetime());
wallet.balanceNanoValue() | rpl::skip(1) | rpl::on_next(
schedule,
lifetime());
_session->wallet().userAddresses().unavailableValue(
) | rpl::skip(1) | rpl::on_next(schedule, lifetime());
fillIfEmpty();
refresh();
}
bool RecentMoneyRecipientsController::active() const {
return _box
&& !_closed
&& _session
&& _show->valid()
&& &_show->session() == _session.get();
}
bool RecentMoneyRecipientsController::canOffer(
not_null<UserData*> user) const {
return active()
&& &user->session() == _session.get()
&& !user->isSelf() // the list a Clear leaves behind
&& SendableUser(_session.get(), peerToUser(user->id)) == user;
}
void RecentMoneyRecipientsController::fillIfEmpty() {
if (!active()) {
return;
}
const auto eligible = [=](not_null<UserData*> user) {
return canOffer(user);
};
_session->recentMoneyRecipients().fillIfEmpty(eligible);
}
void RecentMoneyRecipientsController::watchUsers() {
_userLifetime.destroy();
for (const auto &user : _users) {
user->flagsValue() | rpl::skip(1) | rpl::on_next([=] {
scheduleRefresh();
}, _userLifetime);
using Flag = Data::PeerUpdate::Flag;
_session->changes().peerUpdates(
user,
Flag::FullInfo | Flag::SupportInfo | Flag::OnlineStatus
) | rpl::on_next([=](const Data::PeerUpdate &update) {
if (!active()) {
return;
}
if (update.flags & Flag::OnlineStatus) {
if (const auto row = delegate()->peerListFindRow(user->id.value)) {
row->refreshStatus();
delegate()->peerListUpdateRow(row);
}
}
if (update.flags & (Flag::FullInfo | Flag::SupportInfo)) {
scheduleRefresh();
}
}, _userLifetime);
}
}
void RecentMoneyRecipientsController::refresh() {
if (!active()) {
_shown = false;
return;
}
const auto &users = _session->recentMoneyRecipients().list();
if (_users != users) {
_users = users;
watchUsers();
}
auto rows = std::vector<not_null<UserData*>>();
rows.reserve(_users.size());
for (const auto &user : _users) {
if (canOffer(user)) {
rows.push_back(user);
}
}
const auto count = delegate()->peerListFullRowsCount();
auto changed = (count != int(rows.size()));
for (auto i = 0; !changed && i != count; ++i) {
changed = (delegate()->peerListRowAt(i)->peer() != rows[i]);
}
if (changed) {
while (delegate()->peerListFullRowsCount()) {
delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
}
for (const auto &user : rows) {
delegate()->peerListAppendRow(std::make_unique<PeerListRow>(user));
}
delegate()->peerListRefreshRows();
}
_shown = !rows.empty();
}
void RecentMoneyRecipientsController::scheduleRefresh() {
if (!active() || _refreshQueued) {
return;
}
_refreshQueued = true;
crl::on_main(this, [=] {
_refreshQueued = false;
fillIfEmpty();
refresh();
});
}
void RecentMoneyRecipientsController::rowClicked(
not_null<PeerListRow*> row) {
if (!active()) {
return;
}
const auto user = row->peer()->asUser();
if (!user || !canOffer(user)) {
return;
}
_choose(user);
}
Main::Session &RecentMoneyRecipientsController::session() const {
return *_session;
}
void RecentMoneyRecipientsController::clear() {
if (!active()) {
return;
}
const auto session = _session;
session->recentMoneyRecipients().clear();
if (session) {
session->local().writeSearchSuggestionsIfNeeded();
}
}
rpl::producer<bool> RecentMoneyRecipientsController::shownValue() const {
return _shown.value();
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeRecentMoneyRecipientsList(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
rpl::producer<bool> hidden,
Fn<void(not_null<UserData*>)> choose) {
auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
object_ptr<Ui::VerticalLayout>(box));
const auto wrap = result.data();
const auto container = wrap->entity();
const auto controller = container->lifetime().make_state<
RecentMoneyRecipientsController>(
box,
std::move(show),
std::move(choose));
const auto header = container->add(object_ptr<Ui::RpWidget>(container));
const auto label = Ui::CreateChild<Ui::FlatLabel>(
header,
tr::lng_recent_title(),
st::windowFilterChatsSectionSubtitle);
const auto clear = Ui::CreateChild<Ui::LinkButton>(
header,
QString(),
st::boxLinkButton);
tr::lng_recent_clear() | rpl::on_next([=](const QString &text) {
clear->setText(text);
}, clear->lifetime());
clear->setClickedCallback([=] { controller->clear(); });
rpl::combine(
header->widthValue(),
clear->naturalWidthValue(),
label->heightValue()
) | rpl::on_next([=](int width, int, int) {
const auto &padding = st::walletSendRecentHeaderPadding;
const auto available = std::max(
width - padding.left() - padding.right(),
0);
clear->resizeToNaturalWidth(available);
label->resizeToWidth(std::max(
available - clear->width() - st::walletSendFieldMargin.bottom(),
0));
const auto height = std::max(
st::windowFilterChatsSectionSubtitleHeight,
std::max(label->height(), clear->height())
+ padding.top()
+ padding.bottom());
header->resize(width, height);
label->moveToLeft(padding.left(), (height - label->height()) / 2);
clear->moveToRight(padding.right(), (height - clear->height()) / 2);
}, header->lifetime());
header->paintRequest() | rpl::on_next([=](QRect clip) {
QPainter(header).fillRect(clip, st::searchedBarBg);
}, header->lifetime());
Ui::AddSkip(container, st::walletSendRecentListTopSkip);
controller->setStyleOverrides(&st::peerListSingleRow);
const auto content = container->add(
object_ptr<PeerListContent>(container, controller));
controller->setContent(content);
Ui::AddSkip(container, st::walletSendRecentListSkip);
const auto wasHidden = wrap->lifetime().make_state<bool>(false);
rpl::combine(
controller->shownValue(),
std::move(hidden)
) | rpl::on_next([=](bool shown, bool hide) {
const auto animated = (hide != *wasHidden)
? anim::type::instant
: anim::type::normal;
*wasHidden = hide;
wrap->toggle(shown && !hide, animated);
}, wrap->lifetime());
wrap->finishAnimating();
return result;
}
class MoneyRecipientSearchController final
: public ChatsListBoxController {
public:
MoneyRecipientSearchController(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
Fn<void(not_null<UserData*>)> choose);
Main::Session &session() const override;
void rowClicked(not_null<PeerListRow*> row) override;
void setContent(not_null<PeerListContent*> content);
protected:
std::unique_ptr<Row> createRow(not_null<History*> history) override;
void prepareViewHook() override;
private:
[[nodiscard]] UserData *offered(not_null<PeerData*> peer) const;
const base::weak_qptr<Ui::GenericBox> _box;
const std::shared_ptr<Main::SessionShow> _show;
const not_null<Main::Session*> _session;
const Fn<void(not_null<UserData*>)> _choose;
PeerListContentDelegateShow _delegate;
bool _closed = false;
};
MoneyRecipientSearchController::MoneyRecipientSearchController(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
Fn<void(not_null<UserData*>)> choose)
: ChatsListBoxController(&show->session())
, _box(box)
, _show(std::move(show))
, _session(&_show->session())
, _choose(std::move(choose))
, _delegate(_show) {
}
Main::Session &MoneyRecipientSearchController::session() const {
return *_session;
}
void MoneyRecipientSearchController::setContent(
not_null<PeerListContent*> content) {
_delegate.setContent(content);
setDelegate(&_delegate);
}
void MoneyRecipientSearchController::prepareViewHook() {
_box->boxClosing() | rpl::on_next([=] {
_closed = true;
// WHY: the list outlives boxClosing by the close animation, so drop
// the pending global search now or its late answer fills the list.
search(QString());
}, lifetime());
}
UserData *MoneyRecipientSearchController::offered(
not_null<PeerData*> peer) const {
const auto user = peer->asUser();
return (user
&& &user->session() == _session
&& SendableUser(_session, peerToUser(user->id)) == user)
? user
: nullptr;
}
auto MoneyRecipientSearchController::createRow(not_null<History*> history)
-> std::unique_ptr<Row> {
return offered(history->peer)
? std::make_unique<Row>(history)
: nullptr;
}
void MoneyRecipientSearchController::rowClicked(
not_null<PeerListRow*> row) {
if (!_box
|| _closed
|| !_show->valid()
|| &_show->session() != _session) {
return;
}
const auto user = offered(row->peer());
if (!user) {
return;
}
_choose(user);
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeMoneyRecipientSearchList(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
rpl::producer<QString> query,
Fn<void(not_null<UserData*>)> choose) {
auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
object_ptr<Ui::VerticalLayout>(box));
const auto wrap = result.data();
const auto container = wrap->entity();
const auto controller = container->lifetime().make_state<
MoneyRecipientSearchController>(
box,
std::move(show),
std::move(choose));
Ui::AddSkip(container, st::walletSendRecentListTopSkip);
controller->setStyleOverrides(&st::peerListSingleRow);
const auto content = container->add(
object_ptr<PeerListContent>(container, controller));
controller->setContent(content);
Ui::AddSkip(container, st::walletSendRecentListSkip);
std::move(query) | rpl::on_next([=](const QString &text) {
wrap->toggle(!text.isEmpty(), anim::type::instant);
content->searchQueryChanged(text);
}, wrap->lifetime());
wrap->finishAnimating();
return result;
}
enum class TonNameStatus : uchar {
None,
Pending,
Resolved,
NotFound,
Failed,
};
struct TonNameState {
QString name;
QString address;
QString displayForm;
TonNameStatus status = TonNameStatus::None;
friend bool operator==(
const TonNameState &,
const TonNameState &) = default;
};
[[nodiscard]] bool TonNameRowShown(const TonNameState &state) {
return (state.status == TonNameStatus::Pending)
|| (state.status == TonNameStatus::Resolved);
}
class TonNameResultRow final : public PeerListRow {
public:
TonNameResultRow(const QString &name, const QString &address);
QString generateName() override;
QString generateShortName() override;
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
void paintStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected) override;
private:
const QString _name;
const QString _address;
};
class TonNameResultController final : public PeerListController {
public:
TonNameResultController(
not_null<Main::Session*> session,
Fn<void()> chosen);
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
Main::Session &session() const override;
void setContent(not_null<PeerListContent*> content);
void showState(const TonNameState &state);
private:
const not_null<Main::Session*> _session;
const Fn<void()> _chosen;
PeerListContentDelegateSimple _delegate;
};
// WHY: engine jobs run one at a time and never coalesce, so one lookup
// stays out and the latest name is asked as soon as it returns.
class TonNameLookup final : public base::has_weak_ptr {
public:
explicit TonNameLookup(not_null<Main::Session*> session);
void setName(const QString &name);
void request();
void close();
[[nodiscard]] const TonNameState &current() const;
[[nodiscard]] rpl::producer<TonNameState> value() const;
private:
void start();
void issue();
[[nodiscard]] bool settle(uint64 revision, bool busy);
void resolved(uint64 revision, std::optional<QString> address);
void failed(uint64 revision, DnsLookupError error);
void finish(TonNameStatus status);
const base::weak_ptr<Main::Session> _session;
rpl::variable<TonNameState> _state;
base::Timer _timer;
base::Timer _deadline;
uint64 _revision = 0;
bool _waiting = false;
bool _inFlight = false;
bool _closed = false;
};
TonNameResultRow::TonNameResultRow(
const QString &name,
const QString &address)
: PeerListRow(PeerListRowId(1))
, _name(name)
, _address(address) {
if (_address.isEmpty()) {
setDisabledState(State::Disabled);
}
}
QString TonNameResultRow::generateName() {
return _name;
}
QString TonNameResultRow::generateShortName() {
return _name;
}
PaintRoundImageCallback TonNameResultRow::generatePaintUserpicCallback(
bool forceRound) {
return [](Painter &p, int x, int y, int outerWidth, int size) {
Ui::EmptyUserpic::PaintCurrency(p, x, y, outerWidth, size);
};
}
void TonNameResultRow::paintStatusText(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int availableWidth,
int outerWidth,
bool selected) {
const auto &font = st::contactsStatusFont;
const auto text = _address.isEmpty()
? font->elided(tr::lng_contacts_loading(tr::now), availableWidth)
: font->elided(_address, availableWidth, Qt::ElideMiddle);
p.setFont(font);
p.setPen(selected ? st.statusFgOver : st.statusFg);
p.drawTextLeft(x, y, outerWidth, text);
}
TonNameResultController::TonNameResultController(
not_null<Main::Session*> session,
Fn<void()> chosen)
: _session(session)
, _chosen(std::move(chosen)) {
}
void TonNameResultController::prepare() {
}
void TonNameResultController::rowClicked(not_null<PeerListRow*> row) {
if (_chosen) {
_chosen();
}
}
Main::Session &TonNameResultController::session() const {
return *_session;
}
void TonNameResultController::setContent(
not_null<PeerListContent*> content) {
_delegate.setContent(content);
setDelegate(&_delegate);
}
void TonNameResultController::showState(const TonNameState &state) {
while (delegate()->peerListFullRowsCount()) {
delegate()->peerListRemoveRow(delegate()->peerListRowAt(0));
}
if (TonNameRowShown(state)) {
const auto resolved = (state.status == TonNameStatus::Resolved);
delegate()->peerListAppendRow(std::make_unique<TonNameResultRow>(
state.name,
resolved ? state.displayForm : QString()));
}
delegate()->peerListRefreshRows();
}
TonNameLookup::TonNameLookup(not_null<Main::Session*> session)
: _session(session)
, _timer([this] { start(); })
, _deadline([this] {
if (_waiting) {
finish(TonNameStatus::Failed);
}
}) {
}
void TonNameLookup::setName(const QString &name) {
++_revision;
_waiting = false;
_timer.cancel();
_deadline.cancel();
if (name.isEmpty()) {
_state = TonNameState();
return;
}
_timer.callOnce(AutoSearchTimeout);
_state = TonNameState{ .name = name, .status = TonNameStatus::Pending };
}
void TonNameLookup::request() {
_timer.cancel();
start();
}
void TonNameLookup::close() {
_closed = true;
++_revision;
_waiting = false;
_timer.cancel();
_deadline.cancel();
}
const TonNameState &TonNameLookup::current() const {
return _state.current();
}
rpl::producer<TonNameState> TonNameLookup::value() const {
return _state.value();
}
void TonNameLookup::start() {
const auto name = _state.current().name;
if (_closed || name.isEmpty()) {
return;
}
if (!_waiting) {
_waiting = true;
_deadline.callOnce(kSendUserLoadTimeout);
_state = TonNameState{
.name = name,
.status = TonNameStatus::Pending,
};
}
issue();
}
void TonNameLookup::issue() {
const auto session = _session.get();
if (_closed || !_waiting || _inFlight || !session) {
return;
}
_timer.cancel();
_inFlight = true;
const auto revision = _revision;
session->wallet().resolveDnsName(
_state.current().name,
crl::guard(this, [=, this](std::optional<QString> address) {
resolved(revision, std::move(address));
}),
crl::guard(this, [=, this](DnsLookupError error) {
failed(revision, error);
}));
}
bool TonNameLookup::settle(uint64 revision, bool busy) {
_inFlight = false;
if (_closed || !_waiting) {
return false;
} else if (busy) {
_timer.callOnce(kNameBusyRetryDelay);
return false;
} else if (revision != _revision) {
issue();
return false;
}
return true;
}
void TonNameLookup::resolved(
uint64 revision,
std::optional<QString> address) {
if (!settle(revision, false)) {
return;
} else if (!address) {
finish(TonNameStatus::NotFound);
return;
}
const auto flow = ParseRecipientFlow(*address);
if (!flow) {
finish(TonNameStatus::Failed);
return;
}
_waiting = false;
_timer.cancel();
_deadline.cancel();
_state = TonNameState{
.name = _state.current().name,
.address = *address,
.displayForm = flow->displayForm,
.status = TonNameStatus::Resolved,
};
}
void TonNameLookup::failed(uint64 revision, DnsLookupError error) {
if (settle(revision, (error == DnsLookupError::Busy))) {
finish(TonNameStatus::Failed);
}
}
void TonNameLookup::finish(TonNameStatus status) {
_waiting = false;
_timer.cancel();
_deadline.cancel();
_state = TonNameState{ .name = _state.current().name, .status = status };
}
[[nodiscard]] object_ptr<Ui::RpWidget> MakeTonNameResultList(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
rpl::producer<TonNameState> state,
Fn<void()> chosen) {
auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
object_ptr<Ui::VerticalLayout>(box));
const auto wrap = result.data();
const auto container = wrap->entity();
const auto controller = container->lifetime().make_state<
TonNameResultController>(session, std::move(chosen));
Ui::AddSkip(container, st::walletSendRecentListTopSkip);
controller->setStyleOverrides(&st::peerListSingleRow);
const auto content = container->add(
object_ptr<PeerListContent>(container, controller));
controller->setContent(content);
Ui::AddSkip(container, st::walletSendRecentListSkip);
std::move(state) | rpl::on_next([=](const TonNameState &value) {
controller->showState(value);
wrap->toggle(TonNameRowShown(value), anim::type::instant);
}, wrap->lifetime());
wrap->finishAnimating();
return result;
}
[[nodiscard]] rpl::producer<TextWithEntities> SendRecipientTitle(
not_null<Ui::GenericBox*> box,
const QString &recipient,
Qt::TextElideMode mode) {
return rpl::combine(
box->widthValue(),
rpl::single(rpl::empty) | rpl::then(Lang::Updated())
) | rpl::map([=](int width, rpl::empty_value) {
const auto available = width
- 2 * st::boxTitlePosition.x()
- st::boxTitleClose.width
- st::boxTitleMenu.width;
const auto title = [&](const QString &shown) {
return tr::lng_wallet_send_user_title(
tr::now,
lt_user,
Ui::Text::Colorized(shown),
tr::marked);
};
auto measure = Ui::Text::String();
const auto fits = [&](const TextWithEntities &text) {
measure.setMarkedText(st::giveawayGiftCodeBox.title.style, text);
return measure.maxWidth() <= available;
};
auto full = title(recipient);
if (fits(full)) {
return full;
}
const auto middle = (mode == Qt::ElideMiddle);
const auto shortened = [&](int chars) {
return middle
? ShortAddressForm(recipient, chars)
: (recipient.left(chars) + QChar(0x2026));
};
const auto most = middle
? ((int(recipient.size()) - 1) / 2)
: (int(recipient.size()) - 1);
for (auto chars = most; chars > 1; --chars) {
auto elided = title(shortened(chars));
if (fits(elided)) {
return elided;
}
}
return title(shortened(1));
});
}
struct SendConfirmFee {
std::optional<int64> feeNano;
bool gasless = false;
bool pending = false;
friend bool operator==(
const SendConfirmFee &,
const SendConfirmFee &) = default;
};
struct SendConfirmArgs {
std::shared_ptr<SendDraft> draft;
QString address;
int64 amountNano = 0;
rpl::producer<SendConfirmFee> fee;
rpl::producer<QString> refusal;
rpl::producer<bool> busy;
rpl::producer<bool> canSend;
Fn<void(SendConfirmFee shown)> send;
};
[[nodiscard]] SendConfirmFee QuoteConfirmFee(const SendQuote &quote) {
return {
.feeNano = quote.feeNano,
.gasless = quote.dependencies.gaslessTerms.eligible(
quote.args.amountNano,
quote.args.destination),
};
}
void FillSendConfirmTable(
not_null<Ui::VerticalLayout*> container,
std::shared_ptr<Ui::Show> show,
not_null<Main::Session*> session,
const QString &address,
const SendConfirmFee &fee,
TimeId date) {
const auto table = AddDetailsTableFrame(container);
Ui::AddTableRow(
table,
tr::lng_wallet_details_address(),
AddressValueLabel(table, show, address));
if (fee.feeNano) {
auto item = TransferItem();
item.feeNano = fee.feeNano;
item.gasless = fee.gasless;
AddFeeTableRow(table, std::move(show), session, item);
} else {
AddPendingFeeTableRow(
table,
fee.pending ? DetailsFee::Loading : DetailsFee::Failed);
}
Ui::AddTableRow(
table,
tr::lng_wallet_details_date(),
rpl::single(tr::marked(langDateTime(base::unixtime::parse(date)))));
}
void AddSendCommentLock(
not_null<Ui::InputField*> field,
const style::InputField &st,
std::shared_ptr<SendDraft> draft) {
const auto lock = Ui::CreateChild<Ui::IconButton>(
field,
st::walletSendConfirmLock);
lock->setClickedCallback([=] {
auto comment = draft->comment.current();
comment.isPublic = !comment.isPublic;
draft->comment = std::move(comment);
});
draft->comment.value() | rpl::map([](const SendComment &comment) {
return comment.isPublic;
}) | rpl::distinct_until_changed() | rpl::on_next([=](bool isPublic) {
const auto icon = isPublic
? &st::walletSendConfirmLockOff
: &st::walletSendConfirmLockOn;
lock->setIconOverride(icon, icon);
}, lock->lifetime());
draft->encryptable.value() | rpl::on_next([=](bool encryptable) {
lock->setVisible(encryptable);
}, lock->lifetime());
field->widthValue() | rpl::on_next([=, &st](int) {
lock->moveToRight(
0,
st.textMargins.top()
+ (st.style.font->height - lock->height()) / 2);
}, lock->lifetime());
}
struct SendConfirmNotes {
not_null<Ui::RpWidget*> caption;
not_null<Ui::RpWidget*> refusal;
};
SendConfirmNotes AddSendConfirmNotes(
not_null<Ui::GenericBox*> box,
rpl::producer<bool> captionShown,
rpl::producer<QString> refusal) {
const auto caption = box->addRow(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box,
tr::lng_wallet_comment_public(),
st::walletCommentCaptionLabel),
st::walletCommentCaptionMargin),
style::margins());
caption->toggleOn(std::move(captionShown));
caption->finishAnimating();
const auto shown = box->addRow(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box,
rpl::duplicate(refusal),
st::walletCommentErrorLabel),
st::walletCommentCaptionMargin),
style::margins());
shown->toggleOn(std::move(refusal) | rpl::map([](const QString &text) {
return !text.isEmpty();
}));
shown->finishAnimating();
box->addSkip(st::walletSendConfirmBottomSkip);
return { .caption = caption, .refusal = shown };
}
void WalletSendConfirmBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
SendConfirmArgs args) {
const auto session = &show->session();
const auto draft = args.draft;
const auto address = args.address;
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
// An empty title keeps the band the close button stands in.
box->setTitle(rpl::single(QString()));
auto item = TransferItem();
item.amountNano = args.amountNano;
AddWalletLottie(box);
AddDetailsAmountHeader(
box->verticalLayout(),
item,
st::walletDetailsLottieSkip,
st::walletDetailsAmountBottomSkip,
FiatRateValue(session));
const auto details = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
style::margins());
const auto openedAt = base::unixtime::now();
struct State {
std::optional<SendConfirmFee> built;
SendConfirmFee pending;
SendConfirmFee shown;
bool scheduled = false;
};
const auto state = box->lifetime().make_state<State>();
rpl::combine(
rpl::duplicate(args.fee),
rpl::duplicate(args.busy)
) | rpl::filter([](const SendConfirmFee &fee, bool busy) {
return !busy;
}) | rpl::map([](const SendConfirmFee &fee, auto) {
return fee;
}) | rpl::distinct_until_changed() | rpl::on_next([=](
const SendConfirmFee &fee) {
const auto lost = !fee.feeNano && !fee.pending;
const auto keep = lost && state->built && state->built->feeNano;
state->pending = keep ? *state->built : fee;
if (state->scheduled) {
return;
}
state->scheduled = true;
Ui::PostponeCall(box, [=] {
state->scheduled = false;
// The counted value stands while the next one is counted.
auto shown = state->pending;
if (shown.feeNano) {
state->shown = shown;
} else if (shown.pending && state->shown.feeNano) {
shown = state->shown;
}
if (state->built == shown) {
return;
}
state->built = shown;
details->clear();
FillSendConfirmTable(
details,
box->uiShow(),
session,
address,
shown,
openedAt);
});
}, details->lifetime());
const auto field = box->addRow(
object_ptr<Ui::InputField>(
box,
st::walletSendConfirmCommentField,
Ui::InputField::Mode::NoNewlines,
tr::lng_wallet_send_comment_optional(),
draft->comment.current().text),
st::walletCommentFieldMargin);
BindCommentField(field, draft);
AddSendCommentLock(field, st::walletSendConfirmCommentField, draft);
ApplyCommentLimit(
field,
st::walletSendConfirmLock.width + st::walletCommentLimitSkip);
rpl::duplicate(args.busy) | rpl::on_next([=](bool busy) {
field->setDisabled(busy);
}, field->lifetime());
AddSendConfirmNotes(
box,
draft->comment.value() | rpl::map([](const SendComment &comment) {
return comment.isPublic && !comment.text.isEmpty();
}),
std::move(args.refusal));
const auto send = [=, callback = args.send] {
callback(state->built ? *state->built : SendConfirmFee());
};
const auto button = box->addButton(
BusyFooterLabel(
tr::lng_wallet_send_amount(
lt_amount,
rpl::single(Ui::FormatTonAmount(args.amountNano).full)),
rpl::duplicate(args.busy)),
send).data();
rpl::combine(
std::move(args.canSend),
std::move(args.fee),
rpl::duplicate(args.busy)
) | rpl::on_next([=](
bool canSend,
const SendConfirmFee &fee,
bool busy) {
SetButtonDisabledLook(button, !busy && (!canSend || fee.pending));
}, button->lifetime());
AddBusyFooterSpinner(button, std::move(args.busy));
field->submits() | rpl::on_next(send, field->lifetime());
box->setFocusCallback([=] { field->setFocusFast(); });
AddBoxCloseButton(box);
}
struct CollectibleTransfer {
QString address;
std::shared_ptr<CollectibleMedia> media;
};
struct CollectibleRecipient {
QString destination;
QString tonName;
UserId userId;
bool bounce = true;
};
[[nodiscard]] rpl::producer<QString> CollectibleNameValue(
std::shared_ptr<CollectibleMedia> media,
QString address) {
return rpl::single(rpl::empty) | rpl::then(
media->changed(
) | rpl::filter([=](const QString &changed) {
return (changed == address);
}) | rpl::to_empty
) | rpl::map([=] {
return CollectibleTitleText(media->view(address)).text;
});
}
void CollectibleTransferBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
std::shared_ptr<const CollectibleTransfer> collectible,
CollectibleRecipient recipient) {
const auto session = &show->session();
const auto weakSession = base::make_weak(session);
const auto wallet = &session->wallet();
const auto weak = base::make_weak(box);
const auto identity = wallet->transferWalletIdentity();
const auto sessionValid = [=] {
return weakSession
&& show->valid()
&& (&show->session() == session);
};
const auto current = [=] {
return weak
&& sessionValid()
&& identity
&& wallet->transferWalletIdentityCurrent(*identity);
};
if (!current()) {
box->closeBox();
return;
}
const auto address = collectible->address;
const auto media = collectible->media;
box->setWidth(st::boxWideWidth);
box->setStyle(st::walletDetailsBox);
box->setNoContentMargin(true);
AddBoxCloseButton(box);
const auto openedAt = base::unixtime::now();
const auto item = TransferItem{
.kind = TransferItem::Kind::Collectible,
.incoming = false,
.counterparty = recipient.destination,
.counterpartyBounceable = recipient.bounce,
.counterpartyName = recipient.tonName,
.counterpartyPeer = (recipient.userId
? peerFromUser(recipient.userId).value
: quint64()),
.collectible = address,
.date = openedAt,
.status = TransferItem::Status::Success,
};
media->resolve(address);
AddDetailsCollectibleHeader(
box->verticalLayout(),
session,
media,
item,
st::walletDetailsAmountBottomSkip / 2,
false);
const auto details = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
style::margins());
auto owned = object_ptr<Ui::InputField>(
box,
st::walletCollectibleCommentField,
Ui::InputField::Mode::NoNewlines,
tr::lng_wallet_send_comment_optional());
const auto field = owned.data();
box->addRow(
MakeCommentBubble(box, std::move(owned), st::windowBg),
st::walletCommentFieldMargin);
ApplyCommentLimit(field);
using ShownFee = std::pair<std::optional<int64>, DetailsFee>;
struct State {
std::shared_ptr<const PreparedSend> prepared;
KeyAuthorization authorization;
base::Timer signingWait;
rpl::variable<QString> comment;
rpl::variable<std::optional<int64>> fee;
rpl::variable<SendError> error = SendError::None;
rpl::variable<bool> estimating = false;
rpl::variable<bool> sending = false;
Fn<void()> estimate;
Fn<void()> continueSend;
ShownFee table;
std::optional<ShownFee> built;
uint64 owner = 0;
uint64 revision = 0;
int refusals = 0;
bool tableQueued = false;
bool requoteQueued = false;
bool requoteForced = false;
bool unlocking = false;
bool submitted = false;
bool handedOver = false;
rpl::lifetime signingLifetime;
// WHY: last, so the key ladder cancelled first finds the rest alive.
rpl::lifetime keyLifetime;
};
const auto state = box->lifetime().make_state<State>();
state->owner = wallet->createPreviewOwner(box->lifetime());
wallet->transferWalletIdentityChanges(
) | rpl::filter([=] {
return !current();
}) | rpl::take(1) | rpl::on_next([=] {
box->closeBox();
}, box->lifetime());
const auto args = [=] {
return SendArgs{
.destination = recipient.destination,
.collectible = address,
.userId = recipient.userId,
.comment = SendComment{
.text = state->comment.current(),
.isPublic = true,
},
.bounce = recipient.bounce,
};
};
state->estimate = [=] {
if (state->estimating.current()) {
state->requoteQueued = true;
return;
}
const auto text = state->comment.current();
if (!CommentFits(text)) {
state->prepared = nullptr;
state->fee = std::nullopt;
state->error = SendError::CommentTooLong;
return;
}
const auto revision = ++state->revision;
const auto signing = wallet->signingReady();
state->estimating = true;
wallet->estimateFee(
KeyAuthorization(),
state->owner,
args(),
crl::guard(box, [=](FeeResult result) {
if (revision != state->revision) {
return;
}
const auto forced = base::take(state->requoteForced);
const auto requote = base::take(state->requoteQueued)
&& (forced
|| text != state->comment.current()
|| signing != wallet->signingReady());
const auto error = (result.error == SendError::None
&& !result.prepared)
? SendError::Failed
: result.error;
if (requote || error == SendError::QuoteExpired) {
state->prepared = nullptr;
state->estimating = false;
state->estimate();
} else {
state->prepared = std::move(result.prepared);
state->fee = (error == SendError::None)
? std::make_optional(result.feeNano)
: std::nullopt;
state->error = error;
state->estimating = false;
}
if (state->sending.current() && !state->submitted) {
state->continueSend();
}
}));
};
wallet->signingReadyValue(
) | rpl::skip(1) | rpl::on_next([=](bool ready) {
if (ready) {
state->signingWait.cancel();
}
if (!state->submitted) {
state->prepared = nullptr;
state->estimate();
}
}, box->lifetime());
rpl::merge(
wallet->sendStateValue() | rpl::skip(1) | rpl::to_empty,
wallet->presenceValue() | rpl::skip(1) | rpl::to_empty
) | rpl::on_next([=] {
if (state->sending.current() || state->submitted) {
return;
}
state->prepared = nullptr;
state->requoteForced = true;
state->estimate();
}, box->lifetime());
rpl::merge(
wallet->balanceNanoValue() | rpl::skip(1) | rpl::to_empty,
wallet->custodyUpdates()
) | rpl::on_next([=] {
if (state->sending.current()
|| state->submitted
|| (!state->estimating.current()
&& state->prepared
&& state->error.current() == SendError::None)) {
return;
}
state->prepared = nullptr;
state->requoteForced = true;
state->estimate();
}, box->lifetime());
const auto insufficient = [](std::optional<int64> fee, int64 balance) {
return fee && (CollectibleTransferAttachedNanos() + *fee > balance);
};
const auto refusalText = [=](
SendError error,
std::optional<int64> fee,
int64 balance) {
if (error == SendError::InsufficientBalance
|| error == SendError::InsufficientFees
|| insufficient(fee, balance)) {
return SendErrorText(
SendError::InsufficientFees,
TransferMinNanos(session));
} else if (error == SendError::SigningUnavailable
|| error == SendError::QuoteExpired
|| error == SendError::None) {
return QString();
}
return SendErrorText(error, TransferMinNanos(session));
};
const auto refusalNow = [=] {
return refusalText(
state->error.current(),
state->fee.current(),
wallet->balanceNano());
};
rpl::combine(
state->fee.value(),
state->estimating.value(),
state->error.value()
) | rpl::map([](
std::optional<int64> fee,
bool estimating,
SendError error) {
const auto loading = estimating
|| (error == SendError::None)
|| (error == SendError::SigningUnavailable);
const auto shown = fee
? DetailsFee::Known
: loading
? DetailsFee::Loading
: DetailsFee::Failed;
return ShownFee(fee, shown);
}) | rpl::distinct_until_changed() | rpl::on_next([=](ShownFee shown) {
state->table = shown;
if (state->tableQueued) {
return;
}
state->tableQueued = true;
Ui::PostponeCall(box, [=] {
state->tableQueued = false;
if (state->built == state->table) {
return;
}
state->built = state->table;
auto counted = item;
counted.feeNano = state->table.first;
const auto top = box->scrollTop();
const auto atBottom = (top + box->scrollHeight()
>= box->verticalLayout()->height());
details->clear();
AddDetailsTable(box, details, show, counted, state->table.second);
box->scrollToY(atBottom ? ScrollMax : top);
});
}, details->lifetime());
const auto notes = AddSendConfirmNotes(
box,
state->comment.value() | rpl::map([](const QString &text) {
return !text.isEmpty();
}),
rpl::combine(
state->error.value(),
state->fee.value(),
wallet->balanceNanoValue(),
rpl::single(rpl::empty) | rpl::then(Lang::Updated())
) | rpl::map([=](
SendError error,
std::optional<int64> fee,
int64 balance,
rpl::empty_value) {
return refusalText(error, fee, balance);
}));
rpl::combine(
notes.caption->heightValue(),
notes.refusal->heightValue()
) | rpl::map([](int captionHeight, int refusalHeight) {
return captionHeight + refusalHeight;
}) | rpl::combine_previous(
) | rpl::filter([](int was, int now) {
return (now > was);
}) | rpl::to_empty | rpl::on_next([=] {
box->scrollToY(ScrollMax);
}, box->lifetime());
const auto pressable = [=] {
return !state->sending.current()
&& !state->estimating.current()
&& refusalNow().isEmpty()
&& (state->prepared
|| state->error.current() == SendError::SigningUnavailable);
};
const auto press = [=] {
if (!pressable()) {
return;
}
state->sending = true;
state->refusals = 0;
state->continueSend();
};
const auto button = box->addButton(
BusyFooterLabel(
tr::lng_wallet_collectible_send(
lt_name,
CollectibleNameValue(media, address)),
state->sending.value()),
press).data();
rpl::combine(
state->sending.value(),
state->estimating.value(),
state->error.value(),
state->fee.value(),
wallet->balanceNanoValue()
) | rpl::to_empty | rpl::on_next([=] {
SetButtonDisabledLook(
button,
!state->sending.current() && !pressable());
}, button->lifetime());
AddBusyFooterSpinner(button, state->sending.value());
field->changes() | rpl::on_next([=] {
state->comment = field->getLastText();
if (!state->sending.current()) {
state->estimate();
}
}, field->lifetime());
field->submits() | rpl::on_next(press, field->lifetime());
state->sending.value() | rpl::on_next([=](bool sending) {
field->setDisabled(sending);
}, field->lifetime());
box->setFocusCallback([=] { field->setFocusFast(); });
const auto stop = [=] {
state->authorization = {};
state->signingWait.cancel();
state->signingLifetime.destroy();
state->sending = false;
};
const auto acquireKey = [=] {
state->keyLifetime.destroy();
state->unlocking = true;
AcquireWalletKey(
show,
current,
state->keyLifetime,
crl::guard(box, [=](KeyAuthorization auth) {
state->unlocking = false;
if (!state->sending.current() || state->submitted) {
return;
} else if (!auth.valid()) {
stop();
return;
}
state->authorization = std::move(auth);
state->continueSend();
}),
tr::lng_wallet_restore_text());
};
const auto awaitSigning = [=] {
if (state->signingWait.isActive()) {
return;
}
state->signingLifetime.destroy();
wallet->signingReadyValue(
) | rpl::filter([](bool ready) {
return ready;
}) | rpl::take(1) | rpl::on_next([=] {
state->continueSend();
}, state->signingLifetime);
state->signingWait.callOnce(kSigningReadyTimeout);
};
state->signingWait.setCallback([=] {
stop();
state->error = SendError::Failed;
});
const auto started = [=](SendStarted value) {
state->handedOver = true;
const auto panel = wallet->panel();
if (panel && box->window() == panel->window()) {
wallet->setWindowSend(value.operationId);
}
show->hideLayer();
};
const auto sent = [=](SendError error) {
if (error == SendError::KeyChanged) {
if (weak && !state->handedOver) {
weak->closeBox();
}
if (sessionValid()) {
ShowWalletKeyChanged(show);
}
return;
} else if (!current() || state->handedOver) {
return;
} else if (error == SendError::None
|| error == SendError::SubmissionUnknown) {
show->hideLayer();
return;
}
state->submitted = false;
const auto retry = (error == SendError::QuoteExpired)
|| (error == SendError::SigningUnavailable);
if (retry && ++state->refusals <= kSendRefusalRetries) {
state->estimate();
return;
}
stop();
state->error = (error == SendError::QuoteExpired)
? SendError::Failed
: error;
};
state->continueSend = [=] {
if (!state->sending.current()
|| state->submitted
|| state->unlocking) {
return;
} else if (!current()) {
stop();
return;
} else if (state->estimating.current()) {
return;
} else if (!state->authorization.valid()) {
acquireKey();
return;
} else if (!wallet->signingReady()) {
awaitSigning();
return;
} else if (!state->prepared) {
const auto error = state->error.current();
const auto retry = (error == SendError::None)
|| (error == SendError::SigningUnavailable);
if (retry && ++state->refusals <= kSendRefusalRetries) {
state->estimate();
return;
}
stop();
if (retry) {
state->error = SendError::Failed;
}
return;
} else if (!refusalNow().isEmpty()) {
stop();
return;
}
state->submitted = true;
wallet->send(
state->authorization,
base::take(state->prepared),
crl::guard(session, sent),
crl::guard(session, crl::guard(box, started)));
};
state->estimate();
}
void ShowSendRecipientWallet(
std::shared_ptr<Ui::Show> show,
not_null<UserData*> user,
const QString &address,
Fn<void()> profile);
void WalletSendBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
std::optional<SendFlow> initial,
UserData *user,
Fn<void()> sent,
Fn<void()> notReady,
int64 amountNano,
base::weak_qptr<Ui::BoxContent> origin) {
Expects(user || initial);
// The box this one was opened over comes back unless the user leaves.
const auto discardOrigin = [=] {
if (const auto strong = origin.get()) {
strong->closeBox();
}
};
const auto self = (!user
&& SendsToOwnWallet(&show->session(), initial->destination))
? show->session().user().get()
: nullptr;
const auto recipient = user ? user : self;
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
const auto weak = base::make_weak(box.get());
const auto openProfile = recipient
? Fn<void()>([=] {
const auto window = MakeChatShow(show, true)->resolveWindow();
if (!window) {
return;
}
const auto peer = recipient;
if (weak
&& weak->hasDelegate()
&& window->widget()->window() == weak->window()) {
discardOrigin();
weak->closeBox();
}
window->showPeerInfo(peer);
window->window().activate();
})
: nullptr;
if (recipient) {
// WHY: the box title label is private inside lib_ui and takes no
// click filter, so the name is clickable only while this file owns
// the label itself.
const auto wrap = box->setPinnedToTopContent(
object_ptr<Ui::FixedHeightWidget>(
box,
st::boxTitleHeight - st::boxTopMargin));
const auto title = Ui::CreateChild<Ui::FlatLabel>(
wrap,
tr::lng_wallet_send_user_title(
lt_user,
Info::Profile::NameValue(recipient) | rpl::map([](QString name) {
return tr::link(name);
}),
tr::marked),
st::boxTitle);
title->setClickHandlerFilter([=](const auto &...) {
const auto onstack = openProfile;
onstack();
return false;
});
rpl::combine(
wrap->widthValue(),
title->naturalWidthValue()
) | rpl::on_next([=](int width, int) {
const auto buttons = st::boxTitleClose.width
+ st::boxTitleMenu.width;
title->resizeToNaturalWidth(
width - 2 * st::boxTitlePosition.x() - buttons);
title->moveToLeft(
st::boxTitlePosition.x(),
st::boxTitlePosition.y() - st::boxTopMargin,
width);
}, title->lifetime());
} else {
box->setTitle(initial->tonName.isEmpty()
? SendRecipientTitle(box, initial->displayForm, Qt::ElideMiddle)
: SendRecipientTitle(box, initial->tonName, Qt::ElideRight));
}
AddBoxCloseButton(box);
const auto session = &show->session();
const auto weakSession = base::make_weak(session);
const auto wallet = &session->wallet();
struct State {
std::optional<SendFlow> flow;
std::optional<SendQuoteDependencies> previewDependencies;
base::unique_qptr<Ui::PopupMenu> menu;
base::weak_qptr<Ui::GenericBox> commentBox;
base::weak_qptr<Ui::GenericBox> confirmBox;
std::optional<TransferWalletIdentity> senderIdentity;
QByteArray recipientKey;
uint64 previewRevision = 0;
uint64 loadRevision = 0;
base::Timer loadDeadline;
rpl::lifetime previewLifetime;
bool forceIssued = false;
bool terminal = false;
bool recomputeQueued = false;
bool recipientRequested = false;
bool feeRefreshQueued = false;
rpl::variable<bool> loading = false;
rpl::variable<QString> loadError;
rpl::variable<bool> silentFailure = false;
rpl::variable<int64> amount = 0;
rpl::variable<int64> fee = 0;
rpl::variable<int64> minTransfer = kTransferMinNanosDefault;
std::shared_ptr<SendDraft> draft = std::make_shared<SendDraft>();
rpl::variable<bool> sending = false;
std::optional<SendQuoteDependencies> sendRequest;
std::optional<uint64> sendExpiresAt;
std::optional<SendConfirmFee> heldFee;
KeyAuthorization sendAuthorization;
KeyAuthorization heldAuthorization;
base::Timer heldTimeout;
int sendRefusals = 0;
bool unlocking = false;
bool submitted = false;
bool handedOver = false;
Fn<void()> continueSend;
rpl::variable<bool> previewInsufficient = false;
rpl::variable<SendError> previewError = SendError::None;
rpl::variable<bool> insufficient = false;
rpl::variable<bool> unfunded = false;
rpl::variable<bool> canSend = false;
rpl::variable<bool> raisable = false;
std::shared_ptr<KeyContext> keyContext;
base::Timer signingWait;
bool signingTimedOut = false;
rpl::variable<FiatRate> rate;
rpl::variable<bool> entryFiat = false;
QString previousCurrency;
bool settingUnitText = false;
bool closed = false;
Fn<void()> swapUnit;
};
const auto state = box->lifetime().make_state<State>();
if (initial) {
state->draft = initial->draft;
}
const auto draft = state->draft;
const auto previewOwner = wallet->createPreviewOwner(state->previewLifetime);
state->senderIdentity = user
? std::nullopt
: wallet->transferWalletIdentity();
state->loading = !state->senderIdentity;
state->rate = FiatRateValue(session);
state->minTransfer = TransferMinNanos(session);
const auto userId = user ? peerToUser(user->id) : UserId();
const auto sessionValid = [=] {
return weakSession
&& show->valid()
&& (&show->session() == session);
};
const auto userError = [=] {
if (!sessionValid()
|| (state->senderIdentity
&& !wallet->transferWalletIdentityCurrent(
*state->senderIdentity))) {
return u"WALLET_NOT_READY"_q;
}
const auto error = wallet->userAddresses().forceResolveError(userId);
if (!error.isEmpty()) {
return error;
} else if (session->data().userLoaded(userId) != user) {
return u"WALLET_USER_INVALID"_q;
} else if (state->flow
&& (!user->gramAddress()
|| *user->gramAddress() != state->flow->destination)) {
return u"WALLET_ADDRESS_INVALID"_q;
}
return QString();
};
const auto originValid = [=] {
return weak
&& !state->closed
&& !state->terminal
&& sessionValid()
&& state->senderIdentity
&& wallet->transferWalletIdentityCurrent(*state->senderIdentity)
&& !state->loading.current()
&& state->loadError.current().isEmpty()
&& (!user || (state->flow && userError().isEmpty()));
};
const auto receive = [=] {
if (originValid()
|| (state->unfunded.current()
&& !state->closed
&& !state->terminal
&& sessionValid())) {
ShowWalletReceiveBox(session, box->uiShow());
}
};
const auto editComment = [=] {
if (!originValid()
|| state->commentBox
|| state->sending.current()) {
return;
}
auto editor = Box(
WalletSendCommentBox,
draft,
originValid,
draft->encryptable.value());
const auto raw = editor.data();
state->commentBox = base::make_weak(raw);
raw->boxClosing() | rpl::on_next([=] {
if (weak && state->commentBox.get() == raw) {
state->commentBox = nullptr;
}
}, raw->lifetime());
box->uiShow()->showBox(std::move(editor));
};
const auto toggle = box->addTopButton(st::boxTitleMenu);
toggle->setClickedCallback([=] {
if (!originValid() || state->menu) {
return;
}
state->menu = base::make_unique_q<Ui::PopupMenu>(
box,
st::popupMenuWithIcons);
const auto raw = state->menu.get();
raw->setDestroyedCallback(crl::guard(toggle, [=] {
toggle->setForceRippled(false);
}));
toggle->setForceRippled(true);
raw->addAction(
Ui::Text::FixAmpersandInAction(
tr::lng_wallet_add_funds(tr::now)),
receive,
&st::menuIconAdd);
raw->addAction(
Ui::Text::FixAmpersandInAction(
tr::lng_wallet_comment_title(tr::now)),
editComment,
&st::menuIconChatBubble);
raw->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
const auto scope = WindowPaletteScope(box);
raw->popup(toggle->mapToGlobal(QPoint(
toggle->width(),
toggle->height())));
});
const auto quoteDependencies = [=] {
const auto validSession = sessionValid();
return SendQuoteDependencies{
.senderIdentity = validSession
? wallet->transferWalletIdentity()
: std::nullopt,
.gaslessTerms = validSession
? wallet->gaslessTerms()
: GaslessTerms(),
.destination = state->flow ? state->flow->destination : QString(),
.comment = draft->comment.current(),
.custody = validSession
? wallet->deviceCustodyState()
: DeviceCustodyState(),
.recipientPublicKey = state->recipientKey,
.userId = userId,
.amountNano = state->amount.current(),
.balanceNano = validSession ? wallet->balanceNano() : 0,
.minTransferNano = state->minTransfer.current(),
.bounce = state->flow && state->flow->bounce,
.ready = validSession
&& wallet->presenceCurrent() == Presence::Ready,
.valid = originValid(),
};
};
const auto stopSending = [=] {
state->sending = false;
state->sendRequest.reset();
state->sendExpiresAt.reset();
state->heldFee.reset();
state->sendAuthorization = {};
state->sendRefusals = 0;
state->submitted = false;
state->signingWait.cancel();
state->signingTimedOut = false;
};
const auto scheduleContinueSend = [=] {
Ui::PostponeCall(box, [=] { state->continueSend(); });
};
// The signing client is awaited for a bounded time only. A journal
// recovery that keeps failing would otherwise hold the press forever,
// while letting the press through after the bound has the session
// refuse it typed, the way it did before the wait existed.
const auto awaitSigning = [=] {
if (!state->signingWait.isActive()) {
state->signingWait.callOnce(kSigningReadyTimeout);
}
};
state->signingWait.setCallback([=] {
state->signingTimedOut = true;
scheduleContinueSend();
});
const auto dropHeldKey = [=] {
state->heldAuthorization = {};
state->heldTimeout.cancel();
};
state->heldTimeout.setCallback(dropHeldKey);
const auto failLoading = [=](const QString &error, bool silent = false) {
if (state->closed || state->terminal) {
return;
}
state->terminal = true;
state->silentFailure = silent;
stopSending();
++state->loadRevision;
state->loadDeadline.cancel();
++state->previewRevision;
state->previewLifetime.destroy();
draft->preparing = false;
draft->quote = std::nullopt;
draft->authorization = {};
draft->privateEpoch.reset();
state->flow = std::nullopt;
state->loadError = error.isEmpty() ? u"WALLET_ADDRESS_INVALID"_q : error;
state->loading = false;
if (!silent) {
show->showToast(SendUserLoadErrorText(state->loadError.current()));
}
box->closeBox();
};
box->boxClosing() | rpl::on_next([=] {
state->closed = true;
if (const auto confirm = base::take(state->confirmBox)) {
if (confirm->hasDelegate()) {
confirm->closeBox();
}
}
if (const auto context = base::take(state->keyContext)) {
context->cancel();
}
++state->loadRevision;
state->loadDeadline.cancel();
++state->previewRevision;
state->previewLifetime.destroy();
draft->preparing = false;
draft->quote = std::nullopt;
draft->authorization = {};
draft->privateEpoch.reset();
state->sendAuthorization = {};
dropHeldKey();
state->flow.reset();
}, box->lifetime());
const auto entrySeparator = [=] {
if (!state->entryFiat.current()) {
return Ui::TonAmountSeparator();
}
const auto rule = Ui::LookupCurrencyRule(
state->rate.current().currency);
return QString(QChar(rule.decimal));
};
const auto inner = box->verticalLayout();
auto address = user
? rpl::producer<QString>(state->loading.value(
) | rpl::map([=](bool loading) {
return (!loading && state->flow)
? state->flow->displayForm
: QString();
}))
: rpl::producer<QString>(rpl::single(initial->displayForm));
inner->add(
object_ptr<SendRecipientCard>(
inner,
box->uiShow(),
recipient,
std::move(address),
(recipient
? Fn<void()>([=] {
if (!state->flow) {
return;
}
ShowSendRecipientWallet(
box->uiShow(),
recipient,
state->flow->displayForm,
openProfile);
})
: nullptr)),
style::margins(
st::boxRowPadding.left(),
st::walletSendUserCardTopSkip,
st::boxRowPadding.right(),
0),
style::al_justify);
auto helper = Ui::Text::CustomEmojiHelper();
const auto gramMark = GramMark(
helper,
st::walletSendUserFiatButton.style.font);
auto equivalent = rpl::combine(
state->amount.value(),
state->entryFiat.value(),
state->rate.value()
) | rpl::map([=](int64 amount, bool fiat, const FiatRate &rate) {
if (fiat) {
const auto formatted = Ui::FormatTonAmount(amount);
return AmountLabel{
.prefix = TextWithEntities(gramMark).append(u" "_q),
.amount = formatted.full,
.decimal = formatted.separator,
};
}
const auto rule = Ui::LookupCurrencyRule(rate.currency);
return AmountLabel{
.prefix = tr::marked(QChar('~')),
.amount = FormatFiatAmount(amount, rate),
.decimal = QString(QChar(rule.decimal)),
.suffix = u" "_q + rate.currency,
.unit = rate.currency,
};
});
const auto amountField = AddAmountField(
inner,
st::walletSendUserCardAmountSkip,
{
.value = std::min(
initial ? initial->amountNano : amountNano,
kMaxAmountNano),
.fractionDigits = [=] {
return state->entryFiat.current()
? Ui::LookupCurrencyRule(
state->rate.current().currency).exponent
: 9;
},
.separator = entrySeparator,
.entryFiat = state->entryFiat.value(),
.currency = state->rate.value(
) | rpl::map([](const FiatRate &rate) {
return rate.currency;
}) | rpl::distinct_until_changed(),
.equivalent = std::move(equivalent),
.equivalentContext = helper.context(),
.swap = [=] { state->swapUnit(); },
.equivalentShown = rpl::combine(
state->loading.value(),
state->loadError.value()
) | rpl::map([](bool loading, const QString &error) {
return !loading && error.isEmpty();
}),
});
const auto comment = inner->add(
object_ptr<Ui::SlideWrap<SendCommentBubble>>(
inner,
object_ptr<SendCommentBubble>(
inner,
draft->comment.value(),
rpl::combine(
state->sending.value(),
state->loading.value(),
state->loadError.value()
) | rpl::map([=](bool sending, bool, const QString &) {
return !sending && originValid();
}),
editComment)),
style::margins(),
style::al_justify);
comment->toggleOn(draft->comment.value() | rpl::map([](
const SendComment &value) {
return !value.text.isEmpty();
}));
comment->finishAnimating();
const auto publicWarning = inner->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
inner,
object_ptr<Ui::FlatLabel>(
inner,
tr::lng_wallet_comment_public(),
st::walletSendUserBalanceLabel),
style::margins(
st::walletSendFieldMargin.left(),
0,
st::walletSendFieldMargin.right(),
st::walletSendFieldMargin.bottom())),
style::margins(),
style::al_justify);
publicWarning->entity()->setTryMakeSimilarLines(true);
publicWarning->toggleOn(draft->comment.value() | rpl::map([](
const SendComment &value) {
return value.isPublic && !value.text.isEmpty();
}));
publicWarning->finishAnimating();
const auto updateAmount = [=] {
// The confirmation on top shows this amount, so a rate waits for it.
if (state->settingUnitText || state->confirmBox) {
return;
}
const auto parsed = Ui::ParseTonAmountString(
amountField->getLastText(),
entrySeparator()).value_or(0);
const auto rate = state->rate.current();
state->amount = !state->entryFiat.current()
? parsed
: rate.available()
? std::min(
int64(std::clamp(
base::SafeRound(double(parsed) / rate.perGram),
0.,
double(kMaxAmountNano))),
kMaxAmountNano)
: 0;
};
amountField->changes(
) | rpl::on_next(updateAmount, amountField->lifetime());
updateAmount();
const auto fiatUnitsText = [=](int64 units, int64 quantum) {
const auto formatted = Ui::FormatTonAmount(
units * quantum,
Ui::TonFormatFlag::Simple);
auto result = formatted.wholeString;
if (!formatted.nanoString.isEmpty()) {
result += entrySeparator() + formatted.nanoString;
}
return result;
};
const auto renderUnitText = [=] {
const auto amount = state->amount.current();
if (!state->entryFiat.current()) {
return amount
? Ui::FormatTonAmount(
amount,
Ui::TonFormatFlag::Simple).full
: QString();
}
const auto rate = state->rate.current();
const auto quantum = FiatMinorUnitNanos(rate.currency);
const auto maxUnits = kMaxFiatUnits * (Ui::kNanosInOne / quantum);
const auto units = int64(std::min(
base::SafeRound(amount * rate.perGram / double(quantum)),
double(maxUnits)));
if (!units) {
return QString();
}
return fiatUnitsText(units, quantum);
};
const auto setUnitText = [=] {
state->settingUnitText = true;
Ui::PostponeCall(amountField, [=] {
state->settingUnitText = false;
});
amountField->setText(renderUnitText());
amountField->setFocusFast();
};
const auto switchEntryUnit = [=](bool fiat) {
if (state->entryFiat.current() == fiat
|| (fiat && !state->rate.current().available())) {
return;
}
state->entryFiat = fiat;
setUnitText();
};
const auto raiseToMinimum = [=] {
const auto minimum = state->minTransfer.current();
if (!state->entryFiat.current()) {
amountField->setText(Ui::FormatTonAmount(
minimum,
Ui::TonFormatFlag::Simple).full);
} else {
const auto rate = state->rate.current();
if (!rate.available()) {
return;
}
const auto quantum = FiatMinorUnitNanos(rate.currency);
const auto maxUnits = kMaxFiatUnits * (Ui::kNanosInOne / quantum);
const auto units = int64(std::min(
std::ceil(double(minimum) * rate.perGram / double(quantum)),
double(maxUnits)));
amountField->setText(fiatUnitsText(units, quantum));
}
amountField->setFocusFast();
};
state->swapUnit = [=] { switchEntryUnit(!state->entryFiat.current()); };
state->previousCurrency = state->rate.current().currency;
state->rate.value() | rpl::on_next([=](const FiatRate &now) {
const auto currencyChanged
= (now.currency != state->previousCurrency);
state->previousCurrency = now.currency;
if (!now.available()) {
switchEntryUnit(false);
} else if (state->entryFiat.current()) {
if (currencyChanged) {
setUnitText();
} else {
updateAmount();
}
}
}, box->lifetime());
const auto invalidateFee = [=] {
if (!weak || state->closed || state->terminal) {
return;
}
++state->previewRevision;
draft->preparing = false;
draft->quote = std::nullopt;
draft->authorization = {};
draft->privateEpoch.reset();
state->previewInsufficient = false;
state->previewError = SendError::None;
if (weakSession) {
wallet->cancelFeeEstimate(previewOwner);
}
};
// A comment encrypts for a key Telegram named for the destination, or for
// the one the recipient's `get_public_key` answers. A wallet that was
// never deployed answers neither unless it belongs to a Telegram user, so
// when the engine refuses to encrypt for a destination, the box stops
// offering encryption for it and the comment becomes a public one. The
// send in flight stops there: a comment written to be private is never
// published by the press that was meant to encrypt it.
const auto switchToPlain = [=] {
if (!state->flow) {
return;
}
draft->encryptable = false;
auto comment = draft->comment.current();
if (!comment.isPublic) {
comment.isPublic = true;
draft->comment = std::move(comment);
}
};
const auto prepareFee = [=](KeyAuthorization authorization) {
if (!originValid() || draft->preparing.current()) {
return;
}
invalidateFee();
const auto dependencies = quoteDependencies();
state->previewDependencies = dependencies;
const auto revision = state->previewRevision;
// A key just acquired for this press is followed by the client swap
// to the signing one, and an estimate refused in that window is not
// the press failing: the press waits for the signing client and
// estimates again under the same authorization; a hidden refusal
// during a press is the press's to state, never a silent stop.
const auto fail = [=](SendError error) {
if (revision != state->previewRevision) {
return;
}
const auto swapping = (error == SendError::SigningUnavailable)
&& state->sendAuthorization.valid()
&& !wallet->signingReady()
&& !state->signingTimedOut;
const auto handOff = !swapping
&& (error == SendError::SigningUnavailable)
&& state->sending.current()
&& !state->submitted;
if (swapping) {
awaitSigning();
} else if (!state->submitted && !handOff) {
stopSending();
}
invalidateFee();
state->previewError = error;
if (handOff) {
scheduleContinueSend();
}
};
const auto drifted = [=] {
if (revision != state->previewRevision) {
return;
}
invalidateFee();
state->previewDependencies.reset();
if (state->sending.current() && !state->submitted) {
scheduleContinueSend();
}
};
if (!CommentFits(dependencies.comment.text)) {
fail(SendError::CommentTooLong);
return;
} else if (!dependencies.ready || dependencies.amountNano <= 0) {
fail(SendError::InvalidRequest);
return;
} else if (TransferAmountBelowMinimum(
dependencies.amountNano,
dependencies.minTransferNano)) {
fail(SendError::AmountTooSmall);
return;
} else if (dependencies.amountNano > dependencies.balanceNano) {
fail(SendError::InsufficientBalance);
return;
}
const auto args = SendArgs{
.destination = dependencies.destination,
.amountNano = dependencies.amountNano,
.userId = dependencies.userId,
.comment = dependencies.comment,
.recipientPublicKey = dependencies.recipientPublicKey,
.bounce = dependencies.bounce,
};
const auto isPrivate = !args.comment.text.isEmpty()
&& !args.comment.isPublic;
const auto keyed = isPrivate && authorization.valid();
const auto privateEpoch = keyed
? std::make_optional(wallet->vault().clearEpoch())
: std::nullopt;
draft->preparing = true;
const auto current = [=] {
if (!originValid() || revision != state->previewRevision) {
return false;
}
const auto now = quoteDependencies();
return revision == state->previewRevision && dependencies == now;
};
const auto estimate = crl::guard(session, crl::guard(box, [=](
KeyAuthorization auth) {
if (!current()) {
drifted();
return;
} else if (privateEpoch
&& *privateEpoch != wallet->vault().clearEpoch()) {
fail(SendError::Locked);
return;
}
draft->authorization = auth;
draft->privateEpoch = privateEpoch;
wallet->estimateFee(
std::move(auth),
previewOwner,
args,
crl::guard(session, crl::guard(box, [=](FeeResult result) {
if (!current()) {
drifted();
return;
} else if (privateEpoch
&& (*privateEpoch != wallet->vault().clearEpoch()
|| !draft->authorization.valid()
|| !wallet->vault().unlocked())) {
fail(SendError::Locked);
return;
}
switch (result.error) {
case SendError::None:
if (!result.prepared && (keyed || !isPrivate)) {
fail(SendError::Failed);
return;
}
draft->quote = SendQuote{
.args = args,
.dependencies = dependencies,
.prepared = std::move(result.prepared),
.feeNano = result.feeNano,
.revision = revision,
};
draft->preparing = false;
if (state->sending.current() && !state->submitted) {
scheduleContinueSend();
}
return;
case SendError::InsufficientBalance:
case SendError::InsufficientFees:
fail(result.error);
state->previewInsufficient = true;
return;
case SendError::QuoteExpired:
drifted();
return;
case SendError::CommentEncryptionUnavailable:
if (!state->submitted) {
const auto held = state->sendAuthorization;
stopSending();
state->heldAuthorization = held;
state->heldTimeout.callOnce(kHeldSendKeyTimeout);
}
invalidateFee();
switchToPlain();
return;
case SendError::AmountTooSmall:
case SendError::CommentTooLong:
case SendError::InvalidRequest:
case SendError::PreviousUnresolved:
case SendError::AlreadySending:
case SendError::SigningUnavailable:
case SendError::Locked:
case SendError::Failed:
case SendError::Rejected:
case SendError::DataInvalid:
case SendError::KeyMismatch:
case SendError::KeyChanged:
case SendError::CollectibleUnavailable:
case SendError::CollectibleRejected:
case SendError::Silent:
case SendError::SubmissionUnknown:
fail(result.error);
return;
}
Unexpected("Error value in the send box fee estimate.");
})));
}));
estimate(keyed ? std::move(authorization) : KeyAuthorization());
};
const auto refreshFee = [=] {
if (state->closed || state->terminal) {
return;
} else if (state->unfunded.current()) {
state->previewDependencies.reset();
invalidateFee();
return;
}
const auto dependencies = quoteDependencies();
if (state->previewDependencies == dependencies) {
return;
}
// WHY: an estimate prices the whole input, so a change made while
// one is counting waits for it and is counted once, instead of a
// count queued for every letter typed into the comment.
if (draft->preparing.current() && !state->sending.current()) {
state->feeRefreshQueued = true;
return;
}
state->feeRefreshQueued = false;
state->previewDependencies = dependencies;
invalidateFee();
if (!CommentFits(dependencies.comment.text)) {
state->previewError = SendError::CommentTooLong;
} else if (TransferAmountBelowMinimum(
dependencies.amountNano,
dependencies.minTransferNano)) {
state->previewError = SendError::AmountTooSmall;
} else if (!state->sending.current() && dependencies.amountNano > 0) {
prepareFee({});
}
if (state->sending.current() && !state->submitted) {
scheduleContinueSend();
}
};
// The last counted fee stands while the next one is counted.
draft->quote.value() | rpl::on_next([=](
const std::optional<SendQuote> &quote) {
if (quote) {
state->fee = quote->feeNano;
}
}, box->lifetime());
draft->preparing.changes() | rpl::on_next([=](bool preparing) {
if (preparing || !base::take(state->feeRefreshQueued)) {
return;
}
Ui::PostponeCall(box, refreshFee);
}, box->lifetime());
state->amount.value() | rpl::on_next(refreshFee, box->lifetime());
draft->comment.changes() | rpl::on_next(refreshFee, box->lifetime());
state->loading.changes() | rpl::on_next(refreshFee, box->lifetime());
state->unfunded.changes() | rpl::on_next(refreshFee, box->lifetime());
state->loadError.changes() | rpl::on_next(refreshFee, box->lifetime());
session->appConfig().refreshed() | rpl::on_next([=] {
state->minTransfer = TransferMinNanos(session);
}, box->lifetime());
state->minTransfer.changes() | rpl::on_next(refreshFee, box->lifetime());
wallet->gaslessTermsValue() | rpl::on_next(refreshFee, box->lifetime());
wallet->transferWalletIdentityChanges() | rpl::on_next([=] {
if (!state->senderIdentity && !user && sessionValid()) {
state->senderIdentity = wallet->transferWalletIdentity();
}
refreshFee();
}, box->lifetime());
wallet->signingReadyValue() | rpl::skip(1) | rpl::on_next([=](bool ready) {
if (ready) {
state->signingWait.cancel();
state->signingTimedOut = false;
}
state->previewDependencies.reset();
refreshFee();
}, box->lifetime());
if (!user) {
wallet->custodyUpdates() | rpl::on_next(refreshFee, box->lifetime());
wallet->balanceNanoValue() | rpl::on_next(refreshFee, box->lifetime());
wallet->stateKnownValue() | rpl::on_next(refreshFee, box->lifetime());
wallet->presenceValue() | rpl::on_next(refreshFee, box->lifetime());
}
state->insufficient = rpl::combine(
state->amount.value(),
state->fee.value(),
state->previewInsufficient.value(),
wallet->balanceNanoValue(),
wallet->stateKnownValue(),
wallet->gaslessTermsValue(),
state->unfunded.value()
) | rpl::map([=](
int64 amount,
int64 fee,
bool preview,
int64 balance,
bool known,
const GaslessTerms &terms,
bool unfunded) {
// A fee-free transfer keeps nothing back for the fee, so the whole
// balance is sendable when the offer covers this one.
const auto destination = state->flow
? state->flow->destination
: QString();
const auto reserve = terms.eligible(amount, destination) ? 0 : fee;
return unfunded
|| ((amount > 0)
&& (preview || (known && amount > balance - reserve)));
});
state->canSend = rpl::combine(
state->amount.value(),
state->insufficient.value(),
wallet->stateKnownValue(),
state->previewError.value(),
state->loading.value(),
state->loadError.value()
) | rpl::map([](
int64 amount,
bool insufficient,
bool known,
SendError error,
bool loading,
const QString &loadError) {
return known
&& (amount > 0)
&& !insufficient
&& !loading
&& loadError.isEmpty()
&& (error != SendError::AmountTooSmall)
&& (error != SendError::CommentTooLong)
&& (error != SendError::AlreadySending)
&& (error != SendError::PreviousUnresolved);
});
state->raisable = rpl::combine(
state->amount.value(),
state->minTransfer.value(),
state->loading.value(),
state->loadError.value(),
draft->comment.value(),
state->unfunded.value()
) | rpl::map([](
int64 amount,
int64 minimum,
bool loading,
const QString &loadError,
const SendComment &comment,
bool unfunded) {
return TransferAmountBelowMinimum(amount, minimum)
&& !loading
&& !unfunded
&& loadError.isEmpty()
&& CommentFits(comment.text);
});
const auto balance = inner->add(
object_ptr<Ui::VerticalLayout>(inner),
style::margins(
st::walletSendFieldMargin.left(),
0,
st::walletSendFieldMargin.right(),
st::walletDetailsAmountBottomSkip),
style::al_justify);
const auto balanceWrap = balance->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
balance,
object_ptr<Ui::FlatLabel>(
balance,
rpl::combine(
tr::lng_wallet_send_balance(
lt_amount,
wallet->balanceNanoValue() | rpl::map([](int64 nano) {
return Ui::FormatTonAmount(nano).full;
})),
state->loading.value(),
state->loadError.value()
) | rpl::map([](QString text, bool loading, const QString &error) {
return (loading || !error.isEmpty())
? QString(QChar(0xA0))
: text;
}),
st::walletSendUserBalanceLabel)),
style::al_justify);
balanceWrap->toggleOn(rpl::combine(
state->insufficient.value(),
state->previewError.value(),
state->loading.value(),
state->loadError.value()
) | rpl::map([](
bool insufficient,
SendError error,
bool loading,
const QString &loadError) {
return loading || (!insufficient
&& (error == SendError::None)
&& loadError.isEmpty());
}));
balanceWrap->finishAnimating();
const auto feeWrap = balance->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
balance,
object_ptr<Ui::FlatLabel>(
balance,
tr::lng_wallet_send_network_fee(
lt_amount,
state->fee.value() | rpl::map([](int64 nano) {
return Ui::FormatTonAmount(nano).full;
})),
st::walletSendUserBalanceLabel)),
style::al_justify);
feeWrap->toggleOn(rpl::combine(
state->amount.value(),
state->fee.value(),
wallet->gaslessTermsValue(),
draft->quote.value(),
state->insufficient.value(),
state->previewError.value(),
state->loading.value(),
state->loadError.value()
) | rpl::map([=](
int64 amount,
int64 fee,
const GaslessTerms &terms,
const std::optional<SendQuote> &,
bool insufficient,
SendError error,
bool loading,
const QString &loadError) {
const auto destination = state->flow
? state->flow->destination
: QString();
return (amount > 0)
&& (fee > 0)
&& !terms.eligible(amount, destination)
&& !insufficient
&& (error == SendError::None)
&& !loading
&& loadError.isEmpty();
}));
feeWrap->finishAnimating();
auto showInsufficient = rpl::combine(
state->insufficient.value(),
state->previewError.value(),
state->loading.value(),
state->loadError.value()
) | rpl::map([](
bool insufficient,
SendError error,
bool loading,
const QString &loadError) {
return insufficient
&& (error != SendError::AmountTooSmall)
&& !loading
&& loadError.isEmpty();
});
const auto insufficientWrap = balance->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
balance,
object_ptr<Ui::FlatLabel>(
balance,
tr::lng_wallet_send_error_insufficient(),
st::walletSendUserErrorLabel)),
style::al_justify);
// An error that does not fit one line reads better split evenly than
// with a full line above a single trailing word.
insufficientWrap->entity()->setTryMakeSimilarLines(true);
insufficientWrap->toggleOn(rpl::duplicate(showInsufficient));
insufficientWrap->finishAnimating();
const auto refusalValue = [=](bool insufficientStated) {
return rpl::combine(
state->previewError.value(),
state->insufficient.value(),
state->loading.value(),
state->loadError.value(),
state->silentFailure.value(),
state->minTransfer.value(),
rpl::single(rpl::empty) | rpl::then(Lang::Updated())
) | rpl::map([insufficientStated](
SendError error,
bool insufficient,
bool loading,
QString loadError,
bool silent,
int64 minTransfer,
rpl::empty_value) {
if (loading || silent) {
return QString();
} else if (!loadError.isEmpty()) {
return SendUserLoadErrorText(loadError);
} else if (error != SendError::AmountTooSmall
&& (insufficient
|| error == SendError::InsufficientBalance
|| error == SendError::InsufficientFees)) {
return insufficientStated
? SendErrorText(SendError::InsufficientFees, minTransfer)
: QString();
} else if (error == SendError::SigningUnavailable) {
// A device without the key is not stated: the send press
// acquires it, and the estimate is refused only while no
// client can serve the preview at all.
return QString();
}
return SendErrorText(error, minTransfer);
});
};
auto refusalText = refusalValue(false);
const auto refusalWrap = balance->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
balance,
object_ptr<Ui::FlatLabel>(
balance,
rpl::duplicate(refusalText),
st::walletSendUserErrorLabel)),
style::al_justify);
refusalWrap->entity()->setTryMakeSimilarLines(true);
refusalWrap->toggleOn(std::move(refusalText) | rpl::map([](
const QString &text) {
return !text.isEmpty();
}));
refusalWrap->finishAnimating();
const auto depositWrap = balance->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
balance,
object_ptr<Ui::VerticalLayout>(balance)),
style::al_justify);
const auto depositInner = depositWrap->entity();
const auto deposit = depositInner->add(
object_ptr<Ui::RoundButton>(
depositInner,
tr::lng_wallet_add_funds(),
st::defaultTableSmallButton),
style::margins(0, st::walletDetailsAmountMinorSkip, 0, 0),
style::al_top);
deposit->setTextTransform(Ui::RoundButtonTextTransform::NoTransform);
deposit->setClickedCallback(receive);
depositWrap->toggleOn(std::move(showInsufficient));
depositWrap->finishAnimating();
// This row swaps between a balance, a fee, an error and the deposit
// button, and some of those swaps are instant while others slide, so
// between two of them the row would have nothing in it and the whole box
// would shrink and grow back. It keeps the height of one line whatever
// it currently shows, which also spares the box the jump it made when
// the rate and the balance arrived after it was already on screen.
const auto reserve = balance->add(object_ptr<Ui::RpWidget>(balance));
reserve->resize(reserve->width(), 0);
const auto lineHeight = st::walletSendUserBalanceLabel.style.font->height;
const auto errorHeight = st::walletSendUserErrorLabel.style.font->height;
// The tallest this row goes: the balance and the fee together, or the
// insufficient-funds line with the deposit button under it.
const auto reserved = std::max(
2 * lineHeight,
errorHeight
+ st::walletDetailsAmountMinorSkip
+ st::defaultTableSmallButton.height);
balance->heightValue() | rpl::on_next([=](int height) {
const auto others = height - reserve->height();
const auto add = std::max(reserved - others, 0);
if (reserve->height() != add) {
reserve->resize(reserve->width(), add);
}
}, reserve->lifetime());
const auto requote = [=] {
if (!draft->quote.current() && !draft->preparing.current()) {
state->previewDependencies.reset();
}
refreshFee();
};
const auto refuse = [=](SendError error) {
if (!weak || state->closed) {
return;
}
stopSending();
invalidateFee();
state->previewError = error;
if (error == SendError::InsufficientBalance
|| error == SendError::InsufficientFees) {
state->previewInsufficient = true;
} else if (error == SendError::None) {
requote();
}
};
const auto checkQuote = [=] {
if (!originValid() || !state->flow || draft->preparing.current()) {
return SendError::InvalidRequest;
} else if (state->previewError.current() != SendError::None) {
return state->previewError.current();
} else if (state->insufficient.current()) {
return state->amount.current() > wallet->balanceNano()
? SendError::InsufficientBalance
: SendError::InsufficientFees;
}
const auto quote = draft->quote.current();
const auto dependencies = quoteDependencies();
if (!quote
|| !quote->prepared
|| quote != draft->quote.current()
|| quote->revision != state->previewRevision
|| quote->dependencies != dependencies
|| quote->args.comment != draft->comment.current()) {
return SendError::QuoteExpired;
} else if (draft->privateEpoch
&& (*draft->privateEpoch != wallet->vault().clearEpoch()
|| !draft->authorization.valid()
|| !wallet->vault().unlocked())) {
return SendError::Locked;
}
return SendError::None;
};
// The send press acquires the key the way every key-requiring wallet
// action does, through RunKeyRequiringAction: an unlock when this device
// holds the key, the backup restore with its protection chooser when the
// key is only in the cloud, the phrase import when there is no backup,
// and the conflict resolution first when a different wallet is parked
// here. The context owns every prompt of that ladder, so closing any of
// them without a key ends the press quietly, and closing this box ends
// it with them.
const auto acquireSendKey = [=] {
if (state->unlocking) {
return;
}
state->unlocking = true;
const auto context = std::make_shared<KeyContext>(
show,
nullptr,
originValid,
crl::guard(session, crl::guard(box, [=](KeyAuthorization auth) {
state->unlocking = false;
if (!state->sending.current() || state->submitted) {
return;
} else if (!auth.valid()) {
// A declined key changes nothing the fee was quoted from.
stopSending();
requote();
return;
}
state->sendAuthorization = std::move(auth);
state->continueSend();
})));
state->keyContext = context;
RunKeyRequiringAction(context, [=] {
if (!context->valid()) {
context->cancel();
return;
}
AcquireVaultUnlock({
.show = context,
.done = [=](KeyAuthorization auth) {
context->ready(std::move(auth));
},
});
}, KeyActionKind::ResumeAfterRestore, context,
tr::lng_wallet_restore_text());
};
// One press of the send button is one request: the amount, recipient
// and comment it was pressed with, and at most one unlock. A prepared
// transfer is bound to the exact gasless terms and balance it was
// estimated with, and those are refreshed on their own schedule, for
// example while the unlock prompt is still open. None of that is a
// change the user made and none of it is shown: the transfer is
// prepared again under the same authorization and submitted. The same
// holds when the session refuses a signed transfer as stale, which it
// does only before anything leaves the device; that retry is bounded
// so a refusal that keeps repeating cannot spin forever. When the
// user edits what is being sent, the request just stops quietly.
// WHY: the transfer now reports itself where the user is: in the wallet
// window's list when the box is there, otherwise in the recipient's
// chat, so the box closes instead of reporting anything.
const auto handOver = [=](SendStarted started) {
if (state->closed || !sessionValid()) {
return;
}
const auto panel = wallet->panel();
if (panel && box->window() == panel->window()) {
state->handedOver = true;
wallet->setWindowSend(started.operationId);
if (const auto content = dynamic_cast<Content*>(panel->inner())) {
content->flySendDiamond(started.operationId, amountField);
}
discardOrigin();
show->hideLayer();
return;
} else if (!started.message) {
return;
}
const auto window = MakeChatShow(show, true)->resolveWindow();
if (!window) {
return;
}
state->handedOver = true;
discardOrigin();
box->closeBox();
window->showPeerHistory(started.message.peer);
window->window().activate();
};
state->continueSend = [=] {
if (!state->sending.current()
|| state->submitted
|| state->unlocking
|| draft->preparing.current()) {
return;
} else if (!originValid() || !state->sendRequest) {
refuse(SendError::InvalidRequest);
return;
}
const auto request = *state->sendRequest;
const auto expiresAt = state->sendExpiresAt;
const auto now = quoteDependencies();
if (!state->flow
|| state->flow->expiresAt != expiresAt
|| now.destination != request.destination
|| now.bounce != request.bounce
|| now.amountNano != request.amountNano
|| now.comment != request.comment) {
refuse(SendError::None);
return;
} else if (TransferLinkExpired(expiresAt)) {
refuse(SendError::LinkExpired);
return;
}
if (state->heldFee) {
if (const auto quote = draft->quote.current()) {
const auto shown = *base::take(state->heldFee);
if (shown != QuoteConfirmFee(*quote)) {
const auto held = state->sendAuthorization;
stopSending();
state->heldAuthorization = held;
state->heldTimeout.callOnce(kHeldSendKeyTimeout);
state->previewError = SendError::QuoteExpired;
return;
}
}
}
const auto isPrivate = !request.comment.text.isEmpty()
&& !request.comment.isPublic;
const auto error = checkQuote();
if (error == SendError::SigningUnavailable) {
// No client could serve the preview: the key is still to be
// acquired, or the client is being swapped for the signing one
// right after it was, and the readiness change resumes the
// press. A ready signing client that still refuses, or a swap
// that outlasts the bound, is stated as a failure.
if (!state->sendAuthorization.valid()) {
acquireSendKey();
} else if (wallet->signingReady() || state->signingTimedOut) {
refuse(SendError::Failed);
} else {
awaitSigning();
}
return;
} else if (error == SendError::QuoteExpired) {
if (isPrivate && !state->sendAuthorization.valid()) {
acquireSendKey();
} else {
prepareFee(state->sendAuthorization);
}
return;
} else if (error != SendError::None) {
refuse(error);
return;
} else if (!state->sendAuthorization.valid()) {
acquireSendKey();
return;
} else if (!wallet->signingReady() && !state->signingTimedOut) {
awaitSigning();
return;
}
const auto accepted = *draft->quote.current();
const auto authorization = state->sendAuthorization;
const auto senderIdentity = state->senderIdentity;
const auto displayForm = state->flow->displayForm;
state->submitted = true;
draft->quote = std::nullopt;
draft->authorization = {};
draft->privateEpoch.reset();
const auto valid = [=] {
return sessionValid()
&& senderIdentity
&& wallet->transferWalletIdentityCurrent(*senderIdentity);
};
wallet->send(
authorization,
accepted.prepared,
crl::guard(session, [=](SendError error) {
if (error == SendError::KeyChanged) {
if (weak && !state->closed && !state->handedOver) {
box->closeBox();
}
if (sessionValid()) {
ShowWalletKeyChanged(show);
}
return;
} else if (error == SendError::KeyMismatch
&& (!weak || state->closed || state->handedOver)) {
if (sessionValid()) {
show->showToast(
SendErrorText(error, TransferMinNanos(session)));
}
return;
} else if (!weak || state->closed || !valid()) {
return;
} else if (state->handedOver) {
return;
} else if (error == SendError::QuoteExpired
&& ++state->sendRefusals <= kSendRefusalRetries) {
state->submitted = false;
invalidateFee();
scheduleContinueSend();
return;
} else if (error == SendError::QuoteExpired) {
refuse(SendError::Failed);
return;
} else if (error != SendError::None
&& error != SendError::SubmissionUnknown) {
refuse(error);
return;
}
const auto pending = wallet->pendingSend();
auto item = pending
? wallet->submittedTransaction(pending->operationId)
: std::nullopt;
if (!item && pending) {
item = ItemFromPending(*pending);
}
const auto toast = (error == SendError::None)
? tr::lng_wallet_sent_toast(
tr::now,
lt_address,
ShortAddressForm(displayForm))
: QString();
discardOrigin();
show->hideLayer();
if (!valid()) {
return;
}
if (!toast.isEmpty()) {
show->showToast(toast);
}
if (sent && error == SendError::None) {
sent();
} else if (valid() && item) {
ShowWalletTransactionBox(show, *item);
}
}),
crl::guard(session, crl::guard(box, handOver)));
};
const auto startSend = [=] {
if (!draft->preparing.current()
&& (!draft->quote.current()
|| state->previewDependencies != quoteDependencies())) {
invalidateFee();
}
state->sendRequest = quoteDependencies();
state->sendExpiresAt = state->flow->expiresAt;
state->sendRefusals = 0;
const auto held = state->heldAuthorization;
dropHeldKey();
if (held.grant && held.grant->valid() && wallet->vault().unlocked()) {
state->sendAuthorization = held;
}
state->sending = true;
state->continueSend();
};
const auto confirmSend = [=](const SendConfirmFee &shown) {
if (!state->confirmBox
|| !state->flow
|| state->sending.current()
|| !CommentFits(draft->comment.current().text)
|| !state->canSend.current()) {
return;
}
// WHY: Enter reaches here whatever the button shows, so a press made
// while counting waits and goes on only at the fee shown; this press
// answers the previous held press's "review the new fee" refusal.
if (state->previewError.current() == SendError::QuoteExpired) {
state->previewError = SendError::None;
}
if (draft->preparing.current()) {
state->heldFee = shown;
}
startSend();
};
const auto confirmationClosed = [=](not_null<Ui::GenericBox*> raw) {
if (!weak || state->closed || state->confirmBox.get() != raw.get()) {
return;
}
state->confirmBox = nullptr;
dropHeldKey();
if (state->sending.current() && !state->submitted) {
if (const auto context = base::take(state->keyContext)) {
context->cancel();
}
stopSending();
}
invalidateFee();
state->previewDependencies.reset();
updateAmount();
refreshFee();
};
const auto openConfirmation = [=] {
if (state->confirmBox) {
return;
}
auto fee = rpl::combine(
draft->quote.value(),
draft->preparing.value()
) | rpl::map([](
const std::optional<SendQuote> &quote,
bool preparing) {
return quote
? QuoteConfirmFee(*quote)
: SendConfirmFee{ .pending = preparing };
}) | rpl::distinct_until_changed();
auto confirm = Box(WalletSendConfirmBox, show, SendConfirmArgs{
.draft = draft,
.address = state->flow->displayForm,
.amountNano = state->amount.current(),
.fee = std::move(fee),
.refusal = refusalValue(true),
.busy = state->sending.value(),
.canSend = state->canSend.value(),
.send = crl::guard(box, confirmSend),
});
const auto raw = confirm.data();
state->confirmBox = base::make_weak(raw);
raw->boxClosing() | rpl::on_next(crl::guard(box, [=] {
confirmationClosed(raw);
}), raw->lifetime());
box->uiShow()->showBox(std::move(confirm));
requote();
};
const auto submit = [=] {
if (state->unfunded.current()) {
amountField->showError();
return;
} else if (!originValid() || !state->flow) {
if (user && !state->loading.current()) {
failLoading(userError());
}
return;
} else if (state->sending.current()
|| state->commentBox
|| state->confirmBox
|| !CommentFits(draft->comment.current().text)) {
return;
} else if (TransferAmountBelowMinimum(
state->amount.current(),
state->minTransfer.current())) {
// A press below the minimum only corrects the amount.
raiseToMinimum();
return;
} else if (!state->canSend.current()) {
amountField->showError();
return;
}
if (!user) {
openConfirmation();
return;
}
startSend();
};
auto buttonBusy = rpl::combine(
state->loading.value(),
state->sending.value()
) | rpl::map([](bool loading, bool sending) {
return loading || sending;
});
auto buttonText = BusyFooterLabel(
rpl::combine(
state->amount.value(),
tr::lng_send_button(),
tr::lng_wallet_send_amount(
lt_amount,
state->amount.value() | rpl::map([](int64 amount) {
return Ui::FormatTonAmount(amount).full;
}))
) | rpl::map([](int64 amount, QString empty, QString full) {
return amount > 0 ? full : empty;
}),
rpl::duplicate(buttonBusy));
const auto button = box->addButton(std::move(buttonText), submit).data();
rpl::combine(
state->canSend.value(),
state->raisable.value(),
state->sending.value()
) | rpl::on_next([=](bool canSend, bool raisable, bool sending) {
SetButtonDisabledLook(button, !canSend && !raisable && !sending);
}, button->lifetime());
AddBusyFooterSpinner(button, std::move(buttonBusy));
amountField->submits() | rpl::on_next(submit, amountField->lifetime());
if (!user) {
state->flow = initial;
state->flow->draft = draft;
const auto cached = wallet->userAddresses().publicKey(
state->flow->destination);
state->recipientKey = (self && cached.isEmpty())
? state->senderIdentity->publicKey
: cached;
}
refreshFee();
const auto resolveRecipient = [=] {
if (state->recipientRequested || !originValid() || !state->flow) {
return;
}
state->recipientRequested = true;
wallet->resolveCommentRecipient(
state->flow->destination,
state->flow->bounce,
state->recipientKey,
crl::guard(box, [=](CommentRecipient recipient) {
if (recipient == CommentRecipient::PlainOnly
&& !state->closed
&& !state->terminal) {
switchToPlain();
}
}));
};
state->loading.value() | rpl::on_next(resolveRecipient, box->lifetime());
box->setFocusCallback([=] { amountField->setFocusFast(); });
if (state->loading.current()) {
const auto recompute = [=] {
if (state->closed || state->terminal) {
return;
} else if (!sessionValid()) {
failLoading(u"WALLET_NOT_READY"_q);
return;
}
const auto presence = wallet->presenceCurrent();
const auto error = user
? userError()
: (presence == Presence::Ready)
? QString()
: u"WALLET_NOT_READY"_q;
if (notReady
&& user
&& !state->forceIssued
&& !state->senderIdentity
&& error == u"WALLET_NOT_READY"_q
&& presence != Presence::Unknown
&& presence != Presence::Unavailable) {
const auto onstack = notReady;
failLoading(error, true);
onstack();
return;
} else if (error == u"WALLET_NOT_READY"_q
&& !state->forceIssued
&& (presence == Presence::Unknown
|| presence == Presence::Provisioning)) {
return;
} else if (user
&& !state->forceIssued
&& error == u"WALLET_BALANCE_EMPTY"_q) {
if (!state->unfunded.current()) {
state->senderIdentity = wallet->transferWalletIdentity();
if (!state->senderIdentity) {
failLoading(u"WALLET_NOT_READY"_q);
return;
}
state->loadDeadline.cancel();
state->unfunded = true;
state->loading = false;
}
return;
} else if (!error.isEmpty()) {
failLoading(presence == Presence::Unavailable
? u"WALLET_UNAVAILABLE"_q
: error);
return;
}
if (state->unfunded.current()) {
state->loading = true;
state->unfunded = false;
state->loadDeadline.callOnce(kSendUserLoadTimeout);
} else if (!state->loading.current()) {
refreshFee();
return;
} else if (state->forceIssued) {
return;
}
state->senderIdentity = wallet->transferWalletIdentity();
if (!state->senderIdentity) {
failLoading(u"WALLET_NOT_READY"_q);
return;
} else if (!user) {
state->loadDeadline.cancel();
state->loading = false;
return;
}
state->forceIssued = true;
const auto revision = state->loadRevision;
const auto current = [=] {
return !state->closed
&& !state->terminal
&& revision == state->loadRevision;
};
wallet->userAddresses().forceResolve(
userId,
crl::guard(session, crl::guard(box, [=](QString address) {
if (!current()) {
return;
}
const auto error = userError();
if (!error.isEmpty()) {
failLoading(error);
return;
}
auto flow = ParseRecipientFlow(FormatFriendly(address, false));
if (!flow
|| !user->gramAddress()
|| *user->gramAddress() != flow->destination
|| (initial
&& initial->destination != flow->destination)) {
failLoading(u"WALLET_ADDRESS_INVALID"_q);
return;
}
flow->draft = draft;
if (initial) {
flow->expiresAt = initial->expiresAt;
}
state->flow = std::move(flow);
state->recipientKey
= wallet->userAddresses().publicKey(address);
state->loadDeadline.cancel();
state->loading = false;
})),
crl::guard(session, crl::guard(box, [=](ForceResolveError error) {
if (!current()) {
return;
} else if (error.silent) {
failLoading(error.type, true);
return;
}
const auto changed = userError();
failLoading(changed.isEmpty() ? error.type : changed);
})));
};
const auto schedule = [=] {
if (state->closed || state->terminal || state->recomputeQueued) {
return;
}
state->recomputeQueued = true;
const auto revision = state->loadRevision;
Ui::PostponeCall(box, [=] {
state->recomputeQueued = false;
if (revision == state->loadRevision) {
recompute();
}
});
};
state->loadDeadline.setCallback([=] {
failLoading(state->forceIssued
? u"WALLET_ADDRESS_INVALID"_q
: u"WALLET_NOT_READY"_q);
});
state->loadDeadline.callOnce(kSendUserLoadTimeout);
wallet->stateKnownValue() | rpl::on_next(schedule, box->lifetime());
wallet->balanceNanoValue() | rpl::on_next(schedule, box->lifetime());
wallet->custodyUpdates() | rpl::on_next(schedule, box->lifetime());
if (user) {
wallet->userAddresses().unavailableValue(
) | rpl::on_next(schedule, box->lifetime());
user->flagsValue() | rpl::on_next(schedule, box->lifetime());
session->changes().peerUpdates(
user,
Data::PeerUpdate::Flag::FullInfo
| Data::PeerUpdate::Flag::Name
| Data::PeerUpdate::Flag::SupportInfo
) | rpl::on_next(schedule, box->lifetime());
const auto error = userError();
if (!error.isEmpty()
&& error != u"WALLET_NOT_READY"_q
&& error != u"WALLET_BALANCE_EMPTY"_q) {
failLoading(error);
return;
}
}
wallet->presenceValue() | rpl::on_next(schedule, box->lifetime());
wallet->transferWalletIdentityChanges(
) | rpl::on_next(schedule, box->lifetime());
wallet->refreshState();
}
}
// WHY: an entry point decides about the recipient only from the served
// wallet state, and only the call that ends the wait acts, so an answer
// landing after a timeout or a dismissed busy box changes nothing.
struct SendEntryWait {
std::shared_ptr<Main::SessionShow> show;
rpl::variable<QString> text;
QString timeoutError;
Fn<void()> closeBusy;
rpl::lifetime lifetime;
bool ended = false;
};
bool EndSendEntryWait(const std::shared_ptr<SendEntryWait> &wait) {
if (wait->ended) {
return false;
}
wait->ended = true;
wait->lifetime.destroy();
if (const auto close = base::take(wait->closeBusy)) {
close();
}
return wait->show->valid();
}
void FailSendEntryWait(
const std::shared_ptr<SendEntryWait> &wait,
const QString &error) {
if (EndSendEntryWait(wait)) {
wait->show->showToast(SendUserLoadErrorText(error));
}
}
[[nodiscard]] std::shared_ptr<SendEntryWait> StartSendEntryWait(
std::shared_ptr<Main::SessionShow> show) {
const auto session = &show->session();
const auto wait = std::make_shared<SendEntryWait>();
wait->show = show;
wait->text = tr::lng_wallet_send_wallet_loading(tr::now);
wait->timeoutError = u"WALLET_NOT_READY"_q;
const auto weak = std::weak_ptr<SendEntryWait>(wait);
base::call_delayed(kSendOwnerLookupDelay, session, [=] {
const auto strong = weak.lock();
if (!strong || strong->ended || !show->valid()) {
return;
}
strong->closeBusy = ShowWalletBusyBox(
show,
strong->text.value(),
[=] {
if (const auto strong = weak.lock()) {
strong->closeBusy = nullptr;
EndSendEntryWait(strong);
}
});
});
base::call_delayed(kSendUserLoadTimeout, session, [=] {
if (const auto strong = weak.lock()) {
FailSendEntryWait(strong, strong->timeoutError);
}
});
return wait;
}
void AwaitWalletReady(
const std::shared_ptr<SendEntryWait> &wait,
Fn<void()> ready,
Fn<void()> notReady = nullptr) {
const auto session = &wait->show->session();
const auto wallet = &session->wallet();
if (wallet->presence() == Presence::Ready) {
ready();
return;
}
wallet->refreshState();
// A caller with its own answer shows a wallet still being created.
const auto waitCreated = !notReady;
wallet->presenceValue(
) | rpl::filter([=](Presence presence) {
return (presence != Presence::Unknown)
&& (!waitCreated || presence != Presence::Provisioning);
}) | rpl::take(1) | rpl::on_next([=](Presence presence) {
if (presence == Presence::Ready) {
// The update that made the wallet Ready finishes first.
crl::on_main(session, [=] {
if (!wait->show->valid()) {
EndSendEntryWait(wait);
} else if (!wait->ended) {
ready();
}
});
} else if (notReady) {
if (EndSendEntryWait(wait)) {
notReady();
}
} else {
FailSendEntryWait(wait, (presence == Presence::Unavailable)
? u"WALLET_UNAVAILABLE"_q
: u"WALLET_NOT_READY"_q);
}
}, wait->lifetime);
}
void WhenWalletReady(
std::shared_ptr<Main::SessionShow> show,
Fn<void()> ready,
Fn<void()> notReady = nullptr) {
const auto wait = StartSendEntryWait(std::move(show));
AwaitWalletReady(wait, [=] {
if (EndSendEntryWait(wait)) {
ready();
}
}, std::move(notReady));
}
void OpenSendFlow(
std::shared_ptr<Main::SessionShow> show,
SendFlow flow,
AddressOwner owner,
base::weak_qptr<Ui::BoxContent> origin = nullptr) {
if (TransferLinkExpired(flow.expiresAt)) {
show->showToast(tr::lng_wallet_send_link_expired(tr::now));
return;
}
// WHY: a wallet Telegram names an owner for may not be deployed yet,
// and a bounceable message to one is returned instead of delivered.
if (owner.userId) {
flow.bounce = false;
flow.displayForm = FormatFriendly(flow.destination, false);
}
const auto session = &show->session();
const auto user = SendableUser(session, owner.userId);
const auto toUser = user
&& !user->isSelf()
&& (!user->gramAddress()
|| *user->gramAddress() == flow.destination);
show->showBox(Box(
WalletSendBox,
show,
std::make_optional(std::move(flow)),
toUser ? user : nullptr,
Fn<void()>(),
Fn<void()>(),
int64(0),
origin));
}
// A transfer to a user is gasless, even when the link named a wallet.
void ResolveOwnerAndOpenSendFlow(
std::shared_ptr<Main::SessionShow> show,
SendFlow flow) {
const auto session = &show->session();
const auto wait = StartSendEntryWait(show);
AwaitWalletReady(wait, [=] {
if (SendsToOwnWallet(session, flow.destination)) {
if (EndSendEntryWait(wait)) {
OpenSendFlow(show, flow, AddressOwner());
}
return;
}
wait->text = tr::lng_wallet_send_recipient_loading(tr::now);
wait->timeoutError = u"WALLET_ADDRESS_INVALID"_q;
session->wallet().userAddresses().resolveOwner(
flow.destination,
crl::guard(session, [=](AddressOwner owner) {
if (EndSendEntryWait(wait)) {
OpenSendFlow(show, flow, std::move(owner));
}
}),
crl::guard(session, [=](bool silent) {
if (silent) {
EndSendEntryWait(wait);
} else {
FailSendEntryWait(wait, u"WALLET_ADDRESS_INVALID"_q);
}
}));
});
}
[[nodiscard]] CollectibleRecipient CollectibleRecipientFrom(
not_null<Main::Session*> session,
const SendFlow &flow,
const AddressOwner &owner) {
const auto user = SendableUser(session, owner.userId);
const auto toUser = user
&& !user->isSelf()
&& (!user->gramAddress()
|| *user->gramAddress() == flow.destination);
return {
.destination = flow.destination,
.tonName = flow.tonName,
.userId = toUser ? owner.userId : UserId(),
.bounce = owner.userId ? false : flow.bounce,
};
}
void WalletSendRecipientBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
QString text,
std::shared_ptr<const CollectibleTransfer> collectible) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
box->setTitle(collectible
? tr::lng_gift_transfer_title(
lt_name,
CollectibleNameValue(collectible->media, collectible->address))
: tr::lng_wallet_send_title());
AddBoxCloseButton(box);
const auto session = &show->session();
struct State {
std::optional<SendFlow> flow;
QString name;
rpl::variable<bool> valid = false;
rpl::variable<bool> invalid = false;
rpl::variable<RecipientError> error = RecipientError::Invalid;
rpl::variable<bool> resolving = false;
rpl::variable<QString> search;
rpl::variable<bool> recentHidden = false;
base::Timer deadline;
Fn<void(not_null<UserData*>)> chooseUser;
uint64 revision = 0;
bool closed = false;
bool searchCreated = false;
bool proceedOnName = false;
};
const auto state = box->lifetime().make_state<State>();
const auto lookup = box->lifetime().make_state<TonNameLookup>(session);
const auto choose = [=](not_null<UserData*> user) {
state->chooseUser(user);
};
const auto recipient = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
style::margins(),
style::al_justify);
const auto field = AddSendField(
recipient,
st::walletSendField,
tr::lng_wallet_details_recipient(),
text);
const auto errorWrap = recipient->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
recipient,
object_ptr<Ui::FlatLabel>(
recipient,
(state->error.value()
| rpl::map(RecipientErrorText)
| rpl::flatten_latest()),
st::walletSendErrorLabel)),
style::margins(
st::walletSendFieldMargin.left(),
0,
st::walletSendFieldMargin.right(),
0));
errorWrap->toggleOn(state->invalid.value());
errorWrap->finishAnimating();
recipient->add(MakeRecentMoneyRecipientsList(
box,
show,
state->recentHidden.value(),
choose));
const auto stop = [=] {
++state->revision;
state->deadline.cancel();
state->resolving = false;
};
const auto parse = [=] {
stop();
state->proceedOnName = false;
const auto trimmed = field->getLastText().trimmed();
auto input = ClassifyRecipientInput(trimmed);
state->flow = std::move(input.flow);
state->name = (input.kind == RecipientInputKind::Name)
? trimmed
: QString();
state->valid = state->flow.has_value() || !state->name.isEmpty();
state->invalid = (input.kind == RecipientInputKind::Invalid);
if (state->invalid.current()) {
state->error = RecipientError::Invalid;
}
const auto searching = (input.kind == RecipientInputKind::Search);
if (searching && !state->searchCreated) {
state->searchCreated = true;
recipient->add(MakeMoneyRecipientSearchList(
box,
show,
state->search.value(),
choose));
recipient->resizeToWidth(recipient->width());
}
state->search = searching ? trimmed : QString();
state->recentHidden = searching || !state->name.isEmpty();
lookup->setName(state->name);
};
const auto proceed = [=](SendFlow flow, AddressOwner owner) {
stop();
if (state->closed
|| !show->valid()
|| &show->session() != session) {
return;
} else if (collectible) {
show->showBox(Box(
CollectibleTransferBox,
show,
collectible,
CollectibleRecipientFrom(session, flow, owner)));
return;
}
OpenSendFlow(show, std::move(flow), std::move(owner), box.get());
};
const auto failName = [=](RecipientError error) {
stop();
state->error = error;
state->invalid = true;
};
const auto lookupOwner = [=](SendFlow flow) {
if (SendsToOwnWallet(session, flow.destination)) {
if (collectible) {
failName(RecipientError::OwnWallet);
} else {
proceed(flow, AddressOwner());
}
return;
}
const auto revision = ++state->revision;
const auto answer = [=](AddressOwner owner) {
if (revision == state->revision) {
proceed(flow, std::move(owner));
}
};
const auto fail = [=](bool silent) {
if (revision != state->revision) {
return;
} else if (silent) {
stop();
} else {
failName(RecipientError::LookupFailed);
}
};
state->resolving = true;
state->deadline.setCallback([=] { fail(false); });
// resolveOwner has no deadline of its own.
state->deadline.callOnce(kSendUserLoadTimeout);
session->wallet().userAddresses().resolveOwner(
flow.destination,
crl::guard(session, crl::guard(box, answer)),
crl::guard(session, crl::guard(box, fail)));
};
if (!collectible) {
state->chooseUser = [=](not_null<UserData*> user) {
ChooseMoneyRecipient(box, show, user);
};
} else {
state->chooseUser = [=](not_null<UserData*> user) {
if (state->closed || state->resolving.current()) {
return;
} else if (user->isSelf()) {
failName(RecipientError::OwnWallet);
return;
}
const auto userId = peerToUser(user->id);
const auto revision = ++state->revision;
const auto answer = [=](QString address) {
if (revision != state->revision) {
return;
}
const auto flow = ParseRecipientFlow(
FormatFriendly(address, false));
if (!flow) {
failName(RecipientError::LookupFailed);
} else if (SendsToOwnWallet(session, flow->destination)) {
failName(RecipientError::OwnWallet);
} else {
proceed(*flow, AddressOwner{
.userId = userId,
.address = address,
});
}
};
const auto fail = [=](ForceResolveError error) {
if (revision != state->revision) {
return;
}
stop();
if (!error.silent) {
show->showToast(SendUserLoadErrorText(error.type));
}
};
state->resolving = true;
state->deadline.setCallback([=] {
if (revision == state->revision) {
failName(RecipientError::LookupFailed);
}
});
state->deadline.callOnce(kSendUserLoadTimeout);
session->wallet().userAddresses().forceResolve(
userId,
crl::guard(session, crl::guard(box, answer)),
crl::guard(session, crl::guard(box, fail)));
};
}
const auto proceedName = [=](const TonNameState &value) {
if (auto flow = ParseRecipientFlow(value.address)) {
flow->tonName = value.name;
lookupOwner(std::move(*flow));
} else {
failName(RecipientError::NameFailed);
}
};
const auto submit = [=] {
if (state->closed || state->resolving.current()) {
return;
} else if (!state->name.isEmpty()) {
if (lookup->current().status == TonNameStatus::Resolved) {
proceedName(lookup->current());
} else {
state->proceedOnName = true;
state->resolving = true;
lookup->request();
}
return;
} else if (!state->flow) {
field->showError();
return;
} else if (TransferLinkExpired(state->flow->expiresAt)) {
show->showToast(tr::lng_wallet_send_link_expired(tr::now));
return;
}
lookupOwner(*state->flow);
};
recipient->add(MakeTonNameResultList(
box,
session,
lookup->value(),
submit));
lookup->value() | rpl::on_next([=](const TonNameState &value) {
const auto status = value.status;
if (status == TonNameStatus::Pending) {
state->invalid = false;
} else if ((status == TonNameStatus::NotFound)
|| (status == TonNameStatus::Failed)) {
state->proceedOnName = false;
failName((status == TonNameStatus::NotFound)
? RecipientError::NameNotFound
: RecipientError::NameFailed);
} else if ((status == TonNameStatus::Resolved)
&& base::take(state->proceedOnName)) {
proceedName(value);
}
}, box->lifetime());
const auto button = box->addButton(
BusyFooterLabel(tr::lng_continue(), state->resolving.value()),
submit).data();
state->valid.value() | rpl::on_next([=](bool valid) {
SetButtonDisabledLook(button, !valid);
}, button->lifetime());
AddBusyFooterSpinner(button, state->resolving.value());
field->changes() | rpl::on_next(parse, field->lifetime());
field->submits() | rpl::on_next(submit, field->lifetime());
box->boxClosing() | rpl::on_next([=] {
state->closed = true;
stop();
lookup->close();
}, box->lifetime());
box->setFocusCallback([=] { field->setFocusFast(); });
parse();
}
[[nodiscard]] QString PhraseBoxLottie(int wordsCount) {
return (wordsCount > kImportWordCountShort)
? QString()
: u"wallet/paper"_q;
}
void AddPhraseBoxHeader(
not_null<Ui::GenericBox*> box,
const QString &lottieName,
rpl::producer<QString> title,
rpl::producer<TextWithEntities> text,
const style::FlatLabel &textLabel,
int lottieSize,
const style::margins &lottieMargin,
const style::margins &textMargin) {
if (!lottieName.isEmpty()) {
auto icon = Settings::CreateLottieIcon(
box->verticalLayout(),
{
.name = lottieName,
.sizeOverride = { lottieSize, lottieSize },
},
lottieMargin);
box->verticalLayout()->add(std::move(icon.widget));
box->showFinishes(
) | rpl::on_next([animate = std::move(icon.animate)] {
animate(anim::repeat::once);
}, box->lifetime());
}
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(title),
st::walletPhraseTitleLabel),
(lottieName.isEmpty()
? st::walletPhraseTitlePadding
: st::boxRowPadding),
style::al_top);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
std::move(text),
textLabel),
textMargin,
style::al_top
)->setTryMakeSimilarLines(true);
}
void AddPhraseGrid(
not_null<Ui::GenericBox*> box,
const std::vector<QString> &words) {
const auto count = int(words.size());
const auto grid = box->addRow(
object_ptr<Ui::RpWidget>(box),
st::walletPhraseGridPadding);
const auto rows = count / 2;
auto numberLabels = std::vector<Ui::FlatLabel*>();
auto wordLabels = std::vector<Ui::FlatLabel*>();
for (auto i = 0; i != count; ++i) {
numberLabels.push_back(Ui::CreateChild<Ui::FlatLabel>(
grid,
QString::number(i + 1) + QChar('.'),
st::walletPhraseNumberLabel));
wordLabels.push_back(Ui::CreateChild<Ui::FlatLabel>(
grid,
words[i],
st::walletPhraseWordLabel));
}
const auto rowHeight = wordLabels.front()->height();
grid->resize(
grid->width(),
rows * rowHeight + (rows - 1) * st::walletPhraseRowSkip);
grid->widthValue(
) | rpl::on_next([=](int width) {
const auto column = (width - st::walletPhraseColumnSkip) / 2;
for (auto i = 0; i != int(wordLabels.size()); ++i) {
const auto x = (i < rows)
? 0
: (column + st::walletPhraseColumnSkip);
const auto y = (i % rows)
* (rowHeight + st::walletPhraseRowSkip);
numberLabels[i]->moveToLeft(x, y, width);
wordLabels[i]->moveToLeft(
x + st::walletPhraseNumberWidth,
y,
width);
}
}, grid->lifetime());
}
void WalletPhraseBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
std::vector<QString> words) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
const auto count = int(words.size());
AddPhraseBoxHeader(
box,
PhraseBoxLottie(count),
tr::lng_wallet_phrase_title(),
tr::lng_wallet_phrase_text(
lt_count,
rpl::single(count * 1.) | tr::to_count(),
tr::marked),
st::walletPhraseTextLabel,
st::walletCoverLottieSize,
st::walletPhraseLottieMargin,
st::walletPhraseTextMargin);
AddPhraseGrid(box, words);
AddBoxCloseButton(box);
box->addButton(tr::lng_about_done(), [=] { box->closeBox(); });
SubmitBoxOnEnter(box, [=] { box->closeBox(); });
}
[[nodiscard]] TextWithEntities EnforcementCheckAbout(const QString &error) {
auto result = tr::marked();
const auto prefixes = {
u"PASSWORD_TOO_FRESH_"_q,
u"SESSION_TOO_FRESH_"_q,
};
for (const auto &prefix : prefixes) {
if (!error.startsWith(prefix)) {
continue;
}
const auto seconds = error.mid(prefix.size()).toInt();
if (seconds > 0) {
result.append(tr::lng_wallet_phrase_check_wait(
tr::now,
lt_duration,
tr::marked(Ui::FormatResetCloudPasswordIn(seconds)),
tr::marked)
).append(QChar('\n')).append(QChar('\n'));
}
break;
}
result.append(tr::lng_bots_password_confirm_check_about(
tr::now,
tr::marked));
return result;
}
enum class PhraseOperation {
Reveal,
Restore,
DropParked,
};
void ShowPhraseError(
std::shared_ptr<Main::SessionShow> show,
PhraseOperation operation,
const QString &error) {
const auto text = [&] {
switch (operation) {
case PhraseOperation::Reveal:
return tr::lng_wallet_phrase_error(tr::now);
case PhraseOperation::Restore:
return tr::lng_wallet_restore_error(tr::now);
case PhraseOperation::DropParked:
return tr::lng_wallet_conflict_switch_error(tr::now);
}
Unexpected("Operation in ShowPhraseError.");
}();
const auto &wallet = show->session().wallet();
const auto identity = wallet.transferWalletIdentity();
LOG(("Wallet Error: key access toast operation=%1 error=%2 "
"address=%3 key=%4 revision=%5 presence=%6 device_mode=%7 "
"conflict=%8."
).arg((operation == PhraseOperation::Reveal)
? u"reveal"_q
: (operation == PhraseOperation::Restore)
? u"restore"_q
: u"drop_parked"_q
).arg(error
).arg(identity ? identity->address : u"(none)"_q
).arg(QString::fromLatin1(wallet.publicKey().toHex())
).arg(identity ? identity->revision : 0
).arg(int(wallet.presenceCurrent())
).arg(int(wallet.deviceCustodyState().mode)
).arg(wallet.deviceCustodyState().conflict));
show->showToast(ErrorWithType(text, error));
}
// ReadOnlyRestorable ends as soon as custody is installed, so an entry that
// reaches the cloud password box in that mode is by construction the first
// key use on this device: that box then carries the lock-and-key header
// explaining what the restore is about to do, while any other mode keeps
// the plain prompt with the given description.
[[nodiscard]] bool RestoreIsFirstKeyUse(not_null<Main::Session*> session) {
const auto state = session->wallet().deviceCustodyState();
return (state.mode == DeviceMode::ReadOnlyRestorable);
}
[[nodiscard]] PasscodeBox::CloudFields RestorePasswordFields(
const Core::CloudPasswordState &state,
bool firstKeyUse,
const QString &description) {
auto result = PasscodeBox::CloudFields::From(state);
result.customSubmitButton = tr::lng_passcode_submit();
if (firstKeyUse) {
result.customHeader = PasscodeBox::CloudFields::CustomHeader{
.lottie = u"cloud_password/intro"_q,
.lottieSize = st::walletHowLottieSize,
.lottieMargin = st::walletHowLottieMargin,
.title = tr::lng_settings_cloud_password_check_subtitle(),
.description = tr::lng_wallet_restore_explain_text(),
};
} else {
result.customTitle = tr::lng_bots_password_confirm_title();
result.customDescription = description;
}
return result;
}
// The backup export is sent without a password first, even when the account
// has one, because the server decides whether this export needs it. Only
// its PASSWORD_MISSING answer goes to onPasswordMissing, which asks for the
// cloud password and repeats the request with it; that repeat carries its
// password box here, so its password errors go back to that box.
void RequestPhraseReveal(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> warning,
KeyAuthorization auth,
std::optional<Core::CloudPasswordResult> password,
base::weak_qptr<PasscodeBox> passcode,
Fn<void()> unblock,
std::optional<QByteArray> parkedKey = std::nullopt,
Fn<void(std::vector<QString>)> onWords = nullptr,
Fn<void()> onAuthorized = nullptr,
Fn<void(std::vector<QString>, CustodyOutcome)> onPrepared = nullptr,
Fn<void()> onPromptError = nullptr,
Fn<void()> onPasswordMissing = nullptr) {
auto &wallet = show->session().wallet();
if (!parkedKey && passcode && wallet.revealsLocally()) {
const auto box = base::take(passcode);
password.reset();
box->closeBox();
}
const auto done = crl::guard(warning, [=](
std::vector<QString> words,
CustodyOutcome outcome) {
if (onPrepared) {
onPrepared(std::move(words), outcome);
return;
}
if (passcode) {
passcode->closeBox();
}
if (onWords) {
onWords(std::move(words));
} else {
warning->closeBox();
show->showBox(Box(WalletPhraseBox, show, std::move(words)));
}
if (outcome != CustodyOutcome::Installed) {
show->showToast(tr::lng_wallet_restore_not_saved(tr::now));
}
});
const auto fail = crl::guard(warning, [=](const QString &error) {
if (passcode && passcode->handleCustomCheckError(error)) {
passcode->showLoading(false);
if (onPromptError) {
onPromptError();
} else {
unblock();
}
return;
}
if (onPasswordMissing && error == u"PASSWORD_MISSING"_q) {
onPasswordMissing();
return;
}
unblock();
if (!onWords && !onPrepared) {
warning->closeBox();
}
if (error == u"PHRASE_VAULT_LOCKED"_q) {
if (passcode) {
passcode->closeBox();
}
show->showToast(tr::lng_wallet_vault_locked(tr::now));
return;
}
if (auto box = PrePasswordErrorBox(
error,
&show->session(),
EnforcementCheckAbout(error))) {
if (passcode) {
passcode->closeBox();
}
show->showBox(std::move(box));
return;
}
if (passcode) {
passcode->closeBox();
}
ShowPhraseError(show, PhraseOperation::Reveal, error);
});
auto authorized = Fn<void()>();
if (onAuthorized) {
authorized = crl::guard(warning, [=] {
onAuthorized();
if (passcode) {
passcode->closeBox();
}
});
}
if (parkedKey) {
wallet.revealParked(std::move(auth), *parkedKey, done, fail);
} else {
wallet.revealPhrase(
std::move(auth),
std::move(password),
done,
fail,
std::move(authorized));
}
}
void StartPhraseReveal(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> warning,
KeyAuthorization auth,
Fn<void()> unblock,
std::optional<QByteArray> parkedKey = std::nullopt,
Fn<void(std::vector<QString>)> onWords = nullptr,
Fn<void()> onAuthorized = nullptr,
Fn<void(std::vector<QString>, CustodyOutcome)> onPrepared = nullptr,
Fn<void()> onPromptError = nullptr,
Fn<void()> onPromptClosed = nullptr,
Fn<bool()> onPromptSubmit = nullptr) {
const auto session = &show->session();
// A parked key or a key held on this device goes straight to the words:
// neither asks the server, so neither can be asked for a password, and
// neither reports the backup's answer or hands its words over prepared,
// because only a key restored from Telegram's backup is stored.
if (parkedKey || session->wallet().revealsLocally()) {
RequestPhraseReveal(
show,
warning,
std::move(auth),
std::nullopt,
nullptr,
unblock,
parkedKey,
onWords);
return;
}
// Only this branch restores the key to the device, so only its password
// box carries the first-use explanation header.
const auto firstKeyUse = RestoreIsFirstKeyUse(session);
const auto askPassword = crl::guard(warning, [=] {
const auto cached = session->api().cloudPassword().stateCurrent();
LOG(("Wallet Info: phrase reveal requires password; "
"cached_state=%1 cached_has_password=%2."
).arg(cached.has_value()).arg(cached && cached->hasPassword));
session->api().cloudPassword().reload();
session->api().cloudPassword().state(
) | rpl::take(
1
) | rpl::on_next([=](const Core::CloudPasswordState &state) {
if (!state.hasPassword) {
// The server asked for a password this account does not
// have, so a repeat without one would only be refused again.
unblock();
if (!onWords && !onPrepared) {
warning->closeBox();
}
ShowPhraseError(
show,
PhraseOperation::Reveal,
u"PHRASE_PASSWORD_STATE_MISSING"_q);
return;
}
auto fields = RestorePasswordFields(
state,
firstKeyUse,
tr::lng_wallet_phrase_password_description(tr::now));
fields.customCheckCallback = [=](
const Core::CloudPasswordResult &result,
base::weak_qptr<PasscodeBox> passcode) {
if (onPromptSubmit && !onPromptSubmit()) {
return;
}
if (passcode) {
passcode->showLoading(true);
}
RequestPhraseReveal(
show,
warning,
auth,
result,
passcode,
unblock,
std::nullopt,
onWords,
onAuthorized,
onPrepared,
onPromptError);
};
const auto passcode = show->show(
Box<PasscodeBox>(session, fields));
if (passcode) {
passcode->boxClosing(
) | rpl::on_next([=] {
if (onPromptClosed) {
onPromptClosed();
}
}, warning->lifetime());
}
}, warning->lifetime());
});
RequestPhraseReveal(
show,
warning,
std::move(auth),
std::nullopt,
nullptr,
unblock,
std::nullopt,
onWords,
onAuthorized,
onPrepared,
onPromptError,
askPassword);
}
void WalletPhraseWarningBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
std::optional<QByteArray> parkedKey) {
enum class Phase {
Idle,
Starting,
Authorizing,
Loading,
Ready,
};
struct State {
Phase phase = Phase::Idle;
rpl::variable<bool> loading = false;
bool armed = false;
bool pointerDown = false;
bool absorbEnter = false;
std::optional<std::vector<QString>> words;
CustodyOutcome outcome = CustodyOutcome::Installed;
};
const auto state = box->lifetime().make_state<State>();
box->setWidth(st::boxWideWidth);
box->setStyle(st::walletPillBox);
box->setNoContentMargin(true);
AddPhraseBoxHeader(
box,
u"wallet/paper"_q,
tr::lng_wallet_phrase_intro_title(),
tr::lng_wallet_phrase_intro_text(tr::marked),
st::walletPhraseIntroTextLabel,
st::walletCoverLottieSize,
st::walletPhraseLottieMargin,
st::walletPhraseTextMargin);
const auto container = box->verticalLayout();
const auto addWarning = [&](
rpl::producer<TextWithEntities> text,
const style::icon &icon) {
const auto label = container->add(
object_ptr<Ui::FlatLabel>(
container,
std::move(text),
st::walletPhraseWarnLabel),
st::walletPhraseWarnPadding);
const auto left = Ui::CreateChild<Ui::RpWidget>(container);
left->paintRequest(
) | rpl::on_next([=] {
auto p = Painter(left);
icon.paint(p, 0, 0, left->width());
}, left->lifetime());
left->resize(icon.size());
label->geometryValue(
) | rpl::on_next([=](const QRect &g) {
const auto oneLine = LabelLineHeight(st::walletPhraseWarnLabel);
const auto shift = (g.height() > oneLine)
? st::walletPhraseWarnIconSkip
: (g.height() - left->height()) / 2;
left->moveToLeft(
(g.left() - left->width()) / 2,
g.top() + shift);
}, left->lifetime());
};
Ui::AddSkip(container);
addWarning(
tr::lng_wallet_phrase_warn_share(tr::rich),
st::walletPhraseWarnShareIcon);
Ui::AddSkip(container, st::walletPhraseWarnRowSkip);
addWarning(
tr::lng_wallet_phrase_warn_steal(tr::rich),
st::walletPhraseWarnStealIcon);
Ui::AddSkip(container, st::walletPhraseWarnRowSkip);
addWarning(
tr::lng_wallet_phrase_warn_support(tr::rich),
st::walletPhraseWarnSupportIcon);
Ui::AddSkip(container);
AddBoxCloseButton(box);
const auto idleFromPrompt = [=] {
if (state->phase == Phase::Starting
|| state->phase == Phase::Authorizing) {
state->phase = Phase::Idle;
state->loading = false;
state->armed = false;
state->absorbEnter = false;
state->words.reset();
}
};
const auto idleFromFail = [=] {
if (state->phase == Phase::Ready) {
return;
}
state->phase = Phase::Idle;
state->loading = false;
state->armed = false;
state->absorbEnter = false;
state->words.reset();
};
const auto button = box->addButton(BusyFooterLabel(
tr::lng_wallet_keys_show_phrase(),
state->loading.value()));
button->setClickedCallback([=] {
if (state->phase == Phase::Authorizing
|| state->phase == Phase::Loading
|| state->phase == Phase::Starting) {
return;
} else if (state->phase == Phase::Ready) {
if (!state->armed || !state->words) {
return;
}
auto words = *state->words;
state->words.reset();
box->closeBox();
show->showBox(Box(WalletPhraseBox, show, std::move(words)));
return;
}
state->phase = Phase::Starting;
state->loading = true;
const auto start = [=](KeyAuthorization auth) {
StartPhraseReveal(
show,
box,
std::move(auth),
idleFromFail,
parkedKey,
nullptr,
[=] {
if (state->phase != Phase::Starting
&& state->phase != Phase::Authorizing) {
return;
}
state->phase = Phase::Loading;
state->armed = false;
state->loading = true;
},
[=](std::vector<QString> words, CustodyOutcome outcome) {
if (state->phase != Phase::Loading) {
return;
}
// A stored key always went through the protection
// chooser, and its Save is the explicit activation that
// shows the words right away: nothing typed into the
// password box can carry past it. A store that was
// cancelled or failed keeps the words behind a fresh
// press of this button instead.
if (outcome == CustodyOutcome::Installed) {
state->phase = Phase::Idle;
state->loading = false;
box->closeBox();
show->showBox(
Box(WalletPhraseBox, show, std::move(words)));
return;
}
state->words = std::move(words);
state->outcome = outcome;
state->phase = Phase::Ready;
state->armed = !state->pointerDown;
state->absorbEnter = true;
state->loading = false;
if (const auto strong = button.data()) {
strong->clearState();
}
if (outcome != CustodyOutcome::Installed) {
show->showToast(
tr::lng_wallet_restore_not_saved(tr::now));
}
},
[=] {
if (state->phase == Phase::Authorizing) {
state->phase = Phase::Starting;
}
},
idleFromPrompt,
[=] {
if (state->phase == Phase::Authorizing
|| state->phase == Phase::Loading
|| state->phase == Phase::Ready) {
return false;
}
state->phase = Phase::Authorizing;
return true;
});
};
// A key restored from Telegram's backup is stored through the install
// ladder, whose chooser asks for the vault itself, so that path
// carries no read grant and asks nothing before the backup is
// fetched: the cloud password box comes only when the server asks
// for it. A key held on this device is read right after its unlock,
// whose submit is the explicit activation, as the chooser's Save is.
if (!parkedKey && !show->session().wallet().revealsLocally()) {
start(KeyAuthorization{ .install = MakeCustodyInstaller(show) });
return;
}
AcquireVaultUnlock({
.show = show,
.done = crl::guard(box, [=](KeyAuthorization auth) {
if (state->phase != Phase::Starting) {
return;
} else if (!auth.valid()) {
idleFromPrompt();
return;
}
start(std::move(auth));
}),
});
});
AddBusyFooterSpinner(button, state->loading.value());
const auto isRevealKey = [](int key) {
return key == Qt::Key_Return
|| key == Qt::Key_Enter
|| key == Qt::Key_Space;
};
const auto armAfterRelease = [=] {
crl::on_main(box, [=] {
if (state->phase == Phase::Ready && !state->pointerDown) {
state->armed = true;
}
});
};
const auto filterRevealKey = [=](not_null<QEvent*> e) {
const auto type = e->type();
if (type != QEvent::KeyPress && type != QEvent::KeyRelease) {
return base::EventFilterResult::Continue;
}
const auto keyEvent = static_cast<QKeyEvent*>(e.get());
if (!isRevealKey(keyEvent->key())) {
return base::EventFilterResult::Continue;
}
if (type == QEvent::KeyRelease) {
state->absorbEnter = false;
if (state->phase == Phase::Ready) {
armAfterRelease();
}
return base::EventFilterResult::Continue;
}
if (state->phase == Phase::Ready
&& (state->absorbEnter || keyEvent->isAutoRepeat())) {
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
};
base::install_event_filter(button.data(), [=](not_null<QEvent*> e) {
const auto type = e->type();
if (type == QEvent::MouseButtonPress) {
state->pointerDown = true;
if (state->phase == Phase::Ready) {
state->absorbEnter = false;
state->armed = true;
}
} else if (type == QEvent::MouseButtonRelease) {
state->pointerDown = false;
if (state->phase == Phase::Ready) {
armAfterRelease();
}
}
return filterRevealKey(e);
});
base::install_event_filter(box, [=](not_null<QEvent*> e) {
return filterRevealKey(e);
});
SubmitBoxOnEnter(box, [=] {
if (const auto strong = button.data()) {
strong->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);
}
});
show->session().wallet().transferWalletIdentityChanges(
) | rpl::on_next([=] {
state->words.reset();
state->armed = false;
state->absorbEnter = false;
if (state->phase != Phase::Idle) {
state->phase = Phase::Idle;
state->loading = false;
}
}, box->lifetime());
}
// The warning sheet comes first and its Show press acquires what the reveal
// needs: the vault unlock for a key held on this device, so the box order
// is warning, passcode, phrase, or warning, phrase for an open or retained
// vault; the install ladder for a key restored from Telegram's backup, with
// the cloud password box before it when the server asks for one, and the
// chooser at the store.
void WalletRevealFlow(
std::shared_ptr<Main::SessionShow> show,
std::optional<QByteArray> parkedKey = std::nullopt) {
show->showBox(Box(WalletPhraseWarningBox, show, parkedKey));
}
enum class WalletImportMode {
Replace,
Restore,
};
// The about text replaces the mode's default cover line, so an import that
// serves a specific action can say what the phrase is needed for.
void WalletImportBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
WalletImportMode mode,
Fn<void()> restored,
std::shared_ptr<KeyContext> context,
rpl::producer<QString> about);
void ShowInvalidSecretWords(
std::shared_ptr<Main::SessionShow> show,
bool foreign,
std::shared_ptr<KeyContext> context = nullptr) {
auto args = Ui::ConfirmBoxArgs{
.confirmText = tr::lng_bot_download_retry(),
.title = tr::lng_wallet_import_invalid_title(),
};
if (foreign) {
args.text = tr::lng_wallet_import_invalid_spelling(tr::now)
+ u"\n\n"_q
+ tr::lng_wallet_import_invalid_scheme(tr::now);
} else {
args.text = tr::lng_wallet_import_invalid_spelling(tr::now);
}
const auto box = show->show(Ui::MakeInformBox(std::move(args)));
if (context) {
context->allowPromptRetry(box);
}
}
int AddressGroupsWidth(
const style::font &font,
const QString &address,
int groupsPerLine) {
return groupsPerLine * font->width(address.left(kAddressGroup))
+ (groupsPerLine - 1) * font->width(QChar(' '));
}
void PaintAddressGroups(
QPainter &p,
const style::font &font,
const QString &address,
QPoint origin,
int groupsPerLine) {
const auto groupWidth = font->width(address.left(kAddressGroup));
const auto spaceWidth = font->width(QChar(' '));
const auto groups = kAddressLength / kAddressGroup;
p.setFont(font);
for (auto i = 0; i != groups; ++i) {
const auto line = i / groupsPerLine;
const auto column = i % groupsPerLine;
p.setPen((i % 2) ? st::windowSubTextFg : st::windowFg);
p.drawText(
origin.x() + column * (groupWidth + spaceWidth),
origin.y() + line * font->height + font->ascent,
address.mid(i * kAddressGroup, kAddressGroup));
}
}
void AddAddressPlate(
not_null<Ui::VerticalLayout*> container,
const QString &address,
const style::margins &margin,
std::shared_ptr<Ui::Show> show = nullptr) {
const auto font = st::walletAddressPlateFont->monospace();
const auto inner = st::walletAddressPlateInner;
const auto groups = kAddressLength / kAddressGroup;
const auto lines = groups / kAddressGroupsPerLine;
const auto plate = container->add(
object_ptr<Ui::FixedHeightWidget>(
container,
inner + lines * font->height + inner),
margin);
plate->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(plate);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgOver);
p.drawRoundedRect(
plate->rect(),
st::walletAddressPlateRadius,
st::walletAddressPlateRadius);
const auto lineWidth = AddressGroupsWidth(
font,
address,
kAddressGroupsPerLine);
const auto left = (plate->width() - lineWidth) / 2;
PaintAddressGroups(
p,
font,
address,
{ left, inner },
kAddressGroupsPerLine);
}, plate->lifetime());
if (show) {
const auto copy = Ui::CreateChild<Ui::AbstractButton>(plate);
copy->setClickedCallback(
CopyAddressCallback(std::move(show), address));
plate->sizeValue(
) | rpl::on_next([=](QSize size) {
copy->setGeometry(QRect(QPoint(), size));
}, copy->lifetime());
}
}
void ShowWrongSecretWords(
std::shared_ptr<Main::SessionShow> show,
bool outdated,
std::shared_ptr<KeyContext> context) {
const auto address = outdated
? QString()
: show->session().wallet().addressFriendly(false);
const auto shown = show->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto &padding = st::boxPadding;
const auto plate = (address.size() == kAddressLength);
box->setTitle(tr::lng_wallet_restore_wrong_title());
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
(outdated
? tr::lng_wallet_restore_outdated_text()
: tr::lng_wallet_restore_other_text()),
st::boxLabel),
QMargins(
padding.left(),
0,
padding.right(),
plate ? 0 : padding.bottom()));
if (plate) {
AddAddressPlate(
box->verticalLayout(),
address,
QMargins(
padding.left(),
st::walletAddressPlateSkip,
padding.right(),
padding.bottom()));
}
box->addButton(tr::lng_bot_download_retry(), [=] {
box->closeBox();
});
}));
if (context) {
context->allowPromptRetry(shown);
}
}
void ShowSendRecipientWallet(
std::shared_ptr<Ui::Show> show,
not_null<UserData*> user,
const QString &address,
Fn<void()> profile) {
const auto shown = user->username().isEmpty()
? user->name()
: ('@' + user->username());
show->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto &padding = st::boxPadding;
box->setTitle(tr::lng_wallet_send_user_wallet_title(
lt_name,
rpl::single(user->shortName())));
const auto about = box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_wallet_send_user_wallet_about(
lt_name,
rpl::single(tr::link(shown)),
tr::marked),
st::boxLabel),
QMargins(padding.left(), 0, padding.right(), 0));
about->setClickHandlerFilter([=](const auto &...) {
const auto onstack = profile;
onstack();
return false;
});
AddAddressPlate(
box->verticalLayout(),
address,
QMargins(
padding.left(),
st::walletAddressPlateSkip,
padding.right(),
padding.bottom()),
box->uiShow());
box->addButton(tr::lng_box_ok(), [=] {
box->closeBox();
});
}));
}
// A restore that lands a record swaps the public-key-only client for the
// signing one asynchronously, so a continuation that reads the signing
// client - the rotation offer of a backup disable, a send - would find it
// missing if it ran at once. It runs once the session reports the signing
// client ready. The wait is bounded: a recovery that never settles lets the
// continuation run and be refused typed by the session on its own.
void RunWhenSigningReady(
std::shared_ptr<Main::SessionShow> show,
Fn<void()> action) {
if (!action) {
return;
}
auto &wallet = show->session().wallet();
if (wallet.signingReady()) {
action();
return;
}
const auto weakSession = base::make_weak(&show->session());
const auto lifetime = std::make_shared<rpl::lifetime>();
const auto run = [=] {
const auto owned = base::take(*lifetime);
if (weakSession && show->valid()) {
action();
}
};
const auto timeout = lifetime->make_state<base::Timer>(run);
timeout->callOnce(kSigningReadyTimeout);
wallet.signingReadyValue() | rpl::filter([](bool ready) {
return ready;
}) | rpl::on_next(run, *lifetime);
}
void RequestCustodyRestore(
std::shared_ptr<Main::SessionShow> show,
KeyAuthorization auth,
std::optional<Core::CloudPasswordResult> password,
base::weak_qptr<PasscodeBox> passcode,
Fn<void()> action,
Fn<void()> unblock,
std::shared_ptr<KeyContext> context = nullptr,
Fn<void()> onPasswordMissing = nullptr) {
if (context && !context->valid()) {
context->cancel();
return;
}
const auto purpose = !context
? u"backup_management"_q
: context->scope() ? u"decrypt_comment"_q : u"send"_q;
LOG(("Wallet Info: key restore requested purpose=%1 password_supplied=%2."
).arg(purpose).arg(password.has_value()));
const auto done = [=] {
if (passcode) {
passcode->closeBox();
}
RunWhenSigningReady(show, action);
};
const auto fail = [=](const QString &error) {
const auto contextValid = !context || context->valid();
LOG(("Wallet Error: key restore result purpose=%1 error=%2 "
"context_valid=%3 password_prompt=%4."
).arg(purpose
).arg(error
).arg(contextValid
).arg(bool(passcode)));
if (context && (!contextValid
|| error == u"PHRASE_ORIGIN_EXPIRED"_q
|| error == u"PHRASE_SILENT_ERROR"_q)) {
context->cancel();
return;
}
if (onPasswordMissing && error == u"PASSWORD_MISSING"_q) {
onPasswordMissing();
return;
}
auto terminal = true;
const auto finish = gsl::finally([&] {
if (context && terminal) {
context->cancel();
}
});
if (!context && unblock) {
unblock();
}
// A dismissed protection chooser restored nothing and has nothing to
// state. The cloud password box goes with it, because the proof it
// has already sent cannot be sent a second time.
if (error == u"PHRASE_INSTALL_CANCELLED"_q) {
if (passcode) {
passcode->closeBox();
}
return;
}
if (error == u"PHRASE_INSTALL_FAILED"_q) {
if (passcode) {
passcode->closeBox();
}
show->showToast(tr::lng_wallet_key_save_error(tr::now));
return;
}
if (passcode && passcode->handleCustomCheckError(error)) {
terminal = false;
return;
}
if (error == u"PHRASE_VAULT_LOCKED"_q) {
if (passcode) {
passcode->closeBox();
}
show->showToast(tr::lng_wallet_vault_locked(tr::now));
return;
}
if (auto box = PrePasswordErrorBox(
error,
&show->session(),
EnforcementCheckAbout(error),
context)) {
if (passcode) {
if (context) {
context->closePrompt(passcode);
} else {
passcode->closeBox();
}
}
show->showBox(std::move(box));
terminal = false;
return;
}
ShowPhraseError(show, PhraseOperation::Restore, error);
};
auto &wallet = show->session().wallet();
if (context) {
wallet.restoreFromBackup(
std::move(auth),
std::move(password),
context->scope(),
[=](KeyAuthorization restored) {
context->ready(std::move(restored));
},
fail);
} else {
wallet.restoreFromBackup(
std::move(auth),
std::move(password),
done,
fail);
}
}
// The restore is requested without a password first, even when the account
// has one, because the server decides whether this backup needs it. Only
// its PASSWORD_MISSING answer loads the cloud password state, asks for the
// password and repeats the restore with it.
void StartCustodyRestore(
std::shared_ptr<Main::SessionShow> show,
KeyAuthorization auth,
Fn<void()> action,
Fn<void()> unblock = nullptr,
std::shared_ptr<KeyContext> context = nullptr) {
if (context && !context->valid()) {
context->cancel();
return;
}
const auto session = &show->session();
const auto firstKeyUse = RestoreIsFirstKeyUse(session);
const auto askPassword = [=] {
if (context && !context->valid()) {
context->cancel();
return;
}
const auto cached = session->api().cloudPassword().stateCurrent();
LOG(("Wallet Info: key restore requires password; "
"cached_state=%1 cached_has_password=%2."
).arg(cached.has_value()).arg(cached && cached->hasPassword));
session->api().cloudPassword().reload();
const auto lifetime = std::make_shared<rpl::lifetime>();
if (context) {
context->lifetime().add([=] { lifetime->destroy(); });
const auto timeout = lifetime->make_state<base::Timer>([=] {
const auto owned = base::take(*lifetime);
if (context->valid()) {
LOG(("Wallet Error: restore password state timed out "
"after %1 ms; comment_scope=%2."
).arg(kCommentPasswordStateTimeout
).arg(bool(context->scope())));
ShowPhraseError(
show,
PhraseOperation::Restore,
u"PHRASE_PASSWORD_STATE_TIMEOUT"_q);
}
context->cancel();
});
timeout->callOnce(kCommentPasswordStateTimeout);
}
session->api().cloudPassword().state(
) | rpl::take(
1
) | rpl::on_next([=](const Core::CloudPasswordState &state) {
const auto owned = base::take(*lifetime);
if (context && !context->valid()) {
context->cancel();
return;
}
if (!state.hasPassword) {
// The server asked for a password this account does not
// have, so a repeat without one would only be refused again.
ShowPhraseError(
show,
PhraseOperation::Restore,
u"PHRASE_PASSWORD_STATE_MISSING"_q);
if (context) {
context->cancel();
} else if (unblock) {
unblock();
}
return;
}
auto fields = RestorePasswordFields(
state,
firstKeyUse,
tr::lng_bots_password_confirm_description(tr::now));
fields.customShow = context;
fields.customCheckCallback = [=](
const Core::CloudPasswordResult &result,
base::weak_qptr<PasscodeBox> passcode) {
RequestCustodyRestore(
show,
auth,
result,
passcode,
action,
unblock,
context);
};
const auto passcode = show->show(
Box<PasscodeBox>(session, fields));
if (context) {
context->cancelOnClose(passcode, true);
} else if (unblock) {
unblock();
}
}, *lifetime);
};
RequestCustodyRestore(
show,
std::move(auth),
std::nullopt,
nullptr,
action,
unblock,
context,
askPassword);
}
// The device mode is unknown only while the wallet's state has not reached
// this client, which is the normal state of a freshly logged in account
// outside the Wallet window: nothing else asks for it. A press asks, waits
// for the answer and then climbs the ladder that answer names. The wait is
// bounded, and a wallet the server will not serve is stated once.
void ResolveDeviceCustody(
std::shared_ptr<Main::SessionShow> show,
Fn<void()> resolved,
std::shared_ptr<KeyContext> context) {
const auto weakSession = base::make_weak(&show->session());
auto &wallet = show->session().wallet();
const auto lifetime = std::make_shared<rpl::lifetime>();
wallet.startPolling();
lifetime->add([weakSession] {
if (weakSession) {
weakSession->wallet().stopPolling();
}
});
if (context) {
context->lifetime().add([lifetime] { lifetime->destroy(); });
}
const auto settled = std::make_shared<bool>(false);
const auto finish = [=](bool known) {
if (*settled) {
return;
}
*settled = true;
const auto owned = base::take(*lifetime);
if (!weakSession || !show->valid()) {
if (context) {
context->cancel();
}
return;
} else if (known) {
resolved();
return;
}
show->showToast(tr::lng_wallet_unavailable(tr::now));
if (context) {
context->cancel();
}
};
const auto check = [=] {
if (*settled || !weakSession) {
return;
} else if (context && !context->valid()) {
*settled = true;
const auto owned = base::take(*lifetime);
context->cancel();
return;
}
auto &wallet = weakSession->wallet();
const auto presence = wallet.presence();
if (wallet.deviceCustodyState().mode != DeviceMode::Unknown) {
finish(true);
} else if (presence == Presence::Unavailable
|| presence == Presence::Missing
|| presence == Presence::AddressUnreadable) {
finish(false);
}
};
const auto timeout = lifetime->make_state<base::Timer>([=] {
finish(false);
});
timeout->callOnce(kCustodyResolveTimeout);
rpl::merge(
wallet.transferWalletIdentityChanges(),
wallet.custodyUpdates()
) | rpl::on_next(check, *lifetime);
check();
}
void RunKeyRequiringAction(
std::shared_ptr<Main::SessionShow> show,
Fn<void()> action,
KeyActionKind kind,
std::shared_ptr<KeyContext> context,
rpl::producer<QString> importAbout) {
if (context && !context->valid()) {
context->cancel();
return;
}
const auto state = show->session().wallet().deviceCustodyState();
if (state.conflict) {
// Resolving the conflict lands no key: it drops the parked record
// or exports its phrase. The same press climbs this ladder again
// once the parked wallet is switched away, into whatever the served
// wallet then needs. The export runs over the plain show, because
// the context reads a closed prompt of its own as the end of the
// press, and the export box is closed on the way back to the list.
const auto plain = context ? context->plain() : show;
const auto retry = [=] {
if (context) {
context->acceptClosed();
}
RunKeyRequiringAction(
show,
action,
kind,
context,
rpl::duplicate(importAbout));
};
show->showBox(Box(WalletConflictBox, plain, retry));
} else if (state.mode == DeviceMode::Full) {
action();
} else if (state.mode == DeviceMode::ReadOnlyRestorable) {
if (kind == KeyActionKind::Reveal) {
action();
} else {
StartCustodyRestore(
show,
KeyAuthorization{ .install = context
? context->installer()
: MakeCustodyInstaller(show) },
std::move(action),
nullptr,
context);
}
} else if (state.mode == DeviceMode::ReadOnlyNotRestorable) {
show->showBox(Box(
WalletImportBox,
show,
WalletImportMode::Restore,
(kind == KeyActionKind::ResumeAfterRestore) ? action : nullptr,
context,
std::move(importAbout)));
} else {
ResolveDeviceCustody(show, [=] {
RunKeyRequiringAction(
show,
action,
kind,
context,
rpl::duplicate(importAbout));
}, context);
}
}
void RequestWalletReplace(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> origin,
std::optional<std::vector<QString>> words,
std::optional<Core::CloudPasswordResult> password,
base::weak_qptr<PasscodeBox> passcode,
Fn<void()> unblock,
Fn<void(const QString &text)> showError) {
const auto imported = words.has_value();
const auto done = crl::guard(origin, [=](CustodyOutcome outcome) {
if (passcode) {
passcode->closeBox();
}
// Only an imported replace carries a custody write - replaceWithNew
// hands finishConfirmedReplace no new record - so the imported title
// is the right one here. The replacement itself stands, so this
// states the half that failed instead of a failure, and closes the
// import box with the same layer operation rather than racing a hide.
if (outcome == CustodyOutcome::WriteFailed) {
show->showBox(
Ui::MakeInformBox({
.text = tr::lng_wallet_imported_not_stored(tr::now),
.title = tr::lng_wallet_imported_title(),
}),
Ui::LayerOption::CloseOther);
return;
}
show->hideLayer();
show->showToast({
.title = (imported
? tr::lng_wallet_imported_title(tr::now)
: tr::lng_wallet_created_title(tr::now)),
.text = { imported
? tr::lng_wallet_imported_text(tr::now)
: tr::lng_wallet_created_text(tr::now) },
.icon = &st::toastCheckIcon,
});
});
const auto fail = crl::guard(origin, [=](const QString &error) {
unblock();
// A dismissed protection chooser imported nothing and has nothing to
// state. The cloud password box goes with it, because the proof it
// has already sent cannot be sent a second time.
if (error == u"REPLACE_INSTALL_CANCELLED"_q) {
if (passcode) {
passcode->closeBox();
}
return;
}
if (passcode && passcode->handleCustomCheckError(error)) {
return;
}
if (error == u"REPLACE_VAULT_LOCKED"_q) {
if (passcode) {
passcode->closeBox();
}
show->showToast(tr::lng_wallet_vault_locked(tr::now));
return;
}
if (auto box = PrePasswordErrorBox(
error,
&show->session(),
EnforcementCheckAbout(error))) {
if (passcode) {
passcode->closeBox();
}
show->showBox(std::move(box));
return;
}
if (passcode) {
passcode->closeBox();
}
if (!imported) {
show->showToast(ErrorWithType(
tr::lng_wallet_create_error(tr::now),
error));
} else if (error == u"REPLACE_INVALID_PHRASE"_q
|| error == u"REPLACE_FOREIGN_PHRASE"_q) {
ShowInvalidSecretWords(
show,
error == u"REPLACE_FOREIGN_PHRASE"_q);
} else if (error == u"REPLACE_OUTDATED_PHRASE"_q) {
if (showError) {
showError(QString());
}
ShowWrongSecretWords(show, true, nullptr);
} else {
// WHY: the server checks the proof against the key the chain
// holds for the phrase's address, so a refused phrase stays
// refused; a retry cannot help, only support can.
const auto text = (error == u"REPLACE_KEY_CHANGING"_q)
? tr::lng_wallet_import_key_changing(tr::now)
: (error == u"WALLET_PROOF_INVALID"_q)
? tr::lng_wallet_import_not_verified(tr::now)
: ErrorWithType(tr::lng_wallet_import_failed(tr::now), error);
if (showError) {
showError(text);
} else {
show->showToast(text);
}
}
});
auto &wallet = show->session().wallet();
if (imported) {
wallet.replaceWithImported(
KeyAuthorization{ .install = MakeCustodyInstaller(show) },
std::move(*words),
std::move(password),
done,
fail);
} else {
wallet.replaceWithNew(std::move(password), done, fail);
}
}
void StartWalletReplace(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> origin,
std::optional<std::vector<QString>> words,
Fn<void()> unblock,
Fn<void(const QString &text)> showError = nullptr) {
const auto session = &show->session();
session->api().cloudPassword().reload();
session->api().cloudPassword().state(
) | rpl::take(
1
) | rpl::on_next([=](const Core::CloudPasswordState &state) {
if (!state.hasPassword) {
RequestWalletReplace(
show,
origin,
words,
std::nullopt,
nullptr,
unblock,
showError);
return;
}
auto fields = PasscodeBox::CloudFields::From(state);
fields.customTitle = tr::lng_bots_password_confirm_title();
fields.customDescription = tr::lng_bots_password_confirm_description(
tr::now);
fields.customSubmitButton = tr::lng_passcode_submit();
fields.customCheckCallback = [=](
const Core::CloudPasswordResult &result,
base::weak_qptr<PasscodeBox> passcode) {
RequestWalletReplace(
show,
origin,
words,
result,
passcode,
unblock,
showError);
};
show->showBox(Box<PasscodeBox>(session, fields));
unblock();
}, origin->lifetime());
}
struct BackupDisableProof {
KeyAuthorization auth;
BackupDisableApproval approved;
};
void ShowBackupChangeError(
std::shared_ptr<Main::SessionShow> show,
const QString &error) {
if (error == u"BACKUP_VAULT_LOCKED"_q) {
show->showToast(tr::lng_wallet_vault_locked(tr::now));
} else if (auto box = PrePasswordErrorBox(
error,
&show->session(),
EnforcementCheckAbout(error))) {
show->showBox(std::move(box));
} else if (error == u"BACKUP_PHRASE_OUTDATED"_q) {
ShowKeyChangedBox(show, tr::lng_wallet_send_key_changed_text());
} else if (error == u"BACKUP_KEY_UNCONFIRMED"_q) {
show->showToast(tr::lng_wallet_import_key_changing(tr::now));
} else if (error == u"BACKUP_NOT_VERIFIED"_q) {
show->showToast(tr::lng_wallet_import_not_verified(tr::now));
} else {
show->showToast((error == u"WALLET_BACKUP_NOT_AVAILABLE"_q)
? tr::lng_wallet_backup_about_unavailable(tr::now)
: ErrorWithType(tr::lng_wallet_backup_error(tr::now), error));
}
}
void RequestBackupChange(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> origin,
std::optional<Core::CloudPasswordResult> password,
base::weak_qptr<PasscodeBox> passcode,
Fn<void()> unblock,
Fn<void()> done,
std::optional<BackupDisableProof> proof = std::nullopt) {
const auto succeeded = crl::guard(origin, [=] {
unblock();
if (passcode) {
passcode->closeBox();
}
done();
});
const auto fail = crl::guard(origin, [=](const QString &error) {
unblock();
if (passcode && passcode->handleCustomCheckError(error)) {
return;
} else if (passcode) {
passcode->closeBox();
}
ShowBackupChangeError(show, error);
});
auto &wallet = show->session().wallet();
if (proof) {
wallet.disableBackupWithProof(
std::move(proof->auth),
std::move(proof->approved),
succeeded,
fail);
} else {
wallet.disableBackup(std::move(password), succeeded, fail);
}
}
void StartBackupRequest(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> origin,
Fn<void()> unblock,
Fn<void()> done) {
const auto session = &show->session();
session->api().cloudPassword().reload();
session->api().cloudPassword().state(
) | rpl::take(
1
) | rpl::on_next([=](const Core::CloudPasswordState &state) {
if (!state.hasPassword) {
RequestBackupChange(
show,
origin,
std::nullopt,
nullptr,
unblock,
done);
return;
}
auto fields = PasscodeBox::CloudFields::From(state);
fields.customTitle = tr::lng_bots_password_confirm_title();
fields.customDescription = tr::lng_bots_password_confirm_description(
tr::now);
fields.customSubmitButton = tr::lng_passcode_submit();
fields.customCheckCallback = [=](
const Core::CloudPasswordResult &result,
base::weak_qptr<PasscodeBox> passcode) {
RequestBackupChange(
show,
origin,
result,
passcode,
unblock,
done);
};
show->showBox(Box<PasscodeBox>(session, fields));
unblock();
}, origin->lifetime());
}
// A usable local copy of the served wallet's current key proves ownership,
// so no cloud password is asked. Without one (the served key moved since the
// phrase was shown, or the hardware wrap cannot be opened) no proof can be
// made and the cloud password route stays as before. A locked vault still
// has usable custody: the session reports it locked and asks no password.
void RequestBackupDisable(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> origin,
BackupDisableProof proof,
Fn<void()> unblock,
Fn<void()> done) {
if (!show->session().wallet().revealsLocally()) {
StartBackupRequest(
show,
origin,
std::move(unblock),
std::move(done));
return;
}
RequestBackupChange(
show,
origin,
std::nullopt,
nullptr,
std::move(unblock),
std::move(done),
std::move(proof));
}
void ShowBackupEnabledToast(std::shared_ptr<Main::SessionShow> show) {
show->showToast({
.title = tr::lng_wallet_backup_enabled_title(tr::now),
.text = { tr::lng_wallet_backup_enabled_text(tr::now) },
.icon = &st::toastCheckIcon,
});
}
void StartBackupEnable(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> origin,
not_null<bool*> busy) {
const auto upload = crl::guard(origin, [=](KeyAuthorization auth) {
if (*busy) {
return;
}
*busy = true;
show->session().wallet().enableBackup(
std::move(auth),
crl::guard(origin, [=] {
*busy = false;
ShowBackupEnabledToast(show);
}),
crl::guard(origin, [=](const QString &error) {
*busy = false;
ShowBackupChangeError(show, error);
}));
});
// The restorable and not-restorable arms install custody first and only
// then run this, so the acquisition sits after them and every arm reaches
// enableBackup with a live grant.
RunKeyRequiringAction(show, crl::guard(origin, [=] {
if (*busy) {
return;
}
AcquireVaultUnlock({
.show = show,
.done = [=](KeyAuthorization auth) {
if (auth.valid()) {
upload(std::move(auth));
}
},
});
}), KeyActionKind::ResumeAfterRestore);
}
// WHY: dismissing the write-down or its quiz silently drops the disable,
// so while it still would, the dismissal asks first; no abandons = always.
void GuardBackupDisableDismiss(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
Fn<bool()> abandons) {
struct State {
base::weak_qptr<Ui::GenericBox> confirmation;
};
const auto state = box->lifetime().make_state<State>();
const auto abandoning = [=] {
return !abandons || abandons();
};
const auto confirm = [=] {
if (state->confirmation) {
return;
}
state->confirmation = show->show(Ui::MakeConfirmBox({
.text = tr::lng_wallet_backup_cancel_text(),
.confirmed = crl::guard(box, [=](Fn<void()> close) {
close();
box->closeBox();
}),
.confirmText = tr::lng_box_yes(),
.cancelText = tr::lng_box_no(),
.title = tr::lng_wallet_backup_cancel_title(),
}));
};
box->boxClosing() | rpl::on_next([=] {
if (const auto strong = state->confirmation.get()) {
strong->closeBox();
}
}, box->lifetime());
AddBoxCloseButton(box, [=] {
if (abandoning()) {
confirm();
} else {
box->closeBox();
}
});
const auto isEscape = [](not_null<QEvent*> e) {
return (e->type() == QEvent::KeyPress)
&& (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Escape);
};
base::install_event_filter(box, [=](not_null<QEvent*> e) {
if (!isEscape(e) || !abandoning()) {
return base::EventFilterResult::Continue;
}
confirm();
return base::EventFilterResult::Cancel;
});
box->showFinishes() | rpl::take(1) | rpl::on_next([=] {
const auto layer = box->parentWidget();
const auto stack = BoxLayerStack(box);
if (!layer || !stack) {
return;
}
base::install_event_filter(box, stack, [=](not_null<QEvent*> e) {
const auto dismissal = (e->type() == QEvent::MouseButtonPress)
|| isEscape(e);
if (!dismissal || layer->isHidden() || !abandoning()) {
return base::EventFilterResult::Continue;
}
Ui::PostponeCall(box, confirm);
return base::EventFilterResult::Cancel;
});
}, box->lifetime());
}
void WalletBackupPhraseBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
std::vector<QString> words,
Fn<void(std::vector<QString>)> next,
rpl::producer<QString> title,
rpl::producer<TextWithEntities> text) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
AddPhraseBoxHeader(
box,
PhraseBoxLottie(int(words.size())),
std::move(title),
std::move(text),
st::walletPhraseTextLabel,
st::walletCoverLottieSize,
st::walletPhraseLottieMargin,
st::walletPhraseTextMargin);
AddPhraseGrid(box, words);
GuardBackupDisableDismiss(box, show, nullptr);
const auto shownAt = crl::now();
box->addButton(tr::lng_continue(), [=] {
if (crl::now() - shownAt < kBackupWriteDownDelay) {
show->showBox(Ui::MakeInformBox({
.text = tr::lng_wallet_backup_sure_text(tr::now),
.confirmText = tr::lng_box_ok(),
.title = tr::lng_wallet_backup_sure_title(),
}));
return;
}
next(words);
box->closeBox();
});
}
// WHY: a rotated phrase carries two independent keys - the first 12 words
// restore the anchor and the last 12 the signing key - so the quiz proves
// both halves were written down, not three words of whichever half.
[[nodiscard]] std::vector<int> BackupQuizIndices(int count) {
auto result = std::vector<int>();
result.reserve(kBackupQuizWordCount);
if (count >= kImportWordCountLong) {
const auto signing = count - kImportWordCountShort;
const auto first = base::RandomIndex(signing);
const auto second = base::RandomIndex(signing - 1);
result.push_back(base::RandomIndex(kImportWordCountShort));
result.push_back(kImportWordCountShort + first);
result.push_back(kImportWordCountShort
+ second
+ ((second >= first) ? 1 : 0));
} else {
while (int(result.size()) < kBackupQuizWordCount) {
const auto index = base::RandomIndex(count);
if (!ranges::contains(result, index)) {
result.push_back(index);
}
}
}
ranges::sort(result);
return result;
}
[[nodiscard]] rpl::producer<TextWithEntities> BackupQuizText(
const std::vector<int> &indices) {
const auto number = [](int index) {
return rpl::single(tr::bold(QString::number(index + 1)));
};
return tr::lng_wallet_backup_test_text(
lt_index1,
number(indices[0]),
lt_index2,
number(indices[1]),
lt_index3,
number(indices[2]),
tr::marked);
}
[[nodiscard]] not_null<Ui::InputField*> AddBackupQuizField(
not_null<Ui::VerticalLayout*> container,
int index) {
const auto field = container->add(
object_ptr<Ui::InputField>(
container,
st::walletBackupQuizField,
Ui::InputField::Mode::SingleLine),
st::walletImportFieldMargin);
const auto number = Ui::CreateChild<Ui::FlatLabel>(
field,
QString::number(index + 1) + QChar('.'),
st::walletPhraseNumberLabel);
number->setAttribute(Qt::WA_TransparentForMouseEvents);
field->widthValue(
) | rpl::on_next([=](int width) {
number->moveToLeft(
st::walletImportNumberLeft,
st::walletImportNumberTop,
width);
}, field->lifetime());
return field;
}
[[nodiscard]] bool BackupQuizAnswerMatches(
not_null<Ui::InputField*> field,
const QString &word) {
const auto entered = field->getLastText().trimmed();
return (entered.compare(word.trimmed(), Qt::CaseInsensitive) == 0);
}
void WalletBackupQuizBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
std::vector<QString> words,
Fn<void()> passed,
Fn<void(Fn<void()> lock)> publishLock,
Fn<bool()> abandons) {
Expects(int(words.size()) >= kBackupQuizWordCount);
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
struct State {
std::vector<Ui::InputField*> fields;
std::vector<int> indices;
std::vector<bool> wrong;
};
const auto state = box->lifetime().make_state<State>();
state->indices = BackupQuizIndices(int(words.size()));
state->wrong.resize(kBackupQuizWordCount, false);
AddPhraseBoxHeader(
box,
u"wallet/test"_q,
tr::lng_wallet_backup_test_title(),
BackupQuizText(state->indices),
st::walletPhraseTextLabel,
st::walletPhraseGridLottieSize,
st::walletPhraseGridLottieMargin,
st::walletPhraseGridTextMargin);
const auto container = box->verticalLayout();
Ui::AddSkip(container, st::walletBackupQuizFieldsTopSkip);
for (const auto index : state->indices) {
state->fields.push_back(AddBackupQuizField(container, index));
}
Ui::AddSkip(container, st::walletBackupQuizFieldsBottomSkip);
GuardBackupDisableDismiss(box, show, std::move(abandons));
const auto button = box->addButton(tr::lng_continue());
const auto allFilled = [=] {
return ranges::all_of(state->fields, [](Ui::InputField *field) {
return !field->getLastText().trimmed().isEmpty();
});
};
const auto anyWrong = [=] {
return ranges::contains(state->wrong, true);
};
const auto refreshButton = [=] {
if (const auto raw = button.data()) {
SetButtonDisabledLook(raw, !allFilled() || anyWrong());
}
};
const auto submit = [=] {
if (!allFilled() || anyWrong()) {
return;
}
for (auto i = 0; i != kBackupQuizWordCount; ++i) {
const auto field = state->fields[i];
if (!BackupQuizAnswerMatches(field, words[state->indices[i]])) {
field->showError();
state->wrong[i] = true;
}
}
if (anyWrong()) {
refreshButton();
} else {
passed();
}
};
button->setClickedCallback(submit);
refreshButton();
for (auto i = 0; i != kBackupQuizWordCount; ++i) {
const auto field = state->fields[i];
field->changes() | rpl::on_next([=] {
state->wrong[i] = false;
refreshButton();
}, field->lifetime());
field->submits() | rpl::on_next([=] {
if (i + 1 < kBackupQuizWordCount) {
state->fields[i + 1]->setFocus();
} else {
submit();
}
}, field->lifetime());
}
box->setFocusCallback([=] {
state->fields.front()->setFocusFast();
});
if (publishLock) {
publishLock([=] {
for (const auto field : state->fields) {
field->setDisabled(true);
}
// A disabled field takes no focus, and with the focus left on
// the layer stack Escape closes the box past
// setCloseByEscape(false), so the box holds it itself.
box->setFocusCallback(nullptr);
box->setInnerFocus();
});
}
}
void ShowBackupDisabledToast(std::shared_ptr<Main::SessionShow> show) {
show->showToast({
.title = tr::lng_wallet_backup_disabled_title(tr::now),
.text = { tr::lng_wallet_backup_disabled_text(tr::now) },
.icon = &st::toastCheckIcon,
});
}
void CollectBackupPhrase(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> origin,
KeyAuthorization auth) {
const auto approved = show->session().wallet().backupDisableApproval();
const auto proof = BackupDisableProof{
.auth = auth,
.approved = approved.value_or(BackupDisableApproval()),
};
const auto showQuiz = [=](std::vector<QString> words) {
const auto quiz = std::make_shared<base::weak_qptr<Ui::GenericBox>>();
const auto requesting = std::make_shared<bool>(false);
*quiz = show->show(Box(WalletBackupQuizBox, show, words, [=] {
const auto strong = quiz->get();
if (!strong || *requesting) {
return;
}
*requesting = true;
RequestBackupDisable(
show,
strong,
proof,
[=] { *requesting = false; },
[=] {
if (const auto strong = quiz->get()) {
strong->closeBox();
}
ShowBackupDisabledToast(show);
});
}, nullptr, [=] {
return !*requesting;
}));
};
const auto showPhrase = [=](std::vector<QString> words) {
const auto count = int(words.size());
show->showBox(Box(
WalletBackupPhraseBox,
show,
words,
showQuiz,
tr::lng_wallet_phrase_title(),
tr::lng_wallet_phrase_text(
lt_count,
rpl::single(count * 1.) | tr::to_count(),
tr::marked)));
};
StartPhraseReveal(
show,
origin,
std::move(auth),
[] {},
std::nullopt,
showPhrase);
}
[[nodiscard]] QString RotationFeeText(
tr::phrase<lngtag_amount, lngtag_fiat> phrase,
int64 feeNano,
const FiatRate &rate) {
return phrase(
tr::now,
lt_amount,
Ui::FormatTonAmount(feeNano).full,
lt_fiat,
FormatFiat(feeNano, rate, kFeeFiatDecimals, true));
}
[[nodiscard]] QString RotationFailureReason(const QString &error) {
if (error == u"ROTATION_FEES"_q) {
return tr::lng_wallet_send_error_insufficient(tr::now);
} else if (error == u"ROTATION_ALREADY_SENDING"_q) {
return tr::lng_wallet_send_error_in_progress(tr::now);
} else if (error == u"ROTATION_VAULT_LOCKED"_q) {
return tr::lng_wallet_vault_locked(tr::now);
}
return tr::lng_wallet_backup_rotate_reason_failed(tr::now);
}
void ShowRotationFailedToast(
std::shared_ptr<Main::SessionShow> show,
const QString &error) {
show->showToast({
.title = tr::lng_wallet_backup_rotate_failed_title(tr::now),
.text = { tr::lng_wallet_backup_rotate_failed_text(
tr::now,
lt_reason,
RotationFailureReason(error)) },
.icon = &st::toastCheckIcon,
});
}
void SetBoxBusy(not_null<Ui::GenericBox*> box) {
box->clearButtons();
const auto button = box->addButton(rpl::single(QString()));
SetButtonDisabledLook(button.data(), true);
AddBusyFooterSpinner(button, rpl::single(true));
box->setCloseByOutsideClick(false);
box->setCloseByEscape(false);
}
struct RotationState {
bool submitted = false;
base::weak_qptr<Ui::GenericBox> quiz;
Fn<void()> lockQuiz;
KeyAuthorization auth;
};
void SubmitRotation(
std::shared_ptr<Main::SessionShow> show,
base::weak_qptr<Ui::GenericBox> origin,
not_null<bool*> busy,
std::shared_ptr<RotationState> state) {
const auto quiz = state->quiz.get();
if (!quiz || state->submitted) {
return;
}
state->submitted = true;
state->lockQuiz();
SetBoxBusy(quiz);
const auto closeQuiz = [=] {
if (const auto quiz = state->quiz.get()) {
quiz->closeBox();
}
};
show->session().wallet().submitRotation(state->auth, [=] {
closeQuiz();
const auto strong = origin.get();
if (!strong) {
return;
}
*busy = true;
const auto approved = show->session().wallet().backupDisableApproval();
RequestBackupDisable(
show,
strong,
BackupDisableProof{
.auth = state->auth,
.approved = approved.value_or(BackupDisableApproval()),
},
[=] { *busy = false; },
[=] { ShowBackupDisabledToast(show); });
}, [=](const QString &error) {
closeQuiz();
ShowRotationFailedToast(show, error);
});
}
void ShowRotationPhrase(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> origin,
not_null<bool*> busy,
std::vector<QString> words,
KeyAuthorization auth) {
const auto state = std::make_shared<RotationState>();
state->auth = std::move(auth);
const auto wallet = &show->session().wallet();
const auto weak = base::make_weak(origin);
const auto showQuiz = [=](std::vector<QString> words) {
const auto quiz = show->show(Box(WalletBackupQuizBox, show, words, [=] {
SubmitRotation(show, weak, busy, state);
}, [=](Fn<void()> lock) {
state->lockQuiz = std::move(lock);
}, [=] {
return !state->submitted;
}));
state->quiz = quiz;
quiz->boxClosing() | rpl::on_next([=] {
if (!state->submitted) {
wallet->abandonRotation();
}
}, quiz->lifetime());
};
const auto sheet = show->show(Box(
WalletBackupPhraseBox,
show,
words,
showQuiz,
tr::lng_wallet_backup_new_phrase_title(),
tr::lng_wallet_backup_new_phrase_text(tr::marked)));
sheet->boxClosing() | rpl::on_next([=] {
if (!state->quiz && !state->submitted) {
wallet->abandonRotation();
}
}, sheet->lifetime());
}
void StartRotation(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> origin,
not_null<bool*> busy,
int64 feeNano,
KeyAuthorization auth) {
if (*busy) {
return;
}
*busy = true;
const auto weak = base::make_weak(origin);
show->session().wallet().prepareRotation(auth, feeNano, [=](
std::vector<QString> words) {
const auto strong = weak.get();
if (!strong) {
show->session().wallet().abandonRotation();
return;
}
*busy = false;
ShowRotationPhrase(
show,
strong,
busy,
std::move(words),
auth);
}, crl::guard(origin, [=](const QString &error) {
*busy = false;
ShowRotationFailedToast(show, error);
}));
}
void ShowBackupTopUpAlert(
std::shared_ptr<Main::SessionShow> show,
int64 feeNano,
Fn<void()> topUp) {
const auto rate = show->session().wallet().rates().current();
show->showBox(Ui::MakeConfirmBox({
.text = RotationFeeText(
tr::lng_wallet_backup_topup_text,
feeNano,
rate),
.confirmed = [=](Fn<void()> close) {
close();
topUp();
ShowWalletReceiveBox(&show->session(), show);
},
.confirmText = tr::lng_credits_buy_button_short(),
.cancelText = tr::lng_export_suggest_cancel(),
.title = tr::lng_wallet_backup_topup_title(),
}));
}
struct RotationQuote {
int64 feeNano = 0;
SendError error = SendError::None;
friend inline bool operator==(
const RotationQuote &,
const RotationQuote &) = default;
};
struct BackupDisableState {
KeyAuthorization auth;
rpl::variable<std::optional<RotationQuote>> quote;
rpl::variable<bool> loading = false;
Fn<void()> onQuoted;
bool rotate = false;
bool started = false;
};
[[nodiscard]] bool RotationQuoteUsable(
const std::optional<RotationQuote> &quote) {
return quote
&& (quote->error == SendError::None
|| quote->error == SendError::InsufficientFees);
}
// What a phrase update does, then its network fee, or the top-up that fee
// needs when the balance does not cover it.
[[nodiscard]] rpl::producer<TextWithEntities> BackupUpdateNoteText(
not_null<Main::Session*> session,
rpl::producer<std::optional<RotationQuote>> quote) {
return rpl::combine(
std::move(quote),
FiatRateValue(session)
) | rpl::map([](
const std::optional<RotationQuote> &quote,
const FiatRate &rate) {
auto result = tr::lng_wallet_backup_update_text(tr::now, tr::marked);
if (RotationQuoteUsable(quote)) {
const auto phrase = (quote->error == SendError::None)
? tr::lng_wallet_backup_update_fee
: tr::lng_wallet_backup_topup_text;
result.append(u"\n\n"_q).append(tr::marked(
RotationFeeText(phrase, quote->feeNano, rate)));
}
return result;
});
}
void AddBackupUpdateNote(
not_null<Ui::VerticalLayout*> container,
rpl::producer<TextWithEntities> text,
rpl::producer<bool> shown) {
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::PaddingWrap<Ui::FlatLabel>>>(
container,
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
container,
object_ptr<Ui::FlatLabel>(
container,
std::move(text),
st::walletBackupNoteLabel),
st::walletBackupNotePadding),
st::walletBackupNoteMargin),
st::boxRowPadding);
// The plate is the note's own padding wrap: the slide wrap keeps one
// more around it for the outer margins, and entity() would unwrap all
// the way down to the label.
const auto plate = wrap->wrapped()->wrapped();
plate->paintRequest() | rpl::on_next([=] {
auto p = QPainter(plate);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::walletBackupNoteBg);
p.drawRoundedRect(
plate->rect(),
st::walletBackupNoteRadius,
st::walletBackupNoteRadius);
}, plate->lifetime());
wrap->toggleOn(std::move(shown));
wrap->finishAnimating();
}
// One confirmation for the whole disable: its checkbox decides whether the
// key is rotated on the way, and the fee for that is quoted in the
// background from the moment the box shows. The checkbox slides in only
// once the box has finished showing and the quote is usable, and a quote
// that failed leaves no update to offer. A press before the quote waits
// for it, because the reveal refuses to run beside it, and then goes to the
// write-down of the current words.
void WalletBackupDisableBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
base::weak_qptr<Ui::GenericBox> origin,
not_null<bool*> busy,
std::shared_ptr<BackupDisableState> state,
bool updateOffered) {
const auto session = &show->session();
box->setTitle(tr::lng_wallet_backup_disable_title());
const auto padding = st::boxPadding;
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_wallet_backup_disable_text(),
st::boxLabel),
QMargins(padding.left(), 0, padding.right(), padding.bottom()));
auto update = (Ui::Checkbox*)nullptr;
if (updateOffered) {
const auto wrap = box->addRow(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
object_ptr<Ui::VerticalLayout>(box)),
QMargins());
const auto inner = wrap->entity();
update = inner->add(
object_ptr<Ui::Checkbox>(
inner,
tr::lng_wallet_backup_update_check(tr::now),
false,
st::defaultBoxCheckbox),
st::walletBackupUpdateCheckboxMargin);
AddBackupUpdateNote(
inner,
BackupUpdateNoteText(session, state->quote.value()),
update->checkedValue());
wrap->hide(anim::type::instant);
box->showFinishes() | rpl::take(1) | rpl::map([=] {
return state->quote.value();
}) | rpl::flatten_latest(
) | rpl::filter([=](const std::optional<RotationQuote> &quote) {
return RotationQuoteUsable(quote);
}) | rpl::take(1) | rpl::on_next([=] {
if (!state->started && !state->loading.current()) {
wrap->show(anim::type::normal);
}
}, box->lifetime());
}
// The continuation a press leaves for a quote still out must not own
// the state that stores it, or a quote that never answers - the engine
// drops its callbacks at logout - would leak the state and its grant.
const auto weakState = std::weak_ptr(state);
const auto proceed = [=] {
const auto locked = weakState.lock();
if (!locked) {
return;
}
const auto quote = locked->quote.current();
Expects(quote.has_value());
locked->loading = false;
if (update && RotationQuoteUsable(quote)) {
update->setDisabled(false);
}
if (locked->started) {
return;
}
const auto strong = origin.get();
if (!locked->rotate) {
locked->started = true;
box->closeBox();
if (strong) {
CollectBackupPhrase(show, strong, locked->auth);
}
} else if (quote->error == SendError::None) {
locked->started = true;
box->closeBox();
if (strong) {
StartRotation(
show,
strong,
busy,
quote->feeNano,
locked->auth);
}
} else if (quote->error == SendError::InsufficientFees) {
ShowBackupTopUpAlert(
show,
quote->feeNano,
crl::guard(box, [=] { box->closeBox(); }));
}
};
const auto submit = [=] {
if (state->started || state->loading.current()) {
return;
}
state->rotate = update && update->checked();
if (state->quote.current()) {
proceed();
return;
}
state->loading = true;
if (update) {
update->setDisabled(true);
}
state->onQuoted = crl::guard(box, proceed);
};
const auto button = box->addButton(
BusyFooterLabel(
tr::lng_screen_reader_confirm_disable(),
state->loading.value()),
submit,
st::attentionBoxButton);
AddBusyFooterSpinner(button, state->loading.value());
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
box->events(
) | rpl::on_next([=](not_null<QEvent*> e) {
if (e->type() != QEvent::KeyPress) {
return;
}
const auto k = static_cast<QKeyEvent*>(e.get());
if (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return) {
submit();
}
}, box->lifetime());
}
void ShowBackupDisableBox(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> origin,
not_null<bool*> busy,
KeyAuthorization auth) {
auto &wallet = show->session().wallet();
const auto state = std::make_shared<BackupDisableState>();
state->auth = std::move(auth);
const auto updateOffered = wallet.rotationOffered();
if (updateOffered) {
// The quote runs while the box is up, so the fee is most likely
// known by the time the update checkbox is looked at, and lands in
// its note the moment the quote answers otherwise. It holds the keys
// box busy until then: a quote still out refuses every other custody
// action, a fresh quote beside it first of all, so a confirmation
// cancelled and reopened over it would only ever see failures.
*busy = true;
wallet.quoteRotationFee(state->auth, crl::guard(origin, [=](
FeeResult fee) {
*busy = false;
state->quote = std::make_optional(RotationQuote{
.feeNano = fee.feeNano,
.error = fee.error,
});
if (const auto onQuoted = base::take(state->onQuoted)) {
onQuoted();
}
}));
} else {
state->quote = std::make_optional(RotationQuote{
.error = SendError::SigningUnavailable,
});
}
show->showBox(Box(
WalletBackupDisableBox,
show,
base::make_weak(origin),
busy,
state,
updateOffered));
}
// The Disable press is where this flow acquires its one authorization: the
// quote, the reveal and the rotation store that follow all run under the
// same grant, and a vault emptied under them fails typed instead of asking
// again in the middle of the write-down. The confirmation comes after it,
// because the quote it starts needs that grant.
void StartBackupDisable(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> origin,
not_null<bool*> busy,
not_null<rpl::variable<bool>*> restoring) {
const auto weak = base::make_weak(origin);
const auto confirm = [=] {
AcquireVaultUnlock({
.show = show,
.done = [=](KeyAuthorization auth) {
const auto strong = weak.get();
if (!strong) {
return;
} else if (!auth.valid()) {
*busy = false;
return;
}
ShowBackupDisableBox(show, strong, busy, std::move(auth));
},
});
};
if (show->session().wallet().revealsLocally()) {
confirm();
return;
}
*busy = true;
*restoring = true;
StartCustodyRestore(
show,
KeyAuthorization{ .install = MakeCustodyInstaller(show) },
[=] {
if (weak) {
*busy = false;
*restoring = false;
confirm();
}
},
crl::guard(origin, [=] {
*busy = false;
*restoring = false;
}));
}
struct ImportCover {
not_null<Ui::RpWidget*> widget;
Fn<int()> height;
Fn<void()> updateScroll;
};
[[nodiscard]] ImportCover SetupImportCover(
not_null<Ui::GenericBox*> box,
WalletImportMode mode,
rpl::producer<QString> about) {
const auto spacer = box->verticalLayout()->add(
object_ptr<Ui::RpWidget>(box));
const auto cover = Ui::CreateChild<Ui::RpWidget>(box.get());
cover->show();
struct State {
std::unique_ptr<Lottie::Icon> icon;
Ui::FlatLabel *about = nullptr;
QPainterPath titlePath;
int fullTitleTop = 0;
int maxHeight = 0;
float64 aboutOpacity = -1.;
};
const auto state = cover->lifetime().make_state<State>();
state->icon = Lottie::MakeIcon({
.name = u"wallet/paper"_q,
.sizeOverride = QSize(
st::walletCoverLottieSize,
st::walletCoverLottieSize),
.limitFps = true,
});
state->about = Ui::CreateChild<Ui::FlatLabel>(
cover,
(about
? std::move(about)
: (mode == WalletImportMode::Restore)
? tr::lng_wallet_restore_text()
: tr::lng_wallet_import_text()),
st::walletPhraseTextLabel);
state->about->setAttribute(Qt::WA_TransparentForMouseEvents);
auto title = (mode == WalletImportMode::Restore)
? tr::lng_wallet_restore_title()
: tr::lng_wallet_import_title();
std::move(title) | rpl::on_next([=](const QString &text) {
state->titlePath = QPainterPath();
state->titlePath.addText(
0,
st::boxTitle.style.font->ascent,
st::boxTitle.style.font,
text);
cover->update();
}, cover->lifetime());
const auto countProgress = [=] {
return (state->maxHeight > st::boxTitleHeight)
? std::clamp(
(cover->height() - st::boxTitleHeight)
/ float64(state->maxHeight - st::boxTitleHeight),
0.,
1.)
: 1.;
};
const auto countBodyOpacity = [](float64 progress) {
return 1. - std::clamp((1. - progress) / kCoverBodyPart, 0., 1.);
};
const auto countArtRect = [=](float64 opacity) {
const auto side = st::walletCoverLottieSize * opacity;
return QRectF(
(cover->width() - side) / 2.,
st::walletCoverLottieMargin.top() * opacity,
side,
side);
};
const auto updateScroll = [=] {
if (state->maxHeight <= st::boxTitleHeight) {
return;
}
const auto height = std::clamp(
state->maxHeight - box->scrollTop(),
st::boxTitleHeight,
state->maxHeight);
cover->setGeometry(0, 0, box->width(), height);
const auto opacity = countBodyOpacity(countProgress());
if (state->aboutOpacity != opacity) {
state->aboutOpacity = opacity;
state->about->setOpacity(opacity);
state->about->moveToLeft(
st::boxRowPadding.left(),
int(countArtRect(opacity).bottom())
+ st::walletCoverLottieMargin.bottom()
+ st::boxTitleFont->height
+ st::walletPhraseTextMargin.top());
}
};
const auto relayout = [=] {
const auto width = box->width();
if (width <= 0) {
return;
}
state->about->resizeToWidth(width
- st::boxRowPadding.left()
- st::boxRowPadding.right());
state->fullTitleTop = st::walletCoverLottieMargin.top()
+ st::walletCoverLottieSize
+ st::walletCoverLottieMargin.bottom();
state->maxHeight = state->fullTitleTop
+ st::boxTitleFont->height
+ st::walletPhraseTextMargin.top()
+ state->about->height()
+ st::walletPhraseTextMargin.bottom();
spacer->resize(width, state->maxHeight);
updateScroll();
};
rpl::combine(
box->widthValue(),
state->about->heightValue()
) | rpl::on_next(relayout, cover->lifetime());
cover->paintRequest() | rpl::on_next([=] {
auto p = QPainter(cover);
p.fillRect(cover->rect(), st::boxBg);
auto hq = PainterHighQualityEnabler(p);
const auto progress = countProgress();
const auto opacity = countBodyOpacity(progress);
if (opacity > 0.) {
const auto artRect = countArtRect(opacity);
const auto frame = state->icon->frame(
QSize(int(artRect.width()), int(artRect.height())),
[=] { cover->update(); });
p.setOpacity(opacity);
p.drawImage(artRect, frame.image);
}
p.setOpacity(1.);
const auto titleRect = state->titlePath.boundingRect();
p.translate(
anim::interpolate(
(cover->width() - titleRect.width()) / 2,
st::boxTitlePosition.x(),
1. - progress),
anim::interpolate(
state->fullTitleTop,
st::boxTitlePosition.y(),
1. - progress));
p.translate(titleRect.center());
const auto scale = 1. + kCoverTitleScale * progress;
p.scale(scale, scale);
p.translate(-titleRect.center());
p.fillPath(state->titlePath, st::boxTitleFg);
}, cover->lifetime());
base::install_event_filter(cover, [=](not_null<QEvent*> event) {
if (event->type() == QEvent::Wheel) {
box->sendScrollViewportEvent(event);
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
box->showFinishes() | rpl::on_next([=] {
const auto icon = state->icon.get();
const auto update = [=] { cover->update(); };
if (anim::Disabled()) {
icon->jumpTo(icon->framesCount() - 1, update);
} else {
icon->animate(update, 0, icon->framesCount() - 1);
}
}, cover->lifetime());
return {
.widget = cover,
.height = [=] { return cover->height(); },
.updateScroll = updateScroll,
};
}
void WalletImportBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
WalletImportMode mode,
Fn<void()> restored,
std::shared_ptr<KeyContext> context,
rpl::producer<QString> about) {
if (context) {
context->cancelOnClose(box);
}
box->setWidth(st::boxWideWidth);
box->setStyle(st::giveawayGiftCodeBox);
box->setNoContentMargin(true);
struct State {
std::vector<Ui::InputField*> fields;
std::vector<Ui::RoundButton*> pasteButtons;
std::vector<Ui::CrossButton*> clearButtons;
rpl::variable<int> count = kImportWordCountShort;
rpl::variable<QString> error;
std::vector<Ui::AbstractButton*> suggestionRows;
std::vector<QString> suggestionWords;
int suggestionField = -1;
int suggestionSelected = 0;
int lastFocusedField = -1;
bool importing = false;
bool focusRestored = false;
};
const auto state = box->lifetime().make_state<State>();
const auto cover = SetupImportCover(box, mode, std::move(about));
const auto toggle = box->addRow(
object_ptr<Ui::SettingsSlider>(box, st::settingsSlider),
st::walletImportToggleMargin);
toggle->setSections({
tr::lng_wallet_import_words(
tr::now,
lt_count,
kImportWordCountShort),
tr::lng_wallet_import_words(
tr::now,
lt_count,
kImportWordCountLong),
});
toggle->setActiveSectionFast(0);
const auto addWordField = [=](
not_null<Ui::VerticalLayout*> container,
int index) {
const auto field = container->add(
object_ptr<Ui::InputField>(
container,
st::walletImportField,
Ui::InputField::Mode::SingleLine),
st::walletImportFieldMargin);
const auto number = Ui::CreateChild<Ui::FlatLabel>(
field,
QString::number(index + 1) + QChar('.'),
st::walletPhraseNumberLabel);
number->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto paste = Ui::CreateChild<Ui::RoundButton>(
field,
tr::lng_mac_menu_paste(),
st::walletImportPaste);
paste->setTextTransform(Ui::RoundButtonTextTransform::NoTransform);
paste->hide();
const auto clear = Ui::CreateChild<Ui::CrossButton>(
field,
st::walletImportClear);
field->widthValue(
) | rpl::on_next([=](int width) {
number->moveToLeft(
st::walletImportNumberLeft,
st::walletImportNumberTop,
width);
paste->moveToRight(0, st::walletImportPasteTop);
clear->moveToRight(
st::walletImportClearPosition.x(),
st::walletImportClearPosition.y(),
width);
}, field->lifetime());
state->fields.push_back(field);
state->pasteButtons.push_back(paste);
state->clearButtons.push_back(clear);
};
for (auto i = 0; i != kImportWordCountShort; ++i) {
addWordField(box->verticalLayout(), i);
}
const auto extraWrap = box->verticalLayout()->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box->verticalLayout(),
object_ptr<Ui::VerticalLayout>(box->verticalLayout())));
const auto extra = extraWrap->entity();
for (auto i = kImportWordCountShort; i != kImportWordCountLong; ++i) {
addWordField(extra, i);
}
extraWrap->toggleOn(state->count.value(
) | rpl::map([](int count) { return count == kImportWordCountLong; }));
extraWrap->finishAnimating();
const auto error = box->addRow(
object_ptr<Ui::FlatLabel>(
box,
state->error.value(),
st::walletImportErrorLabel),
style::margins(
st::boxRowPadding.left(),
st::walletImportErrorSkip,
st::boxRowPadding.right(),
0),
style::al_top);
error->setVisible(false);
state->error.value() | rpl::on_next([=](const QString &text) {
error->setVisible(!text.isEmpty());
}, error->lifetime());
AddBoxCloseButton(box);
const auto wordAt = [=](int index) {
return state->fields[index]->getLastText().trimmed().toLower();
};
const auto requiredCount = [=] {
const auto count = state->count.current();
for (auto i = kImportWordCountShort; i != count; ++i) {
if (!wordAt(i).isEmpty()) {
return count;
}
}
return kImportWordCountShort;
};
const auto markWord = [=](int index, bool typing) {
const auto field = state->fields[index];
const auto word = wordAt(index);
if (word.isEmpty()
|| (typing
? !WordlistSuggestions(word, 1).empty()
: IsWordlistWord(word))) {
return;
}
field->showErrorNoFocus();
if (typing) {
field->finishAnimating();
}
};
const auto revealField = [=](not_null<Ui::InputField*> field) {
const auto top = field->mapTo(box, QPoint()).y() + box->scrollTop();
box->scrollToY(top - st::boxTitleHeight, top + field->height());
};
const auto refreshAccessories = [=](int index) {
const auto field = state->fields[index];
const auto focused = field->hasFocus();
const auto empty = field->getLastText().isEmpty();
state->pasteButtons[index]->setVisible(focused && empty);
state->clearButtons[index]->toggle(
focused && !empty,
anim::type::instant);
};
const auto applyCount = [=](int count) {
if (state->count.current() != count) {
state->count = count;
}
const auto section = (count == kImportWordCountLong) ? 1 : 0;
if (toggle->activeSection() != section) {
toggle->setActiveSection(section);
}
};
const auto distributePaste = [=](const QStringList &words) {
const auto count = int(words.size());
if (count != kImportWordCountShort
&& count != kImportWordCountLong) {
state->error = tr::lng_wallet_import_paste_error(tr::now);
return;
}
applyCount(count);
for (auto i = 0; i != count; ++i) {
state->fields[i]->setText(words[i].toLower());
state->fields[i]->forceProcessContentsChanges();
}
state->error = QString();
const auto last = state->fields[count - 1];
crl::on_main(last, [=] {
last->setFocus();
last->setCursorPosition(last->getLastText().size());
});
};
const auto submit = [=] {
if (context && !context->valid()) {
context->cancel();
return;
}
if (state->importing) {
return;
}
const auto count = requiredCount();
auto words = std::vector<QString>();
words.reserve(count);
for (auto i = 0; i != count; ++i) {
words.push_back(wordAt(i));
}
const auto empty = ranges::find(words, QString());
const auto wrong = (empty != end(words))
? empty
: ranges::find_if(words, [](const QString &word) {
return !IsWordlistWord(word);
});
if (wrong != end(words)) {
const auto field = state->fields[wrong - begin(words)];
revealField(field);
if (wrong->isEmpty()) {
field->setFocus();
} else {
field->showError();
}
return;
}
const auto match = DetectPhraseMatch(words);
if (match != PhraseMatch::Rotation) {
state->error = QString();
ShowInvalidSecretWords(show, match == PhraseMatch::Foreign, context);
return;
}
state->importing = true;
if (mode == WalletImportMode::Restore) {
const auto done = crl::guard(box, [=] {
if (restored) {
box->closeBox();
RunWhenSigningReady(show, restored);
} else {
show->hideLayer();
show->showToast({
.title = tr::lng_wallet_imported_title(tr::now),
.text = { tr::lng_wallet_imported_text(tr::now) },
.icon = &st::toastCheckIcon,
});
}
});
const auto fail = crl::guard(box, [=](const QString &error) {
if (context && (!context->valid()
|| error == u"PHRASE_ORIGIN_EXPIRED"_q
|| error == u"PHRASE_SILENT_ERROR"_q
|| error == u"PHRASE_INSTALL_CANCELLED"_q)) {
context->cancel();
return;
}
state->importing = false;
if (error == u"PHRASE_INVALID_PHRASE"_q
|| error == u"PHRASE_FOREIGN_PHRASE"_q) {
state->error = QString();
ShowInvalidSecretWords(
show,
error == u"PHRASE_FOREIGN_PHRASE"_q,
context);
return;
} else if (error == u"PHRASE_OTHER_WALLET"_q
|| error == u"PHRASE_OUTDATED"_q) {
state->error = QString();
ShowWrongSecretWords(
show,
error == u"PHRASE_OUTDATED"_q,
context);
return;
}
// A dismissed protection chooser restored nothing and has
// nothing to state, so the form simply stays as it was.
// A locked vault is stated on this label, not in the toast
// its replace and backup-enable siblings use: unlocking
// the vault leaves the typed words ready to resubmit.
state->error = (error == u"PHRASE_INSTALL_CANCELLED"_q)
? QString()
: (error == u"PHRASE_INSTALL_FAILED"_q)
? tr::lng_wallet_key_save_error(tr::now)
: (error == u"PHRASE_KEY_CHANGING"_q)
? tr::lng_wallet_import_key_changing(tr::now)
: (error == u"PHRASE_VAULT_LOCKED"_q)
? tr::lng_wallet_vault_locked(tr::now)
: ErrorWithType(
tr::lng_wallet_import_failed(tr::now),
error);
if (context) {
show->showToast(state->error.current());
context->cancel();
}
});
if (context) {
show->session().wallet().restoreFromPhrase(
KeyAuthorization{ .install = context->installer() },
std::move(words),
context->scope(),
[=](KeyAuthorization auth) {
context->ready(std::move(auth));
},
fail);
} else {
show->session().wallet().restoreFromPhrase(
KeyAuthorization{ .install = MakeCustodyInstaller(show) },
std::move(words),
done,
fail);
}
} else {
StartWalletReplace(show, box, std::move(words), [=] {
state->importing = false;
}, [=](const QString &text) {
state->error = text;
});
}
};
box->addButton(tr::lng_wallet_import_button(), submit);
box->setFocusCallback([=] {
state->fields.front()->setFocusFast();
});
const auto suggestions = Ui::CreateChild<Ui::RpWidget>(box.get());
suggestions->hide();
suggestions->setFocusPolicy(Qt::NoFocus);
suggestions->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(suggestions);
auto hq = PainterHighQualityEnabler(p);
const auto inner = suggestions->rect().marginsRemoved(
st::boxRoundShadow.extend);
Ui::Shadow::paint(
p,
inner,
suggestions->width(),
st::boxRoundShadow);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBg);
p.drawRoundedRect(inner, st::boxRadius, st::boxRadius);
}, suggestions->lifetime());
const auto hideSuggestions = [=] {
state->suggestionField = -1;
state->suggestionWords.clear();
suggestions->hide();
};
const auto repositionSuggestions = [=] {
const auto index = state->suggestionField;
if (index < 0) {
return;
}
const auto field = state->fields[index];
const auto &extend = st::boxRoundShadow.extend;
const auto &padding = st::walletImportSuggestionsPadding;
const auto &rowPadding = st::walletImportSuggestionRowPadding;
auto textWidth = 0;
for (const auto &word : state->suggestionWords) {
textWidth = std::max(textWidth, st::normalFont->width(word));
}
const auto innerWidth = std::min(
padding.left()
+ rowPadding.left()
+ textWidth
+ rowPadding.right()
+ padding.right(),
field->width());
const auto count = int(state->suggestionWords.size());
const auto innerHeight = padding.top()
+ count * st::walletImportSuggestionRowHeight
+ padding.bottom();
suggestions->resize(
extend.left() + innerWidth + extend.right(),
extend.top() + innerHeight + extend.bottom());
for (auto i = 0; i != int(state->suggestionRows.size()); ++i) {
const auto row = state->suggestionRows[i];
row->setVisible(i < count);
row->setGeometry(
extend.left() + padding.left(),
extend.top()
+ padding.top()
+ i * st::walletImportSuggestionRowHeight,
innerWidth - padding.left() - padding.right(),
st::walletImportSuggestionRowHeight);
}
const auto fieldTopLeft = field->mapTo(box, QPoint(0, 0));
const auto fieldTop = fieldTopLeft.y();
const auto below = fieldTop
+ field->height()
+ st::walletImportSuggestionsSkip;
const auto flip = (below + innerHeight > box->height());
const auto top = flip
? (fieldTop
- st::walletImportSuggestionsSkip
- innerHeight
- extend.top())
: (below - extend.top());
suggestions->move(fieldTopLeft.x() - extend.left(), top);
if (fieldTop + field->height() <= cover.height()
|| fieldTop >= box->height()) {
suggestions->hide();
} else {
suggestions->show();
}
};
const auto refreshSuggestions = [=](int index) {
const auto field = state->fields[index];
const auto typed = wordAt(index);
auto words = typed.isEmpty()
? std::vector<QString>()
: WordlistSuggestions(typed, kImportSuggestionsLimit);
if (!field->hasFocus()
|| words.empty()
|| (words.size() == 1 && words.front() == typed)) {
hideSuggestions();
return;
}
state->suggestionField = index;
state->suggestionWords = std::move(words);
state->suggestionSelected = 0;
suggestions->raise();
repositionSuggestions();
suggestions->update();
};
const auto acceptSuggestion = [=] {
const auto index = state->suggestionField;
if (index < 0
|| state->suggestionWords.empty()
|| suggestions->isHidden()) {
return false;
}
const auto selected = std::clamp(
state->suggestionSelected,
0,
int(state->suggestionWords.size()) - 1);
const auto word = state->suggestionWords[selected];
const auto field = state->fields[index];
const auto typed = wordAt(index);
hideSuggestions();
if (word == typed) {
return false;
}
field->setText(word);
field->forceProcessContentsChanges();
if (index + 1 < state->count.current()) {
state->fields[index + 1]->setFocus();
} else {
field->setCursorPosition(word.size());
}
return true;
};
const auto moveSuggestionSelection = [=](int delta) {
const auto count = int(state->suggestionWords.size());
if (!count) {
return;
}
state->suggestionSelected = std::clamp(
state->suggestionSelected + delta,
0,
count - 1);
suggestions->update();
};
for (auto i = 0; i != kImportSuggestionsLimit; ++i) {
const auto row = Ui::CreateChild<Ui::AbstractButton>(suggestions);
row->setPointerCursor(true);
row->setFocusPolicy(Qt::NoFocus);
row->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(row);
if (i == state->suggestionSelected) {
p.fillRect(row->rect(), st::windowBgOver);
}
if (i >= int(state->suggestionWords.size())
|| state->suggestionField < 0) {
return;
}
const auto &word = state->suggestionWords[i];
const auto typed = wordAt(state->suggestionField);
const auto prefix = word.startsWith(typed)
? typed
: QString();
const auto font = st::normalFont;
p.setFont(font);
const auto left = st::walletImportSuggestionRowPadding.left();
const auto baseline = (row->height() - font->height) / 2
+ font->ascent;
p.setPen(st::windowFg);
p.drawText(left, baseline, prefix);
p.setPen(st::windowSubTextFg);
p.drawText(
left + font->width(prefix),
baseline,
word.mid(prefix.size()));
}, row->lifetime());
row->setClickedCallback([=] {
state->suggestionSelected = i;
acceptSuggestion();
});
state->suggestionRows.push_back(row);
}
toggle->sectionActivated(
) | rpl::on_next([=](int section) {
applyCount((section == 1)
? kImportWordCountLong
: kImportWordCountShort);
}, toggle->lifetime());
state->count.changes() | rpl::on_next([=] {
state->error = QString();
hideSuggestions();
}, box->lifetime());
for (auto i = 0; i != kImportWordCountLong; ++i) {
const auto field = state->fields[i];
state->pasteButtons[i]->setClickedCallback([=] {
const auto text = QGuiApplication::clipboard()->text();
const auto words = SplitPhraseWords(text);
if (words.size() > 1) {
distributePaste(words);
} else {
field->setText(text.trimmed().toLower());
field->forceProcessContentsChanges();
field->setFocusFast();
}
});
state->clearButtons[i]->setClickedCallback([=] {
field->setText(QString());
field->forceProcessContentsChanges();
field->setFocusFast();
});
field->setMimeDataHook([=](
not_null<const QMimeData*> data,
Ui::InputField::MimeAction action) {
const auto text = data->hasText() ? data->text() : QString();
const auto words = SplitPhraseWords(text);
if (words.size() < 2) {
return false;
}
if (action == Ui::InputField::MimeAction::Check) {
return true;
}
distributePaste(words);
return true;
});
field->submits() | rpl::on_next([=] {
if (acceptSuggestion()) {
return;
}
if (i + 1 < state->count.current()) {
state->fields[i + 1]->setFocus();
} else {
submit();
}
}, field->lifetime());
field->tabbed() | rpl::on_next([=](
not_null<Ui::InputField::TabbedRequest*> request) {
if (request->backward) {
if (i > 0) {
request->handled = true;
state->fields[i - 1]->setFocus();
}
return;
}
request->handled = true;
if (acceptSuggestion()) {
return;
}
if (i + 1 < state->count.current()) {
state->fields[i + 1]->setFocus();
}
}, field->lifetime());
base::install_event_filter(field->rawTextEdit(), [=](
not_null<QEvent*> event) {
if (event->type() == QEvent::FocusIn) {
const auto focus = static_cast<QFocusEvent*>(event.get());
const auto reason = focus->reason();
// Focus given back to the same field keeps a manual scroll.
state->focusRestored = (state->lastFocusedField == i)
&& (reason == Qt::ActiveWindowFocusReason
|| reason == Qt::PopupFocusReason);
state->lastFocusedField = i;
return base::EventFilterResult::Continue;
}
if (event->type() != QEvent::KeyPress) {
return base::EventFilterResult::Continue;
}
const auto key = static_cast<QKeyEvent*>(event.get())->key();
const auto shown = (state->suggestionField == i)
&& !suggestions->isHidden();
if (shown && key == Qt::Key_Down) {
moveSuggestionSelection(1);
return base::EventFilterResult::Cancel;
} else if (shown && key == Qt::Key_Up) {
moveSuggestionSelection(-1);
return base::EventFilterResult::Cancel;
} else if (shown && key == Qt::Key_Escape) {
hideSuggestions();
return base::EventFilterResult::Cancel;
} else if (key == Qt::Key_Backspace
&& field->getLastText().isEmpty()
&& i > 0) {
state->fields[i - 1]->setFocus();
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
field->changes() | rpl::on_next([=] {
state->error = QString();
refreshAccessories(i);
refreshSuggestions(i);
if (field->hasFocus()) {
revealField(field);
}
// WHY: every content-change pass starts by clearing the error,
// including the one forceProcessContentsChanges() postpones after
// setText(), so marking is postponed to land after it.
Ui::PostponeCall(field, [=] {
markWord(i, field->hasFocus());
});
}, field->lifetime());
field->focusedChanges() | rpl::on_next([=](bool focused) {
markWord(i, false);
refreshAccessories(i);
if (focused) {
refreshSuggestions(i);
if (!base::take(state->focusRestored)) {
revealField(field);
}
} else if (state->suggestionField == i) {
hideSuggestions();
}
}, field->lifetime());
refreshAccessories(i);
}
// A pasted 24-word phrase focuses its last field while this still opens.
extraWrap->heightValue(
) | rpl::skip(1) | rpl::on_next([=] {
if (!extraWrap->toggled()) {
return;
}
for (auto i = kImportWordCountShort; i != kImportWordCountLong; ++i) {
if (state->fields[i]->hasFocus()) {
revealField(state->fields[i]);
return;
}
}
}, extraWrap->lifetime());
box->widthValue() | rpl::skip(1) | rpl::on_next([=] {
repositionSuggestions();
}, box->lifetime());
box->setInitScrollCallback([=] {
cover.widget->raise();
cover.updateScroll();
box->scrolls() | rpl::on_next([=] {
cover.updateScroll();
repositionSuggestions();
}, box->lifetime());
});
}
void WalletReplaceBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_wallet_replace_title(),
st::walletReplaceTitleLabel),
st::walletReplaceTitleMargin);
const auto create = box->addRow(
object_ptr<Ui::RoundButton>(
box,
tr::lng_wallet_replace_create(),
st::walletSendButton),
st::walletReplaceButtonMargin,
style::al_justify);
create->setTextTransform(Ui::RoundButtonTextTransform::NoTransform);
const auto creating = box->lifetime().make_state<bool>(false);
create->setClickedCallback([=] {
if (*creating) {
return;
}
*creating = true;
StartWalletReplace(show, box, std::nullopt, [=] {
*creating = false;
});
});
const auto import = box->addRow(
object_ptr<Ui::RoundButton>(
box,
tr::lng_wallet_replace_import(),
st::walletSendButton),
st::walletReplaceButtonMargin,
style::al_justify);
import->setTextTransform(Ui::RoundButtonTextTransform::NoTransform);
import->setClickedCallback([=] {
box->closeBox();
show->showBox(Box(
WalletImportBox,
show,
WalletImportMode::Replace,
nullptr,
nullptr,
nullptr));
});
Ui::AddSkip(box->verticalLayout());
}
void WalletConflictBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show,
Fn<void()> switched) {
struct State {
bool busy = false;
};
// The box closes on its own once a drop leaves no parked record, and
// that close continues the action that hit the conflict. Every other
// close, including one while a drop is still pending, is the user giving
// up, and the drop's completion then continues nothing. The outcome
// outlives the box, because the settling close can destroy it before
// the drop's callback runs.
struct Outcome {
bool settled = false;
bool cancelled = false;
};
const auto state = box->lifetime().make_state<State>();
const auto outcome = std::make_shared<Outcome>();
const auto closed = [=] {
if (!outcome->settled) {
outcome->cancelled = true;
}
};
box->boxClosing() | rpl::on_next(closed, box->lifetime());
box->lifetime().add(closed);
const auto wallet = &show->session().wallet();
box->setStyle(st::walletConflictBox);
Ui::AddSkip(box->verticalLayout(), st::walletConflictBoxTopSkip);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_wallet_conflict_text(),
st::boxLabel),
st::boxRowPadding);
const auto content = box->verticalLayout()->add(
object_ptr<Ui::VerticalLayout>(box->verticalLayout()));
const auto rebuild = [=] {
const auto parked = wallet->parkedRecords();
if (parked.empty()) {
outcome->settled = true;
box->closeBox();
return;
}
content->clear();
auto first = true;
for (const auto &record : parked) {
if (!first) {
Ui::AddSkip(content);
Ui::AddDivider(content);
}
first = false;
const auto address = FormatFriendly(
CanonicalAddress(record.address),
false);
if (address.size() == kAddressLength) {
AddAddressPlate(
content,
address,
QMargins(
st::boxRowPadding.left(),
st::walletAddressPlateSkip,
st::boxRowPadding.right(),
st::walletAddressPlateSkip));
} else {
Ui::AddSkip(content);
}
const auto key = record.publicKey;
const auto showPhrase = content->add(
object_ptr<Ui::RoundButton>(
content,
tr::lng_wallet_keys_show_phrase(),
st::defaultLightButton),
st::boxRowPadding,
style::al_justify);
showPhrase->setFullRadius(true);
showPhrase->setClickedCallback([=] {
WalletRevealFlow(show, key);
});
Ui::AddSkip(content);
const auto switchNow = content->add(
object_ptr<Ui::RoundButton>(
content,
tr::lng_wallet_conflict_switch(),
st::attentionBoxButton),
st::boxRowPadding,
style::al_justify);
switchNow->setFullRadius(true);
switchNow->setClickedCallback([=] {
if (state->busy) {
return;
}
state->busy = true;
// Dropping the last parked record fires the custody update
// that closes this emptied box from rebuild() before this
// callback runs, and with another box underneath that close
// destroys it at once, so the continuation is not guarded
// by the box. It runs only once no parked record is left,
// with more of them listed the box stays for the next one,
// and never after the user closed the box while the drop
// was still pending.
const auto weak = base::make_weak(box.get());
wallet->dropParked(key, [=] {
const auto resolved = !wallet->deviceCustodyState().conflict;
if (const auto strong = weak.get()) {
state->busy = false;
if (resolved && switched && strong->hasDelegate()) {
outcome->settled = true;
strong->closeBox();
}
}
if (resolved && switched && !outcome->cancelled) {
switched();
}
}, crl::guard(box, [=](const QString &error) {
state->busy = false;
ShowPhraseError(show, PhraseOperation::DropParked, error);
}));
});
}
if (const auto width = content->width()) {
content->resizeToWidth(width);
}
};
rebuild();
wallet->custodyUpdates(
) | rpl::on_next(rebuild, box->lifetime());
wallet->presenceValue(
) | rpl::filter([](Presence presence) {
return (presence != Presence::Ready);
}) | rpl::on_next([=] {
box->closeBox();
}, box->lifetime());
Ui::AddSkip(box->verticalLayout());
const auto cancel = box->verticalLayout()->add(
object_ptr<Ui::RoundButton>(
box,
tr::lng_cancel(),
st::defaultLightButton),
st::boxRowPadding,
style::al_justify);
cancel->setFullRadius(true);
cancel->setClickedCallback([=] {
box->closeBox();
});
}
void AddBackupSection(
not_null<Ui::VerticalLayout*> container,
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::GenericBox*> box) {
auto &wallet = show->session().wallet();
const auto busy = box->lifetime().make_state<bool>(false);
const auto restoring = box->lifetime().make_state<rpl::variable<bool>>(
false);
Ui::AddSubsectionTitle(container, tr::lng_wallet_backup_section());
const auto disable = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container,
Settings::CreateButtonWithIcon(
container,
tr::lng_wallet_backup_disable(),
st::settingsAttentionButton)));
disable->toggleOn(wallet.capabilitiesValue(
) | rpl::map([](const WalletCapabilities &capabilities) {
return capabilities.backupEnabled;
}));
disable->finishAnimating();
AddRowSpinner(disable->entity(), restoring->value());
disable->entity()->addClickHandler([=] {
if (*busy) {
return;
}
RunKeyRequiringAction(show, crl::guard(box, [=] {
StartBackupDisable(show, box, busy, restoring);
}), KeyActionKind::Reveal);
});
const auto enable = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container,
Settings::CreateButtonWithIcon(
container,
tr::lng_wallet_backup_enable(),
st::settingsButtonNoIcon)));
enable->toggleOn(wallet.capabilitiesValue(
) | rpl::map([](const WalletCapabilities &capabilities) {
return capabilities.canEnableBackup && !capabilities.backupEnabled;
}));
enable->finishAnimating();
enable->entity()->addClickHandler([=] {
StartBackupEnable(show, box, busy);
});
Ui::AddSkip(container);
Ui::AddDividerText(container, wallet.capabilitiesValue(
) | rpl::map([](const WalletCapabilities &capabilities) {
return (capabilities.backupEnabled || capabilities.canEnableBackup)
? tr::lng_wallet_backup_about_on()
: tr::lng_wallet_backup_about_unavailable();
}) | rpl::flatten_latest());
Ui::AddSkip(container);
}
void WalletKeysBackupBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show) {
auto &wallet = show->session().wallet();
if (wallet.presence() != Presence::Ready) {
box->closeBox();
return;
}
box->setTitle(tr::lng_wallet_keys_title());
const auto container = box->verticalLayout();
Ui::AddSkip(container);
Ui::AddSubsectionTitle(container, tr::lng_wallet_phrase_intro_title());
const auto phrase = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container,
Settings::CreateButtonWithIcon(
container,
tr::lng_wallet_keys_show_phrase(),
st::settingsButtonNoIcon)));
phrase->toggleOn(rpl::combine(
wallet.capabilitiesValue(),
wallet.deviceCustodyStateValue()
) | rpl::map([](
const WalletCapabilities &capabilities,
const DeviceCustodyState &custody) {
return capabilities.canExportPhrase
|| (custody.mode == DeviceMode::Full);
}));
phrase->finishAnimating();
phrase->entity()->addClickHandler([=] {
RunKeyRequiringAction(show, [=] {
WalletRevealFlow(show);
}, KeyActionKind::Reveal);
});
const auto restore = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container,
Settings::CreateButtonWithIcon(
container,
tr::lng_wallet_keys_restore(),
st::settingsButtonNoIcon)));
restore->toggleOn(wallet.deviceCustodyStateValue(
) | rpl::map([](const DeviceCustodyState &custody) {
return (custody.mode == DeviceMode::ReadOnlyNotRestorable);
}));
restore->finishAnimating();
restore->entity()->addClickHandler([=] {
show->showBox(Box(
WalletImportBox,
show,
WalletImportMode::Restore,
nullptr,
nullptr,
nullptr));
});
Ui::AddSkip(container);
Ui::AddDividerText(container, tr::lng_wallet_keys_phrase_about());
Ui::AddSkip(container);
AddBackupSection(container, show, box);
Settings::AddButtonWithIcon(
container,
tr::lng_wallet_keys_delete(),
st::settingsAttentionButton
)->addClickHandler([=] {
show->showBox(Ui::MakeConfirmBox({
.text = tr::lng_wallet_delete_text(tr::now),
.confirmed = [=](Fn<void()> close) {
close();
show->showBox(Box(WalletReplaceBox, show));
},
.confirmText = tr::lng_suggest_warn_delete_anyway(),
.confirmStyle = &st::attentionBoxButton,
.title = tr::lng_wallet_delete_title(),
}));
});
Ui::AddSkip(container);
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
}
[[nodiscard]] BalancePalette CardBalancePalette() {
return {
.mark = st::activeButtonFg->c,
.amount = st::activeButtonFg->c,
.secondary = st::activeButtonFg->c,
};
}
[[nodiscard]] BalancePalette SettledBalancePalette() {
return {
.mark = st::windowActiveTextFg->c,
.amount = st::windowBoldFg->c,
.secondary = st::windowSubTextFg->c,
};
}
[[nodiscard]] Wallet::AmountStyle MoneyAmountStyle() {
return {
.big = st::walletCardBalanceMajorLabel.style.font,
.small = st::walletCardBalanceMinorLabel.style.font,
.additionWidth = st::walletCardMarkSize,
.additionSkip = st::walletCardIconMargin.right(),
.tickerSkip = st::walletCardTickerSkip,
};
}
[[nodiscard]] float64 BalanceAmountScale(float64 progress) {
const auto settled = st::walletBalanceHeaderMajorFont->height
/ float64(st::walletCardBalanceMajorLabel.style.font->height);
return 1. + (settled - 1.) * progress;
}
[[nodiscard]] float64 BalanceFiatScale(float64 progress) {
const auto settled = st::walletBalanceHeaderFiatFont->height
/ float64(st::walletCardFiatLabel.style.font->height);
return 1. + (settled - 1.) * progress;
}
[[nodiscard]] float64 BalanceSettledTop() {
const auto block = st::walletBalanceHeaderMajorFont->height
+ st::walletBalanceHeaderLineSkip
+ st::walletBalanceHeaderFiatFont->height;
return (st::separatePanelTitleHeight - block) / 2.
- st::separatePanelTitleHeight;
}
// The balance rows ride the folding card: each anchor is a card-local
// rest offset placed in Content coordinates against the card's rest rect
// and mapped through the fold transform, and it then travels to its
// landed position in the title band. The x weight is the square of the
// fold progress, so the row leaves the folded quad through its top edge
// instead of sliding out through the narrowing left corner.
[[nodiscard]] QPointF BalanceRowPosition(
const CardFold &fold,
int restTop,
float64 landedTop) {
const auto anchor = fold.transform.map(QPointF(
fold.rest.x() + st::walletCardContentLeft,
fold.rest.y() + restTop));
const auto landedLeft = float64(st::separatePanelTitleLeft);
return QPointF(
anchor.x() + (landedLeft - anchor.x()) * fold.fold * fold.fold,
anchor.y() + (landedTop - anchor.y()) * fold.fold);
}
[[nodiscard]] QRectF MarkInkBounds(const QImage &image) {
auto left = image.width();
auto top = image.height();
auto right = -1;
auto bottom = -1;
for (auto y = 0; y != image.height(); ++y) {
for (auto x = 0; x != image.width(); ++x) {
if (qAlpha(image.pixel(x, y))) {
left = std::min(left, x);
top = std::min(top, y);
right = std::max(right, x);
bottom = std::max(bottom, y);
}
}
}
if (right < 0) {
return QRectF();
}
const auto ratio = image.devicePixelRatio();
return QRectF(
left / ratio,
top / ratio,
(right - left + 1) / ratio,
(bottom - top + 1) / ratio);
}
BalanceInk::BalanceInk() {
const auto &font = st::walletCardBalanceMajorLabel.style.font;
const auto canvas = GramDiamondCanvas(font);
_markFrame = QRectF(
-int(base::SafeRound(canvas * kGramDiamondLeft)),
font->ascent - int(base::SafeRound(canvas * kGramDiamondBottom)),
canvas,
canvas);
_markLottieVisible = QRectF(
_markFrame.x() + canvas * kGramDiamondLeft,
_markFrame.y() + canvas * kGramDiamondTop,
canvas * (kGramDiamondRight - kGramDiamondLeft),
canvas * (kGramDiamondBottom - kGramDiamondTop));
_markLottie = Lottie::MakeIcon({
.name = u"gram_white"_q,
.sizeOverride = { canvas, canvas },
});
}
void BalanceInk::setContent(
CreditsAmount amount,
const QString &fiat,
BalanceStyle style) {
_balance = amount;
_fiatText = fiat;
_style = style;
refresh();
}
void BalanceInk::setOuterWidth(int outerWidth) {
if (_outerWidth == outerWidth) {
return;
}
_outerWidth = outerWidth;
refresh();
}
void BalanceInk::playMark(Fn<void()> repaint) {
if (!_markLottie->valid()) {
return;
}
_markRepaint = repaint;
_markLottie->animate(
std::move(repaint),
0,
_markLottie->framesCount() - 1);
}
void BalanceInk::refresh() {
const auto &fiatFont = st::walletCardFiatLabel.style.font;
const auto exact = (_style != BalanceStyle::Balance);
const auto amountNano = _balance.whole() * Ui::kNanosInOne
+ _balance.nano();
const auto precise = exact
? Ui::FormatTonAmount(amountNano)
: Ui::FormattedTonAmount();
const auto minor = exact
? (precise.nanoString.isEmpty()
? QString()
: (precise.separator + precise.nanoString))
: _balance.nano()
? GramMinorPart(amountNano)
: QString();
const auto ticker = GramTicker();
const auto cardWidth = _outerWidth
- st::walletCardMargin.left()
- st::walletCardMargin.right();
const auto qrLeft = CardQrRect(cardWidth).x();
const auto sign = (_style == BalanceStyle::Minus)
? QString(kMinus)
: (_style == BalanceStyle::Plus)
? u"+"_q
: QString();
const auto full = exact
? (sign + precise.wholeString)
: GramMajorPart(amountNano);
_painter.setContent(MoneyAmountStyle(), {
.whole = full,
.fraction = minor,
.ticker = ticker,
});
_painter.setAvailableWidth(_outerWidth
? (qrLeft - st::walletCardContentSkip - st::walletCardContentLeft)
: 0);
_fiat = QPainterPath();
_fiat.addText(0, fiatFont->ascent, fiatFont, _fiatText);
_fiatWidth = fiatFont->width(_fiatText);
_markCard = Ui::Earn::IconCurrencyMono(
st::walletCardMarkSize,
CardBalancePalette().mark);
_markSettled = Ui::Earn::IconCurrencyTwoTone(
st::walletCardMarkSize,
SettledBalancePalette().mark);
_markTop = Ui::Earn::AlignedMarkTop(
st::walletCardBalanceMajorLabel.style.font,
_markCard);
_markMonoVisible = MarkInkBounds(_markCard).translated(0., _markTop);
}
QRectF BalanceInk::amountRect(const CardFold &fold) const {
const auto scale = BalanceAmountScale(fold.fold);
const auto width = _painter.size().width() * scale;
const auto height = st::walletCardBalanceMajorLabel.style.font->height
* scale;
const auto position = BalanceRowPosition(
fold,
st::walletCardBalanceTop,
BalanceSettledTop());
return QRectF(position.x(), position.y(), width, height);
}
QRectF BalanceInk::fiatRect(const CardFold &fold) const {
const auto scale = BalanceFiatScale(fold.fold);
const auto width = _fiatWidth * scale;
const auto height = st::walletCardFiatLabel.style.font->height * scale;
const auto position = BalanceRowPosition(
fold,
st::walletCardFiatTop,
BalanceSettledTop()
+ st::walletBalanceHeaderMajorFont->height
+ st::walletBalanceHeaderLineSkip);
return QRectF(position.x(), position.y(), width, height);
}
QRectF BalanceInk::markVisible(float64 fold) const {
const auto &from = _markLottieVisible;
const auto &to = _markMonoVisible;
return QRectF(
from.x() + (to.x() - from.x()) * fold,
from.y() + (to.y() - from.y()) * fold,
from.width() + (to.width() - from.width()) * fold,
from.height() + (to.height() - from.height()) * fold);
}
QRectF BalanceInk::markDrawRect(
float64 fold,
const QRectF &box,
const QRectF &visible) const {
const auto target = markVisible(fold);
const auto k = target.height() / visible.height();
return QRectF(
target.center().x() - (visible.center().x() - box.x()) * k,
target.y() - (visible.y() - box.y()) * k,
box.width() * k,
box.height() * k);
}
void BalanceInk::paintMark(
QPainter &p,
float64 fold,
const QImage &mono,
bool card) const {
const auto monoBox = QRectF(
0.,
_markTop,
st::walletCardMarkSize,
st::walletCardMarkSize);
if (_markLottieVisible.height() <= 0.
|| _markMonoVisible.height() <= 0.) {
p.drawImage(monoBox, mono);
return;
}
// WHY: both passes map their own diamond onto one shared box, so the
// diamond crossing the folding card's edge stays one shape, lottie
// inside and mono outside, with no step at the seam.
if (card && _markLottie->valid()) {
if (!_markLottie->animating() && _markLottie->frameIndex() != 0) {
_markLottie->jumpTo(0, _markRepaint);
}
p.drawImage(
markDrawRect(fold, _markFrame, _markLottieVisible),
_markLottie->frame());
} else {
p.drawImage(markDrawRect(fold, monoBox, _markMonoVisible), mono);
}
}
void BalanceInk::paintPass(
QPainter &p,
const CardFold &fold,
const BalancePalette &palette,
const QImage &mark,
bool card,
float64 secondaryOpacity) const {
p.save();
p.setTransform(groupTransform(fold), true);
paintMark(p, fold.fold, mark, card);
_painter.paint(p, {
.digits = palette.amount,
.ticker = palette.secondary,
.tickerOpacity = secondaryOpacity,
});
p.restore();
const auto fiat = fiatRect(fold);
const auto fiatScale = BalanceFiatScale(fold.fold);
p.save();
p.setOpacity(p.opacity() * secondaryOpacity);
p.translate(fiat.x(), fiat.y());
p.scale(fiatScale, fiatScale);
p.fillPath(_fiat, palette.secondary);
p.restore();
}
QTransform BalanceInk::groupTransform(const CardFold &fold) const {
const auto amount = amountRect(fold);
const auto foldScale = BalanceAmountScale(fold.fold);
const auto fit = _painter.scale();
auto result = QTransform();
result.translate(amount.x(), amount.y());
result.scale(foldScale, foldScale);
result.translate(0., (1. - fit) * _painter.naturalHeight() / 2.);
result.scale(fit, fit);
return result;
}
void BalanceInk::paint(
QPainter &p,
const CardFold &fold,
const QRegion &cardOutline,
QRect clip) const {
const auto ink = boundingRect(fold);
const auto inside = cardOutline.intersected(QRegion(clip));
if (inside.intersects(ink)) {
p.save();
p.setClipRegion(inside, Qt::IntersectClip);
paintPass(
p,
fold,
CardBalancePalette(),
_markCard,
true,
st::walletCardSecondaryOpacity);
p.restore();
}
const auto outside = QRegion(clip) - inside;
if (outside.intersects(ink)) {
p.save();
p.setClipRegion(outside, Qt::IntersectClip);
paintPass(
p,
fold,
SettledBalancePalette(),
_markSettled,
false,
1.);
p.restore();
}
}
QRect BalanceInk::boundingRect(const CardFold &fold) const {
const auto amount = amountRect(fold);
const auto fiat = fiatRect(fold);
return amount.united(fiat).toAlignedRect();
}
QRect BalanceInk::markRect(QRect cardRest) const {
const auto fit = _painter.scale();
const auto origin = QPointF(
cardRest.x() + st::walletCardContentLeft,
cardRest.y()
+ st::walletCardBalanceTop
+ (1. - fit) * _painter.naturalHeight() / 2.);
const auto top = int(base::SafeRound(_markTop));
return QRectF(
origin + QPointF(0., top * fit),
QSizeF(st::walletCardMarkSize, st::walletCardMarkSize) * fit
).toAlignedRect();
}
QRect BalanceInk::markPaintRect(const CardFold &fold) const {
if (_markLottieVisible.height() <= 0.) {
return QRect();
}
return groupTransform(fold).mapRect(
markDrawRect(fold.fold, _markFrame, _markLottieVisible)
).toAlignedRect().marginsAdded({ 1, 1, 1, 1 });
}
[[nodiscard]] rpl::producer<TextWithEntities> CardNameValue(
not_null<Main::Session*> session) {
return Info::Profile::NameValue(
session->user()
) | rpl::map([](QString name) {
return tr::marked(std::move(name));
});
}
void SetupCardBalance(
not_null<BalanceInk*> ink,
not_null<Main::Session*> session,
Fn<void()> repaint,
not_null<Ui::RpWidget*> owner) {
rpl::combine(
session->wallet().balanceNanoValue(),
FiatRateValue(session)
) | rpl::on_next([=](int64 nano, FiatRate rate) {
const auto scope = WindowPaletteScope(owner);
ink->setContent(
CreditsAmount(
nano / Ui::kNanosInOne,
nano % Ui::kNanosInOne,
CreditsType::Ton),
FormatFiat(nano, rate));
repaint();
}, owner->lifetime());
}
void SetupCardMark(
not_null<BalanceInk*> ink,
not_null<Ui::RpWidget*> owner,
std::shared_ptr<bool> played,
Fn<void()> repaint) {
// WHY: a box's layer is shown and hidden again synchronously inside its
// show animation, so the card re-reads the window's activation on every
// show and starts the play only once it stays visible.
const auto open = owner->lifetime().make_state<bool>(false);
owner->events(
) | rpl::filter([](not_null<QEvent*> e) {
return (e->type() == QEvent::Show);
}) | rpl::map([=] {
return rpl::combine(
owner->windowActiveValue(),
PowerSaving::OnValue(PowerSaving::kStickersChat),
anim::Disables());
}) | rpl::flatten_latest(
) | rpl::on_next([=](bool active, bool saving, bool off) {
*open = active && !saving && !off;
if (*open && !*played) {
InvokeQueued(owner, [=] {
if (*open && !*played && owner->isVisible()) {
*played = true;
ink->playMark(repaint);
}
});
}
}, owner->lifetime());
}
Card::Card(
QWidget *parent,
std::shared_ptr<Main::SessionShow> show,
rpl::producer<TextWithEntities> name)
: RpWidget(parent)
, _show(std::move(show))
, _nameStyle(st::defaultTextStyle) {
_nameStyle.font = st::walletCardNameFont->monospace();
_show->session().wallet().presenceValue(
) | rpl::on_next([=](Presence) {
refreshAddress();
}, lifetime());
std::move(name) | rpl::on_next([=](TextWithEntities name) {
_name.setMarkedText(
_nameStyle,
tr::upper(std::move(name)),
kMarkupTextOptions,
Core::TextContext({
.session = &_show->session(),
.repaint = crl::guard(this, [=] { update(); }),
}));
invalidateCache();
update();
}, lifetime());
}
void Card::setFold(const CardFold &fold) {
_fold = fold;
setVisible(fold.valid && fold.opacity > 0.);
update();
}
const CardFold &Card::fold() const {
return _fold;
}
QPolygonF Card::paintedQuad() const {
return _fold.quad;
}
QPolygonF Card::paintedOutline() const {
if (!_fold.valid) {
return QPolygonF();
}
auto path = QPainterPath();
path.addRoundedRect(
QRectF(_fold.rest),
st::walletCardRadius,
st::walletCardRadius);
return _fold.transform.map(path.toFillPolygon());
}
void Card::invalidateCache() {
_cache = QImage();
}
void Card::followCursor() {
if (!_angle) {
_angle = std::make_unique<CardAngle>();
}
}
QRectF Card::paintedRect() const {
return _fold.quad.boundingRect().translated(-QPointF(pos()));
}
float64 Card::paintAngle() {
if (!_angle) {
return 0.;
}
_angle->track(this, this, paintedRect());
return _angle->value(crl::now());
}
QRect Card::restRect() const {
return QRect(
0,
height() - st::walletCardHeight,
width(),
st::walletCardHeight);
}
void Card::refreshAddress() {
auto &wallet = _show->session().wallet();
const auto address = wallet.addressFriendly(false);
_addressLine1 = _addressLine2 = QString();
if (address.size() == kAddressLength) {
_addressLine1 = GroupedAddressLine(address, 0);
_addressLine2 = GroupedAddressLine(address, kAddressLength / 2);
}
invalidateCache();
update();
}
void Card::validateCache(float64 angle) {
const auto ratio = style::DevicePixelRatio();
const auto size = restRect().size() * ratio;
// WHY: the cached fold image carries the sweep, so it is keyed by the
// angle it was painted at.
if (!_cache.isNull() && _cache.size() == size && _cacheAngle == angle) {
return;
}
_cacheAngle = angle;
_cache = QImage(size, QImage::Format_ARGB32_Premultiplied);
_cache.setDevicePixelRatio(ratio);
_cache.fill(Qt::transparent);
auto q = Painter(&_cache);
auto hq = PainterHighQualityEnabler(q);
paintContent(q, angle);
}
void Card::paintEvent(QPaintEvent *e) {
if (!_fold.valid || _fold.opacity <= 0.) {
return;
}
const auto angle = paintAngle();
auto p = Painter(this);
if (!_fold.fold) {
auto hq = PainterHighQualityEnabler(p);
p.translate(restRect().topLeft());
paintContent(p, angle);
return;
}
validateCache(angle);
auto hq = PainterHighQualityEnabler(p);
p.setOpacity(_fold.opacity);
p.translate(-x(), -y());
p.setTransform(_fold.transform, true);
p.drawImage(QRectF(_fold.rest), _cache);
}
void Card::paintContent(Painter &p, float64 angle) {
const auto size = restRect().size();
_background.paint(p, QRect(QPoint(), size), angle);
const auto qr = CardQrRect(size.width());
PaintCardQrPlate(p, qr);
st::walletCardQrIcon.paintInCenter(p, qr, CardQrIconFg());
const auto &nameFont = _nameStyle.font;
const auto addressFont = st::walletCardAddressFont->monospace();
const auto addressBaseline = size.width()
- st::walletCardAddressRight
- addressFont->height
- addressFont->ascent;
const auto stripLeft = addressBaseline - addressFont->descent;
const auto nameMax = stripLeft
- st::walletCardContentSkip
- st::walletCardContentLeft;
p.setPen(st::activeButtonFg);
_name.drawLeftElided(
p,
st::walletCardContentLeft,
size.height() - st::walletCardNameBottom - nameFont->ascent,
nameMax,
size.width(),
1);
if (!_addressLine1.isEmpty()) {
p.setPen(st::windowActiveTextFg);
p.setFont(addressFont);
p.save();
p.translate(addressBaseline, st::walletCardAddressSkip);
p.rotate(90);
p.drawText(0, 0, _addressLine1);
p.drawText(0, -addressFont->height, _addressLine2);
p.restore();
}
}
CardFold ComputeCardFold(QRect cardRest, float64 fold) {
auto result = CardFold();
result.rest = cardRest;
result.fold = fold;
result.topY = cardRest.top() - cardRest.top() * fold;
result.bottomY = result.topY + cardRest.height() * (1. - fold);
result.opacity = 1. - std::pow(fold, st::walletCardFoldFadePower);
if (result.bottomY - result.topY < kCardFoldMinHeight) {
return result;
}
const auto bottomWidth = cardRest.width()
* (1. - (1. - st::walletCardFoldBottomScale) * fold);
const auto topWidth = bottomWidth
* (1. - (1. - st::walletCardFoldTopScale) * fold);
const auto center = cardRest.left() + cardRest.width() / 2.;
const auto rest = QRectF(cardRest);
const auto restQuad = QPolygonF({
rest.topLeft(),
rest.topRight(),
rest.bottomRight(),
rest.bottomLeft(),
});
auto quad = QPolygonF({
QPointF(center - topWidth / 2., result.topY),
QPointF(center + topWidth / 2., result.topY),
QPointF(center + bottomWidth / 2., result.bottomY),
QPointF(center - bottomWidth / 2., result.bottomY),
});
auto transform = QTransform();
if (!QTransform::quadToQuad(restQuad, quad, transform)) {
return result;
}
result.quad = std::move(quad);
result.transform = transform;
result.valid = true;
return result;
}
Content::Content(
not_null<Ui::SeparatePanel*> panel,
std::shared_ptr<Main::SessionShow> show)
: RpWidget(panel)
, _show(std::move(show))
, _panel(panel)
, _scroll(this, st::defaultScrollArea)
, _loadMoreCheck([this] { checkLoadMore(); }) {
auto &wallet = _show->session().wallet();
wallet.startPolling();
setupContent();
_scroll->show();
}
Content::~Content() {
_show->session().wallet().stopPolling();
}
[[nodiscard]] bool HistoryShown(not_null<Main::Session*> session) {
const auto wallet = &session->wallet();
return !wallet->listsGated()
&& (!wallet->historyVisibleEmpty()
|| !wallet->listedSubmittedTransactions().empty()
|| wallet->sendingTransaction(wallet->windowSend()));
}
[[nodiscard]] rpl::producer<bool> HistoryShownValue(
not_null<Main::Session*> session) {
const auto wallet = &session->wallet();
return rpl::single(rpl::empty) | rpl::then(rpl::merge(
wallet->historyUpdates(),
wallet->sendStateValue() | rpl::to_empty,
wallet->listsGatedValue() | rpl::to_empty
)) | rpl::map([=] {
return HistoryShown(session);
}) | rpl::distinct_until_changed();
}
[[nodiscard]] rpl::producer<bool> CollectiblesShownValue(
not_null<Main::Session*> session) {
const auto wallet = &session->wallet();
return rpl::single(rpl::empty) | rpl::then(rpl::merge(
wallet->collectiblesUpdates(),
wallet->listsGatedValue() | rpl::to_empty
)) | rpl::map([=] {
return !wallet->listsGated() && !wallet->collectibles().empty();
}) | rpl::distinct_until_changed();
}
void PaintBottomRoundedPlate(
QPainter &p,
QRect rect,
const style::color &bg) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(bg);
p.drawRoundedRect(
rect.marginsAdded({ 0, 2 * st::callRadius, 0, 0 }),
st::callRadius,
st::callRadius);
}
// The three faces the area under the card can show once the lists are known
// to be empty. They are derived from one producer and are mutually exclusive
// by construction, so no two of the wraps below can ever be open at once.
enum class EmptyFace {
None,
About,
Unavailable,
Unreachable,
};
struct ListRowKey {
std::string operationId;
QString id;
friend bool operator==(
const ListRowKey &,
const ListRowKey &) = default;
};
struct ListedRow {
ListRowKey key;
not_null<Ui::RpWidget*> widget;
};
struct ListAnchorRow {
ListRowKey key;
int top = 0;
};
[[nodiscard]] std::vector<ListAnchorRow> CountListAnchor(
const std::vector<ListedRow> &rows,
not_null<QWidget*> column,
int visibleTop) {
if (rows.empty() || visibleTop <= 0) {
return {};
}
auto tops = std::vector<int>();
tops.reserve(rows.size());
for (const auto &row : rows) {
tops.push_back(Ui::MapFrom(column, row.widget, QPoint()).y());
}
if (visibleTop < tops.front()) {
return {};
}
const auto count = int(rows.size());
auto index = count - 1;
for (auto i = 0; i != count; ++i) {
if (tops[i] + rows[i].widget->height() > visibleTop) {
index = i;
break;
}
}
auto result = std::vector<ListAnchorRow>();
result.reserve(count);
const auto add = [&](int i) {
const auto &key = rows[i].key;
if (!key.operationId.empty() || !key.id.isEmpty()) {
result.push_back({ .key = key, .top = tops[i] });
}
};
for (auto i = index; i != count; ++i) {
add(i);
}
for (auto i = index - 1; i >= 0; --i) {
add(i);
}
return result;
}
[[nodiscard]] int CountKeptScrollTop(
const std::vector<ListAnchorRow> &anchor,
const std::vector<ListedRow> &rows,
not_null<QWidget*> column,
int scrollTop,
int reserve,
int maxTop) {
auto shift = 0;
for (const auto &entry : anchor) {
const auto found = ranges::find(rows, entry.key, &ListedRow::key);
if (found != end(rows)) {
shift = Ui::MapFrom(column, found->widget, QPoint()).y()
- entry.top;
break;
}
}
const auto lower = anchor.empty() ? 0 : std::min(reserve, maxTop);
return std::clamp(scrollTop + shift, lower, maxTop);
}
void Content::setupContent() {
_container = _scroll->setOwnedWidget(
object_ptr<Ui::RpWidget>(_scroll.data()));
_column = Ui::CreateChild<Ui::PaddingWrap<Ui::VerticalLayout>>(
_container,
object_ptr<Ui::VerticalLayout>(_container),
style::margins());
_column->show();
const auto column = _column->entity();
const auto wallet = &_show->session().wallet();
auto collectiblesShown = CollectiblesShownValue(&_show->session());
setupPinned();
setupBalance();
setupTabs(rpl::duplicate(collectiblesShown));
setupStrip();
setupListsLoading();
setupProtectRow();
const auto media = std::make_shared<CollectibleMedia>(&_show->session());
const auto wrap = column->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
column,
object_ptr<Ui::VerticalLayout>(column)));
const auto about = wrap->entity();
Ui::AddSkip(about, st::walletAboutTopSkip);
const auto addEntry = [&](
rpl::producer<QString> title,
rpl::producer<QString> text,
const style::icon &icon) {
const auto top = about->add(
object_ptr<Ui::FlatLabel>(
about,
std::move(title),
st::walletAboutTitleLabel),
st::walletAboutPadding);
Ui::AddSkip(about, st::walletAboutTitleSkip);
about->add(
object_ptr<Ui::FlatLabel>(
about,
std::move(text),
st::walletAboutTextLabel),
st::walletAboutPadding);
const auto left = Ui::CreateChild<Ui::RpWidget>(about);
left->paintRequest(
) | rpl::on_next([=] {
auto p = Painter(left);
icon.paint(p, 0, 0, left->width());
}, left->lifetime());
left->resize(icon.size());
top->geometryValue(
) | rpl::on_next([=](const QRect &g) {
left->moveToLeft(
st::walletAboutIconLeft,
g.top() + (top->height() - left->height()) / 2);
}, left->lifetime());
};
addEntry(
tr::lng_wallet_about_instant_title(),
tr::lng_wallet_about_instant_text(),
st::walletAboutInstantIcon);
Ui::AddSkip(about, st::walletAboutRowSkip);
addEntry(
tr::lng_wallet_about_fees_title(),
tr::lng_wallet_about_fees_text(
lt_count,
GaslessDailyTransfersValue(&_show->session()) | tr::to_count()),
st::walletAboutFeesIcon);
Ui::AddSkip(about, st::walletAboutRowSkip);
addEntry(
tr::lng_wallet_about_chain_title(),
tr::lng_wallet_about_chain_text(),
st::walletAboutChainIcon);
Ui::AddSkip(about, st::walletAboutBottomSkip);
auto emptyFace = rpl::combine(
HistoryShownValue(&_show->session()),
wallet->collectiblesTabValue(),
wallet->listsEmptyStateValue(),
wallet->presenceValue()
) | rpl::map([](
bool history,
bool collectibles,
ListsEmptyState lists,
Presence presence) {
return (!lists.confirmedEmpty || history || collectibles)
? EmptyFace::None
: (presence == Presence::Unavailable)
? EmptyFace::Unavailable
: (lists.unreachable || (presence == Presence::AddressUnreadable))
? EmptyFace::Unreachable
: EmptyFace::About;
});
wrap->toggleOn(rpl::duplicate(emptyFace) | rpl::map(
rpl::mappers::_1 == EmptyFace::About));
wrap->finishAnimating();
const auto statementWrap = column->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
column,
object_ptr<Ui::VerticalLayout>(column)));
const auto statement = statementWrap->entity();
Ui::AddSkip(statement, st::walletAboutTopSkip);
statement->add(
object_ptr<Ui::FlatLabel>(
statement,
rpl::combine(
tr::lng_wallet_unavailable(),
tr::lng_wallet_state_error(),
rpl::duplicate(emptyFace)
) | rpl::map([](
const QString &unavailable,
const QString &error,
EmptyFace face) {
return (face == EmptyFace::Unavailable) ? unavailable : error;
}),
st::walletAboutTextLabel),
st::boxRowPadding,
style::al_top);
Ui::AddSkip(statement, st::walletAboutBottomSkip);
statementWrap->toggleOn(std::move(emptyFace) | rpl::map(
(rpl::mappers::_1 == EmptyFace::Unavailable)
|| (rpl::mappers::_1 == EmptyFace::Unreachable)));
statementWrap->finishAnimating();
const auto rowsTopSkip = column->add(Ui::CreateSlideSkipWidget(
column,
st::walletRowsTopSkip));
rowsTopSkip->toggleOn(rpl::combine(
std::move(collectiblesShown),
wallet->collectiblesTabValue(),
HistoryShownValue(&_show->session())
) | rpl::map([](bool available, bool collectibles, bool history) {
return history || (available && collectibles);
}));
rowsTopSkip->finishAnimating();
const auto listWrap = column->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
column,
object_ptr<Ui::VerticalLayout>(column)));
const auto rows = listWrap->entity();
const auto list = rows->add(object_ptr<Ui::VerticalLayout>(rows));
struct ListPlace {
std::vector<ListedRow> rows;
int lastTop = 0;
bool rebuilding = false;
};
const auto place = lifetime().make_state<ListPlace>();
place->lastTop = _scroll->scrollTop();
const auto listedRows = [=] {
auto result = std::vector<ListedRow>();
result.reserve(place->rows.size() + 1);
if (_sendingRow) {
result.push_back({
.key = { .operationId = _sendingRow->operationId },
.widget = _sendingRow->slot,
});
}
result.insert(end(result), begin(place->rows), end(place->rows));
return result;
};
const auto fitContainer = [=] {
const auto height = _column->height();
_container->resize(
_container->width(),
(place->rebuilding
? std::max(height, _container->height())
: height));
};
const auto releaseSending = [=] {
if (_sendingRow) {
delete _sendingRow->slot;
_sendingRow = nullptr;
}
};
const auto rebuild = list->lifetime().make_state<Fn<void()>>();
const auto holdSending = [=](
const std::string &operationId,
const TransferItem &item,
bool sending) {
auto created = false;
if (!_sendingRow || _sendingRow->operationId != operationId) {
releaseSending();
_sendingRow = std::make_unique<SendingRow>(SendingRow{
.operationId = operationId,
.slot = rows->insert(
0,
object_ptr<Ui::VerticalLayout>(rows)),
.revealPending = true,
});
created = true;
}
auto content = HistoryRowContent();
if (sending) {
auto shown = item;
shown.status = TransferItem::Status::Success;
content = RowContentFromItem(shown, &_show->session());
content.date = tr::lng_wallet_row_sending(tr::now);
} else {
content = RowContentFromItem(item, &_show->session());
}
_sendingRow->item = item;
const auto slot = _sendingRow->slot;
const auto look = _sendingRow->look;
const auto settle = look
&& !sending
&& (look->settling()
|| (!look->settled()
&& !anim::Disabled()
&& look->surfaceShown()
&& look->inView()
&& (content.itemAmount
== _sendingRow->content.itemAmount)));
if (look && sending) {
if (content != _sendingRow->content) {
look->setContent(content);
_sendingRow->content = std::move(content);
}
return created;
} else if (settle) {
if (!look->settling() || content != _sendingRow->content) {
// Only the diamond's landing bursts; a collectible lands none.
const auto burst = !content.itemAmount
&& (item.status == TransferItem::Status::Success);
look->settle(
content,
burst,
crl::guard(list, [=] { (*rebuild)(); }));
_sendingRow->content = std::move(content);
}
return created;
} else if (!slot->count()
|| _sendingRow->look
|| content != _sendingRow->content) {
slot->clear();
_sendingRow->look = nullptr;
const auto click = [=] {
if (_sendingRow) {
ShowWalletTransactionBox(_show, _sendingRow->item, media);
}
};
if (sending) {
_sendingRow->look = AddSendingHistoryRow(
slot,
column,
listWrap,
content,
media,
click);
} else {
AddHistoryRow(slot, content, click, media);
}
_sendingRow->content = std::move(content);
}
return created;
};
const auto rebuildList = [=] {
const auto scrollTop = _scroll->scrollTop();
const auto anchor = listWrap->toggled()
? CountListAnchor(listedRows(), column, scrollTop - _reserve)
: std::vector<ListAnchorRow>();
place->rows.clear();
// WHY: clear() collapses the column and the scroll area clamps to it
// at once, so the geometry readers wait for the rebuilt rows, which
// then keep their place on screen in one move.
place->rebuilding = true;
list->clear();
const auto &history = wallet->history();
auto submitted = wallet->listedSubmittedTransactions();
// A row sits by date, so old failures stop covering fresh history.
ranges::stable_sort(submitted, ranges::greater(), [](const auto &entry) {
return entry.item.date.value_or(kUndatedRowDate);
});
const auto addItem = [=](const TransferItem &item, ListRowKey key) {
const auto content = RowContentFromItem(item, &_show->session());
place->rows.push_back({
.key = std::move(key),
.widget = AddHistoryRow(list, content, [=] {
ShowWalletTransactionBox(_show, item, media);
}, media),
});
};
auto created = false;
if (HistoryShown(&_show->session())) {
const auto &op = wallet->windowSend();
const auto sending = wallet->sendingTransaction(op);
const auto settled = (op.empty() || sending)
? end(submitted)
: ranges::find(
submitted,
op,
&ListedSubmittedTransfer::operationId);
const auto newest = [&] {
auto result = TimeId();
for (const auto &item : history) {
if (item.date && !wallet->historyItemHidden(item)) {
result = std::max(result, *item.date);
}
}
for (auto i = begin(submitted); i != end(submitted); ++i) {
if (i != settled) {
result = std::max(
result,
i->item.date.value_or(kUndatedRowDate));
}
}
return result;
};
const auto settling = !sending
&& _sendingRow
&& _sendingRow->operationId == op
&& _sendingRow->look
&& _sendingRow->look->settling();
const auto kept = !settling
? std::optional<TransferItem>()
: (settled != end(submitted))
? std::make_optional(settled->item)
: wallet->submittedTransaction(op);
const auto held = sending
|| kept
|| (settled != end(submitted)
&& _sendingRow
&& _sendingRow->operationId == op
&& (settled->item.date.value_or(kUndatedRowDate)
>= newest()));
if (!held) {
releaseSending();
} else if (sending) {
created = holdSending(op, *sending, true);
} else {
created = holdSending(
op,
kept ? *kept : settled->item,
false);
}
const auto skipId = !held
? QString()
: sending
? sending->id
: kept
? kept->id
: QString();
auto next = begin(submitted);
const auto addNext = [&] {
const auto &entry = *(next++);
if (!held || entry.operationId != op) {
addItem(entry.item, { .operationId = entry.operationId });
}
};
const auto addNewerThan = [&](TimeId date) {
while (next != end(submitted)
&& next->item.date.value_or(kUndatedRowDate) >= date) {
addNext();
}
};
for (const auto &item : history) {
if (item.date) {
addNewerThan(*item.date);
}
if (!wallet->historyItemHidden(item)
&& (skipId.isEmpty() || item.id != skipId)) {
addItem(item, { .id = item.id });
}
}
while (next != end(submitted)) {
addNext();
}
Ui::AddSkip(list, st::walletRowsTopSkip);
} else {
releaseSending();
}
if (const auto width = rows->width()) {
rows->resizeToWidth(width);
}
if (width() && height()) {
const auto columnHeight = column->height();
const auto regions = countRegions(columnHeight);
_scroll->scrollToY(CountKeptScrollTop(
anchor,
listedRows(),
column,
scrollTop,
_reserve,
std::max(
columnHeight + regions.reserve - regions.scrollHeight,
0)));
}
place->rebuilding = false;
fitContainer();
updateRegions();
if (created) {
Ui::PostponeCall(this, [=] { revealSendingRow(); });
}
};
*rebuild = rebuildList;
rpl::merge(
wallet->historyUpdates(),
wallet->collectiblesUpdates(),
wallet->sendStateValue() | rpl::to_empty,
wallet->listsGatedValue() | rpl::to_empty
) | rpl::on_next(rebuildList, list->lifetime());
listWrap->toggleOn(TransactionsShownValue(&_show->session()));
listWrap->finishAnimating();
const auto collectiblesWrap = column->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
column,
object_ptr<Ui::VerticalLayout>(column)));
AddCollectiblesList(collectiblesWrap->entity(), _show, media);
collectiblesWrap->toggleOn(wallet->collectiblesTabValue());
collectiblesWrap->finishAnimating();
_scroll->scrolls(
) | rpl::on_next([=] {
// A reader who moved the list down is asking for more of it, so
// the bound the session spends on hidden transaction pages is
// re-armed here, and only for that. Every other way this fires is
// a clamp nobody made - a section sliding shut, a
// resize growing the viewport - and a clamp can only lower the
// position, so requiring it to grow rejects all of them. The last
// seen position is kept beside the handler and not inside it,
// because rpl invokes a copy of the handler on every emission and
// a value captured in it would never carry to the next one.
const auto top = _scroll->scrollTop();
// A rebuild's anchored move grows it too, and is no reader's scroll.
const auto moved = !place->rebuilding && (top > place->lastTop);
place->lastTop = top;
if (moved) {
wallet->resetHiddenHistoryPages();
}
_loadMoreCheck.call();
}, lifetime());
_scroll->scrollTopValue(
) | rpl::on_next([=](int) {
if (place->rebuilding) {
return;
}
updatePinned();
updateVisibleArea();
}, lifetime());
_container->widthValue(
) | rpl::on_next([=](int width) {
_column->resizeToWidth(width);
}, _column->lifetime());
_column->heightValue(
) | rpl::on_next([=] {
fitContainer();
}, _column->lifetime());
_pinnedInner->heightValue(
) | rpl::on_next([=] {
updateRegions();
}, lifetime());
_column->entity()->heightValue(
) | rpl::on_next([=] {
if (!place->rebuilding) {
updateRegions();
}
_loadMoreCheck.call();
}, lifetime());
_pinnedBackground->raise();
_card->raise();
_pinned->raise();
_cardButton->raise();
_tabsShadow->raise();
_headerShadow->raise();
_stripShadow->raise();
_strip->raise();
const auto local = &_show->session().local();
if (!local->readPref<bool>(kIntroTooltipShownPref)) {
local->writePref<bool>(kIntroTooltipShownPref, true);
SetupIntroTooltip(this, _card, [=] {
return (foldProgress() > 0.)
? QRect()
: _ink->markRect(cardRest());
}, _pinned->heightValue() | rpl::to_empty);
}
}
void Content::setupPinned() {
_pinnedBackground = Ui::CreateChild<Ui::RpWidget>(this);
_pinnedBackground->setAttribute(Qt::WA_TransparentForMouseEvents);
_pinnedBackground->setGeometry(QRect());
_pinnedBackground->show();
_pinned = Ui::CreateChild<Ui::RpWidget>(this);
_pinned->show();
_pinnedInner = Ui::CreateChild<Ui::VerticalLayout>(_pinned);
_pinnedInner->show();
setupInfoIsland();
Ui::AddSkip(_pinnedInner, st::walletCardTopSkip);
_cardPlaceholder = _pinnedInner->add(
object_ptr<Ui::FixedHeightWidget>(
_pinnedInner,
st::walletCardHeight),
st::walletCardMargin);
_card = Ui::CreateChild<Card>(
this,
_show,
CardNameValue(&_show->session()));
_card->setGeometry(Ui::MapFrom(
this,
_cardPlaceholder,
_cardPlaceholder->rect()));
_card->setAttribute(Qt::WA_TransparentForMouseEvents);
_card->show();
_card->followCursor();
_cardButton = Ui::CreateChild<Ui::AbstractButton>(this);
_cardButton->setClickedCallback([=] {
ShowWalletReceiveBox(&_show->session(), _show);
});
_cardButton->show();
const auto buttons = _pinnedInner->add(
object_ptr<Ui::FixedHeightWidget>(
_pinnedInner,
st::walletSendButton.height),
st::walletSendButtonMargin,
style::al_justify);
const auto addPill = [&](
rpl::producer<QString> text,
Fn<void()> callback) {
const auto button = Ui::CreateChild<Ui::RoundButton>(
buttons,
std::move(text),
st::walletSendButton);
button->setTextTransform(Ui::RoundButtonTextTransform::NoTransform);
button->setClickedCallback(std::move(callback));
button->show();
return button;
};
const auto addFunds = addPill(tr::lng_wallet_add_funds(), [=] {
ShowWalletReceiveBox(&_show->session(), _show);
});
const auto send = addPill(tr::lng_send_button(), [show = _show] {
WhenWalletReady(show, [=] {
show->showBox(Box(
WalletSendRecipientBox,
show,
QString(),
nullptr));
});
});
buttons->widthValue(
) | rpl::on_next([=](int width) {
const auto single = (width - st::walletButtonsSkip) / 2;
addFunds->setFullWidth(single);
addFunds->moveToLeft(0, 0, width);
const auto left = single + st::walletButtonsSkip;
send->setFullWidth(width - left);
send->moveToLeft(left, 0, width);
}, buttons->lifetime());
_show->session().wallet().presenceValue(
) | rpl::on_next([=](Presence presence) {
const auto ready = (presence == Presence::Ready);
for (const auto button : { addFunds, send }) {
SetButtonDisabledLook(button, !ready);
}
}, buttons->lifetime());
Ui::AddSkip(_pinnedInner, st::walletHeaderBottomSkip);
_headerShadow = Ui::CreateChild<Ui::PlainShadow>(this);
_pinnedBackground->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(_pinnedBackground);
const auto height = _pinnedBackground->height();
const auto tabsTop = height - pinnedMin();
p.fillRect(
0,
0,
_pinnedBackground->width(),
tabsTop,
st::windowBgOver);
if (tabsTop < height) {
p.fillRect(
0,
tabsTop,
_pinnedBackground->width(),
height - tabsTop,
st::windowBg);
}
}, _pinnedBackground->lifetime());
const auto forwardWheel = [=](not_null<QEvent*> e) {
if (e->type() != QEvent::Wheel) {
return base::EventFilterResult::Continue;
}
_scroll->viewportEvent(e);
return base::EventFilterResult::Cancel;
};
base::install_event_filter(_pinned, forwardWheel);
base::install_event_filter(_cardButton, forwardWheel);
}
[[nodiscard]] TextWithEntities IslandAmount(
const TextWithEntities &mark,
CreditsAmount amount) {
auto result = mark;
result.append(QChar(' '));
result.append(Ui::Text::Colorized(
Lang::FormatCreditsAmountToShort(amount).string));
return result;
}
void AddIslandRowLabel(
not_null<InfoIslandEntry*> button,
rpl::producer<TextWithEntities> text,
Ui::Text::MarkedContext context) {
const auto label = Ui::CreateChild<Ui::FlatLabel>(
button.get(),
std::move(text),
st::walletIslandRowLabel,
st::defaultPopupMenu,
std::move(context));
label->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setTryMakeSimilarLines(true);
const auto updateLabelGeometry = [=] {
const auto &padding = st::walletIslandRow.padding;
const auto available = button->width()
- padding.left()
- st::walletIslandLabelRightSkip;
if (available <= 0) {
return;
}
label->resizeToWidth(available);
button->setMinimalHeight(label->height()
+ padding.top()
+ padding.bottom());
label->moveToLeft(
padding.left(),
(button->height() - label->height()) / 2,
button->width());
};
button->widthValue(
) | rpl::on_next(updateLabelGeometry, button->lifetime());
label->heightValue(
) | rpl::on_next(updateLabelGeometry, label->lifetime());
}
void Content::setupInfoIsland() {
auto owned = object_ptr<InfoIsland>(_pinnedInner);
const auto island = owned.data();
const auto wrap = _pinnedInner->add(
object_ptr<Ui::SlideWrap<InfoIsland>>(
_pinnedInner,
std::move(owned),
style::margins(0, st::walletCardTopSkip, 0, 0)));
setupCustodyEntry(island);
setupWaltEntry(island);
setupEarningsEntry(island);
setupOldWalletEntry(island);
wrap->toggleOn(island->anyShownValue(), anim::type::normal);
}
void Content::setupWaltEntry(not_null<InfoIsland*> island) {
const auto session = &_show->session();
const auto wrap = island->add(
object_ptr<InfoIslandEntry>(
island,
tr::lng_wallet_walt_existing(),
st::walletIslandRow));
const auto button = wrap->entity();
AddRowChevron(button);
button->setClickedCallback([=] {
const auto url = session->wallet().existingWaltBalanceUrl();
if (!url.isEmpty()) {
OpenWalletUrl(session, _show, url);
}
});
wrap->toggleOn(session->wallet().existingWaltBalanceUrlValue(
) | rpl::map([](const QString &url) {
return !url.isEmpty();
}), anim::type::normal);
}
void Content::setupEarningsEntry(not_null<InfoIsland*> island) {
const auto session = &_show->session();
const auto wrap = island->add(
object_ptr<InfoIslandEntry>(island, nullptr, st::walletIslandRow));
const auto button = wrap->entity();
AddRowChevron(button);
auto helper = Ui::Text::CustomEmojiHelper();
const auto mark = GramMark(helper, st::walletIslandRowLabel.style.font);
AddIslandRowLabel(
button,
tr::lng_wallet_earnings_existing(
lt_amount,
session->credits().tonBalanceValue(
) | rpl::map([=](CreditsAmount value) {
return IslandAmount(mark, value);
}),
tr::marked),
helper.context());
button->setClickedCallback([=] {
if (const auto window = session->tryResolveWindow()) {
window->showSettings(Settings::CurrencyId());
window->window().activate();
}
});
session->credits().tonLoad();
wrap->toggleOn(session->credits().tonBalanceValue(
) | rpl::map([](CreditsAmount value) {
return !value.empty();
}), anim::type::normal);
}
void Content::setupProtectRow() {
const auto session = &_show->session();
const auto column = _column->entity();
const auto wrap = column->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
column,
object_ptr<Ui::VerticalLayout>(column)));
const auto inner = wrap->entity();
Ui::AddSkip(inner, st::walletProtectRowSkip);
const auto button = Settings::AddButtonWithIcon(
inner,
tr::lng_wallet_protect_account(),
st::walletProtectRow,
{ .icon = &st::walletProtectRowIcon });
AddRowChevron(button);
button->setClickedCallback([=] {
_show->showBox(Box(WalletCloudPasswordIntroBox, _show));
});
auto &cloud = session->api().cloudPassword();
cloud.reload();
auto off = rpl::single(false) | rpl::then(cloud.state(
) | rpl::map([](const Core::CloudPasswordState &state) {
return !state.hasPassword && state.unconfirmedPattern.isEmpty();
}));
auto nonEmpty = rpl::combine(
session->wallet().balanceNanoValue(),
HistoryShownValue(session)
) | rpl::map([](int64 balance, bool history) {
return (balance != 0) || history;
});
wrap->toggleOn(rpl::combine(
std::move(off),
std::move(nonEmpty)
) | rpl::map([](bool off, bool nonEmpty) {
return off && nonEmpty;
}) | rpl::distinct_until_changed(), anim::type::instant);
}
void Content::setupBalance() {
_ink = std::make_unique<BalanceInk>();
_pinnedBalance = Ui::CreateChild<Ui::RpWidget>(_pinned);
_pinnedBalance->setAttribute(Qt::WA_TransparentForMouseEvents);
_pinnedBalance->show();
_pinnedBalance->raise();
_pinnedBalance->paintRequest(
) | rpl::on_next([=](QRect clip) {
const auto fold = cardFold();
const auto ink = _ink->boundingRect(fold);
if (!clip.intersects(ink)) {
return;
}
auto p = QPainter(_pinnedBalance);
auto hq = PainterHighQualityEnabler(p);
_ink->paint(p, fold, cardOutline(), clip);
}, _pinnedBalance->lifetime());
_titleBalance.reset(Ui::CreateChild<Ui::RpWidget>(window()));
_titleBalance->setAttribute(Qt::WA_TransparentForMouseEvents);
_titleBalance->show();
_titleBalance->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(_titleBalance.get());
paintTitle(p, cardFold().fold);
auto hq = PainterHighQualityEnabler(p);
p.translate(0, st::separatePanelTitleHeight);
_ink->paint(
p,
cardFold(),
cardOutline(),
_titleBalance->rect().translated(
0,
-st::separatePanelTitleHeight));
}, _titleBalance->lifetime());
const auto repaintBalance = [=] {
_pinnedBalance->update();
_titleBalance->update();
_paintedInk = _ink->boundingRect(cardFold());
};
widthValue(
) | rpl::on_next([=](int width) {
const auto scope = WindowPaletteScope(this);
_ink->setOuterWidth(width);
repaintBalance();
}, lifetime());
SetupCardBalance(
_ink.get(),
&_show->session(),
repaintBalance,
this);
style::PaletteChanged(
) | rpl::on_next([=] {
const auto scope = WindowPaletteScope(this);
_ink->refresh();
_card->invalidateCache();
_card->update();
_pinnedBackground->update();
repaintBalance();
}, lifetime());
tr::lng_wallet_menu(
) | rpl::on_next([=](const QString &title) {
_title.setText(
st::separatePanelTitle.style,
title,
kPlainTextOptions);
const auto scope = WindowPaletteScope(this);
_ink->refresh();
repaintBalance();
}, lifetime());
SetupCardMark(_ink.get(), this, std::make_shared<bool>(), [=] {
const auto mark = _ink->markPaintRect(cardFold());
_pinnedBalance->update(mark);
_titleBalance->update(
mark.translated(0, st::separatePanelTitleHeight));
});
}
const CardFold &Content::cardFold() const {
return _card->fold();
}
QRegion Content::cardOutline() const {
if (_card->isHidden()) {
return {};
}
return QRegion(_card->paintedQuad().toPolygon());
}
void Content::paintTitle(QPainter &p, float64 fold) {
const auto &st = st::separatePanelTitle;
const auto left = st::separatePanelTitleLeft;
const auto top = st::separatePanelTitleTop;
const auto textWidth = std::min(
(width()
- left
- st::separatePanelClose.width
- st::separatePanelMenu.width),
_title.maxWidth());
const auto fullHeight = _title.countHeight(textWidth);
const auto titleHeight = std::min(fullHeight, st.maxHeight);
const auto elided = (st.maxHeight < fullHeight)
|| (textWidth < _title.maxWidth());
const auto lineHeight = std::max(
st.style.lineHeight,
st.style.font->height);
const auto box = QRect(left, top, textWidth, titleHeight);
p.save();
p.setClipRect(box);
if (fold > 0.) {
const auto scale = 1. - (1. - st::walletTitleFoldScale) * fold;
const auto center = QPointF(left, top + titleHeight / 2.);
p.setOpacity(1. - fold);
p.translate(center);
p.scale(scale, scale);
p.translate(-center);
}
p.setPen(_panel->titleOverridePalette()->windowFg()->c);
_title.draw(p, {
.position = { left, top },
.availableWidth = textWidth,
.align = st.align,
.clip = box,
.palette = &st.palette,
.elisionHeight = (elided ? std::max(st.maxHeight, lineHeight) : 0),
.elisionLines = 0,
});
p.restore();
}
void Content::setupTabs(rpl::producer<bool> collectiblesShown) {
_tabsWrap = _pinnedInner->add(
object_ptr<Ui::SlideWrap<Ui::SettingsSlider>>(
_pinnedInner,
object_ptr<Ui::SettingsSlider>(
_pinnedInner,
st::walletTabsSlider)));
const auto tabs = _tabsWrap->entity();
tabs->setSections({
tr::lng_wallet_rows_title(tr::now),
tr::lng_wallet_rows_collectibles(tr::now),
});
tabs->fitWidthToSections();
tabs->setNaturalWidth(tabs->width());
_tabsShadow = Ui::CreateChild<Ui::PlainShadow>(this);
// Without collectibles the first tab stands alone, scrolling with the list.
const auto column = _column->entity();
const auto title = column->insert(
0,
object_ptr<Ui::SlideWrap<Ui::SettingsSlider>>(
column,
object_ptr<Ui::SettingsSlider>(column, st::walletRowsTitle)));
const auto label = title->entity();
label->setAttribute(Qt::WA_TransparentForMouseEvents);
tr::lng_wallet_rows_title(
) | rpl::on_next([=](const QString &text) {
label->setSections({ text });
label->fitWidthToSections();
label->setNaturalWidth(label->width());
}, label->lifetime());
const auto wallet = &_show->session().wallet();
tabs->setActiveSectionFast(wallet->collectiblesTab() ? 1 : 0);
tabs->sectionActivated(
) | rpl::on_next([=](int index) {
wallet->setCollectiblesTab(index == 1);
_scroll->scrollToY(0);
}, tabs->lifetime());
wallet->collectiblesTabValue(
) | rpl::on_next([=](bool collectibles) {
const auto index = collectibles ? 1 : 0;
if (tabs->activeSection() != index) {
tabs->setActiveSectionFast(index);
}
}, tabs->lifetime());
rpl::combine(
std::move(collectiblesShown),
TransactionsShownValue(&_show->session())
) | rpl::map([](bool shown, bool transactions) {
return std::make_pair(shown, transactions && !shown);
}) | rpl::distinct_until_changed(
) | rpl::on_next([=](std::pair<bool, bool> sections) {
// WHY: the section that shows goes in before the one that hides, so
// no height in between lets the scroll area clamp a scrolled reader.
if (sections.second) {
title->toggle(true, anim::type::instant);
}
_tabsShown = sections.first;
_tabsWrap->toggle(sections.first, anim::type::instant);
_tabsShadow->setVisible(sections.first);
if (!sections.second) {
title->toggle(false, anim::type::instant);
}
updateRegions();
}, lifetime());
}
void Content::setupStrip() {
_stripShadow = Ui::CreateChild<Ui::PlainShadow>(this);
_strip = Ui::CreateChild<Ui::RpWidget>(this);
_strip->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(_strip);
PaintBottomRoundedPlate(p, _strip->rect(), st::windowBgOver);
}, _strip->lifetime());
const auto wallet = &_show->session().wallet();
auto minAmount = rpl::single(rpl::empty) | rpl::then(
wallet->historyUpdates()
) | rpl::map([=] {
return wallet->transferMinNanos();
}) | rpl::distinct_until_changed(
) | rpl::map([](int64 nanos) {
return Ui::FormatTonAmount(nanos).full;
});
const auto hint = Ui::CreateChild<Ui::FlatLabel>(
_strip,
tr::lng_wallet_rows_hidden_below(lt_amount, std::move(minAmount)),
st::defaultSubTextLabel);
hint->setAttribute(Qt::WA_TransparentForMouseEvents);
hint->show();
rpl::combine(
_strip->sizeValue(),
hint->sizeValue()
) | rpl::on_next([=](QSize size, QSize) {
hint->moveToLeft(
st::boxRowPadding.left(),
(size.height() - hint->height()) / 2,
size.width());
}, hint->lifetime());
TransactionsShownValue(
&_show->session()
) | rpl::on_next([=](bool shown) {
_stripShown = shown;
_strip->setVisible(shown);
_stripShadow->setVisible(shown);
updateRegions();
}, lifetime());
}
void Content::setupListsLoading() {
_listsLoading = Ui::CreateChild<Ui::RpWidget>(this);
_listsLoading->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto &loading = st::walletListsLoading;
const auto side = loading.size.height() + 2 * loading.thickness;
const auto indicator = Info::Statistics::InfiniteRadialAnimationWidget(
_listsLoading,
side,
&loading);
indicator->setAttribute(Qt::WA_TransparentForMouseEvents);
Info::Statistics::AddChildToWidgetCenter(_listsLoading, indicator);
const auto addCaption = [=](rpl::producer<QString> text) {
const auto caption = Ui::CreateChild<Ui::FlatLabel>(
_listsLoading,
std::move(text),
st::walletAboutTextLabel);
caption->setAttribute(Qt::WA_TransparentForMouseEvents);
_listsLoading->sizeValue(
) | rpl::on_next([=](QSize size) {
caption->resizeToNaturalWidth(size.width());
caption->moveToLeft(
(size.width() - caption->width()) / 2,
((size.height() + side) / 2) + st::walletAboutTitleSkip,
size.width());
}, caption->lifetime());
return caption;
};
const auto caption = addCaption(tr::lng_wallet_provisioning());
_show->session().wallet().presenceValue(
) | rpl::map(
rpl::mappers::_1 == Presence::Provisioning
) | rpl::on_next([=](bool provisioning) {
caption->setVisible(provisioning);
}, caption->lifetime());
const auto walkingCaption = addCaption(
tr::lng_wallet_history_searching());
// The gate is not the only state with nothing to paint. A feed whose
// loaded pages are all hidden while the server still offers a cursor is
// walking towards a row it can show, and this indicator - the one an
// unsettled feed already renders, with its caption bound to
// Provisioning and so hidden here - is the only face that says so. It
// resolves into rows or into the About face when the cursor exhausts.
// A visible row or a pending send makes it a lie, and so does the
// Collectibles tab, whose own list this region does not describe.
rpl::combine(
_show->session().wallet().listsGatedValue(),
_show->session().wallet().historyLoadingMoreValue(),
_show->session().wallet().collectiblesTabValue(),
HistoryShownValue(&_show->session())
) | rpl::map([](
bool gated,
bool loadingMore,
bool collectiblesTab,
bool historyShown) {
// The gate and the walk are the region's two reasons to show, and
// only the walk is the one the second caption speaks for, so both
// bits leave here together: written by one handler, the caption
// can neither outlive the region nor appear without it.
// updateListsGate() makes Provisioning - the state the first
// caption is bound to - one of the gate's own disjuncts, so the
// two captions are mutually exclusive by the same expression.
return std::make_pair(
gated,
!gated && loadingMore && !collectiblesTab && !historyShown);
}) | rpl::distinct_until_changed(
) | rpl::on_next([=](std::pair<bool, bool> face) {
const auto shown = face.first || face.second;
walkingCaption->setVisible(face.second);
indicator->setVisible(shown);
_listsLoading->setVisible(shown);
updateRegions();
}, lifetime());
}
void Content::setupCustodyEntry(not_null<InfoIsland*> island) {
const auto wrap = island->add(
object_ptr<InfoIslandEntry>(island, nullptr, st::walletIslandRow));
wrap->toggle(false, anim::type::instant);
const auto button = wrap->entity();
_custodyBarLabel = Ui::CreateChild<Ui::FlatLabel>(
button,
st::walletInfoBarLabel);
_custodyBarLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
_custodyBarLabel->setTryMakeSimilarLines(true);
const auto updateLabelGeometry = [=] {
const auto available = button->width()
- 2 * st::walletInfoBarLabelSkip;
if (available <= 0) {
return;
}
_custodyBarLabel->resizeToWidth(
std::min(_custodyBarLabel->textMaxWidth(), available));
const auto &padding = st::walletIslandRow.padding;
button->setMinimalHeight(_custodyBarLabel->height()
+ padding.top()
+ padding.bottom());
_custodyBarLabel->moveToLeft(
(button->width() - _custodyBarLabel->width()) / 2,
(button->height() - _custodyBarLabel->height()) / 2,
button->width());
};
button->widthValue(
) | rpl::on_next(updateLabelGeometry, button->lifetime());
button->setClickedCallback([=] {
_show->showBox(Box(
WalletImportBox,
_show,
WalletImportMode::Restore,
nullptr,
nullptr,
nullptr));
});
// The old wallet entry takes a conflict, and resolving it comes first.
auto &wallet = _show->session().wallet();
rpl::combine(
wallet.deviceCustodyStateValue(),
wallet.presenceValue()
) | rpl::map([](DeviceCustodyState state, Presence presence) {
return (presence == Presence::Ready)
&& !state.conflict
&& (state.mode == DeviceMode::ReadOnlyNotRestorable);
}) | rpl::distinct_until_changed(
) | rpl::on_next([=](bool shown) {
if (shown) {
_custodyBarLabel->setText(tr::lng_wallet_readonly_bar(tr::now));
updateLabelGeometry();
}
wrap->toggle(shown, anim::type::normal);
}, lifetime());
}
void Content::setupOldWalletEntry(not_null<InfoIsland*> island) {
const auto wallet = &_show->session().wallet();
const auto wrap = island->add(
object_ptr<InfoIslandEntry>(island, nullptr, st::walletIslandRow));
wrap->toggle(false, anim::type::instant);
const auto button = wrap->entity();
AddRowChevron(button);
auto helper = Ui::Text::CustomEmojiHelper();
const auto mark = GramMark(helper, st::walletIslandRowLabel.style.font);
AddIslandRowLabel(
button,
wallet->parkedBalanceNanoValue(
) | rpl::map([=](std::optional<int64> nano) {
return (nano && *nano > 0)
? tr::lng_wallet_conflict_existing(
lt_amount,
rpl::single(IslandAmount(mark, CreditsAmount(
*nano / Ui::kNanosInOne,
*nano % Ui::kNanosInOne,
CreditsType::Ton))),
tr::marked)
: tr::lng_wallet_conflict_bar(tr::marked);
}) | rpl::flatten_latest(),
helper.context());
button->setClickedCallback([=] {
_show->showBox(Box(WalletConflictBox, _show, nullptr));
});
wrap->toggleOn(rpl::combine(
wallet->deviceCustodyStateValue(),
wallet->presenceValue()
) | rpl::map([](DeviceCustodyState state, Presence presence) {
return (presence == Presence::Ready) && state.conflict;
}), anim::type::normal);
}
int Content::pinnedMax() const {
return _pinnedInner->height();
}
int Content::pinnedMin() const {
return _tabsShown ? st::walletTabsSlider.height : 0;
}
QRect Content::cardRest() const {
return QRect(
_cardPlaceholder->x(),
_cardPlaceholder->y(),
_cardPlaceholder->width(),
st::walletCardHeight);
}
Content::Regions Content::countRegions(int columnHeight) const {
const auto max = pinnedMax();
const auto min = pinnedMin();
const auto stripHeight = _stripShown
? (st::walletRowsHintHeight + st::lineWidth)
: 0;
const auto open = height() - max - stripHeight;
const auto reserve = (columnHeight > open) ? (max - min) : 0;
const auto scrollTop = max - reserve;
return {
.reserve = reserve,
.scrollTop = scrollTop,
.scrollHeight = std::max(0, height() - scrollTop - stripHeight),
};
}
float64 Content::foldProgress() const {
// The card's rest bottom, measured down from the pinned top, is the
// placeholder's own bottom inside _pinnedInner, because that layout's
// top sits at the pinned top with nothing scrolled away. So this is
// the scroll distance that carries the card's bottom edge up to the
// title bar, and no Content-space rest rect has to be re-derived.
const auto travel = _cardPlaceholder->y() + _cardPlaceholder->height();
const auto scrolled = std::clamp(_scroll->scrollTop(), 0, _reserve);
return std::clamp(scrolled / float64(travel), 0., 1.);
}
void Content::updateRegions() {
if (!width() || !height()) {
return;
}
_container->resize(width(), _container->height());
if (_pinnedInner->widthNoMargins() != width()) {
_pinnedInner->resizeToWidth(width());
}
const auto max = pinnedMax();
const auto regions = countRegions(_column->entity()->height());
_reserve = regions.reserve;
_column->setPadding({ 0, _reserve, 0, 0 });
_scroll->setGeometry(
0,
regions.scrollTop,
width(),
regions.scrollHeight);
if (_listsLoading && !_listsLoading->isHidden()) {
_listsLoading->setGeometry(0, max, width(), height() - max);
}
const auto body = Ui::MapFrom(window(), this, rect());
_titleBalance->setGeometry(
body.x(),
body.y() - st::separatePanelTitleHeight,
(width()
- st::separatePanelClose.width
- st::separatePanelMenu.width),
st::separatePanelTitleHeight);
updatePinned();
if (_stripShown) {
const auto stripTop = std::max(
regions.scrollTop,
height() - st::walletRowsHintHeight);
_stripShadow->setGeometry(
0,
stripTop - st::lineWidth,
width(),
st::lineWidth);
_strip->setGeometry(0, stripTop, width(), st::walletRowsHintHeight);
}
_headerShadow->setGeometry(0, 0, width(), st::lineWidth);
updateVisibleArea();
}
bool Content::revealSendingRow() {
if (!_sendingRow || !base::take(_sendingRow->revealPending)) {
return false;
}
auto &wallet = _show->session().wallet();
if (wallet.collectiblesTab()) {
wallet.setCollectiblesTab(false);
_scroll->scrollToY(0);
return false;
}
const auto slot = _sendingRow->slot;
const auto top = Ui::MapFrom(_container, slot, QPoint()).y();
const auto bottom = top + slot->height();
const auto scrollTop = _scroll->scrollTop();
if (top < std::max(scrollTop, _reserve)) {
_scroll->scrollToY(top);
} else if (bottom > scrollTop + _scroll->height()) {
_scroll->scrollToY(bottom - _scroll->height());
}
return true;
}
void Content::flySendDiamond(
const std::string &operationId,
not_null<Ui::TonAmountInput*> amount) {
const auto current = [&]() -> SendingHistoryRow* {
return (_sendingRow && _sendingRow->operationId == operationId)
? _sendingRow->look
: nullptr;
};
if (!current() || anim::Disabled()) {
return;
}
const auto body = dynamic_cast<Ui::RpWidget*>(parentWidget());
const auto now = crl::now();
const auto revealed = body && revealSendingRow();
const auto look = current();
if (!look) {
return;
}
auto diamond = AmountDiamond();
if (revealed && look->surfaceShown()) {
const auto slot = look->diamondTarget(now).translated(
QPointF(Ui::MapFrom(_scroll.data(), look, QPoint())));
const auto occluded = _reserve
- std::clamp(_scroll->scrollTop(), 0, _reserve);
if (slot.top() >= occluded && slot.bottom() <= _scroll->height()) {
diamond = TakeAmountDiamond(amount);
}
}
if (!diamond.icon) {
look->scheduleBump(now + kSendingRowFlightDuration);
return;
}
look->awaitDiamond();
struct State {
DiamondFlight *flight = nullptr;
bool landed = false;
};
const auto state = std::make_shared<State>();
const auto weak = base::make_weak(look);
const auto loopStarted = SendingDiamondLoopStart(diamond.icon.get(), now);
_diamondFlight = std::make_unique<DiamondFlight>(DiamondFlightArgs{
.body = body,
.icon = std::move(diamond.icon),
.from = diamond.global,
.loopStarted = loopStarted,
.duration = kSendingRowFlightDuration,
.target = [=](crl::time at) -> std::optional<QRectF> {
const auto row = weak.get();
if (!row || !row->surfaceShown()) {
return std::nullopt;
}
return row->diamondTarget(at).translated(
QPointF(Ui::MapFrom(body, row, QPoint())));
},
.landed = [=](std::unique_ptr<Lottie::Icon> icon, crl::time loop) {
if (const auto row = weak.get()) {
state->landed = true;
row->landDiamond(std::move(icon), loop);
}
},
.finished = [=] {
if (const auto row = state->landed ? nullptr : weak.get()) {
row->cancelDiamondAwait();
}
crl::on_main(this, [=] {
if (_diamondFlight.get() == state->flight) {
_diamondFlight = nullptr;
}
});
},
});
state->flight = _diamondFlight.get();
}
void Content::updateVisibleArea() {
const auto top = _scroll->scrollTop();
_column->setVisibleTopBottom(top, top + _scroll->height());
}
void Content::updatePinned() {
if (!width() || !height()) {
return;
}
const auto max = pinnedMax();
const auto min = pinnedMin();
const auto top = std::clamp(_scroll->scrollTop(), 0, _reserve);
const auto height = max - top;
_pinnedInner->moveToLeft(0, height - max, width());
_pinned->setGeometry(0, 0, width(), height);
_pinnedBackground->setGeometry(0, 0, width(), height);
const auto rest = cardRest();
const auto fold = ComputeCardFold(rest, foldProgress());
const auto cardWidget = QRect(
QPoint(rest.left(), 0),
rest.bottomRight());
_card->setGeometry(cardWidget);
_card->setFold(fold);
const auto shown = fold.valid && fold.opacity > 0.;
if (shown) {
const auto outline = _card->paintedOutline();
const auto bounds = outline.boundingRect().toAlignedRect();
_cardButton->setGeometry(bounds);
_cardButton->setMask(QRegion(
outline.translated(-bounds.topLeft()).toPolygon()));
}
_cardButton->setVisible(shown);
_scroll->setVerticalBarTopSkip(height - min);
_tabsShadow->setGeometry(0, height, width(), st::lineWidth);
_headerShadow->setVisible(height == min);
_pinnedBalance->setGeometry(_pinned->rect());
// The card's dirty area is its whole widget rect, which contains
// every folded quad, and the ink's is the union of the rects it was
// and is painted into.
const auto inkPainted = _ink->boundingRect(fold);
const auto inkDirty = _paintedInk.united(inkPainted);
_paintedInk = inkPainted;
update(cardWidget);
update(inkDirty);
_pinnedBackground->update(cardWidget);
_pinnedBalance->update(cardWidget);
_pinnedBalance->update(inkDirty);
if (_paintedHeight == height && _paintedMin == min) {
return;
}
_paintedHeight = height;
_paintedMin = min;
_pinnedBackground->update();
// Reaching here means the pinned height changed, and with it the
// scrolled distance the fold progress is computed from. The band
// overlay carries the fading title and the end of the balance's
// travel, and it is at most the title bar's height tall, so it is
// repainted whole rather than tracked rect by rect.
_titleBalance->update();
}
void Content::checkLoadMore() {
auto &wallet = _show->session().wallet();
if (wallet.listsGated()) {
return;
}
const auto collectibles = wallet.collectiblesTab();
const auto hasNext = collectibles
? wallet.collectiblesHasNext()
: wallet.historyHasNext();
if (!hasNext) {
return;
}
if (_scroll->scrollTop() + _scroll->height() >= _scroll->scrollTopMax()) {
if (collectibles) {
wallet.loadMoreCollectibles();
} else {
wallet.loadMoreHistory();
}
}
}
void Content::focusInEvent(QFocusEvent *e) {
_scroll->setFocus();
}
void Content::resizeEvent(QResizeEvent *e) {
updateRegions();
_loadMoreCheck.call();
}
void Content::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
if (_stripShown) {
p.fillRect(0, 0, width(), _strip->y(), st::windowBg);
return;
}
PaintBottomRoundedPlate(p, rect(), st::windowBg);
}
class CurrencyListWidget final : public Ui::RpWidget {
public:
CurrencyListWidget(
not_null<QWidget*> parent,
std::shared_ptr<Main::SessionShow> show,
Fn<void(QString)> chosen);
void updateFilter(const QString &query);
void selectSkip(int direction);
void selectSkipPage(int height, int direction);
void chooseSelected();
void scrollToCurrent();
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> mustScrollTo() const;
protected:
void paintEvent(QPaintEvent *e) override;
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
private:
struct Row {
QString code;
QString name;
QStringList words;
};
[[nodiscard]] const std::vector<Row> &current() const;
[[nodiscard]] bool rowMatches(const Row &row) const;
void refreshRows();
void refreshFiltered();
void refreshHeight();
void updateSelected(QPoint localPos);
void setSelected(int index);
void setPressed(int pressed);
void updateRow(int index);
const std::shared_ptr<Main::SessionShow> _show;
const Fn<void(QString)> _chosen;
QString _activeCode;
QStringList _filter;
std::vector<Row> _rows;
std::vector<Row> _filtered;
std::vector<std::unique_ptr<Ui::RippleAnimation>> _ripples;
int _selected = -1;
int _pressed = -1;
bool _mouseSelection = false;
rpl::event_stream<Ui::ScrollToRequest> _mustScrollTo;
};
[[nodiscard]] bool ForwardCurrencyNavigation(
not_null<QKeyEvent*> e,
not_null<CurrencyListWidget*> list,
int pageHeight) {
if (e->key() == Qt::Key_Down) {
list->selectSkip(1);
} else if (e->key() == Qt::Key_Up) {
list->selectSkip(-1);
} else if (e->key() == Qt::Key_PageDown) {
list->selectSkipPage(pageHeight, 1);
} else if (e->key() == Qt::Key_PageUp) {
list->selectSkipPage(pageHeight, -1);
} else {
return false;
}
return true;
}
void WalletChooseCurrencyBox(
not_null<Ui::GenericBox*> box,
std::shared_ptr<Main::SessionShow> show) {
box->setTitle(tr::lng_wallet_currency_title());
box->setWidth(st::boxWideWidth);
box->setMaxHeight(st::boxMaxListHeight);
AddBoxCloseButton(box);
const auto select = box->setPinnedToTopContent(
object_ptr<Ui::MultiSelect>(
box,
st::defaultMultiSelect,
tr::lng_country_ph()));
const auto list = box->addRow(
object_ptr<CurrencyListWidget>(
box,
show,
[=](QString code) {
show->session().wallet().rates().setCurrency(code);
box->closeBox();
}),
style::margins());
box->setFocusCallback([=] { select->setInnerFocus(); });
select->setQueryChangedCallback([=](const QString &query) {
box->scrollToY(0);
list->updateFilter(query);
});
select->setSubmittedCallback([=](Qt::KeyboardModifiers) {
list->chooseSelected();
});
select->setCancelledCallback([=] { box->closeBox(); });
list->mustScrollTo(
) | rpl::on_next([=](Ui::ScrollToRequest request) {
box->scrollToY(request.ymin, request.ymax);
}, list->lifetime());
base::install_event_filter(select, [=](not_null<QEvent*> e) {
if (e->type() != QEvent::KeyPress) {
return base::EventFilterResult::Continue;
}
const auto key = static_cast<QKeyEvent*>(e.get());
const auto pageHeight = box->height() - select->height();
return ForwardCurrencyNavigation(key, list, pageHeight)
? base::EventFilterResult::Cancel
: base::EventFilterResult::Continue;
});
box->setShowFinishedCallback([=] { list->scrollToCurrent(); });
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
}
CurrencyListWidget::CurrencyListWidget(
not_null<QWidget*> parent,
std::shared_ptr<Main::SessionShow> show,
Fn<void(QString)> chosen)
: RpWidget(parent)
, _show(std::move(show))
, _chosen(std::move(chosen)) {
setAttribute(Qt::WA_OpaquePaintEvent);
_show->session().wallet().rates().value(
) | rpl::on_next([=] {
refreshRows();
}, lifetime());
}
void CurrencyListWidget::updateFilter(const QString &query) {
auto filter = TextUtilities::PrepareSearchWords(query);
if (_filter == filter) {
return;
}
_filter = std::move(filter);
refreshFiltered();
_selected = current().empty() ? -1 : 0;
update();
}
void CurrencyListWidget::selectSkip(int direction) {
_mouseSelection = false;
const auto &list = current();
const auto moved = _selected + direction;
const auto next = (moved <= 0)
? (list.empty() ? -1 : 0)
: (moved >= int(list.size()))
? -1
: moved;
setSelected(next);
if (_selected >= 0) {
_mustScrollTo.fire(Ui::ScrollToRequest(
st::walletCurrencyListSkip
+ _selected * st::walletCurrencyRowHeight,
st::walletCurrencyListSkip
+ (_selected + 1) * st::walletCurrencyRowHeight));
}
}
void CurrencyListWidget::selectSkipPage(int height, int direction) {
const auto rows = height / st::walletCurrencyRowHeight;
if (!rows) {
return;
}
selectSkip(rows * direction);
}
void CurrencyListWidget::chooseSelected() {
const auto &list = current();
if (_selected < 0 || _selected >= int(list.size())) {
return;
}
const auto code = list[_selected].code;
_chosen(code);
}
void CurrencyListWidget::scrollToCurrent() {
const auto &list = current();
for (auto i = 0, count = int(list.size()); i != count; ++i) {
if (list[i].code == _activeCode) {
_mustScrollTo.fire(Ui::ScrollToRequest(
st::walletCurrencyListSkip
+ i * st::walletCurrencyRowHeight,
st::walletCurrencyListSkip
+ (i + 1) * st::walletCurrencyRowHeight));
return;
}
}
}
rpl::producer<Ui::ScrollToRequest> CurrencyListWidget::mustScrollTo() const {
return _mustScrollTo.events();
}
void CurrencyListWidget::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto clip = e->rect();
p.setClipRect(clip);
const auto &list = current();
if (list.empty()) {
p.fillRect(clip, st::windowBg);
p.setFont(st::noContactsFont);
p.setPen(st::noContactsColor);
p.drawText(
QRect(0, 0, width(), st::noContactsHeight),
tr::lng_wallet_currency_none(tr::now),
style::al_center);
return;
}
const auto skip = st::walletCurrencyListSkip;
const auto rowHeight = st::walletCurrencyRowHeight;
const auto count = int(list.size());
const auto top = QRect(0, 0, width(), skip);
if (clip.intersects(top)) {
p.fillRect(clip.intersected(top), st::windowBg);
}
const auto from = std::clamp((clip.y() - skip) / rowHeight, 0, count);
const auto till = std::clamp(
(clip.y() + clip.height() - skip + rowHeight - 1) / rowHeight,
0,
count);
const auto &icon = st::walletCurrencyCheckIcon;
const auto left = st::walletCurrencyRowPadding.left();
const auto right = st::walletCurrencyRowPadding.right();
for (auto i = from; i != till; ++i) {
const auto &row = list[i];
const auto selected = (i == (_pressed >= 0 ? _pressed : _selected));
const auto y = skip + i * rowHeight;
p.fillRect(
0,
y,
width(),
rowHeight,
selected ? st::windowBgOver : st::windowBg);
if (i < int(_ripples.size()) && _ripples[i]) {
_ripples[i]->paint(p, 0, y, width());
if (_ripples[i]->empty()) {
_ripples[i].reset();
}
}
const auto textTop = y + st::walletCurrencyRowPadding.top();
p.setFont(st::walletCurrencyRowCodeFont);
p.setPen(st::walletCurrencyRowCodeFg);
p.drawTextLeft(left, textTop, width(), row.code);
if (!row.name.isEmpty()) {
const auto codeWidth = st::walletCurrencyRowCodeFont->width(
row.code);
const auto nameLeft = left
+ codeWidth
+ st::walletCurrencyRowNameSkip;
const auto available = width()
- nameLeft
- right
- icon.width()
- st::walletCurrencyRowNameSkip;
p.setFont(st::normalFont);
p.setPen(st::walletCurrencyRowNameFg);
p.drawTextLeft(
nameLeft,
textTop,
width(),
st::normalFont->elided(row.name, available));
}
if (row.code == _activeCode) {
icon.paint(
p,
width() - right - icon.width(),
y + (rowHeight - icon.height()) / 2,
width());
}
}
}
void CurrencyListWidget::enterEventHook(QEnterEvent *e) {
setMouseTracking(true);
}
void CurrencyListWidget::leaveEventHook(QEvent *e) {
_mouseSelection = false;
setMouseTracking(false);
setSelected(-1);
}
void CurrencyListWidget::mouseMoveEvent(QMouseEvent *e) {
_mouseSelection = true;
updateSelected(e->pos());
}
void CurrencyListWidget::mousePressEvent(QMouseEvent *e) {
_mouseSelection = true;
updateSelected(e->pos());
setPressed(_selected);
const auto &list = current();
if (_pressed < 0 || _pressed >= int(list.size())) {
return;
}
if (int(_ripples.size()) <= _pressed) {
_ripples.resize(_pressed + 1);
}
if (!_ripples[_pressed]) {
auto mask = Ui::RippleAnimation::RectMask(
QSize(width(), st::walletCurrencyRowHeight));
_ripples[_pressed] = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
[this, index = _pressed] { updateRow(index); });
_ripples[_pressed]->add(e->pos() - QPoint(
0,
st::walletCurrencyListSkip
+ _pressed * st::walletCurrencyRowHeight));
}
}
void CurrencyListWidget::mouseReleaseEvent(QMouseEvent *e) {
const auto pressed = _pressed;
setPressed(-1);
updateRow(_selected);
if (e->button() == Qt::LeftButton
&& pressed >= 0
&& pressed == _selected) {
chooseSelected();
}
}
auto CurrencyListWidget::current() const
-> const std::vector<CurrencyListWidget::Row> & {
return _filtered;
}
bool CurrencyListWidget::rowMatches(const Row &row) const {
return ranges::all_of(_filter, [&](const QString &word) {
return ranges::any_of(row.words, [&](const QString &name) {
return name.startsWith(word);
});
});
}
void CurrencyListWidget::refreshRows() {
auto &rates = _show->session().wallet().rates();
_activeCode = rates.current().currency;
_rows.clear();
const auto codes = rates.currencies();
const auto top = TopCurrencies(&_show->session());
auto ordered = std::vector<QString>();
ordered.reserve(codes.size());
for (const auto &code : top) {
if (ranges::contains(codes, code)) {
ordered.push_back(code);
}
}
for (const auto &code : codes) {
if (!ranges::contains(top, code)) {
ordered.push_back(code);
}
}
_rows.reserve(ordered.size());
for (const auto &code : ordered) {
const auto names = LookupCurrencyNames(code);
_rows.push_back({
.code = code,
.name = (names.localized.isEmpty()
? names.english
: names.localized),
.words = TextUtilities::PrepareSearchWords(QStringList{
code,
names.english,
names.localized,
}.join(QChar(' '))),
});
}
refreshFiltered();
}
void CurrencyListWidget::refreshFiltered() {
_filtered.clear();
_filtered.reserve(_rows.size());
for (const auto &row : _rows) {
if (_filter.isEmpty() || rowMatches(row)) {
_filtered.push_back(row);
}
}
_ripples.clear();
_pressed = -1;
if (_selected >= int(current().size())) {
_selected = -1;
}
refreshHeight();
update();
}
void CurrencyListWidget::refreshHeight() {
const auto &list = current();
resize(
width(),
list.empty()
? st::noContactsHeight
: (st::walletCurrencyListSkip
+ int(list.size()) * st::walletCurrencyRowHeight));
}
void CurrencyListWidget::updateSelected(QPoint localPos) {
if (!_mouseSelection) {
return;
}
const auto in = visibleRegion().boundingRect().contains(
mapFromGlobal(QCursor::pos()));
const auto &list = current();
const auto skip = st::walletCurrencyListSkip;
const auto rowsHeight = int(list.size()) * st::walletCurrencyRowHeight;
const auto selected = (in
&& localPos.y() >= skip
&& localPos.y() < skip + rowsHeight)
? ((localPos.y() - skip) / st::walletCurrencyRowHeight)
: -1;
setSelected(selected);
}
void CurrencyListWidget::setSelected(int index) {
if (_selected == index) {
return;
}
updateRow(_selected);
_selected = index;
updateRow(_selected);
}
void CurrencyListWidget::setPressed(int pressed) {
if (_pressed >= 0
&& _pressed < int(_ripples.size())
&& _ripples[_pressed]) {
_ripples[_pressed]->lastStop();
}
_pressed = pressed;
}
void CurrencyListWidget::updateRow(int index) {
if (index >= 0) {
update(
0,
st::walletCurrencyListSkip
+ index * st::walletCurrencyRowHeight,
width(),
st::walletCurrencyRowHeight);
}
}
void AcquireKeyThroughLadder(
std::shared_ptr<Main::SessionShow> show,
std::shared_ptr<CommentScope> scope,
Fn<bool()> current,
rpl::lifetime &lifetime,
Fn<void(KeyAuthorization)> done,
rpl::producer<QString> importAbout) {
const auto context = std::make_shared<KeyContext>(
std::move(show),
std::move(scope),
std::move(current),
std::move(done));
lifetime.add([context] { context->cancel(); });
RunKeyRequiringAction(context, [=] {
if (!context->valid()) {
context->cancel();
return;
}
AcquireVaultUnlock({
.show = context,
.done = [=](KeyAuthorization auth) {
context->ready(std::move(auth));
},
});
}, KeyActionKind::ResumeAfterRestore, context,
std::move(importAbout));
}
} // namespace
void AcquireTransferCommentKey(
std::shared_ptr<Main::SessionShow> show,
std::shared_ptr<CommentScope> scope,
Fn<bool()> current,
rpl::lifetime &lifetime,
Fn<void(KeyAuthorization)> done) {
AcquireKeyThroughLadder(
std::move(show),
std::move(scope),
std::move(current),
lifetime,
std::move(done),
nullptr);
}
void AcquireWalletKey(
std::shared_ptr<Main::SessionShow> show,
Fn<bool()> current,
rpl::lifetime &lifetime,
Fn<void(KeyAuthorization)> done,
rpl::producer<QString> importAbout) {
AcquireKeyThroughLadder(
std::move(show),
nullptr,
std::move(current),
lifetime,
std::move(done),
std::move(importAbout));
}
void ShowTransactionDetails(
std::shared_ptr<Main::SessionShow> show,
TransferItem item,
bool partial,
std::shared_ptr<CollectibleMedia> media,
Fn<bool()> originCurrent,
rpl::producer<> originInvalidated,
Fn<void()> openWallet,
Ui::LayerOptions options) {
if (!show || !show->valid()
|| (originCurrent && !originCurrent())) {
return;
}
show->showBox(Box(
WalletTransactionBox,
show,
std::move(item),
partial,
std::move(media),
std::move(originCurrent),
std::move(originInvalidated),
std::move(openWallet),
rpl::producer<TransferItem>()), options);
}
void ShowSubmittedTransfer(
std::shared_ptr<Main::SessionShow> show,
const std::string &operationId) {
if (!show || !show->valid()) {
return;
}
const auto wallet = &show->session().wallet();
const auto find = [=]() -> std::optional<TransferItem> {
if (auto item = wallet->trackedTransaction(operationId)) {
return item;
}
const auto pending = wallet->pendingSend();
return (pending && pending->operationId == operationId)
? std::make_optional(ItemFromPending(*pending))
: std::nullopt;
};
if (const auto item = find()) {
ShowWalletTransactionBox(
show,
*item,
nullptr,
wallet->historyUpdates() | rpl::map(find) | rpl::filter_optional());
}
}
bool ShowFirstGramsIfPending(std::shared_ptr<Main::SessionShow> show) {
if (!show || !show->valid()) {
return false;
}
const auto session = &show->session();
if (!session->promoSuggestions().current(
Data::PromoSuggestions::SugWalletFirstIncomingTransfer())) {
return false;
}
show->showBox(Box(WalletFirstGramsBox, session));
return true;
}
rpl::producer<bool> TransactionsShownValue(
not_null<Main::Session*> session) {
return rpl::combine(
HistoryShownValue(session),
session->wallet().collectiblesTabValue()
) | rpl::map([](bool history, bool collectibles) {
return history && !collectibles;
}) | rpl::distinct_until_changed();
}
base::unique_qptr<Ui::RpWidget> CreateContent(
not_null<Ui::SeparatePanel*> panel,
std::shared_ptr<Main::SessionShow> show) {
return base::make_unique_q<Content>(panel, std::move(show));
}
object_ptr<Ui::RpWidget> MakeWalletCard(
QWidget *parent,
std::shared_ptr<Main::SessionShow> show,
std::shared_ptr<bool> markPlayed) {
auto result = object_ptr<Ui::FixedHeightWidget>(
parent,
st::walletCardHeight);
const auto raw = result.data();
raw->setNaturalWidth(PanelCardWidth());
const auto card = Ui::CreateChild<Card>(
raw,
show,
CardNameValue(&show->session()));
card->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto overlay = Ui::CreateChild<Ui::RpWidget>(raw);
overlay->setAttribute(Qt::WA_TransparentForMouseEvents);
overlay->show();
overlay->raise();
const auto ink = raw->lifetime().make_state<BalanceInk>();
{
const auto scope = WindowPaletteScope(raw);
ink->setOuterWidth(st::walletPanelSize.width());
}
raw->sizeValue(
) | rpl::on_next([=](QSize size) {
const auto rest = QRect(QPoint(), size);
card->setGeometry(rest);
card->setFold(ComputeCardFold(rest, 0.));
overlay->setGeometry(rest);
}, raw->lifetime());
overlay->paintRequest(
) | rpl::on_next([=](QRect clip) {
auto p = QPainter(overlay);
auto hq = PainterHighQualityEnabler(p);
ink->paint(
p,
ComputeCardFold(raw->rect(), 0.),
QRegion(raw->rect()),
clip);
}, overlay->lifetime());
SetupCardBalance(
ink,
&show->session(),
[=] { overlay->update(); },
raw);
SetupCardMark(ink, raw, std::move(markPlayed), [=] {
overlay->update(
ink->markPaintRect(ComputeCardFold(raw->rect(), 0.)));
});
rpl::single(rpl::empty) | rpl::then(
style::PaletteChanged()
) | rpl::on_next([=] {
const auto scope = WindowPaletteScope(raw);
ink->refresh();
card->invalidateCache();
card->update();
overlay->update();
}, raw->lifetime());
return result;
}
object_ptr<Ui::RpWidget> MakeTransferCard(
QWidget *parent,
not_null<Main::Session*> session,
TransferCardArgs args) {
auto result = object_ptr<Ui::FixedHeightWidget>(
parent,
st::walletCardHeight);
const auto raw = result.data();
raw->setNaturalWidth(PanelCardWidth());
struct State {
BalanceInk ink;
CardBackground background;
QStringList lines;
};
const auto state = raw->lifetime().make_state<State>();
{
const auto scope = WindowPaletteScope(raw);
state->ink.setOuterWidth(st::walletPanelSize.width());
}
state->lines = TransferCardLines(args.destination, args.recipients);
const auto info = Ui::CreateChild<Ui::AbstractButton>(raw);
info->setClickedCallback(std::move(args.info));
info->show();
raw->sizeValue(
) | rpl::on_next([=](QSize size) {
info->setGeometry(TransferCardInfoRect(size.width()));
}, info->lifetime());
raw->paintRequest(
) | rpl::on_next([=](QRect clip) {
auto p = QPainter(raw);
auto hq = PainterHighQualityEnabler(p);
const auto rect = raw->rect();
state->background.paint(p, rect, 0.);
const auto plate = TransferCardInfoRect(rect.width());
PaintCardQrPlate(p, plate);
st::walletCardInfoIcon.paintInCenter(p, plate, CardQrIconFg());
const auto font
= st::walletDetailsCollectionLabel.style.font->monospace();
const auto &lines = state->lines;
auto baseline = rect.height()
- st::walletCardNameBottom
- (int(lines.size()) - 1) * font->height;
p.setPen(st::activeButtonFg);
p.setFont(font);
for (const auto &line : lines) {
p.drawText(st::walletCardContentLeft, baseline, line);
baseline += font->height;
}
state->ink.paint(p, ComputeCardFold(rect, 0.), QRegion(rect), clip);
}, raw->lifetime());
const auto amount = args.netNano.value_or(-args.totalNano);
const auto magnitude = std::abs(amount);
const auto style = (amount > 0)
? BalanceStyle::Plus
: ((amount < 0) || !args.netNano)
? BalanceStyle::Minus
: BalanceStyle::Exact;
FiatRateValue(
session
) | rpl::on_next([=](FiatRate rate) {
const auto scope = WindowPaletteScope(raw);
state->ink.setContent(
CreditsAmount(
magnitude / Ui::kNanosInOne,
magnitude % Ui::kNanosInOne,
CreditsType::Ton),
FormatFiat(magnitude, rate),
style);
raw->update();
}, raw->lifetime());
rpl::single(rpl::empty) | rpl::then(
style::PaletteChanged()
) | rpl::on_next([=] {
const auto scope = WindowPaletteScope(raw);
state->ink.refresh();
raw->update();
}, raw->lifetime());
SetupCardMark(&state->ink, raw, std::move(args.markPlayed), [=] {
raw->update(
state->ink.markPaintRect(ComputeCardFold(raw->rect(), 0.)));
});
return result;
}
void FillMenu(
std::shared_ptr<Main::SessionShow> show,
const Ui::Menu::MenuCallback &addAction) {
const auto currency = show->session().wallet().rates().current().currency;
addAction(
(Ui::Text::FixAmpersandInAction(
tr::lng_wallet_menu_currency(tr::now))
+ u"\t"_q
+ currency),
[=] {
show->showBox(Box(WalletChooseCurrencyBox, show));
},
&st::walletMenuCurrencyIcon);
// The menu is rebuilt on every open, so this reading is live: the entry
// is absent in both read-only modes and while the mode is still Unknown.
const auto custody = show->session().wallet().deviceCustodyState();
if (custody.mode == DeviceMode::Full) {
addAction(
Ui::Text::FixAmpersandInAction(
tr::lng_wallet_protection_title(tr::now)),
[=] {
ShowKeyProtectionBox(
show,
{ .mode = KeyProtectionMode::Change });
},
&st::menuIconLock);
}
if (show->session().wallet().presence() == Presence::Ready) {
addAction(
Ui::Text::FixAmpersandInAction(
tr::lng_wallet_keys_title(tr::now)),
[=] { show->showBox(Box(WalletKeysBackupBox, show)); },
&st::menuIconPermissions);
const auto &store = show->session().wallet().tonConnect();
const auto connected = ranges::any_of(
ranges::views::values(store.sessions()),
TonConnectSessionConnected);
if (connected) {
addAction(
Ui::Text::FixAmpersandInAction(
tr::lng_wallet_apps_title(tr::now)),
[=] { show->showBox(Box(TonConnectAppsBox, show)); },
&st::menuIconLink);
}
}
addAction({ .isSeparator = true });
addAction(
Ui::Text::FixAmpersandInAction(tr::lng_wallet_how_menu(tr::now)),
[=] { show->showBox(Box(WalletHowItWorksBox, &show->session())); },
&st::menuIconFaq);
}
bool TransferLinkValid(const QString &url) {
return ParseRecipientFlow(url).has_value();
}
void ShowTransferLink(
std::shared_ptr<Main::SessionShow> show,
const QString &url) {
const auto flow = ParseRecipientFlow(url);
if (!flow) {
return;
} else if (TransferLinkExpired(flow->expiresAt)) {
show->showToast(tr::lng_wallet_send_link_expired(tr::now));
return;
}
ResolveOwnerAndOpenSendFlow(show, *flow);
}
void ShowWalletConflict(
std::shared_ptr<Main::SessionShow> show,
Fn<void()> switched) {
show->showBox(Box(WalletConflictBox, show, std::move(switched)));
}
Fn<void()> ShowWalletBusyBox(
std::shared_ptr<Main::SessionShow> show,
rpl::producer<QString> text,
Fn<void()> dismissed) {
auto box = Box(WalletBusyBox, std::move(text));
const auto weak = base::make_weak(box.data());
const auto closing = std::make_shared<bool>(false);
box->boxClosing() | rpl::on_next([=] {
if (!*closing && dismissed) {
dismissed();
}
}, box->lifetime());
show->showBox(std::move(box));
return [=] {
*closing = true;
if (const auto strong = weak.get()) {
if (strong->hasDelegate()) {
strong->closeBox();
}
}
};
}
void ShowSendToUser(
std::shared_ptr<Main::SessionShow> show,
not_null<UserData*> user,
Fn<void()> sent,
int64 amountNano,
Fn<void()> notReady,
base::weak_qptr<Ui::BoxContent> origin) {
if (!show || !show->valid() || &show->session() != &user->session()) {
return;
}
const auto session = &show->session();
const auto userId = peerToUser(user->id);
if (session->data().userLoaded(userId) != user) {
return;
}
WhenWalletReady(show, [=] {
const auto error = session->wallet().userAddresses(
).forceResolveError(userId);
if (!error.isEmpty() && error != u"WALLET_BALANCE_EMPTY"_q) {
show->showToast(SendUserLoadErrorText(error));
return;
} else if (user->isSelf()) {
const auto identity = session->wallet().transferWalletIdentity();
auto flow = identity
? ParseRecipientFlow(FormatFriendly(identity->address, false))
: std::nullopt;
if (!flow) {
show->showToast(SendUserLoadErrorText(u"WALLET_NOT_READY"_q));
return;
}
flow->amountNano = amountNano;
show->showBox(Box(
WalletSendBox,
show,
std::move(flow),
static_cast<UserData*>(nullptr),
sent,
notReady,
amountNano,
origin));
return;
}
show->showBox(Box(
WalletSendBox,
show,
std::nullopt,
user.get(),
sent,
notReady,
amountNano,
origin));
}, notReady);
}
void ShowCollectibleTransfer(
std::shared_ptr<Main::SessionShow> show,
std::shared_ptr<CollectibleMedia> media,
const QString &collectible) {
if (!show || !show->valid() || !media || collectible.isEmpty()) {
return;
}
media->resolve(collectible);
const auto weak = std::weak_ptr<CollectibleMedia>(media);
WhenWalletReady(show, [=] {
const auto strong = weak.lock();
if (!strong || !show->valid()) {
return;
}
show->showBox(Box(
WalletSendRecipientBox,
show,
QString(),
std::make_shared<const CollectibleTransfer>(CollectibleTransfer{
.address = collectible,
.media = strong,
})));
});
}
void ShowSendToLinkRecipient(
std::shared_ptr<Main::SessionShow> show,
const QString &recipient,
int64 amountNano) {
if (!show || !show->valid()) {
return;
}
const auto session = &show->session();
const auto invalid = [=] {
show->showToast(tr::lng_wallet_send_link_invalid(tr::now));
};
if (auto flow = ParseRecipientFlow(recipient)) {
if (!flow->amountNano) {
flow->amountNano = amountNano;
}
ResolveOwnerAndOpenSendFlow(show, *flow);
return;
}
const auto username = recipient.startsWith('@')
? recipient.mid(1)
: recipient;
if (!qthelp::regex_match(u"^[a-zA-Z0-9\\_]+$"_q, username, {})) {
invalid();
return;
}
const auto open = [=](PeerData *peer) {
if (!show->valid() || &show->session() != session) {
return;
}
const auto user = peer ? peer->asUser() : nullptr;
if (!user) {
show->showToast(tr::lng_wallet_send_user_unavailable(tr::now));
return;
}
ShowSendToUser(show, user, nullptr, amountNano);
};
if (const auto peer = session->data().peerByUsername(username)) {
open(peer);
return;
}
session->api().request(MTPcontacts_ResolveUsername(
MTP_flags(0),
MTP_string(username),
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());
open(peerId ? session->data().peer(peerId).get() : nullptr);
}).fail([=] {
if (show->valid() && &show->session() == session) {
show->showToast(
tr::lng_username_not_found(tr::now, lt_user, username));
}
}).send();
}
} // namespace Wallet